[React 공식 Doc 가이드 #11] Lifting State Up React

- 이 글은 React 공식 홈페이지 Docs v16.8.3 (https://reactjs.org/docs) 에 기반하여 작성한 글입니다.
- Lifting State Up
때로는 같은 data에 대하여 여러개의 Component가 영향을 받는 경우가 있다. 이런 경우에 해당 Component 들의 최소 공통 조상 레벨로 state 를 올려서 공유하면 유용하다.
이번 Section 에서는 주어진 온도에서 물이 끓을 수 있느냐를 판단하는 온도계산기를 만들면서 State를 올린다는게 무슨말인지 이해해보자.
처음에 BoilingVerdict 라는 Component 로 시작을 할 것이다. 섭씨 온도를 prop 으로 받아서 물이 끓을 수 있는지 여부를 출력하는 기능을 가지고 있다.
 1
2
3
4
5
6
7
8
9
10
11
12
function BoilingVerdict(props){
if(props.celsius >= 100){
return (<p> The water would boil.</p>);
}
return (<p>The water would not boil.</p>);
}


ReactDOM.render(
<BoilingVerdict celsius={111} />,
document.getElementById('root')
);
celsius attribute 에 따라서 조건에 따른 p 태그를 출력한다. 
<input>에 온도를 입력하면 this.state.temperature에 value 를 가지고 있는 Calculator Component 만들어보자. 온도를 입력해서 물이 끓느냐를 출력하는 부분은 앞에서 만든 BoilingVerdict Component 를 재사용 할 것이다.
 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
class Calculator extends React.Component{

constructor(props) {
super(props);
this.state = {
temperature: ''
};

this.handleInput = this.handleInput.bind(this);
}

handleInput(event) {
this.setState({temperature: event.target.value});
}

render() {

const temperature = this.state.temperature;

return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input name="temperature"
onChange={this.handleInput} />
<BoilingVerdict celsius={temperature} />
</fieldset>
);

}

}


ReactDOM.render(
<Calculator />,
document.getElementById('root')
);

- Adding a Second Input
새로운 요구사항을 추가해보자. 섭씨 input 에 이어서 화씨 input 을 추가하고 두 값을 동기화 시킨다.
앞에서 만든 Calculator Component 를 살펴보자. 출력부는 변하지 않겠지만 섭씨와 화씨를 입력해야 하므로, 입력부분을 새로운 Component 로 추출하고 이를 TemperatureInput 이라고 정의하자.
섭씨 화씨 구분을 위해 prop에 scale 변수로 "c", "f" 로 넘겨줄 것이다.
 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
const scaleNames = {
c: 'celsius',
f: 'Fahrenheit'
};

class TemperatureInput extends React.Component{
constructor(props){
super(props);
this.state = {
temperature: ''
};

this.handleInput = this.handleInput.bind(this);
}

handleInput(event) {
this.setState({temperature: event.target.value});
}

render() {
const temperature = this.state.temperature;
const scale = this.props.scale;

return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input name="temperature"
value={temperature}
onChange={this.handleInput} />
</fieldset>
);
}
}

이제 Caculator 는 2개의 input 을 rendering 하게 되었다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Calculator extends React.Component{

constructor(props) {
super(props);
}

render() {

return (
<div>
<TemperatureInput scale='c' />
<TemperatureInput scale='f' />
</div>
);

}
}

그런데 문제가 있다. 결과를 보면 알겠지만 섭씨를 바꿀때 화씨가 동기화되고 그 역도 되어야 하는데 그렇지 않다. 그리고 BolingVerdict component 로 출력을 하지 않는다. 온도값에 따라 출력을 해야 하지만 일단 온도값을 어떻게 참조 해야 할지 정하지 못해서 출력부분을 임시로 제거하였다.
- Writing Conversion Functions
우선 화씨 -> 섭씨, 그 반대를 계산하는 함수를 작성해야 한다.
1
2
3
4
5
6
7
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
위의 두 함수는 숫자로 변환한다. 따라서 1번째 인자인 온도를 string 형태도 받고, 2번째 인자로 화씨, 섭씨 변환함수를 받아서 string 으로 return 하는 함수를 작성해보자.
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();
}

해당함수는 유효하지 않은 온도에 대해서 빈 문자열을 return 하고, 유효한 숫자값에 대해서는 소수점 아래 3자리 까지 되돌려줄 것이다.
ex) tryConvert('abc', toCelsius) -> "", tryConvert('10.22', toFahrenheit) -> '50.396'
- Lifting State Up
현재 2개의 TemperatureInput 은 각자 state 를 따로 가지고 있지만 섭씨 화씨 온도를 동기화 해야 한다.
React 에서 state 를 공유하는 법은 가장 가까운 공통 조상 레벨의 Component 로 공유할 값을 올리면 된다. 이를 "lifting state up" 이라고 부른다. 각자 가지고 있는 온도값을 제거하고 TemperatureInput 의 온도를 Calculator 로 옮겨보자.
Calculator 가 공유 state 를 소유하게 되면 두개의 input 에 대해 "source of truth"(controlled component 참조)의 개념이 된다. 그렇게 되면 2개의 Component 의 value 는 일관성을 가지게 된다. 두 개의 TemperatureInput Component 들은 부모의 Calculator component 로 부터 prop 을 통해 참조하기 때문에 2개의 input 은 언제나 동기화 된다.
어떻게 가능한지 천천히 살펴보자.
우선 Temperature component 안에서 this.state.temperature 부분을 this.props.temperature 로 변경한다. 나중에는 Calculator 에서 해당값을 넘겨주게 바꿔야 할 것이다.
1
2
3
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
알다시피 props 는 read-only 이다. temperature 를 따로따로 가지고 있었을 때는 TemperatureInput 에서는 단지 this.setState() 호출만 가능했었다.
그러나 지금은 temperature 가 부모로 부터 오는 prop 으로 변경되었기 때문에 TemperatureInput 은 해당값을 더이상 제어하지 않는다.
이러한 해결방식을 "controlled" component 라고 칭한다. 단지 DOM <input> 은 value 를 받아서 onChange 의 prop 으로 넘겨주면 TemperatureInput component 에서는 부모로 부터 temperature 와 onTemperatureChange 를 props 로 받아서 적절한 처리를 할 수 있다.
이제 TemperatureInput 은 temperature 를 갱신할 때 this.props.onTemperatureChange 를 호출할 것이다.
1
2
3
4
handleInput(e) {
//this.setState({temperature: event.target.value});
this.props.onTemperatureChange(e.target.value);
}
onTemperatureChange prop 은 Calculator Component 로 부터 temperature 와 함께 prop 으로 전달된다. onTemperatureChange 함수는 온도값이 변경 될 때 그 변화를 handling 하고, 두 input 은 새로운 값으로 re-rendering 된다.
Caculator 를 변경하기 전에, TemperatureInput component 변화를 요약해보자. 
1. TemperatureInput component 안에 있던 state(temperature) 를 제거하고, props 로 해당값을 참조하게 변경하였다.
2. this.setState() 로 변경하던 부분을 Calculator component 로 부터 넘어온 함수(this.props.onTemperatureChange()) 를 호출하게 변경하였다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TemperatureInput extends React.Component{
constructor(props){
super(props);

this.handleInput = this.handleInput.bind(this);
}

handleInput(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>

</fieldset>
);
}
}

TemperatureInput 의 최종코드는 위와 같이 변경된다.
이제 Calculator component 를 변경해보자.
현재 temperature 와 scale 을 state 로 가지고 있다. 이 state 는 input 태그들로 부터 "lifted up" 되었고, 두 입력으로 부터 단일 소스 저장소 역할(source of truth)을 한다. 이 2개의 state 값들이 우리의 요구사항을 rendering 하기 위한 최소 조건이다.
만약 우리가 섭씨 37도를 입력한다면 Calculator 의 component 는 {temperature: '37', scale: 'c'} 가 될 것이고 화씨 212 를 입력한다면 {temperature: '212', scale: 'f'} 가 될 것이다.
가장 최근에 입력된 숫자값과 해당 숫자값이 섭씨 화씨를 나타내는지만 알면 된다. 같은 state 로 부터 값이 계산되기 때문에 input 들간의 동기화가 되는 것이다.
 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
39
40
41
42
43
44
45
class Calculator extends React.Component{

constructor(props) {
super(props);
this.state = {
scale: 'c',
temperature: ''
};

this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheightChange = this.handleFahrenheightChange.bind(this);
}

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

handleFahrenheightChange(temperature){
this.setState({
scale: 'f',
temperature: 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.handleFahrenheightChange}/>
</div>
);

}
}

이제 어떤 input 박스에 입력을 하던간에 Calculator 의 this.stats.scale 과 this.state.temperature 값은 갱신 된다. 하나의 input 값을 입력하게 되면 다른 input 값은 해당값을 기반으로 언제나 다시 계산된다.
input 을 입력할 때 무슨일이 일어나는지 차례대로 살펴보자.
1. React 는 <input> 태그의 onChange 를 호출한다. 예제의 경우엔 TemperatureInput component 의 handleChange 메소드가 해당 메소드가 된다.
2. TemperatureInput의 handleChange 는 다시 this.props.onTemperatureChange() 를 입력된 값을 전달하면서 호출한다. onTemperatureChange 는 부모 Component 인 Calculator 로 부터 온 것이다.
3. 섭씨 TemperatureInput 의 onTemperatureChange 는 Calculator 의 handleCelsiusChange 메소드 이고, 화씨 TemperatureInput의 onTemperatureChange 는 Calculator 의 handleFahrenheightChange 메소드이다. 어떤 input 값이 변경되냐의 따라서 Calculator 의 위의 두 메소드중 하나의 메소드가 호출 된다.
4. 3.에서 호출된 메소드 안에서 Calculator component 는 this.setState() 를 호출해서 state 를 변경하고 React 는 re-rendering 을 하게 된다.
5. React 는 Calculator component 의 render 메소드를 호출해서 UI 를 변경한다. 입력한 scale(섭씨, 화씨) 여부와 현재 온도를 기반으로 두 input 의 값은 모두 다시 계산 된다. 이 때 온도 변환이 일어난다.
6. React 는 Calculator 로 부터 전달된 새로운 props 와 함께 각 TemperatureInput 의 render 메소드들을 호출한다. 이때 UI 의 어떤점이 변경되어야 하는지 알게 된다.
7. React 는 BoilingVerdict 의 render 메소드를 호출하고, 섭씨 온도를 props 로 전달한다.
8. React DOM 은 입력된 값과 비교하여 boiling verdict DOM 을 갱신한다. 우리가 섭씨를 입력했따면 섭시 input 은 현재값을 단순히 표시하고, 다른 input 은 변환되어 갱신된다.
- Lessons Learned
React app 에서 변경되는 data 는 하나의 소스 저장소("single source of truth") 가 되어야 한다. 대부분의 state 는 Component 에 rendering 을 위해서 처음에 초기화 된다. 그리고 만약 다른 Component 도 같은 값이 필요하게 되면, 필요한 두 개의 Component 의 최소 공통 조상 레벨로 "lift up" 하면 된다. 서로 다른 두개의 state 를 동기화 시키지 말고, top-down data 흐름 구조를 이용해야 한다.
이런 Lifting state 는 two-way binding 접근 방식보다 많은 코드를 필요로 하지만 bug 를 찾기 쉽게 해준다. 



덧글

댓글 입력 영역