종종 동일한 데이터에 대한 변경사항을 여러 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에 대한 변경사항부터 요약해보자.
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이 갱신된다. 입력 필드 중 하나는 있는 그대로의 값을 받으므로 사용자가 입력한 값이 보존되고, 다른 입력 필드의 값은 항상 다른 하나에 기반해 재계산된다.