React 데이터 흐름

Dongwoo Joo·2023년 3월 31일
0

codestates bootcamp

목록 보기
23/48

주의사항
쉽게 설명하고, 기억하기 위해 비유가 많이 들어가 있습니다.
따라서, 전문적이지 않은 부분의 내용이 있으니 참고해서 읽어주세요.

학습 내용

React 데이터 흐름

React에서의 데이터 흐름은 단방향 데이터 흐름이다.
부모 노드에서 자식 노드로 전송하는 Top-down 방식이다.
이 원칙은 react의 핵심이다.

비유해서 설명해보자.
단방향: 좁은도로 일방통행(한쪽 방향으로만 이동이 가능)
양방향: 일반도로 우측통행(양쪽 방향으로 교차 이동이 가능)

React에서의 데이터 흐름은 한쪽에서만 흐른다.
방향: 부모 -> 자식
자식 컴포넌트에서 데이터를 사용하려면, 부모 컴포넌트의 데이터를 가져와서 사용해야 한다.
컴포넌트 외부에서 props를 이용해서 데이터를 마치 전달인자 혹은 속성처럼 받을 수 있다.

비유해서 설명해보자.
데이터는 마치 돈과 같다.
자식이 돈을 사용하려면, 돈이 없기 때문에, 부모에게 용돈을 받아서 사용해야 한다.

만약 앱의 크기가 커짐에 따라, 데이터가 많아져서 부모에게서 데이터를 받기 어려운 구조만큼 된다면, 데이터를 다른 곳에서 가져와야 하는데 대표적으로 리덕스라는 라이브러리를 사용한다. -> 이것도 좀 비유해보자면, 앱의 크기가 커진다는 것은 자식이 많아졌다는 것이다. 이제 돈을 부모에게서 빌리는 것이 아니라, 은행에서 가져와서 사용한다. 그 은행의 역할이 리덕스 스토어라고 생각하자.

전달인자: arguments
속성: attributes

상태 위치 정하기

상태가 특정 컴포넌트에만 유의미하다면,
특정 컴포넌트에 두면 되므로 크게 문제되지 않는다.

하지만 하나의 상태를 기반으로 두 컴포넌트가 영향을 받는다면,
이때에는 상태 데이터를 공통으로 소유하고 있는 컴포넌트에 상태를 위치시켜야 한다.
=> 두 개의 자식 컴포넌트가 하나의 상태에 접근 시,
두 자식의 공통 부모 컴포넌트에 상태를 위치한다.

웹에서 데이터를 다루는 2가지 방법(복습)

변하면 안되는 값 -> props
변할 수 있는 값 -> state

Props

Props는 컴포넌트의 Property(속성)을 의미한다.
외부로부터 전달받은 값이며, 웹 애플리케이션에서 해당 컴포넌트가 가진 속성에 해당한다.
부모 컴포넌트로부터 전달받은 값이다.
props는 객체 형태이며, 어떤 타입의 값도 넣어 전달할 수 있다.
함부로 변경될 수 없는 읽기 전용(read-only) 객체이다.

만약 읽기 전용 객체가 아니라면,
props를 전달받은 하위 컴포넌트 내에서 props를 직접 수정 시,
props를 전달한 상위 컴포넌트 값에 영향을 미칠 수 있게 된다.(side effect)

=> 개발자가 의도하지 않은 부작용(side effect)이 생기고,
이는 React의 단방향, 하향식 데이터 흐름 원칙에 위배된다.
(React is all about one-way data flow down the component hierarchy)

State

state는 상태이다. 상태는 변할 수 있다.
즉, state는 컴포넌트 내에서 변할 수 있는 값이다.

만약 웹 개발 시, 회원가입 기능을 만든다고 하자.
회원의 데이터를 입력받는 과정을 구현해야 하는데,

이름, 주민번호, 전화번호, 나이
변하지 않는 값, 변할 수 있는 값을 구분해보자.

Props: 주민번호, 이름
state: 전화번호, 나이
주민번호는 바꿀 수 없고, 이름도 거의 변하지 않는다. => props
나이는 매년 바뀌고, 전화번호도 바뀔 수 있다. => state

애매한 props, state 구분하기 위한 질문

부모로부터 props를 통해 전달되는가? True -> props
시간이 지나도 변하지 않는가? True -> props
컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가? True -> props
*3번째 질문은 더 이해가 필요하다.

State 끌어올리기(Lifting State Up)

개요

그런데, 질문이 있다. 왜 상태에서는 역방향 데이터 흐름이라는 느낌이 드는가?
상태 위치를 정하고 나서 생각해보니,
부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해 변하는 것을 발견할 수 있다.

예를 들어보자.
쇼핑몰 웹 페이지를 만드는데,
메인 컴포넌트에 상품이 리스트화(진열)되어 있고,
개별 상품 컴포넌트 내부에 새로운 장바구니 추가 기능이 있다.

장바구니에 추가하는 이러한,
버튼을 통한 액션은, 부모의 상태를 변화시켜야 한다.
하위 컴포넌트에서의 클릭 이벤트가,
부모의 상태를 바꿔야 하는 상황인 것이다.

이를 해결하는 방법이 state 끌어올리기(Lifting state up) 이다.

결론: 이는 상태를 변경시키는 함수를 하위 컴포넌트에 Props로 전달해서 해결할 수 있다.
마치 콜백 함수를 사용하는 방법과 비슷하다.

상태 끌어올리기

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

하위 컴포넌트의 어떤 이벤트로 인해,
상위 컴포넌트의 상태가 바뀌는 것은 마치 단방향 데이터 흐름에 어긋나 보인다.

React는 해결책을 제시한다.
상위 컴포넌트의 "상태를 변경하는 함수" 자체를
하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다.
이것은 여전히 단방향 데이터 흐름의 원칙에 부함하는 해결 방법이다.

마치 스코프의 개념과 유사하다.
상위 컴포넌트에서 함수를 정의하고,
하위 컴포넌트에서 콜백 함수로서 사용한다.

예제 1

import React, { useState } from "react";

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

  const handleChangeValue = (textValue) => {
    setValue(textValue); // 입력한 텍스트 값을 콜백함수의 인자로 넘길 수 있다.
  };
// -> 이 함수는 상태를 변경하는 함수이다.

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

function ChildComponent({handleButtonClick}) { // 콜백 함수를 실행
  const handleClick = () => {
   handleButtonClick(textValue)
  };

// 마치 고차 함수가 인자로 받은 함수를 실행하듯, props를 전달받은 함수를 컴포넌트 내에서 실행할 수 있게 된다. "상태 변경 함수"는 버튼이 클릭될 때 실행되기를 원하므로, 해당 부분에 콜백 함수를 실행한다.
// 

  return <button handleButtonClick={handleClick}>값 변경</button>;
} // onClick -> handleButtonClick 으로 변경. 왜? 버튼 클릭 이벤트에 따라 상태를 변경하는 것을 명확히 표현하기 위해서.

예제 2

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>
      1번 주석
      <NewTweetForm onButtonClick={addNewTweet}/>
      {/*  */}
      <ul id="tweets">
        {tweets.map((t) => (
          <SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
            {t.content}
          </SingleTweet>
        ))}
      </ul>
    </div>
  );
}

2번
// NewTweetForm 컴포넌트에서 새 트윗 객체로,
// onButtonClick props를 호출하도록, onClickSubmit 함수를 업데이트 해야 한다.

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

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

3번
  const onClickSubmit = () => {
    let newTweet = {
      uuid: Math.floor(Math.random() * 10000),
      writer: currentUser,
      date: new Date().toISOString().substring(0, 10),
      content: newTweetContent
    };
    onButtonClick(newTweet); // onClickSubmit 함수 실행 시, onButtonClick도 실행
    setNewTweetContent(""); // 클릭 후, textarea를 빈 문자열로 만든다.
    // TDOO: 여기서 newTweet이 addNewTweet에 전달되어야 합니다.
  };

  return (
    <div id="writing-area">
      <textarea id="new-tweet-content"
      value={newTweetContent}
      // 위에서 정의한 상태는 newTweetContent이다.
      // 따라서, 이벤트 값으로 newTweetContent가 온다.
      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;

되새김질 로직 분석

1번 주석
addNewTweet 함수를 NewTweetForm 컴포넌트에,
prop으로 전달해야, Submit 버튼을 클릭 시, 호출할 수 있다

코드

<NewTweetForm onButtonClick={addNewTweet}/>

addNewTweet 함수가 실행되면,
onButtonClick 이라는 한 개의 Prop을 전달한다. 값은 addNewTweet 이다.
왜? -> 부모 Twittler 컴포넌트에서, 자식 NewTweetForm 컴포넌트로 onButtonClick(=prop)을 전달하기 위해서.

2번 주석
NewTweetForm 컴포넌트에서 새 트윗 객체로
onButtonClick props를 호출하도록,
onClickSubmit 함수를 업데이트 해야 한다.

3번 주석
트윗이 제출된 후 텍스트 영역을 지우기 위해
"setNewTweetContent"에 대한 호출도 추가해야 한다.

4번 주석
"NewTweetForm" 컴포넌트의 "New Tweet" 버튼에 이벤트 리스너를 추가해야 한다.

profile
pursue nature

0개의 댓글