2 minute read

React 데이터 흐름

데이터 흐름

  • bottom-up (상향식) 개발

React에서 개발 방식의 가장 큰 특징은 컴포넌트 단위로 나누어 개발한다는 것이다. 컴포넌트 단위를 찾고나면 각 컴포넌트를 만들고 조립하는 방식으로 웹 페이지 또는 앱을 만들어 나간다. 이 방식을 bottom-up(상향식) 개발 방식이라고 한다. 상향식 개발의 장점은 테스트가 쉽고 확장성이 좋다는 점이다. 이때 중요한 점은 하나의 컴포넌트가 한 가지의 기능만 가지도록 만드는 것이다. 그래야 이후 오류 발생시 오류가 발생한 컴포넌트를 쉽게 찾고 수정할 수 있다.

  • Top-down (하향식) data flow

리액트를 대표하는 키워드 중 하나는 one-way data flow (단방향 데이터 흐름)이다. 컴포넌트는 컴포넌트 바깥에서 props를 통해 데이터를 전달받을 수 있다. 이때 데이터를 전달하는 주체는 부모 컴포넌트고 따라서 데이터의 흐름은 top-down (하향식) 방식이고, 자식 컴포넌트는 props를 통해 전달받은 데이터가 어디에서부터 전달되었는지 알지 못한다. 이것이 one-way data flow (단방향 데이터 흐름)이다.


State 끌어올리기 (Lifting State Up)

때로는 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야 할 필요가 있다. 이럴때 사용되는 것이 state 끌어올리기이다. 리액트 공식문서에는 다음과 같이 설명했다.

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

다음 예제를 살펴보자. 부모와 자식 컴포넌트가 트리구조 형태로 하나씩 존재하고 상태를 변경할 수 있는 메소드가 존재한다.

import React, { useState } from "react";

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

  const handleChangeValue = () => {
    setValue("보여줄게 완전히 달라진 값");
  };

  return (
    <div>
      <div>값은 {value} 입니다</div>
      <ChildComponent handleButtonClick={handleChangeValue} />
    </div>
  );
}

function ChildComponent({ handleButtonClick }) {
  const handleClick = () => {
    handleButtonClick();
  };

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

상태를 변경하는 함수 handleChangeValue를 props로 자식 컴포넌트에 전달하고, 전달된 함수가 하위 컴포넌트에서 버튼클릭 이벤트handleButtonClick에 따라 상태를 변경할 수 있게 됐다.

이처럼 하위 컴포넌트에서 이벤트를 통해 컴포넌트간 데이터를 전달할 수 있게 되는 것을 State 끌어올리기라고 한다.


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

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

  const handleChangeValue = (newValue) => {
    setValue(newValue);
  };

  // ...생략...
}

function ChildComponent({ handleButtonClick }) {
  const handleClick = () => {
    handleButtonClick('넘겨줄게 자식이 원하는 값')
  }

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



다음 예제도 보면서 분석해보자

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]);
  };

  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}
          </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,
    };
    onButtonClick(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;


참고

리액트 공식 문서