https://reactjs.org/docs/lifting-state-up.html - 번역 글

종종 여러 컴포넌트에 동일한 변경 데이터 변경을 반영하고 싶은 필요가 있습니다. 이럴 때 공통 조상에 state를 끌어올리는 걸 권장합니다. 어떻게 하는 지 살펴봅시다.

이 섹션에서는 주어진 온도에서 물의 끊음 여부를 계산하는 온도 계산기를 작성합니다.

먼저 BoilingVerdict 컴포넌트입니다. 이 컴포넌트는 prop으로 celsius온도를 받고, 물이 충분히 끓었는지 표시 합니다.

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

그리고 Calculator 컴포넌트를 만듭니다. 이 컴포넌트는 온도를 입력받을 <input> 을 렌더링하고, 그 값을 this.state.temperature 로 넣습니다.

input의 입력값으로 BoilingVerdict를 랜더링 합니다.

class Calculator 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;
    return (
      <fieldset>
        <legend>Enter temperature in Celsius:</legend>
        <input
          value={temperature}
          onChange={this.handleChange} />

        <BoilingVerdict
          celsius={parseFloat(temperature)} />

      </fieldset>
    );
  }
}

CodePen


Adding a Second Input(두번째 Input 추가하기)

요구사항을 추가하여 섭씨 입력과 화씨 입력을 제공하고 둘의 상태를 동기화 합니다.

Calculator 컴포넌트에 TemperatureInput컴포넌트를 추출 합니다. cf값을 넣을 수 있는 scale prop을 추가합니다.

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

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;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

이제 Calculator 컴포넌트에 두개의 온도 입력 컴포넌트 TemperatureInput를 추가 하여 랜더링 하도록 합니다.

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

CodePen

두 개의 온도 입력 컴포넌트 TemperatureInput를 가지고 있지만, 구 중 하나에서 온도를 입력하면 다른 온도 입력 컴포넌트는 업데이트되지 않습니다.
이것은 최초 요구 사항인 값의 동기화 조건에 충족되지 않습니다.

또한 Calculator에서 BoilingVerdict도 표시할 수 없습니다. Calculator는 현재 온도가 TemperatureInput안에 존재 하기 때문에 현재 온도를 알 수 없습니다.


Writing Conversion Functions(변환 함수 작성)

섭씨와 화씨를 변환하는 두 가지 함수를 작성합니다.

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

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

두 함수는 숫자를 변환합니다. 문자열 temperature과 변환 함수를 인자로 받아 문자열을 반환하는 또 다른 함수를 작성할 것입니다. 다른 input기반으로 한 input 값을 계산하는데 사용합니다.

아래 함수는 유효하지 않은 temperature 에 빈 문자열을 반환하고, 출력을 세번째 소수점 이하부터 반올림합니다.

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

예를 들어, tryConvert('abc', toCelsius) 는 빈 문자열을 반환하고, tryConvert('10.22', toFahrenheit)50.396 을 반환합니다.


Lifting State Up

TemperatureInput 컴포넌트는 모두 각 컴포넌트의 state에서 값을 독립적으로 유지합니다.

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;
    // ...  

우리는 두 입력이 서로 동기화되기를 바랍니다. Celsius 입력을 업데이트 할 때, 화씨 입력은 변환 된 온도를 반영해야하며 반대의 경우도 마찬가지입니다.

React에서 공유 상태는 필요로하는 컴포넌트의 가장 가까운 공통 조상으로 이동하여 처리합니다. 이것을 lifting state up라고합니다. TemperatureInput 컴포넌트에서 로컬 state를 삭제하고 Calculator로 옮깁니다.

만약 Calculator 가 공유되는 state를 가지면, 이는 두 개의 input에서 사용할 수 있는 현재 온도에 대한 신뢰 가능한 소스가 됩니다. 이를 통해 서로에게 일관된 값을 가질 수 있도록 지시할 수 있습니다. 양쪽 TemperatureInput 컴포넌트의 props가 같은 부모 Calculator 컴포넌트에서 오므로, 두 input은 항상 동기화 상태입니다.

이제 단계별로 어떻게 동작하는 지 살펴봅시다.

먼저 TemperatureInput 컴포넌트의 this.state.temperaturethis.props.temperature 로 변경합니다. 추후에 Calculator에서 props를 전달해야할 필요가 있지만 지금은 this.props.temperature 값이 존재한다고 가정해봅시다.

render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;
    // ...

props는 읽기 전용 입니다. temperature가 로컬 state 일 때는 this.setState() 를 호출해 변경하면 되었습니다. 하지만 이제는 temperature가 부모로부터 props으로 전달받기 때문에, TemperatureInput 이 값을 변경 할 수 없습니다.

React에서는 이 문제를 해결하기 위해 보통 “제어되는” 컴포넌트를 만듭니다. DOM <input>valueonChange prop을 받는 것처럼, TemperatureInput 컴포넌트도 부모 Calculator 컴포넌트로부터 temperatureonTemperatureChange prop을 받게 만들 수 있습니다.

이제 TemperatureInput 가 그 온도를 업데이트하고 싶을 때 this.props.onTemperatureChange를 호출합니다.

handleChange(e) {
    // Before: this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
    // ...

노트
사용자 지정 컴포넌트의 props 이름이 temperature onTemperatureChange인것은 특별한 의미가 없습니다.
이 속성은 valueonchange같은 이름으로 지정할 수도 있습니다.

onTemperatureChange prop은 부모 Calculator 컴포넌트에서 temperature prop과 함께 전달 됩니다. 이 함수는 자체 로컬 state를 수정하여 변경사항을 제어하므로 두 input을 새 값으로 새로 렌더링합니다. 새로운 Calculator class는 곧바로 살펴봅시다.

Calculator 변경을 하기전에, TemperatureInput 컴포넌트의 변경사항을 다시한번 살펴봅시다. 로컬 state를 컴포넌트에서 제거하고 this.state.temperature를 읽어오는 대신 this.props.temperature를 읽어옵니다. 변경사항이 생겼을 때 this.setState() 를 호출하는 대신 Calculator 에서 전달한 this.props.onTemperatureChange() 를 호출합니다.

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }

다시 Calculator 컴포넌트로 돌아와서.

현재 inputtemperaturescale 을 로컬 state에 저장합니다. 이 stateinput으로부터 “끌어올려지며”, 두개의 값 모두를 “신뢰 가능한 소스”로 제공합니다. 두 input을 렌더링하기 위해 알아야하는 모든 데이터를 최소한으로 표현한 것입니다.

예를 들어, 섭씨 input37을 입력하면, Calculator 컴포넌트의 상태는 아래와 같습니다.

{
  temperature: '37',
  scale: 'c'
}

그리고 화씨 input을 212로 수정하면, Calculator 컴포넌트의 상태는 아래와 같습니다.

{
  temperature: '212',
  scale: 'f'
}

양쪽 값을 모두 저장할 수도 있지만 이는 불필요합니다. 가장 최근에 변경된 input값과 그것이 나타내는 scale을 저장하는 것만으로 충분합니다. 현재의 temperaturescale 만으로도 다른 input 값을 추론할 수 있습니다.

input은 그 값을 같은 state에서 계산해오기 때문에 동기화 상태를 유지합니다.

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'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }

  render() {
    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;

    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />

        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />

        <BoilingVerdict
          celsius={parseFloat(celsius)} />

      </div>
    );
  }
}

CodePen

이제 어떤 input을 편집하던지 Calculator 안의 this.state.temperaturethis.state.scale이 업데이트됩니다. input 중 하나가 값을 그대로 가져오므로 모든 사용자 입력이 보존되고 다른 input값은 그 값을 기반으로 다시 계산합니다.

input을 수정할 때 무슨 일이 일어나는 지 다시 살펴봅시다.

  • React는 DOM <input> 이 바뀔 때마다 onChange 로 정의된 함수를 호출합니다. 이 케이스에서는 TemperatureInput 컴포넌트의 handleChange 메서드가 이 역할을 수행합니다.
  • TemperatureInput 컴포넌트 안의 handleChange 메서드는 새로운 값으로 this.props.onTemperatureChange()를 호출합니다. onTemperatureChange를 포함한 props는 부모 컴포넌트인 Calculator에서 제공합니다.
  • 이전에 렌더링되었을 때, Calculator는 섭씨 TemperatureInputonTemperatureChangeCalculatorhandleCelsiusChange이고, 화씨 TemperatureInputonTemperatureChange 메서드는 CalculatorhandleFahrenheitChange 메서드로 정의합니다. 따라서 수정한 입력에 따라 두 Calculator 메서드 중 하나가 호출됩니다.
  • 이 메서드들에서 Calculator 컴포넌트는 React에게 새로운 입력 값과 막 수정된 input의 현재 scalethis.setState()를 호출해 스스로를 다시 렌더링하도록 요청합니다.
  • React는 Calculator 컴포넌트의 render 메서드를 호출하여 UI가 어떻게 보여야하는 지 알아냅니다. 두 입력 값은 현재 온도와 활성 scale에 따라 다시 계산됩니다. 온도 변환은 여기에서 수행합니다.
  • React는 Calculator 에서 계산한 새 props로 개별 TemperatureInputrender메서드를 호출합니다. 이를 통해 UI가 어떻게 보여야하는 지 알아냅니다.
  • React DOM은 이상적인 입력 값과 매치하는 DOM을 업데이트합니다. 방금 수정한 input은 현재 값을 받고, 다른 input은 변환 후 온도를 업데이트합니다.

모든 업데이트는 같은 단계를 통하기 때문에 input이 동기화 상태를 유지합니다.


Lessons Learned(배운내용)

React 애플리케이션에서 변경되는 모든 데이터에 대한 단일 "진실"이 있어야합니다. 일반적으로 state는 렌더링을 위해 필요한 컴포넌트에 먼저 추가 합니다. 그런 다음 다른 컴포넌트에서도 필요 하다면 가장 가까운 공통 조상까지 들어 올릴 수 있습니다. 다른 구성 요소간에 상태를 동기화하려는 대신 top-down data flow(하향식 데이터 흐름)에 의존해야합니다 .

state를 올리는 것은 two-way(양방향) 바인딩 접근방식보다 더 많은 boilerplate코드가 포함되지만 그 이점으로 디버깅이 쉬워집니다. 특정 컴포넌트에 모든 state가 “존재”하고 해당 컴포넌트만 변경될 수 있기 때문에, 버그가 발생하는 상황이 크게 줄어듭니다. 또한 유저 입력을 거부하거나 변형하는 커스텀 로직을 구현할 수도 있습니다.

propsstate에서 파생될 수 있는 게 있다면, 그건 state여서는 안됩니다. 예를 들어, celsiusValuefahrenheitValue 를 둘 다 저장하는 대신 마지막으로 수정된 temperature 와 그 scale 만 보관합니다. 다른 input 값은 render() 메서드에서 그 값들을 가지고 계산할 수 있습니다. 이를 통해 유저 입력에서 정밀도를 잃지 않고 다른 필드에서 반올림을 하거나 지울 수 있습니다.

UI에서 문제가 발생하면 React Developer Tools를 사용해서 state 업데이트를 담당하는 컴포넌트를 찾을 때까지 prop을 검사하고 트리를 위로 옮길 수 있습니다. 이제 버그를 소스에서 추적할 수 있습니다.

develop tool