Section2

Section 2 [React] - 데이터 흐름

Heemok 2023. 5. 30. 11:02

2023-05-30(화)

React 데이터 흐름 개요

 

학습 목표

  • React에서의 데이터 흐름, 단방향 데이터 흐름을 이해할 수 있다.
  • 어떤 컴포넌트에 state가 위치해야 하는지 알 수 있다.
  • State 끌어올리기의 개념을 이해할 수 있다.
    • 상태 변경 함수가 정의된 컴포넌트와, 상태 변경 함수를 호출하는 컴포넌트가 다름을 알 수 있다.

React의 개발 방삭의 가장 큰 특징은 페이지 단위가 아닌, 컴포넌트 단위로 시작한다는 점이 가장 큰 특징입니다.

일단 해당 이미지와 같이 프로토타입을 전달받은 후에 먼저 컴포넌트를 찾아봅시다.

 

다음과 같이 컴포넌트를 찾아낸 후, 먼저 컴포넌트를 만들고, 다시 페이지를 조립해 나갑니다.

 

검색창 = <Seacrch>

홈 = <Link>

트윗입력폼 = <NewTweetForm>

트윗게시글 = <SingleTweet>

 

이렇게 상향식(bottom-up)으로 앱을 만듭니다.

이것의 가장 큰 장점은 테스트가 쉽고, 확장성이 좋습니다. 그래서 기획자나,ux디자이너로부터 디자인을 받은 후

이를 컴포넌트 계층 구조로 나누는 것이 가장 먼저 해야할 일입니다.

 

트위터 클론 tweet 에서 이미지와 같은 형태로 컴포넌트 디자인을 합니다.

해당 디자인은 단일 책임 원칙에 따른 구분입니다. 하나의 컴포넌트는 한 가지 일만 합니다.

이를 트리구조로 나타내면 다음 이미지와 같습니다.

컴포넌트를 만드는 방법은 앞서 배웠기에, 이제는 데이터를 어디에 둘지를 결정합니다.

 

컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 마치 전달인자(arguments) 혹은 속성(attributes)처럼

전달받을 수 있습니다.

 

즉 데이터를 전달하는 주체는 부모 컴포넌트가 됩니다. 이는 데이터의 흐름이 하향식(top-dowm)임을 의미합니다.

이 원칙은 매우 중요합니다. 얼마나 중요하냐면, 단방향 데이터 흐름(one-way date flow)이라는 키워드가

React를 대표하는 설명 중 하나일 정도입니다.

또한 컴포넌트를  props 를 통해 전달받은 데이터가 어디서 왔는지 전혀 알지 못합니다.

 

애플리케이션에서 필요한 데이터가 무엇인지 먼저 정의해봅니다.

다음과 같은 데이터를 생각해 볼 수 있습니다.

 

-Twittler 애플리케이션이 갖고 있는 상태-

1. 전체 트윗 목록

2. 사용자가 작성 중인 새로운 트윗 내용

 

이중에 변하지 않는 값이 무엇일까요?

- 사용자가 입력중인 이벤트에 따라 얼마든지 변할 수 있습니다. 이것은 상태(state)입니다.

트윗 목록이 추가 또는 삭제될 여지가 없다면 사실 state로 둘 필요가 없지만 우리는 새 트윗 추가라는

기능을 염두에 두고 있으므로, 트윗 목록 역시 state입니다.

 

모든 데이터를 상태로 둘 필요는 없습니다. 사실 상태는 최소화 하는것이 가장 좋습니다.

상태가 많아질수록 애플리케이션은 복잡해집니다. 어떤 데이터를 상태로 두어야 하는지 3가지 질문을 통해 판단합시다.

 

- 부모로부터 props를 통해 전달됩니까? 

- 시간이 지나도 변하지 않나요?

- 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가요?

 

state(상태)를 어디에 위치시켜야 하는지 살펴봅시다

 

상태가 특정 컴포넌트에서만 유의미하다면, 특정 컴포넌트에만 두면 되니까 크게 어렵지 않지만,

만일 하나의 상태를 기반으로 두 컴포넌트가 영향을 받는다면 이때에는 공통 소유 컴포넌트를 찾아 

그곳에 상태를 위치해야 합니다.

즉, 두 개의 자식 컴포넌트가 하나의 상태에 접근하고자 할 때는 두 자식의 공통 부모 컴포넌트에 상태를 위치해야합니다.

 

앞서 정의한 데이터를 기반으로 위치를 정해봅시다.

"전체 트윗 목록" 상태는 어디에 위치하는 것이 좋을까요? 전체 트윗 목록은, Tweets에서 필요로 하는 데이터입니다.

여기서, 만약 새 글을 추가하는 이벤트가 발생할 경우, 이때 전체 트윗 목록에 새로운 트윗 객체를 추가할 수

있어야 합니다. 즉, 두 컴포넌트 모두 트윗 목록에 의존합니다.

그렇다면, 두 컴포넌트위 부모인 Twittler에서 전체 트윗 목록 상태를 위치하면 가능하게 됩니다.

 

"사용자가 작성 중인 새로운 트윗 내용" 이라는 상태를 위치해봅시다.

 

NewTweetForm에서는 사용자가 트윗 내용을 작성할 수 있습니다.

사용자 입력에 따라 값이 변하므로, "작성 중인 트윗 내용"은 상태입니다. 그럼 Tweets나 다른 컴포넌트가

작성중인 내용을 가질 필요가 있을까요?

 

NewTweetForm은 그저 버튼이 눌린 후 완성된 하나의 트윗 객체를 전체 트윗목록에 전달하기만 하면 됩니다.

입력에 따라 실시간으로 다른 컴포넌트가 변한다면 모를까, 여기에서는 그렇지 않으므로

다른 컴포넌트와 공유할 필요가 없습니다. "작성 중인 트윗 내용"이라는 상태는 NewTweetForm에 두면 됩니다.

 

앞서 리액트는 단방향에 데이터 흐름을 가지고 있다고 말씀드렸는데

갑자기 역방향 데이터 흐름이라는 이야기가 나올까요?

 

상태 위치를 전부 정하고 나서 생각해보니, 부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해 변하는 것을

발견할 수 있을 것입니다.

바로 새로운 트윗 추가의 대표적인 예죠, 버튼을 통한 이 액션은, 부모의 상태를 변화시켜야 합니다.

하위 컴포넌트(NewTweetForm)에서의 클릭 이벤트가, 부모의 상태를 바꾸어야만 하는 상황이 왔습니다.

이를 어떻게 해결할까요?

이를 해결할 수 있는 키워드가 바로 "State 끌어올리기" (Lifting state up) 입니다.

결론부터 말하자면, 이는 상태를 변경시키는 함수(handler)를 하위 컴포넌트에 props로 전달해서 해결할 수 있습니다.

이는 마치 콜백함수를 사용하는 방법과 비슷합니다.

 

 

예제문제 

import React, { useState } from "react";
import "./styles.css";

const currentUser = "김코딩";

function Twittler() {
const [tweets, setTweets] = useState([
{
uuid: 1,
writer: "김코딩",
date: "2020-10-10",
content: "안녕 리액트"
},
{
uuid: 2,
writer: "박해커",
date: "2020-10-12",
content: "좋아 코드스테이츠!"
}
]);

const addNewTweet = (newTweet) => {
setTweets([...tweets, newTweet]);
}; // 이 상태 변경 함수가 NewTweetForm에 의해 실행되어야 합니다.

return (
<div>
<div>작성자: {currentUser}</div>
<NewTweetForm addNewTweet={addNewTweet} />
<ul id="tweets">
{tweets.map((t) => (
<SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
{t.content}
</SingleTweet>
))}
</ul>
</div>
);
}

function NewTweetForm({ addNewTweet }) {
const [newTweetContent, setNewTweetContent] = useState("");

const onTextChange = (e) => {
setNewTweetContent(e.target.value);
};

const onClickSubmit = () => {
let newTweet = {
uuid: Math.floor(Math.random() * 10000),
writer: currentUser,
date: new Date().toISOString().substring(0, 10),
content: newTweetContent
};
// TDOO: 여기서 newTweet이 addNewTweet에 전달되어야 합니다.
addNewTweet(newTweet);
};

return (
<div id="writing-area">
<textarea id="new-tweet-content" onChange={onTextChange}></textarea>
<button id="submit-new-tweet" onClick={onClickSubmit}>
새 글 쓰기
</button>
</div>
);
}

function SingleTweet({ writer, date, children }) {
return (
<li className="tweet">
<div className="writer">{writer}</div>
<div className="date">{date}</div>
<div>{children}</div>
</li>
);
}

export default Twittler;

 


상태 끌어올리기에 대해 좀 더 자세히 알아보도록 하겠습니다.

 

단방향 데이터 흐름이라는 원칙에 따라, 하위 컴포넌트는 상위 컴포넌트로부터 전달받은 데이터의 형태 혹은 타입이 무엇인지만 알 수 있습니다. 데이터가 state로부터 왔는지, 하드코딩으로 입력한 내용인지는 알지 못합니다.

 

그러므로 하위 컴포넌트에서의 어떤 이벤트로 인해 상위 컴포넌트의 상태가 바뀌는 것은 마치 "역방향 데이터 흐름"과 같이 조금 이상하게 들릴 수 있습니다. React가 제시하는 해결책은 다음과 같습니다.

상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다

여전히 단방향 데이터 흐름의 원칙에 부합하는 해결 방법입니다. 바로 이것이 "상태 끌어올리기"입니다.

 


자율 학습

-

Part 1: 항공권 목록 필터링

Main.js

메인 (부모컴포넌트)에 있는 search함수가
search 컴포넌트 (자식컴포넌트)의 검색 버튼 클릭시 실행되어야 한다 - 상태끌어올리기를 사용하자

export default function Main() {
  const [condition, setCondition] = useState({
    departure: 'ICN'
  })
  const [flightList, setFlightList] = useState(json)
  const [isloading, setloading] = useState(false)

  const search = ({ departure, destination }) => {
    if (condition.departure !== departure || condition.destination !== destination) {
      console.log('condition 상태를 변경시킵니다')

      setCondition({departure, destination}) 
      // 1 상태변경 함수인 serach에 condition변경 함수인 setCondition을 이용해 값을 바꿔준다
    }
  }

  useEffect(async() =>{
    setloading(true)
    setFlightList(await getFlight(condition))
    setloading(false)
  }, [condition])


  const filterByCondition = (flight) => {
    let pass = true;
    if (condition.departure) {
      pass = pass && flight.departure === condition.departure
    }
    if (condition.destination) {
      pass = pass && flight.destination === condition.destination
    }
    return pass;
  }

  global.search = search 

  return (
    <div>
      <Head>
        <title>States Airline</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <h1>
          여행가고 싶을 땐, States Airline
        </h1>
        <Search onSearch={search}/> 
// 2 Search 컴포넌트에는 상태 변경 함수 `search`가 `onSearch` props로 전달되어야 한다
        <div className="table">
          <div className="row-header">
            <div className="col">출발</div>
            <div className="col">도착</div>
            <div className="col">출발 시각</div>
            <div className="col">도착 시각</div>
            <div className="col"></div>
          </div>
          {<FlightList list={flightList.filter(filterByCondition)} />}
        
        </div>

        <div className="debug-area">
          <Debug condition={condition} />
        </div>
      </main>
    </div>
  )
}

Search.js

import { useState } from 'react'

function Search({onSearch}) { 
// 3 onSearch props를 받아온다
  const [textDestination, setTextDestination] = useState('')

  const handleChange = (e) => {
    setTextDestination(e.target.value.toUpperCase())
  }

  const handleKeyPress = (e) => {
    if (e.type === 'keypress' && e.code === 'Enter') {
      handleSearchClick()
    }
  }

  const handleSearchClick = () => {
    console.log('검색 버튼을 누르거나, 엔터를 치면 search 함수가 실행됩니다')

  // 4 상태 변경 함수 `search`는 Search 컴포넌트의 `검색` 버튼 클릭 시 실행되어야 합니다 
  // 검색 버튼을 클릭했을때 onChange는 handleChange값이 변경된다고 아래에 적혀있다
  // handleChange는 도착지 값이 변경되는 것을 알려주고 있고 
  // 출발지는 고정하고 도착지의 값을 textDestination으로 변경해주면 된다 
  // 서치 함수에서 setCondition({departure, destination}) 객체형태로 값을 받아오는걸 알 수 있으니
  // 똑같이 값을 넣어줘야 한다
    onSearch({departure : "ICN", destination : textDestination})
  }

  return <fieldset>
    <legend>공항 코드를 입력하고, 검색하세요</legend>
    <span>출발지</span>
    <input id="input-departure" type="text" disabled value="ICN"></input>
    <span>도착지</span>
    <input id="input-destination" type="text" value={textDestination} onChange={handleChange} placeholder="CJU, BKK, PUS 중 하나를 입력하세요" onKeyPress={handleKeyPress} />
    <button id="search-btn" onClick={handleSearchClick}>검색</button>
  </fieldset>
}

export default Search
 
 
 
 

 

'Section2' 카테고리의 다른 글

Section2 Unit9-[React] Effect Hook  (0) 2023.05.31
Section 2 Unit8 [Postman]  (0) 2023.05.26
Section 2 Unit8 [REST API]  (0) 2023.05.25
Section 2 Unit7 [HTTP/네트워크]-2  (0) 2023.05.24
Section 2 Unit7 [HTTP/네트워크]  (0) 2023.05.24