본문 바로가기

[React] 데이터 흐름 & State 끌어올리기 (Lifting State Up)

[React] 데이터 흐름 & State 끌어올리기 (Lifting State Up)

 

이전 글 참고 : React 데이터의 흐름 

*  State 끌어올리기 
 상태 변경 함수가 정의된 컴포넌트
 상태 변경 함수를 호출하는 컴포넌트
*  React 공식 문서의 주요 개념

 

 일단 이거부터 보고 가자 ㅋㅋㅋㅋ

  (bgm과 같이 보는 유튜브 쇼츠 링크 클릭!)

 

 


 

역방향 데이터? (스포: 아니다.)

 

저번 글과 비교하며 컴포넌트들의 달라진 관계를 보면,

  • Tweets 컴포넌트에서 관리하고 있었던 데이터(전체 트윗 목록)은
  • NewTweetForm 컴포넌트를 통해 변경이 된다.

 

 

  • 즉, 새 글을 추가하는 이벤트가 발생할 경우, 이때 전체 트윗 목록에 새로운 트윗 객체를 추가할 수 있어야 한다. 
  • NewTweetFormTweets 두 컴포넌트 모두 트윗 목록에 의존한다.
  • 그렇다면, 두 컴포넌트의 부모인 Twittler에서 전체 트윗 목록 상태를 관리해야 한다.

 

[정리]

상태가 하나의 컴포넌트에만 영향을 준다면 그 컴포넌트에만 위치해도 된다. 하지만 두 컴포넌트가 하나의 상태로부터 영향을 받는다면 두 컴포넌트 상위에 상태를 공유하는 컴포넌트가 존재하는 것이 좋다. 단방향 데이터 흐름을 유지하고, 같은 상태를 공유할 수 있는 바람직한 방법이기 때문이다. 

 

 

[ "작성 중인 트윗 내용"이라는 상태는 어떻게 위치시킬까? ]
 
NewTweetForm에서는 사용자가 트윗 내용을 작성할 수 있다.
-  사용자 입력에 따라 값이 변하므로 "작성 중인 트윗 내용"은 상태이다.

* Tweets나 다른 컴포넌트가 작성중인 내용을 가질 필요가 없다. 
* NewTweetForm은 그저 버튼이 눌린 후 완성된 하나의 트윗 객체를 전체 트윗 목록에 전달하기만 하면 된다.
* 입력에 따라 실시간으로 다른 컴포넌트가 변한다면 모를까, 여기에서는 그렇지 않으므로 다른 컴포넌트와 공유할 필요가 없다.

-> 따라서 "작성 중인 트윗 내용"이라는 상태는 NewTweetForm에 두는 것으로 충분하다. 

 

리액트는 단방향 데이터 흐름 구조가 아닌가?

리액트는 단방향 데이터 흐름을 가지고 있다. BUT!

  • 상태 위치를 전부 정하고 나서 생각해보니, 부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해 변하는 것을 발견할 수 있을 것이다.
  • 바로 새로운 트윗 추가가 대표적인 예로, 버튼을 통한 이 액션은, 부모의 상태를 변화시켜야 한다.

 

  • 하위 컴포넌트(NewTweetForm)에서의 클릭 이벤트가, 부모의 상태를 바꾸어야만 하는 상황이 왔다.
  • 이를 해결할 수 있는 키워드는 바로 "State 끌어올리기(Lifting state up)"이다.
  •  컴포넌트는 props 형태로 속성을 내려 받아 인자로 사용할 수가 있다.
  • 결론부터 말하자면, 이는 상태를 변경시키는 함수(handler)를 하위 컴포넌트에 props로 전달해서 해결할 수 있다.
    -> 마치 콜백 함수를 사용하는 방법과 비슷
  • React 공식 문서의 주요 개념을 읽어보는 것을 추천한다.

 

동일한 데이터를 여러 컴포넌트에 반영하고 싶다면 가장 가까운 공통 조상으로 state를 끌어올리는 것이 좋다.

 

 


 

상태 끌어올리기

 

단방향 데이터 흐름이라는 원칙에 따라,

  • 하위 컴포넌트는 상위 컴포넌트로부터 전달받은 데이터의 형태 혹은 타입이 무엇인지만 알 수 있다.
  • 데이터가 state로부터 왔는지, 하드코딩으로 입력한 내용인지는 알지 못한다.
  • 그러므로 하위 컴포넌트에서의 어떤 이벤트로 인해 상위 컴포넌트의 상태가 바뀌는 것은 마치 "역방향 데이터 흐름"과 같이 조금 이상하게 들릴 수 있다.

 

React가 제시하는 해결책은 다음과 같다.

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

 

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

 

 

 

<!-- 잠깐 복습 -->

callback 다시보기

콜백(callback)은 다른 함수(고차 함수)의 인자로 전달되는 함수를 의미한다.

// 고차함수
function each(array, iterator) {
  for(let i = 0; i < array.length; i++) {
    let element = array[i]
    iterator(element, i, array)
  }
}

// 콜백 함수
function printElement(element) {
  console.log(element)
}

each(['hello', 'world'], printElement);

<!-- 복습 끝 -->

 

 

상태를 변경하는 함수는 무엇이며, 하위 컴포넌트로는 어떻게 전달할 수 있을까?

 

아래 코드의 상위 컴포넌트(ParentComponent)에서 상태를 변경하는 함수는 handleChangeValue 이다.

  • 하위 컴포넌트로의 전달은 props를 이용한다.
  • props 이름은 적절하게 지어준다.
    예) 하위 컴포넌트가 버튼 클릭 이벤트에 따라 상태를 변경하려고 하므로 이름은 handleButtonClick이라고 지어준다.
function ParentComponent() {
  const [value, setValue] = useState("날 바꿔줘!");   // 부모 컴포넌트의 state hook
 
  const handleChangeValue = () => {                 // 상태를 바꿔주는 함수
    setValue("보여줄게 완전히 달라진 값");
  };

  return (
    <div>
      <div>값은 {value} 입니다</div>
      <!--자식 컴포넌트(ChildComponent)에게 HandleButtonClick이라는 이름으로 함수를 전달한다.-->
      <ChildComponent handleButtonClick={handleChangeValue}  /> 
    </div>
  );
}

 

<ChildComponent> 는 마치 고차 함수가 인자로 받은 함수를 실행하듯, props로 전달받은 함수를 컴포넌트 내에서 실행할 수 있게 된다.

  • "상태 변경 함수"는 버튼이 클릭할 때 실행되기를 원하므로, 해당 부분에 콜백 함수를 실행한다.
function ChildComponent({ handleButtonClick }) {
  const handleClick = () => {
    // Q. 이 버튼을 눌러서 부모의 상태를 바꿀 순 없을까?
    // A. 인자로 받은 상태 변경 함수를 실행하자!

    handleButtonClick()
  }

// 버튼을 클릭하면 handleClick이라는 함수가 실행되며 부모로 부터 받은 handleButtonClick을 호출한다.
  return (
    <button onClick={handleClick}>값 변경</button>
  )
}

 

필요에 따라 설정할 값을 콜백 함수의 인자로 넘길 수도 있다.

function ParentComponent() {
  const [value, setValue] = useState("날 바꿔줘!");

  const handleChangeValue = (newValue) => {   //newValue의 값으로 value 값 변경 
    setValue(newValue);
  };

  // ...생략...
}

function ChildComponent({ handleButtonClick }) {
  const handleClick = () => {
    handleButtonClick('넘겨줄게 자식이 원하는 값')  // 이 값이 newValue의 전달인자로 들어간다.
  }

  return (
    <button onClick={handleClick}>값 변경</button>
  )
}

 

이 원리를 통해 컴포넌트 간 데이터 교환을 구현해낸다.

보다 실용적인 예제는 공식 문서에서 찾을 수 있다.

 

 

[정리]

React는 기본적으로 단방향 데이터 흐름이라는 원칙을 가진다.
-> 상위 컴포넌트에서 하위로는 이동할 수 있지만 반대로는 불가하다. 

그러나 종종 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영할 필요가 생긴다.
-> 이 때, state 끌어올리기 개념으로 데이터 변경사항을 상위 컴포넌트로 전달할 수 있다.
-> 데이터를 직접 상위로 전달하는 것과는 다르게, state를 직접 전달하는 것이 아닌 
     state 갱신 함수를 전달 받아 해당 함수를 실행시키는 원리이다.
 

 


 

Twittler 예제 분석하기

 

앞서 React 데이터 흐름 설명에 등장한 Twittler의 소스코드가 있다. (컴포넌트 구성과 상태 및 상태 변경 함수)

  • 어떻게 <NewTweetForm> 컴포넌트에서 tweets 상태를 변화시킬 수 있을까?
  • 어떻게 State 끌어올리기를 적용할 수 있을까?

 

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

const currentUser = "김코딩";                   // 여기서 작성자는 고정해둠 

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

// 이 상태 변경 함수가 자식 컴포넌트의 ** NewTweetForm **에 의해 실행되어야 한다.
  const addNewTweet = (newTweet) => { 
    setTweets([...tweets, newTweet]);
  }; 


// 새글 쓰기 기능의 자식 컴포넌트에게 addNewTweet 함수를 onButtonClick이라는 이름으로 내려보낸다 
  return (
    <div>
      <div>작성자: {currentUser}</div>                         
      <NewTweetForm onButtonClick= {addNewTweet}/>
      <ul id="tweets">
        {tweets.map((t) => (
          <SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
            {t.content}      // ** 설명(1) 내용은 <SingleTweet> 사이에 보낸다. (props.children 사용)
          </SingleTweet> 
        ))}
      </ul>
    </div>
  );
}
function NewTweetForm({ onButtonClick }) {                      // 새 글을 쓰는 기능 
  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
    };
    // 여기서 newTweet이 addNewTweet에 전달되어 부모의 tweets 상태가 변경된다.
    onButtonClick(newTweet);
  };

  return (
    <div id="writing-area">    
<!--내용을 쓰면 onTextChange 함수가 호출, newTweetContent 상태를 변경한다.-->    
      <textarea id="new-tweet-content" onChange={onTextChange}></textarea>
<!--버튼을 클릭하면 onClickSubmit 함수가 호출, newTweet 객체를 생성해서 부모로부터 받은 함수를 호출한다-->      
      <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>                          // ** 설명(1)
    </li>
  );
}

export default Twittler;

 

설명(1)

  • props 형태를 살펴보니 ({writer, date, children}) 형식으로 전달이 되고 있었다. 
  • <div> 태그에 역시 <div>{children}</div> 로 옆에 내용이 출력되는 모습을 볼 수 있다.

 

시도 1) 배운 대로 props 와 props.children을 사용해보았다. (중괄호 빼야함)

잘 된다.

 

중괄호를 빼고 props만 넣고 props.writer... 등등을 넣어주어도 되긴 한다. 

writer, date도 같이 넣어주었다.

 

 

시도 2) {children} 대신 {아무 단어}나 넣어보았다. (중괄호 넣고)

이렇게 {children}을
{whatever}로 바꿔보았다. 역시나 안 된다.

 

[정리]

부모 컴포넌트에서 자식 컴포넌트로 아래와 같이 내려보낼 때,

<SingleTweet key={t.uuid} writer={t.writer} date={t.date}>{t.content}</SingleTweet>

자식 컴포넌트에서는 
(1) props로 받아서, 자식 컴포넌트 내에서 {props.writer}, {props.children} 등으로 사용할 수도 있지만 
(2) 좀 더 명확하게 props가 무엇인지 명시하는 {writer, date, children} 자체를 내려받아서 {writer}, {children} 등으로 사용하는 것이 가독성이 좋다고 느껴진다. 

 

 

더보기

~~  index.js 와 styles.css  ~~

// ** index.js

import React from "react";
import ReactDOM from "react-dom";

import Twittler from "./Twittler";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <Twittler />
  </React.StrictMode>,
  rootElement
);

 

/* styles.css */

#new-tweet-content {
  width: 80%;
  height: 100px;
}

#submit-new-tweet {
  width: 20%;
}

#writing-area {
  display: flex;
}

#tweets {
  padding: 0;
  margin: 0;
}

li.tweet {
  list-style: none;
  border: 1px solid transparent;
  padding: 10px;
  margin: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

 

 
728x90
⬆︎