본문 바로가기
개발

[ReactJS] State 끌어 올리기(Lifting State Up)

by 마스터누누 2017. 6. 13.
728x90
반응형

State 끌어 올리기(Lifting State Up)



1
2
3
4
5
6
function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}
cs


종종 여러 컴포넌트가 동일한 변경 데이터를 반영해야한다.

이 때는 가장 가까운 상위 컴포넌트가 state값을 관리하는 것이 좋다.


이번 포스팅에서는 주어진 온도에서 물이 끓는지 여부를 계산하는 온도 계산기를 작성한다

코딩은 위의 BoilingVerdict라는 컴포넌트 부터 시작한다.

이 컴포넌트는 섭씨 온도를 받아서 물을 끓일만큼 충분한지 출력한다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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>
    );
  }
}
cs


다음으로 Calculator라는 컴포넌트를 만든다.

온도를 입력할 수 있는 input을 랜더링하고 이 값을 this.state.temperature에 유지한다.

또한 현재 입력값에 대해 BoilingVerdict를 랜더링한다.




두번째 입력 추가


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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>
    );
  }
}
cs


온도 계산기의 새로운 요구사항은 섭씨 뿐만아니라 화씨 입력도 제공하고 동기화 상태를 유지한다는 것이다.

Calculator에서 Temperature 컴포넌트를 추출해서 이를 구현할 수 있다.

"C" 또는 "F" 중에 하나가 될 새로운 Props를 추가하자.




1
2
3
4
5
6
7
8
9
10
class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}
cs


과정이 완료되었으면 Calculator 컴포넌트에 TemperatureInput 2개를 추가한다.

이것은 각각 섭씨와 화씨에 대한 입력 폼이다.

현재는 두 입력값이 동기화 되지 않으므로 요구사항에서 어긋난다.


또한 Calculator는 BoilingVerdict를 표시할 수 없고 현재 온도를 알 수 없다

왜냐하면 이 값들은 TemperatureInput 안에 숨겨져 있기 때문이다.





변환 함수 구현


1
2
3
4
5
6
7
function toCelsius(fahrenheit) {
  return (fahrenheit - 32* 5 / 9;
}
 
function toFahrenheit(celsius) {
  return (celsius * 9 / 5+ 32;
}
cs


동기화를 위해서 첫번째로, 섭씨에서 화씨로 변할수 있는 두가지 함수를 작성한다.

이 두 함수는 숫자를 변환한다.





1
2
3
4
5
6
7
8
9
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();
}
cs


다음으로, 문자열 형식인 온도와 방금 작성한 변환 함수를 인자로 받아 문자열을 리턴하는 또 다른 함수를 만든다.

다른 입력을 기반으로 하나의 입력값을 계산하는데 이 값을 사용한다.

이 함수는 잘못된 온도가 들어오면 빈 문자열을 반환하고 출력은 세번째 소수점 이하로 반올림 한다.

예를 들어, tryConvert('abc', toCelsius)는 빈 문자열을 반환하고 

tryConvert('10.22', Fahrenheit)는 '50 .396'을 반환한다.





State 끌어 올리기


1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
 
cs


현재 두 TemperatureInput 컴포넌트는 로컬 상태에서 독립적으로 값을 유지한다.

그러나 이 두 값이 동기화 되는것을 구현해야한다.

화씨를 입력하면 섭씨가 동기화 되어야 하며 반대의 경우에도 마찬가지이다.


리액트에서는 state를 컨트롤할 때 state 공유를 필요로하는 컴포넌트들의 가장 가까운 조상에게 위임한다.

이것을 "끌어올리기(Lifting Up)"이라고 한다.

따라서, TemperatureInput에서 로컬 상태를 제거하고 대신 Calculator로 옮긴다.


Calculator가 공유 state를 가지면 두 입력의 현재 온도에 대한 "source of truth"가 된다.

이는 서로에게 일관성 있는 값을 갖도록 지시할 수 있다.

다시 말해, 두 TemperatureInput 컴포넌트의 props가 동일한 상위 Calculator 컴포넌트에서 나오기 때문에 

두 입력이 항상 동기화 된다.





1
2
3
4
  render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;
  }    
cs


이것들이 어떻게 단계적으로 동작하는지 살펴보자 

먼저, TemperatureInput의 this.state.temperature를 this.props.temperature로 바꾼다.





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


props는 읽기전용 이다.

온도가 로컬 state에 있을 때 TemperatureInput은 this.setState()를 호출하여 변경할 수 있다.

그러나 현재는 값이 상위 컴포넌트에서부터 전달되므로 TemperatureInput에서 변경이 불가능하다.

 

일반적으로 리액트에서는 이와 같은 상황을 컴포넌트를 "제어"함으로써 해결된다.

DOM에서 <input>값이 onChange와 value라는 props를 모두 받아들이듯이

사용자가 만든 TemperatureInput도 temperature와 onTemperatureChange를 모두 받아들일 수 있다.


위의 코드를 추가하면 TemperatureInput이 온도를 변경하려고 할 때

this.props.onTemperatureChange를 호출한다.

온도 변경 핸들러가 호출되면 상위 컴포넌트의 state값이 변경되고 리액트는 이를 감지하여 새로 랜더링을 한다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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>
    );
  }
}
cs


Calculator의 변경사항을 살펴보기 전에 TemperatureInput 컴포넌트의 변경에 대해 요약해보자

우선 TemperatureInput안에 있던 로컬 state를 제거하고 상위 컴포넌트로 부터 state값을 받는다.

때문에, 기존에 this.state.temperature값은 this.props.temperature으로 변경된다.


또한 온도값 변경 핸들러도 같이 받아오기 때문에 this.setState()를 사용하는 대신

Calculator에서 받아온 this.props.onTemperatureChange()를 호출한다.





1
2
3
4
5
6
7
8
9
{
  temperature: '37',
  scale: 'c'
}
 
{
  temperature: '212',
  scale: 'f'
}
cs


이제 Calculator 컴포넌트를 살펴보자 

현재 입력의 온도와 스케일을 로컬 state로 저장한다.

이것은 우리가 입력값으로 부터 "들어 올린 상태이며" 두 컴포넌트에 대한 "source of truth"로 작용할 것이다.

이렇게 두 컴포넌트를 랜더링 하기 위해 알아야 할 모든 데이터를 최소한으로 나타낸다.


예를 들어 Celsius에 37을 입력하면 Calculator의 state는 위의 첫번째 코드와 같다.

Fahrenheit에 212를 입력하면 Calculator의 state는 위의 두번째 코드와 같다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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>
    );
  }
}
cs


초기에 우리는 두 값을 따로 입력받아 저장하는 것을 고려했지만, 

차례차례 검증을 통해 이것이 불필요한 일임을 알게되었다.

상위 컴포넌트에서 가장 최근에 변경된 입력값과 그것이 나타내는 스케일을 저장하는 것으로 충분하다

이와 같은 방식으로 저장한다면 온도와 스케일 만으로 다른 입력값을 추론할 수 있다.

그리고 입력값은 동일한 상태에서 계산되기 때문에 동기화를 유지한다.


이제 입력값을 편집해도 Calculator의 this.state.temperature 및 this.state.scale이 업데이트 된다.

입력 중 하나가 그대로 값을 가져오므로 모든 사용자 입력이 보존되고 다른 입력값은 이를 기반으로 다시 계산된다.



반응형

'개발' 카테고리의 다른 글

[Django] lotto - 장고와 MTV  (0) 2017.06.28
[Django] 개발 환경 세팅  (0) 2017.06.21
[ReactJS] 입력폼(Forms)  (0) 2017.06.13
[ReactJS] 리스트와 키(Lists and Keys)  (0) 2017.06.13
[ReactJS] 조건부 랜더링(Conditional Rendering)  (0) 2017.06.13

댓글