본문 바로가기

Frontend Study

[FE_Bootcamp] 35일차_React 데이터 흐름

블로그를 무려 8일만에 작성하게 되었다. 여러 원인들이 있었지만,.,.,..,,,

일단 지금 배우고 있는 웹 서버가 너무 어렵다,..,.,.,,,

할일 많음 + 수업시간 외 공부의 연타로 블로그를 쓸 여유가 없었다고 하자. 

사실 내용이 좀 어렵다보니 의욕이 확 죽어버린 탓도 있긴 하다,..,,.,.흑

여튼, 이 지옥같은 웹 서버 단원도 슬슬 마무리가 되어가니, 복습 겸 블로그 작성을 재개해야겠다

 

1. 리액트 앱의 구조

리액트를 처음 배울 때, 리액트는 '컴포넌트'를 기반으로 만들어진다고 했다.

즉, 여러개의 컴포넌트를 만든 뒤 각 컴포넌트들을 조립하여 하나의 웹 애플리케이션을 만드는 것이다.

웹 애플리케이션에서 하나의 컴포넌트는 하나의 기능만을 담당한다. 

 

상암 보조경기장 예약 페이지

 

상암 보조경기장 예약 페이지를 예로 들어보자.

커다란 3개의 컴포넌트가 있고, 각 컴포넌트 안에는 또 각 기능을 하는 컴포넌트들이 들어있다.

<SelectDate>는 예약 날짜만을 선택하고, <SelectTime>은 예약 시간만을 선택하는 컴포넌트이다.

이 컴포넌트들을 트리 구조로 나타내보자

 

트리 구조로 나타낸 예약 페이지

 

예약을 하려면, SelectCondition의 하위 컴포넌트들로부터 예약 데이터를 만들고, 이 데이터를 예약 목록에 넣은 뒤  Reserve 컴포넌트에 전달하여 ReserveCondition으로 예약 데이터 확인 후 GetReserve로 예약을 확정하는 과정을 거쳐야 한다. 

이때 Reserve 컴포넌트는 '상위 컴포넌트'에서 데이터를 props로 전달 받는다.

 

데이터가 전달되는 과정

 

이는 데이터 흐름이 부모 -> 자식. 즉, '하향식'임을 의미한다.

반대로, 자식 컴포넌트에서 부모 컴포넌트로는 데이터를 넘겨줄 수 없다. 

이를 '단방향 흐름'이라 하고, 리액트의 중요한 특성중 하나이다.

 

고요속의 외침 게임

 

또한, 컴포넌트는 이 데이터가 어디서 왔는지 출처를 알 수 없다.

고요속의 외침 게임을 생각해보자. 맨 마지막에 문제를 맞히는 사람은 바로 앞 사람이 전달해주는 단어만 듣고 문제를 맞혀야 한다. 

만약 오답이 전달되었다 해도, 마지막 사람은 내 앞사람이 잘못 전달했는지, 앞앞사람이 잘못 전달했는지 전혀 알 수 없다. 

컴포넌트 역시 이 데이터가 바로 위 컴포넌트로부터 왔는지, 그 위로부터 왔는지, 부모 컴포넌트로부터 왔는지 모른다는 특징이 있다.


2. 데이터의 정의

위에서 말한 데이터는 '변하는 값'과 '변하지 않는 값'으로 나누어진다. 

예약 페이지에서, 날짜와 시간, 인원수, 이용금액은 선택에 따라 계속해서 바뀐다.

따라서 이 데이터들은 '상태'라고 둘 수 있다.

또한, 이 데이터들을 담고 있다가 Reserve 컴포넌트로 전달하는 '예약 목록' 역시 데이터에 따라 계속해서 바뀌므로 상태라고 볼 수 있다.

 

다만, 상태는 적을수록 좋다. 상태가 계속해서 바뀐다면 애플리케이션은 복잡해질 것이고, 처리속도나 렌더링 속도에 영향을 줄 수있다. 따라서 우리는 어떤 데이터를 상태로 두어야 하는지 알아야 할 필요가 있다.

  • 부모로부터 props를 통해 전달되지 않음
  • 시간이 지나면 변함
  • 컴포넌트 안의 다른 state나 props를 가지고 계산이 불가능

위 조건을 만족하는 데이터들은 모두 상태라고 볼 수 있다


그렇다면 상태는 어디에 위치해야 하는 것일까?

 

 

선택 위치만을 바꿔주는 컴포넌트

 

<SelectDate>라는 컴포넌트만 존재한다고 해보자

<SelectDate>는 달력에서 날짜를 선택하면 해당 날짜의 배경색을 바꿔주는 기능을 가지고 있다.

이 컴포넌트에서, 상태는 '날짜'가 될 것이다.

또한, <SelectDate> 컴포넌트 이외에 다른 컴포넌트는 존재하지 않기 때문에 이 상태는 컴포넌트 안에서만 유용하다.

 

선택 위치에 따라 다른 컴포넌트도 바뀐다

 

하지만, 대부분의 웹 애플리케이션은 여러개의 컴포넌트로 이루어져 있고, 한 컴포넌트가 바뀌면 다른 컴포넌트도 바뀌는 형식을 취하고 있다. 

위 예시에서도 날짜에 따라 선택 가능 시간과 예약 정보가 바뀌는 것을 볼 수 있다.

이때는 여러 개의 컴포넌트가 하나의 상태에 의존적이기 때문에 한 컴포넌트 안에만 상태를 위치시킬 수 없다.

따라서, 상태에 영향을 받는 컴포넌트들의 부모 컴포넌트를 찾은 뒤 그곳에 상태를 위치시켜 주어야 한다.

 

위 트리를 다시 보자

 

 

  • SelectDate는 SelectTime과 SelectCondition에 영향을 준다
  • SelectTime은 SelectCondition에 영향을 준다
  • SelectHeadcount는 SelectCondition에 영향을 준다
  • SelectCondition은 Reserve에 영향을 준다

한마디로, SelectDate가 바뀌면 ReserveCondition과 GetReserve까지 바뀐다는 것이다.

그렇기 때문에, SelectDate에 의해 바뀌는 상태는 SelectCondition과 Reserve를 공통 자식으로 가지고 있는 ReservePage에 위치해야 한다.

 

리액트에서의 데이터 흐름

 

SelectDate를 클릭함으로써 SelectCondition이 바뀌고, 이 SelectCondition으로부터 전달된 데이터는 ReservePage의 예약 대기 리스트를 바꾼다. 그리고 이 예약 대기 리스트가 Reserve로 전달되어 화면을 바꾸고, 예약을 가능하게 하는 것이다.

따라서, SelectCondition과 Reserve에게 모두 영향을 주고받는 state인 '예약 대기 리스트'는 두 컴포넌트의 부모 컴포넌트인 ReservePage에 위치해야 하는 것이다.

 

그런데, 분명히 위에서 리액트는 '하향식 흐름'을 갖는다고 했다. 허나 지금 같은 경우에는 하위 컴포넌트의 데이터 변화로 인해 상위 컴포넌트의 데이터까지 바뀌고 있다.

이러면 단방향 흐름 원칙에 어긋나는 것이 아닐까?

 

위와 같은 문제를 해결하기 위해, 우리는 상태를 변경시키는 함수(handler)를 하위 컴포넌트에 props로 전달해 콜백함수처럼 사용하며 해결할 수 있다. 이를 'State 끌어올리기(Lifting State Up)'라고 한다.


3. State 끌어올리기

state 끌어올리기를 간단히 설명하자면, 상위 컴포넌트의 상태를 변경하는 함수를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행하여 상위 컴포넌트의 상태를 바꾸는 것이다. 

 

상태 끌어올리기 예제

 

자식 컴포넌트의 input에 값을 입력하면, 입력한 값에 따라 부모 컴포넌트의 출력이 바뀐다.

자식 컴포넌트의 값이 바뀌는데 부모 컴포넌트의 출력이 바뀌면 하향식이 아니라 상향식 데이터 흐름이라고 착각할 수 있지만, state 끌어올리기를 통해 구현한 것이므로 단방향 흐름 원칙에는 어긋나지 않는다.

코드를 보며 이해해 보자

 

function ParentComponent() {
  //부모 컴포넌트에 상태를 설정
  let [text, setText] = useState('아직 아무 값도 입력하지 않았습니다!')

  //상태를 바꾸는 함수를 생성
  const changeValue = function(newText){
    setText(newText)
  }
  
  return (
    <div>
      <div>
        <h1>나는 부모 컴포넌트입니다.</h1>
        <div>
          자식 컴포넌트에서 입력한 값은<br/>
          //상태를 출력
          <h3>{text}</h3>
        </div>
      </div>
      {/* 자식 컴포넌트를 만들 때, inputHandler라는 프로퍼티에 changeValue 함수를 전달 */}
      <ChildComponent inputHandler={changeValue} />
    </div> 
  )
}

 

부모 컴포넌트에 상태 text와 전달할 함수 changeValue를 선언하고, 자식 컴포넌트를 만들면서 프로퍼티로 선언한 함수를 전달해 준다.

자식 컴포넌트는 inputHandler라는 프로퍼티를 전달받는데, 이 inputHandler 안에는 changeValue가 들어 있다.

즉, inputHandler = changeValue. 콜백함수의 원리를 생각하면 편리하다.

  • text : 우리가 바꾸고 싶은 상태
  • setText : 원하는 값으로 text를 바꾸는 함수
  • changeValue : setText에 원하는 값을 전달해주는 함수
  • inputHandler : changeValue를 자식 컴포넌트에 전달하기 위한 프로퍼티 이름

 

function ChildComponent({inputHandler}){
  //부모 컴포넌트로부터 전달받은 inputHandler을 사용

  // 입력이 바뀔때마다 실행되는 typeHandler 함수 선언
  const typeHandler = function(event){

    // 이벤트가 일어나는 부분(=input)의 값을 가져옴
    let inputValue = event.target.inputValue

    // inputHandler = changeValue에 input의 값을 넣음
    inputHandler(inputValue)
  }

  return(
    <div>
      <h1>나는 자식 컴포넌트입니다.</h1>
      <div>내가 입력한 문자는<br/>
      {/* 입력이 바뀔 때 마다 typeHandler 함수 실행 */}
      <input type='text' placeholder='자식 컴포넌트의 입력' onChange={typeHandler}></input>
      </div>
    </div>
  )
}

 

부모 컴포넌트에서 자식 컴포넌트를 렌더링하면서 inputHandler = {changeValue}라는 프로퍼티를 전달하였다.

따라서, 자식 컴포넌트는 inputHandler라는 이름으로 changeValue 함수를 사용할 수 있게 되었다.

 

우리는 텍스트를 입력할 때 마다 출력이 바뀌게 하고 싶으므로, input 태그 안에 onChange 메서드를 넣어주고, 입력마다 실행될 함수를 새로 만든다. 이 함수가 typeHandler 함수이다.

입력이 바뀔 때 마다 typeHandler 함수가 실행되므로, typeHandler 함수 안에 changeValue 함수를 넣어 준다면 우리가 원하는 출력을 얻어낼 수 있을 것 같다.

 

자식 컴포넌트 안에서 changeValue는 inputHandler라는 이름으로 쓰이기 때문에, typeHandler 함수 안에 inputHandler 함수를 넣어준다. 그리고 event.target.value를 사용해 input에 입력된 값을 inputHandler 함수의 전달인자로 전달한다.

매 입력마다 typeHandler가 실행되기 때문에 전달인자를 받은 inputHandler도 매 입력마다 실행되고, changeValue 역시 계속 실행된다. 

changeValue는 input에 입력된 값을 다시 setText에게 전달하고, setText는 text를 전달받은 값으로 바꾼다.

 

이미지로 나타낸 상태 끌어올리기

 

다음과 같은 과정을 거친다고 보면 되겠다.


밀린 블로그를 쓰자니 정말 힘들다,.,.,.,,,

이번주는 과연 주말까지 이용해서 쓸 수 있을지,..,.,

어쩄거나 나 파이팅,..,!