(React 공식문서의 main concepts 번역 글입니다.)
함께보면 이해가 쏙쏙
여러 component들에게 data의 변화를 모두 반영해야 할 때
각 component의 state를 통합해야 한다.
통합한 state는 가장 가까운 부모 component가 관리한다.
이것이 state를 끌어올리는 것, 즉 lifting state up이다.
2가지 다른 종류 의 온도를 입력받을 수 있는 component를 만들자.
하나를 입력하면 동시에 다른 하나가 업데이트 되는 component이다.
입력받은 온도를 계산하여 물이 끓는지 표시하는 이 component를
예시로 활용하여 lifting state up 과정을 알아보자.
먼저, BoilingVerdict라는 component를 만들겠다.
props로 celsius 온도를 받아서, 그 온도가 물이 끓 수 있는 온도인지 나타내는 component이다.
function BoilingVerdict(props) {
if(props.celsius >= 100) {
return <p>The water would boil.</p>
}
return <p>The water would not boil.</p>
}
다음은 Calculator component이다.
<input>
을 render하고 그 value값을 state로 관리한다.
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>
);
}
}
2가지 다른 종류 의 온도를 입력받기 위해서
Celsius를 입력받는 <input>
외에 추가로
Fahrenheit 를 입력받는 <input>
을 더하자
이를 위해, 먼저 Calculator component에서
TemperatureInput component를 extract 한다.
온도를 표시하기 위해 각 component에 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>
);
}
}
<input>
이 2개이므로 2종류의 온도를 입력받을 수 있다.
하지만 하나의 <input>
에 값을 입력해도
다른 <input>
의 값이 동시에 업데이트 되지 않는다.
또한 BoilingVerdict도 표시할 수 없다.
왜냐하면 Calculator에서는 현재 온도를 알 수 없기 때문이다.
(현재 온도는 TemperatureInput의 state로 숨겨져 있다.)
2가지 다른 종류의 온도 값을
F는 C로, C는 F로 바꿔주는 함수 2개를 만들자.
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
toCelsius는 F를 C로
toFahrenheit는 C를 F로 바꾸는 함수이다.
하지만 숫자 값 을 입력받아 return한다.
숫자 값 이 아닌 문자 값 을 입력받고 return해야 하므로
새로운 함수가 하나 더 필요하다.
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하고
나머지 경우, 문자 값으로 온도를 입력받아 문자 값을 return한다.
모든 준비는 끝났다. 이제 state를 통합한 후 끌어올릴 차례이다.
현재는 아래처럼 2개의 TemperatureInput component에서
독립적으로 각각의 value값을 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; // ...
그 결과 한 value값이 업데이트 된다해도
다른 value값이 자동으로 업데이트 되지 않는다.
이 문제를 해결하기 위해서 React에서는 state를 통합한다.
그 통합한 state를 가장 가까운 부모 component가 관리한다.
이것이 lifting state up이다.
기존 TemperatureInput에 있던 state를
부모 component인 Calculator로 끌어 올려서 관리 하도록 하고
원래 있던 TemperatureInput에서는 제거한다.
Calculator가 가진 state가 바로 통합 state이다.
2개의 TemperatureInput은 같은 부모 component를 가진다.
이 부모 component(Calculator)가
각 TemperatureInpute에게 props로 통합 state를 전달한다.
이 과정을 단계별로 살펴보자.
먼저 TemperatureInput에서 state를 제거한다.
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature; // ...
state가 props로 바뀐것을 확인할 수 있다.
이 props는 나중에 부모 component(Calculator)로부터 받게 될
통합 state값이 할당된다.
props는 read-only이다. temperature값이
props로 바뀌었으므로 temperature값을 수정할 수 없다.
React에서 이런 경우 controlled component를 만들어 해결한다.
DOM <input>
비슷하게 value, onChange를 props로 전달한다.
즉, Calculator(부모 component)가 TemperatureInput에게
temperature, onTemperatureChange를 전달한다.
TemperatureInput이 temperature(Caculator의 state)를
change하기 위해 onTemperatureChange를 call한다.
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value); // ...
이름(temperature, onTemperatureChange)은 convention이다.
onTemperatureChange는 Calculator로부터 전달받은 props이다.
따라서 TemperatureInput에서 입력받은 input 값을
Calculator에게 전달할 수 있다.
또한, Calculator에서 선언되어 있으므로
Calculator안에 있는 state(temperature값)을 바꿀 수 있다.
(state값을 입력받은 input값으로 바꾸는 것이 가능)
위 과정에 따라 Calculator를 수정하기 이전에,
먼저 Temperature 수정을 마무리하자.
this.state.temperature를 this.props.temperature로
setState()를 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>
);
}
}
이제 다시 Caculator로 돌아가보자.
temperature와 scale을 state
로 저장한 것을 확인할 수 있다.
각 input의 state를 끌어올려서 통합한 것이 이 state
이다.
이 과정이 Lifting state up 이다.
만약 우리가 37을 Celsius input 에 입력한다면
Caculator component의 state는 아래와 같이 될것이다.
{
temperature: '37',
scale: 'c'
}
나중에 우리가 Fahrenheit에 212를 입력한다면
Caculator component의 state는 아래와 같이 된다.
{
temperature: '212',
scale: 'f'
}
각 temperatureInput에서 temperature를 입력받지만
하나의 통합한 state로 관리할 수 있다.
한 temperature를 입력받으면 공식을 활용하여
나머지 temperature를 추론할 수 있기 때문이다.
각 temperatureInput의 value값은 sync 된다.
통합된 state에 의해 value값이 render되기 때문이다.
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:temperature}); }
handleFahrenheitChange(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.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
어떤 temperatureInput에 값을 입력하던 상관없이
Caculator의 통합된 state가 업데이트되고
나머지 temperatureInput가 수정된 값으로 render된다.
Celsius TemperatureInput <input>
에 값을 입력했다고 가정해보자.
아래와 같은 과정으로 render가 진행된다.
<input>
(DOM)의 onChange
를 call한다.TemperatureInput
의 handleChange
가 실행된다.TemperatureInput
의 props인 onTemperatureChange에 할당된 함수(handleCelsiusChange)가 실행된다.<input>
에 입력한 값을 parameter로 받는다.<input>
에 입력한 값을 반영하여 Calculator의 state가 수정된다.<input>
value는 celsius와 fahrenheit로 수정되어 할당된다.모든 업데이트가 위 과정으로 진행되므로 temperature는 sync를 유지한다.
(동시에 바뀔 수 있게 되었음)
통합된 state는 React에서 중요하다.
보통 state는 rendering을 위해서 component에 추가된다.
그리고나서 만약 다른 component도 함께 state를 필요로하면
그 component들의 공통된 부모 component로 state를 끌어올린다.
각 component의 state를 sync하도록 노력하기보다
공통된 부모 component로 통합된 state를 끌어올려 관리한다.
이것이 Top-down data flow를 활용하는 방법이다.
Lifting state는 각 state를 나눠서 관리하는 것보다
기본적인 방법이다. 또한 bug도 적다.
component가 통합된 state를 관리하게 되면서
표면적인 bug가 줄어들었다.
게다가, user input을 관리하는 로직도 수정이 쉬워졌다.
위에서 celciusValue나 fahrenheitValue를 저장하지 않고
temperature와 scale(통합된 state)을 저장했다.
한쪽 <input>
에서 입력을 받으면 <input>
의 값은
항상 render()안에서 계산이 되어 전달되었다.
그 결과 오차 없이 두 <input>
의 값을 render할 수 있었다.
만약 UI에서 잘못된 값이 render 된 부분을 발견한다면
해당 data, 즉 props가 어떠한 부모 component로부터 왔는지
component tree의 상위를 탐색하여 파악할 수 있다.
그 결과, bug를 쉽게 발견하여 고칠 수 있다.