State 끌어올리기

Yeom Jae Seon·2021년 2월 9일
0

React공식문서 공부

목록 보기
9/11
post-thumbnail

도입


React를 사용해서 프로젝트를 진행하다보면 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야할 때가 종종 있다. 이럴때는 글 제목 처럼, 이번 주제처럼 가장 가까운 공통 조상 컴포넌트로 state를 끌어올리는 것이 좋다.

이번에는 공식문서의 예제와 완전 동일한 예시인 주어진 온도에서 물의 끓는 여부를 추정하는 온도 계산기를 만들어 볼것이다.(너무 예시가 좋다생각해서.)
그럼 시작해보자

  • 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야할 필요가 있을 때는 가장 가까운 공통 조상 컴포넌트로 state를 끌어올리는 것이 좋다.

시작


먼저 섭씨온도를 의미하는 celsius라는 prop을 받아서 이온도가 물이 끓기에 충분한지 안한지(어떻게 보면 우리가 만드려고 하는 결론)에 대한 여부를 출력하는 BoilingVerdict컴포넌트를 만들어보자.

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>물 끓을거에요</p>;
  }
  return <p>물 안끓을 거에요</p>;
}

단순히 celsius props을 받아서 조건에 따라 조건부 렌더링을 하는 것 뿐인 함수 컴포넌트이다.

이젠 온도를 입력해 계산을 하는 Calculator컴포넌트를 만들어보자.
이 컴포넌트는 온도를 입력할수있는 <input>과 그 값을 this.state.temperature에 저장한다.
(저번 시간에 배웠던 form은 없지만 제어컴포넌트방식이다.)

또한 this.state.temperatureBoilingVerdict컴포넌트로 props로 전달해 BoilingVerdict컴포넌트를 렌더링한다.

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

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>물 끓을거에요</p>;
  }
  return <p>물 안끓을 거에요</p>;
}

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      temperature: ""
    };
  }
  handleChange = (e) => {
    this.setState({ temperature: e.target.value });
  };
  render() {
    return (
      <fieldset>
        <legend>Enter temperature in Celsius:</legend>
        <input value={this.state.temperature} onChange={this.handleChange} />
        <BoilingVerdict celsius={parseFloat(this.state.temperature)} />
      </fieldset>
    );
  }
}

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

지금까지 만든걸 설명하자면 Calculator컴포넌트에서는 this.state.temperature이 있고 이를 통해서 input을 관리한다.
그리고 자식컴포넌트로는 BoilingVerdict가있고 props로 이 this.state.temperature을 넘겨주고 이 값에따라서 BoilingVerdict컴포넌트는 다른 결과를 렌더링한다.
여기까진 데이터 하나(this.state.temperature)에 따라 BoilingVerdict하나의 컴포넌트에 만 영향을 끼치기 때문에 state를 props로 전달하는 단방향식 데이터흐름으로 별 다른 액션은 필요없어보인다.

  • 하나의 state를 가진컴포넌트와 그 state를 props로 넘기는 자식컴포넌트를 가지는 Calculator컴포넌트를 만듬

두 번째 Input 추가하기


지금까진 섭씨 온도를 입력받아 물이 끓는지 안끓는지만 알수있었지만 이젠 화씨 온도도 입력받을수 있게 변경하고 두 입력간에 동기화 상태를 유지해보자.
단순히 지금상태에서 input을 추가하는게 아닌 추가로 여기서 컴포넌트 추출로 가독성좀 높여보자(컴포넌트 재사용성도 좀높이고)

그렇게되면 섭씨온도를 입력받을수있는 컴포넌트와 화씨온도를 입력받을수 있는 컴포넌트로 추출해 낼수 있겠다.
그리고 각컴포넌트에서 입력을 받기 때문에 추출된 컴포넌트로 state를 옮기면 되겠다.

둘다 온도를 입력받는 컴포넌트이므로 같은 컴포넌트로 구성하고 props를 통해서만 구분하도록 하자
시작!

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

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>물 끓을거에요</p>;
  }
  return <p>물 안끓을 거에요</p>;
}

const scaleNames = {
  c: "Celsius",
  f: "Fahrenheit"
};

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      temperature: ""
    };
  }
  handleChange = (e) => {
    this.setState({ temperature: e.target.value });
  };
  render() {
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[this.props.scale]}:</legend>
        <input value={this.state.temperature} onChange={this.handleChange} />
      </fieldset>
    );
  }
}

class Calculator extends React.Component {
  render() {
    return (
      <>
        <TemperatureInput scale = 'c'/>
        <TemperatureInput scale = 'f'/>
      </>
    );
  }
}

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

이제 입력을 받을수있는 TemperatureInput컴포넌트를 만들었고 Calculator컴포넌트에서 TemperatureInput컴포넌트를 렌더링하며 props로 scale을 넘기며 섭씨온도 입력받는 컴포넌트인지 화씨온더 입력받는 컴포넌트인지만 구분했다.
그렇게되는과정에서 Temperature컴포넌트 내에서 this.state.temperature이 존재하게 되었고 TemperatureInput컴포넌트가 두개 렌더링 되므로 각각의 this.state.temperature이 생기게 되었고 두 컴포넌트 서로간에는 존재 자체도 모르는 독립성때문에 두 입력필드는 동기화 되지 않고있다.
우리는 섭씨온도와 화씨온도가 동기화가 되는걸 원하고 있다.

또한 Calculator컴포넌트에는 state가 없기 때문에 우리의 목표이자 결과인 BoilingVerdict컴포넌트도 보여줄수가 없다.(온도에 따라 물이끓는지 안끓는지 렌더링하는 컴포넌트였다.)
state가 TemperatureInput으로 이동되어 다른 컴포넌트인 Calculator는 state가 존재하는지도 모르기 때문이다.

이때 우리는 state 끌어올리기가 필요하다는 생각이 들것이다.
하나의 데이터의 변화를 여러 컴포넌트에 반영해야할 필요가 있으므로 가장 공통의 조상(부모)컴포넌트로 state를 끌어올리는 처음에 했던 생각, 우리의 목표.

  • 섭씨온도와 화씨온도를 입력받는 컴포넌트로 추출해내었지만 각 컴포넌트간에 입력필드(state)가 동기화도 되지않고 부모 컴포넌트인 Calculator컴포넌트에선 state도 없기 때문에 결과를 알려준 BoilingVerdict컴포넌트도 Calculator컴포넌트내에 존재하지 않고 있다.

변환 함수 작성하기


일단 두 입력필드(섭씨, 화씨)를 동기화 하기위해 섭씨를 화씨로, 화씨를 섭씨로 변환하는 함수를 만들어보자.

function toCelsius(fahrenheit) {
  return ((fahrenheit - 32) * 5) / 9;
}
function toFahrenheit(celsius) {
  return (celsius * 9) / 5 + 32;
}

이젠 이 두함수를 인자로 사용할 함수를 만들어보자.

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return "";
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

toCelsius가 오면 그에 맞게 문자열로 반환하고 toFahrenheit가 인자로오면 그에 맞게 문자열로 반환해서 리턴하는 함수이다.

  • TemperatureInput컴포넌트의 state의 동기화를 위한 함수를 작성함

State끌어올리기


그럼 이제 TemperatureInput컴포넌트들이 각각 가지고있는 state를 가장 가까운 부모 컴포넌트로 끌어올리는 state끌어올리기 작업을 통해서 하나의 데이터변화에 여러 컴포넌트들에 동시에 반영이되는것을 해보자.

(state를 끌어올리는 생각은 React는단방향식 데이터흐름에 의존하기에 당연한 생각이다, state를 끌어올린다음 props로 state를 전달하기 위해.)

TemperatureInput컴포넌트 각각에 존재하던 this.state.temperature state를 Calculator(가장 가까운 부모 컴포넌트)로 끌어올리면 Calculator컴포넌트는 공유될 state를 가지고 있으므로 두 입력필드(현재의 온도를 나타낼)의 진리의 원천이 된다.

이를 통해서, 공유될 state를 부모컴포넌트에서 관리함을 통해서, 두 입력필드간 서로 일관된 값을 가질수 있게된다.(동기화가 가능하게된다.)
TemperatureInput컴포넌트에서 받아서 input에 전달할 value는 동일한 부모컴포넌트인 Calculator가 가지고있는 state에서 props로 전달할 this.state.props인 동일한 값이기 때문이다.

이 과정을 하기위해선
1. TemperatureInput컴포넌트로 this.state.temperature를 props로 전달해야함.
2. TemperatureInput컴포넌트에는 state가 존재하지 않게 되기 때문에 setState불가능 -> 입력필드 업데이트가 TemperatureInput컴포넌트 자체에선 불가능하기 때문에 Calculator컴포넌트에서 setState하는 함수를 props로 받아야한다.

이 두과정을 한 TemperatureInput컴포넌트는

class TemperatureInput extends React.Component {
  handleChange = (e) => {
    this.props.onTemperatureChange(e.target.value);
  };
  render() {
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[this.props.scale]}:</legend>
        <input value={this.props.temperature} onChange={this.handleChange} />
      </fieldset>
    );
  }
}

이런모습을 띄게된다.
보면 TemperatureInput의 state는 없어지고 props로 받고 state가 없으니 setState도 안되므로 (props는 읽기전용이므로 props에 대해서 TemperatureInput컴포넌트 자체에서 어떠한 변화를 주는건 더더욱이 안됨..) props로 받은 onTemperatureChange함수의 인자에 e.target.value를 전달하고있다.

이제는 섭씨와 화씨의 입력을 받는 TemperatureInput컴포넌트는 공통의 부모컴포넌트인 Calculator로부터 동일한 state를 받으므로 동기화가 될것이다.

이제 Calculator컴포넌트를 적절하게 손봐서 하나의 진리의 원천this.state.temperature를 가지고있는 지역 state를 props로 넘겨보자.
그 동시에 이제 Calculator컴포넌트에서 자체 state를 가지고있으므로 우리가 알고싶은 물이 끓는지 안끓는지에 대한 결과를 알려주는 BoilingVerdict컴포넌트도 Calculator에서 렌더링이 가능하겠다..

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

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>물 끓을거에요</p>;
  }
  return <p>물 안끓을 거에요</p>;
}

function toCelsius(fahrenheit) {
  return ((fahrenheit - 32) * 5) / 9;
}
function toFahrenheit(celsius) {
  return (celsius * 9) / 5 + 32;
}

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return "";
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

class TemperatureInput extends React.Component {
  handleChange = (e) => {
    this.props.onTemperatureChange(e.target.value);
  };
  render() {
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[this.props.scale]}:</legend>
        <input value={this.props.temperature} onChange={this.handleChange} />
      </fieldset>
    );
  }
}

const scaleNames = {
  c: "Celsius",
  f: "Fahrenheit"
};

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      temperature: "",
      scale: "c"
    };
  }
  handleCelsiusChange = (temperature) => {
    this.setState({ scale: "c", temperature });
  };

  handleFahrenheitChange = (temperature) => {
    this.setState({ scale: "f", temperature });
  };

  render() {
    const celsius =
      this.state.scale === "f"
        ? tryConvert(this.state.temperature, toCelsius)
        : this.state.temperature;
    const fahrenheit =
      this.state.scale === "c"
        ? tryConvert(this.state.temperature, toFahrenheit)
        : this.state.temperature;
    return (
      <>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange}
        />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange}
        />
        <BoilingVerdict celsius={parseFloat(celsius)} />
      </>
    );
  }
}

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

이제 어떠한 입력필드를 수정하던간에 하나의 진리의 원천인 Calculator의 state가 업데이트 되고 이 하나의 state의 변경에 따라서 여러 컴포넌트들이 동시에 반영이 될수 있게 되었다.
state끌어올리기를 사용함으로써 하나의 데이터 변화를 여러 컴포넌트들에 반영을 성공했다.

이젠 좀더 구체적으로 과정을 얘기해보자
먼저 입력필드에 사용자가 입력할 때를 기점으로 과정을 얘기해보겠다.

  • React DOM은 input의 onChange가 발생할때마다 TemperatureInput컴포넌트의 handleChange메소드가 호출된다.
  • handleChange메소드는 this.props.onTemperatureChange()를 호출하고 인자로 e.target.value도 전달한다.
  • this.props.onTemperatureChange()는 부모 컴포넌트인 Calculator에서 제공한 것이므로 섭씨입력인지 화씨입력인지에 따라 handleCelsiusChangehandleFahrenheit메소드가 호출되고 각 메소드에는 setState가 있으므로 state가 업데이트 되고 변경된 state에 맞춰 리렌더링이 일어난다.
  • Calculator에서 리렌더링이 일어나면 Calculator렌더함수에 정의된대로 TemperatureInput컴포넌트들과 BoilingVerdict컴포넌트가 호출되고 변경된 state에 맞게 props로 다시 전달이 된다.
  • 각각 호출된 TemperatureInput컴포넌트 두개와 BoilingVerdict컴포넌트는 변경된 props에 맞게 render()함수가 다시 호출되어 자식컴포넌트인 이 세개의컴포넌트에서도 리렌더링이 일어난다.
  • 변경된 값에 맞춰서 React DOM은 변경된 React 엘리먼트를 보고 새롭게 DOM을 구성한다.(효율적으로 구성.)

-> 이 과정으로 하나의 데이터변경에 따른 여러 컴포넌트(섭씨TemperatureInput, 화씨TemperatureInput, BoilingVerdict)가 변경이 반영이된다.

  • state끌어올리기 방식을 직접 사용해봄으로써 state를 끌어올리지 않으면 당연하지만 하나의 데이터에 따른 여러 컴포넌트가 동기화가 되지않는걸 경험했고 state를 끌어올려 진리의 원천인 state를 만들면 해당 state가 변경됨에 따라 여러 컴포넌트들이 동시에 반영이되는걸 경험했다.
  • 이는 React가 단방향식 데이터흐름이기 때문이다.
  • 즉, state 끌어올리기 방식은 부모 컴포넌트에서 state를 가지고있게 하여 해당 state가 필요로하는 자식컴포넌트에 props로 전달하는 하향식, 단방향식 데이터흐름을 구현한 것이라 할수 있겠다.

느낀점


React 어플리케이션에서 변경이 일어나는 데이터에 대해선 진리의 원천을 하나만 두어야한다. 그래야 여러 자식 컴포넌트들이 하나의 진리의 원천에 대해서 props를 통해 동기화(진리의 원천 state변경에 따른 반영)가 될 수 있기 때문이다.

직접 만들면서 경험한거지만 우리는 컴포넌트를 만들때 state의 설정을 일단은 해당 컴포넌트가 필요로 할거같은 컴포넌트에 추가를 했다.

예를들면 TemperatureInput컴포넌트에 this.state.temperature를 설정한 것처럼.
여기서 추가로 다른 컴포넌트들 역시 이 state가 필요하면 그때서야 가장 가까운 부모컴포넌트로 state를 끌어올리면된다.
(우리가 TemperatureInput -> Calculator로 state를 끌어올린것처럼.)

React공식문서에서는 여러 컴포넌트들간의 state를 동기화하려고 애써 고생하지말고 state를 끌어올림으로써 단방향식 데이터흐름을 이용하는 걸 추천한다.
이유는 state를 끌어올려 단방향식 데이터흐름을 이용하는 것은 버그를 찾기도 쉽고 컴포넌트간의 격리(독립성)도 쉽게 만든다는 장점이 있기 때문이다.
(state는 컴포넌트 내에 존재하기 때문에 버그가 존재할수 있는 범위가 줄어들기 때문.)

추가로 알아야할건 props또는 state로부터 계산될수 있다면 그 값은 state로 두지 말라는 것이다.
예를들면 우리는 this.state.temperature를 사용해서 render()내부에서 celsiusfahrenheit를 계산했다.

this.state = {celsius : ~, fahrenheit : ~, scale: ~}

이런식으로 state를 만들지말고 주어진 state로 render()함수 내에서 계산하라는 뜻이다.

이는 해당 컴포넌트를 보다더 재사용높게 만들것이다.

  • state 끌어올림으로 하나의 데이터의 변경에 여러 컴포넌트들이 동시에 반영이 되는 경험을 했다.
  • 이는 단방향식 데이터흐름방식을 이용한 것이다.
  • 단방향식 데이터흐름을 통해 state를 구성하는 것이 좋은 이유는 버그가 줄어들기 때문이다, 즉 보다 더 어플리케이션이 예측가능하게 된다는 것이다.

위 내용은 공식문서를 보고 공부한 내용입니다.
https://ko.reactjs.org/docs/lifting-state-up.html

0개의 댓글