ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ReactJS] Lifting State Up
    프로그래밍/웹 2018. 4. 2. 12:35
    Lifting-State-Up.md

    React Lifting State Up: 홈페이지 원문

    Lifting State Up

    다수의 컴포넌트가 하나의 데이터 변경에 대해 다같이 변화할 필요가 있는 경우가 있다. 그런 경우 같이 공유하는 데이터을 state를 저장할 컴포넌트를 가장 최상단 컴포넌트로 지정한뒤, 하위 컴포넌트로 전달하는것이 바람직하다.

    예제를 위해서 간단하게 물의 끓는점을 판별하는 온도 계산기를 작성하고 테스트 해보도록 하자.

    일단은 BoilingVerdict 컴포넌트를 생성하고, celsius props에 온도를 전달 받은 뒤 끓는 점 판별 여부를 출력하도록 한다. 단순하게 끓는 점 판별만을 하므로 함수형 컴포넌트로 작성한다.

    이후 유저로부터 <input> 엘리먼트를 통해 온도를 입력 받고 state.temperature에 저장하는 Calculator 컴포넌트를 구현한다. 내부에서는 BoilingVerdict 컴포넌트를 사용하여 끓는 점 여부를 출력한다.

     
    x
    function BoilingVerdict(props) {
      if (props.celsius >= 100) {
        return <p>The water would boil.</p>;
      }
      return <p>The water would not boil.</p>;
    }
    class Calculator extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.state = {temperature: ''}; // 온도 저장 변수 초기화. 처음 화면에는 아무것도 없도록
      }
      // input 에 사용자가 
      handleChange(e) {
        this.setState({temperature: e.target.value}); // 사용자가 입력한 값을 저장
      }
      render() {
        const temperature = this.state.temperature;
        return (
          <fieldset>
            <legend>Enter temperature in Celsius:</legend>
            {/* input 에 입력될 값을 조절 */}
            <input
              value={temperature}
              onChange={this.handleChange} />
            <BoilingVerdict
              celsius={parseFloat(temperature)} />
          </fieldset>
        );
      }
    }

    https://codepen.io/gaearon/pen/ZXeOBm?editors=0010

     

    Adding a Second Input

    섭씨 온도 말고 화씨 온도도 추가하고 섭씨온도와 화씨 온도 데이터를 항상 똑같게 유지하는 코드를 추가해보자

    일단 온도를 측정하는 Calculator 에서 사용자의 입력을 받는 부분만 따로 떼어낸다.

     
    xxxxxxxxxx
    const scaleNames = {
      c: 'Celsius',
      f: 'Fahrenheit'
    }; // 기준 표기법에 따라서 UI를 달리하기 위한 딕셔너리
    class TemperatureInput extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.state = {temperature: ''};
      }
      handleChange(e) {
        this.setState({temperature: e.target.value});
      }
      render() {
        const temperature = this.state.temperature;
        const scale = this.props.scale; // 상위 컴포넌트에서 props로 scale을 전달
        return (
          <fieldset>
            <legend>Enter temperature in {scaleNames[scale]}:</legend>
            <input value={temperature}
                   onChange={this.handleChange} />
          </fieldset>
        );
      }
    }
    // 간결화된 Calculator 컴포넌트
    class Calculator extends React.Component {
      render() {
        return (
          <div>
            <TemperatureInput scale="c" />
            <TemperatureInput scale="f" />
          </div>
        );
      }
    }

    코드펜에서 확인

    코드에서도 볼 수 있고, 코드펜에서 확인도 할 수 있듯이, 2군데에 온도를 입력할 수 있도록 변경했지만, 아직 두개의 온도가 서로 동기화 되고 있지는 않다. 또한 끓는점 여부를 판별하는 BoilingVerdict 컴포넌트도 사용하지 않고 있다. 사용자 입력 폼을 TemparatureInput 컴포넌트로 분리하면서 온도 정보가 이동되어 Calculator 컴포넌트에서 온도를 확인할 수도 없다.

     

    Writing Conversion Functions

    일단 섭씨 온도와 화씨 온도의 동기화를 위해 온도 변환 함수를 작성해보자.

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

    위의 함수들을 사용해서 온도변환을 수행하기 위한 헬퍼 함수를 작성해보자. 온도 변환 헬퍼 함수는 기준 온도에 따라서 사용하길 원하는 변환 함수를 매개변수로 입력받는다.

     
    x
    function tryConvert(temperature, convert) {
      const input = parseFloat(temperature); // 유저에게 입력받은 것은 문자열이므로 숫자로 변환
      if (Number.isNaN(input)) { // 잘못된 입력일 경우 변환을 시도하지 않음
        return '';
      }
      const output = convert(input); // 입력받은 변환함수를 사용해서 온도 변환
      const rounded = Math.round(output * 1000) / 1000; // 소숫점 3자리까지만 표기
      return rounded.toString(); // 숫자를 문자열로 변환하여 반환
    }

    tryConvert 함수를 사용할때는 tryConvert('10.22', toFahrenheit) 와 같이 온도와 변환함수를 매개변수로 사용하면 된다.

     

    Lifting State Up

    현재까지 작성한 코드를 보면, Calculator 컴포넌트에서 두개의 TemperatureInput을 렌더링 하고, 각각의 TemperatureInput 컴포넌트는 고유의 temperatur 온도 state를 관리하고 있다. 이 경우 두 컴포넌트 간의 온도 정보가 동기화 되지 않는 문제가 있다. 본래의 목적 자체가 섭씨/화씨 온도를 동기화 시켜서 화면에 표시하는 것이므로 이는 수정되어야 하는 문제이다.

    리액트에서 컴포넌트 간에 state를 공유할 필요가 있을 경우에는, 해당 state를 상위 컴포넌트로 이동하고 props로 전달하는 방법을 사용한다. 이런 방식을 간편하게 lifting state up 이라고 부른다. TemperatureInput 컴포넌트에서 사용하는 온도 state를 제거하고 Calculator에서 관리하도록 코드를 수정해보자.

    일단은 TemperaturInput 컴포넌트에서 사용하던 state.temperature를 전부 props.temperature로 바꾸자.

     
    x
    class TemperatureInput extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
      }
      
      // 상위 컴포넌트에서 onTemperatureChange에 전달해주는 콜백함수를 사용한다. 
      handleChange(e) {
        this.props.onTemperatureChange(e.target.value);
      }
      render() {
        const temperature = this.props.temperature; // state 대신 props를 사용한다.
        const scale = this.props.scale;
        return (
          <fieldset>
            <legend>Enter temperature in {scaleNames[scale]}:</legend>
            <input value={temperature}
                   onChange={this.handleChange} />
          </fieldset>
        );
      }
    }

    상위 컴포넌트인 Calculator에서 온도를 관리하고 TemperatureInput 컴포넌트로 전달해줄 것이므로, state 온도 정보들을 전부 props로 변경하였다. props는 read-only 이므로 사용자가 온도를 입력해도 props의 temperature에 온도 정보를 저장할 수 없다. 온도 정보는 실제로 컴포넌트의 state로 관리하는 Calculator에 저장해야 한다. 그러나 컴포넌트 내부의 state를 다른 컴포넌트에서 직접 접근 할 수 없으므로, Calculator 컴포넌트가 TemperatureInput을 렌더링 할때 props로 전달해주는 콜백함수를 사용하도록 한다. 콜백함수는 onTemperatureChange에 전달 된다.

    여기서 props에 전달되는 변수들인 temperature나 onTemperatureChange와 같은 이름은 큰 의미 없이, 코드를 쉽게 구분할 수 있기 위함일 뿐이다. input 엘리먼트에서 설명했던 valueonChange와 같은 이름을 사용하여 전달하는 것도 일반적인 방법이다.

    Calculator 컴포넌트에서 온도 정보를 관리하도록 변경하자. 온도 정보는 섭씨온도 하나로만 관리되는 것이 아니고 화씨온도 정보도 저장하고 있으므로, 이전에 사용하던 state에 온도 단위를 저장하는 state까지 추가하도록 하자.

     
    xxxxxxxxxx
    /* 
      Calculator 컴포넌트에는 아래의 두 형태 중 한가지만 저장된다.
      예시를 위해 두 형태 모두 표기하였다. 
    */
    {
      temparature: '37',
      scale: 'c'
    } // 섭씨 온도 input 폼에 입력했을 때 저장되는 형태
    {
      temparature: '212',
      scale: 'f'
    } // 화씨 온도 input 폼에 입력했을 때 저장되는 형태.

    섭씨 온도와 화씨 온도 각각을 따로 state에 저장해도 되지만, 기존에 만들어 놓은 온도 변환 함수들을 사용하면 다른 온도 단위로 변환할 수 있으므로 하나의 온도 state와 단위를 저장하는 state만 존재하면 충분하다.

    state에 온도 정보와 단위 정보를 저장하고, 온도와 사용자 입력에 따른 UI 변화를 위한 함수를 TemperatureInput 컴포넌트로 전달하는

     
    xxxxxxxxxx
    class Calculator extends React.Component {
      constructor(props) {
        super(props);
        this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
        this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
        this.state = {temperature: '', scale: 'c'}; // 온도 정보와 단위 정보를 저장한다. 
      }
      // 이제 온도 정보는 Calculator 컴포넌트에서 관리한다. 
      // 유저가 어느곳에 입력하는지에 따라서 온도 단위인 c/f도 같이 저장한다. 
      handleCelsiusChange(temperature) {
        this.setState({scale: 'c', temperature});
      }
      handleFahrenheitChange(temperature) {
        this.setState({scale: 'f', temperature});
      }
      render() {
        /*
          이제 Calculator 컴포넌트에서 온도 정보를 관리하므로 
          관련 변수들을 추가하였다. 
          현재 온도 단위에 따라서 섭씨/화씨를 변환하는 코드도 추가하였다. 
        */
        const scale = this.state.scale;
        const temperature = this.state.temperature;
        const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
        const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
        // TemperatureInput 을 렌더링 할때 온도 정보/단위 정보/유저 입력 저장 함수를 같이 전달한다.
        return (
          <div>
            <TemperatureInput
              scale="c"
              temperature={celsius}
              onTemperatureChange={this.handleCelsiusChange} />
            <TemperatureInput
              scale="f"
              temperature={fahrenheit}
              onTemperatureChange={this.handleFahrenheitChange} />
            <BoilingVerdict
              celsius={parseFloat(celsius)} />
          </div>
        );
      }
    }

    코드펜에서 확인

    위처럼 Calculator 컴포넌트까지 수정하고 나면, 유저가 어느 입력 폼에 온도를 입력하든 Calculator 컴포넌트의 state로 온도 정보가 업데이트 된다. 한쪽의 input 폼에 입력되면 다른쪽의 input 폼 내용도 자동으로 변환된다.

     

    Lessons Learned

    상태 정보의 저장이 필요할 경우 컴포넌트의 state에 정보를 추가하고 관리한다. 만일 해당 state를 다른 컴포넌트와 공유할 필요가 있다면, 공통 상위 컴포넌트를 찾아서 그 컴포넌트가 state를 관리하도록 하고 하위 컴포넌트들은 props로 전달받아서 사용하도록 한다. 이러한 작업을 lifting state up 이라고 부른다. 또한 상위 컴포넌트에서 데이터 정보를 하위 컴포넌트로 전달하므로 top-down data flow 라고 할 수도 있다.

    state를 상위 컴포넌트로 이동하고 사용하는 일은 boilerplate 코드를 더 작성하는 작업을 포함하지만, two-way binding 기법을 사용하는 것보다 버그를 찾는데 훨씬 수월하다. state를 변환하는 컴포넌트가 오직 1개밖에 존재 하지 않으므로, 해당 state와 관련된 버그가 있을 경우 1개의 컴포넌트의 setState 관련 구문만 확인하면 되는것이다. 또한 직접적으로 state를 관리하므로 사용자 입력 데이터를 처리하는 추가적인 함수들을 마음껏 작성 할 수 있다.

    섭씨/화씨 온도를 모두 저장하지 않고 온도 단위와 함께 하나의 온도정보만을 저장했던 것처럼, props나 state의 다른 정보로부터 새로운 정보를 생성할 수 있다면 굳이 state에 저장할 필요가 없다. 하나의 state만 관리하고 변환 함수 (converter function)를 통해 다른 데이터를 생성함으로써, 다른 데이터와 정확하게 동기화를 해줄 수 있는 장점이 있다.

    리액트를 이용해 작업을 하다가 UI 상에 문제가 보일 경우, [React Developer Tools]를 사용하면 실시간으로 props와 state 및 UI 변화를 확인할 수 있다. 이를 통해 디버깅을 쉽게 할 수 있다. 아래의 이미지가 디버깅 예제이다.

    React Developer Tools

     

    '프로그래밍 > ' 카테고리의 다른 글

    [ReactJS] Thinking in React  (0) 2018.04.05
    [ReactJS] Composition vs Inheritance  (0) 2018.04.03
    [ReactJS] Forms  (0) 2018.03.30
    [ReactJS] Lists and Keys  (0) 2018.03.29
    [ReactJS] Conditional Rendering  (0) 2018.03.10
Designed by Tistory.