React) 8. State 끌어올리기

divedeepp·2022년 2월 4일
0

React

목록 보기
8/11

종종 동일한 데이터에 대한 변경사항을 여러 component에 반영해야 할 때가 있다. 이럴때는 가장 가까운 공통 부모의 state를 끌어올리는 것이 좋다.

이번 글에서는 주어진 온도에서 물의 끓는 여부를 추정하는 온도 계산기를 만들어보자.


하나의 단위만 다루는 온도 계산기

우선, BoilingVerdict component를 만들자. 이 component는 celsius props를 받아서 이 온도가 물이 끓기에 충분한지 여부를 출력한다.

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

그 다음은 Calculator component이다. 이 component는 온도를 입력할 수 있는 <input> element를 렌더링하고, 그 값을 this.state.temperature에 저장한다. 또한, 현재 입력값에 대한 BoilingVerdict component를 렌더링한다.

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>
        <legent>Enter temperature in Celsius:</legend>
        <input
          value={temperature} 
          onChange={this.handleChange} />
        <BoilingVerdic
          celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

온도 단위 추가하기

새 요구사항으로써 섭씨 입력 필드뿐만 아니라 화씨 입력 필드를 추가하고 두 필드 간에 동기화 상태를 유지해보자.

Calculator에서 TemperatureInput component를 빼내는 작업부터 시작한다. 또한 "c" 또는 "f"의 값을 가질 수 있는 scale props를 추가한다.

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 component가 분리된 두 개의 온도 입력 필드를 렌더링하도록 변경한다.

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

이제 두 개의 입력 필드를 갖게 되었다. 하지만 둘 중 하나에 온도를 입력하더라도 다른 하나는 갱신되지 않는 문제가 있다. 이것은 두 입력 필드간에 동기화 상태를 유지하고자 했던 원래 요구사항과 맞지 않다.

또한, Calculator component에서 BoilingVerdict도 역시 보여줄 수 없는 상황이다. 현재 입력된 온도 정보가 TemperatureInput 안에 숨겨져 있으므로, Calculator component는 그 값을 알 수 없기 때문이다.


두 단위에 대해 동기화

이를 해결하기 위해 먼저, 섭씨를 화씨로, 또는 그 반대로 변환해주는 함수를 작성하자.

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

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

이 두 함수는 숫자를 변환한다. 이제 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();
}

현재는 두 TemperatureInput component가 각각의 입력값을 각자의 state에 독립적으로 저장하고 있다.

두 입력값을 서로 동기화된 상태를 만들기 위해서, React에서는 state를 공유하는 일은 그 값을 필요로 하는 component 간의 가장 가까운 공통 부모로 state를 끌어올림으로써 수행할 수 있다.

이제 TemperatureInput component가 개별적으로 가지고 있던 local state를 지우는 대신 Calculator component로 그 값을 옮겨놓자.

Calculator component가 공유되는 state를 소유하고 있으면, 이를 통해 두 입력 필드가 서로 간에 일관된 값을 유지하도록 만들 수 있다. 두 TemperatureInput component의 props가 같은 부모인 Calculator component로부터 전달되기 때문에, 두 입력 필드는 항상 동기화된 상태를 유지할 수 있다.

어떻게 동작하는지 살펴보자.

우선, TemperatureInput component에서 this.state.temperature를 this.props.temperature로 바꾼다.

props는 읽기 전용이기 때문에, TemperatureInput은 props 값을 제어할 능력이 없다. React에서는 보통 이 문제를 해결하기 위해 controlled component방식을 사용한다.

DOM <input>이 value와 onChange props를 받는것처럼, TemperatureInput component 역시 temperature와 onTemperatureChange props를 자신의 부모인 Calculator component로부터 건네받을 수 있다.

이제 TemperatureInput에서 온도를 갱신하고 싶으면 this.props.onTemperatureChange를 호출하면 된다.

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

Calculator component의 변경사항을 들여다보기 전에 TemperatureInput component에 대한 변경사항부터 요약해보자.

  1. 이 component의 local state를 제거했으며, this.state.temperature 대신에 this.props.temperature를 읽어오도록 변경했다.
  2. state를 변경하고 싶을 경우, this.setState() 대신에 Calculator component로부터 건네받은 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 component를 살펴보자.

temperature와 scale의 현재 입력값을 이 component의 local state에 저장한다. 이것은 우리가 입력 필드로부터 끌어올린 state이다.

두 입력 필드의 값이 동일한 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>
    );
  }
}

이제 어떤 입력 필드를 수정하든 간에 Calculator component의 this.state.temperature와 this.state.scale이 갱신된다. 입력 필드 중 하나는 있는 그대로의 값을 받으므로 사용자가 입력한 값이 보존되고, 다른 입력 필드의 값은 항상 다른 하나에 기반해 재계산된다.

profile
더깊이

0개의 댓글