이 포스팅은 https://reactjs.org/docs/lifting-state-up.html 에 있는 포스팅을 번역한 것입니다. 오역이나 의역이 있을 수 있습니다. 지적해주시면 확인 후 바로 정정하겠습니다.
original source of this posting is from https://reactjs.org/docs/lifting-state-up.html If the original author requests deletion, it will be deleted immediately.
Translated by Jake Seo (서진규)
- https://velog.io/@jakeseo_me
- https://github.com/n00nietzsche
리액트 공식문서로 배워보자 #10, 상태 끌어올리기
종종 몇몇 컴포넌트들은 같은 변화하는 데이터를 반영할 필요가 있습니다. 우리는 공유된 상태를 그들의 가장 가까운 공유하는 조상으로 끌어올리는 것을 권장합니다. 실제로는 어떻게 동작하는지 살펴봅시다.
이 섹션에서, 우리는 물이 주어진 온도에서 끓는지 계산하는 온도 계산기를 만들 것입니다.
우리는 먼저 BoilingVerdict
라는 컴포넌트와 함께 진행해볼 겁니다. 이 컴포넌트는 celsius
기온을 prop으로 받습니다. 그리고 그 온도가 물을 끓이기에 충분한지 출력해줍니다.
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
다음으로 우리는 Calculator
라고 불리는 컴포넌트를 만들 것입니다. 이 컴포넌트는 기온을 입력할 수 있는 <input>
을 렌더링하고 그 값을 this.state.temperature
에 보관합니다.
추가적으로, 이 컴포넌트는 현재 입력 값에 대한 BoilingVerdict
를 렌더링합니다.
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>
);
}
}
우리의 새로운 요구사항은 섭씨 온도 입력 값에 추가로 화씨 온도 입력을 제공하고 계속 싱크를 유지하는 것입니다.
Calculator
로부터 TemperatureInput
컴포넌트를 추출함으로써 시작할 수 있습니다. 우리는 여기에 새로운 scale
prop을 추가할 것입니다. scale
은 "e"
나 "f"
가 될 수 있습니다.
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
를 2개의 분리된 온도 입력 창으로 변경할 수 있습니다.
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
이제 우리는 2개의 입력 창을 가졌습니다. 하지만 둘 중 하나에 온도를 입력할 때, 나머지 것은 업데이트되지 않습니다. 이건 우리의 요구사항에 반대됩니다. 우리는 싱크를 맞추고 싶습니다.
또 우리는 Calculator
로부터 BoilingVerdict
를 보여줄 수 없습니다. Calculator
는 현재의 온도를 알 수 없습니다. 왜냐면 현재의 온도는 TemperatureInput
내부에 숨어있기 때문입니다.
먼저, 우리는 화씨에서 섭씨, 섭씨에서 화씨로 온도를 변경하는 2가지 함수를 작성할 것입니다.
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
이 두가지 함수는 숫자를 변환합니다. 우리는 문자열 temperature
를 받고 변환 함수를 인자로 가져와 문자열을 반환하는 다른 함수를 만들 것입니다. 다른 입력 값을 기반으로 입력 값을 계산하는데 이것을 이용할 것입니다.
이 함수는 유효하지 않은 temperature
에 대해서 빈 문자열을 리턴합니다. 그리고 이 함수는 출력 값을 3번째 소수점에서 반올림합니다.
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();
}
예를 들면, tryConvert('abc', toCelsius)
는 빈 문자열을 반환합니다. 그리고 tryConvert('10.22', toFahrenheit)
은 50.396
을 반환합니다.
현재, 두 개의 TemperatureInput
컴포넌트는 독립적으로 그들의 값을 지역 상태에 저장합니다.
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;
// ...
하지만, 우리는 이 두 개의 입력 값이 서로 싱크가 맞길 원합니다. 우리가 섭씨 입력 값을 업데이트 했을 때, 화씨 입력 값은 변환된 값을 반영해야 합니다. 그리고 반대도 같습니다.
리액트에서, 상태를 공유하는 것은 상태를 상태가 필요한 컴포넌트들의 가장 가까운 공통 조상으로 이동시킴으로써 공유할 수 있습니다. 이러한 테크닉은 "상태 끌어올리기(lifting state up)"라고 불립니다. 우리는 지역 상태를 TemperatureInput
으로부터 제거할 것이고 이 상태를 대신 Calculator
로 옮길 것입니다.
만일, Calculator
가 공유된 상태를 소유한다면, Calculator
는 두 입력값의 현재의 온도에 대한 "source of truth(유일한 원천)"이 됩니다. Calculator
는 두 입력 값 모두에게 서로 일관성 있는 값을 갖게 할 수 있습니다. 두 TemperatureInput
의 props가 같은 부모인 Calculator
컴포넌트에서 오기 때문에, 두개의 입력 값은 항상 싱크가 맞습니다.
이게 어떻게 작동하는지 단계단계 살펴봅시다.
먼저, 우리는 this.state.temperature
와 TemperatureInput
컴포넌트 내부의 this.props.temperature
를 교체할 것입니다. 나중에는 Calculator
로부터 this.props.calculator
를 넘겨야겠지만, 지금은 this.props.temperature
가 이미 존재한다고 쳐봅시다.
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
// ...
우리는 props는 읽기 전용이란 것을 알고 있습니다. temperature
가 지역 상태에 있을 때, TemperatureInput
은 그냥 temperature
를 변경하기 위해서 this.setState()
를 호출할 수 있습니다. 하지만, 이제 temperature
는 부모로부터 오는 prop이기 때문에, TemperatureInput
는 더이상 통제 권한이 없습니다.
리액트에서, 이러한 문제는 컴포넌트를 통제된(controlled) 상태로 만듦으로써 해결 가능합니다. DOM <input>
이 value
그리고 onChange
prop을 받는 것처럼, custom TemperatureInput
역시 temperature
그리고 onTemperatureChange
props 를 부모로부터 받을 수 있습니다.
이제, TemperatureInput
이 온도를 업데이트하기 원할 때, 이 컴포넌트는 this.props.onTemperatureChange
를 호출합니다.
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
// ...
알아둬야 할 것
커스텀 컴포넌트에서 temperature
또는 onTemperatureChange
prop 이름들이 특별한 의미를 갖진 않습니다. 아무런 다른 이름으로 부를 수도 있습니다. 일반적인 컨벤션과 같이 value
그리고 onChange
처럼 불러도 됩니다.
onTemperatureChange
prop은 Calculator
컴포넌트에 의해 temperature
prop과 같이 전달될 것입니다. 이 prop은 지역 상태가 수정됨에 따라 일어나는 변화를 다룰(handle) 것이고 그럼으로써 두 입력값 모두를 새로운 값으로 재렌더링도 할 것입니다. 새로운 Calculator
구현을 곧 보게될 것입니다.
Calculator
의 변화 속으로 다이빙하기 전에, TemperatureInput
컴포넌트의 변화들을 다시 살펴봅시다. 우리는 지역 상태를 없애버렸습니다. 그리고 this.state.temperature
를 읽었는데 이젠 그 대신에 우린 this.props.temperature
를 읽습니다. this.setState()
를 호출하는 대신에, 우리가 변화를 원할 때, 이제 우린 this.props.onTemperatureChange()
를 호출합니다. 이 함수는 Calculator
에서 상속받은 것입니다.
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
컴포넌트로 가봅시다.
우리는 현재 입력된 temperature
그리고 scale
을 지역 상태에 저장할 것입니다. 이건 우리가 입력값으로부터 "끌어올린" 상태입니다. 그리고 이 입력 값은 "source of truth"로 두 입력 값에 제공될 것입니다. 이건 이 두개의 입력창을 렌더링하기 위해서 우리가 알 필요가 있는 최소한의 설명입니다.
예를 들어, 우리가 37을 섭씨 입력 값에 넣으면, Calculator
컴포넌트는
{
temperature: '37',
scale: 'c'
}
만일 우리가 나중에 화씨(Fahrenheit) 필드를 212로 수정하면, Calculator
는 다음과 같습니다.
{
temperature: '212',
scale: 'f'
}
우리는 두 입력 값의 값을 저장할 수 있지만 꼭 그렇게 할 필요가 없다는 것을 압니다. 최근에 변경된 입력 값과 그 값의 단위를 저장하는 것만으로 충분합니다. 우린 그럼 현재 입력된 1개의 temperature
와 scale
을 기반으로 다른 입력 값을 추론할 수 있습니다.
입력 값은 동기화된 상태를 유지합니다. 왜냐하면 값들이 같은 상태로부터 계산되었기 때문입니다.
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
속 this.state.temperature
와 this.state.scale
는 업데이트 됩니다. 둘 중 하나는 입력 값을 그대로 얻으므로 어떤 사용자 입력 값이 보존되고, 다른 입력 값은 이미 입력된 값을 기반으로 재계산됩니다.
여러분이 입력 값을 수정할때, 어떤 일이 일어나는지 다시 한번 되짚어봅시다.
<input>
에서 onChanged
로 명시된 함수를 호출합니다. 우리의 경우엔, TemperatureInput
컴포넌트 내부에 있는 handleChange
메소드입니다.TemperatureInput
컴포넌트 내부에 있는 handleChange
메소드는 this.props.onTemperatureChange()
메소드를 새롭게 입력한 값을 이용하여 호출합니다. 이 onTmperatureChange
를 포함하는 props는 이 부모 컴포넌트 Calculator
로부터 제공받습니다.Calculator
는 섭씨 TemperatureInput
의 onTemperatureChange
는 Calculator
의 handleCelsiusChange
메소드이고 화씨 TemperatureInput
의 onTemperatureChange
는 Calculator
의 handleFahrenheitChange
메소드라고 명시했습니다. 어떤 입력 값이 수정되냐에 따라 두 개의 Calculator
메소드 중 어떤 것이 호출될지 결정합니다.Calculator
컴포넌트는 새로운 입력 숫자값 그리고 우리가 수정한 현재의 scale 값과 함께 this.setState()
메소드를 호출함으로써 리액트에게 재렌더링하라는 명령을 보냅니다.Calculator
컴포넌트의 render
메소드를 호출합니다. 두 입력 값은 현재 온도와 활성화된 scale을 기반으로 다시 계산됩니다. 온도 변환은 여기서 이뤄집니다.Calculator
에 의해 명시된 새로운 props값과 함께 각 TemperatureInput
컴포넌트의 render
메소드를 호출합니다. 리액트는 UI가 어떻게 생겨야 하는지 학습합니다.BoilingVerdict
컴포넌트의 render
메소드를 호출하고, 섭씨 온도를 props로 받습니다.모든 업데이트는 같은 과정을 거치고 그래서 모든 입력 값은 계속 싱크를 유지합니다.
리액트 어플리케이션에서 변화하는 데이터에 대해 단 하나의 "source of truth"만 있는 것이 바람직합니다. 주로, 상태는 처음 렌더링을 위해 상태가 필요한 컴포넌트에 추가됩니다. 그 후에, 만일 다른 컴포넌트들도 상태가 필요하다면, 여러분은 상태를 그들의 가장 가까운 공유하는 조상으로 끌어올릴 수 있습니다. 다른 컴포넌트들 사이의 상태의 싱크를 맞추는 것 대신에, 여러분은 top-down data flow에 의존하는 것이 바람직합니다.
상태를 끌어올리는 것은 two-way 바인딩 접근법보다 더 많은 "보일러 플레이트(boilerplate)" 코드를 작성하는 것을 포함합니다. 하지만 이점으로서, 버그를 찾고 격리시키기 위해 더 적은 노동력을 요구로 합니다. 어떤 상태가 어떤 컴포넌트 안에 살고 있고 그 컴포넌트가 혼자 상태를 변환할 수 있기 때문에, 버그에 대한 표면적인 공간은 매우 줄어듭니다. 추가적으로, 여러분은 유저의 입력 값을 거부하거나 변환하기 위한 커스텀 로직을 구현할 수 있습니다.
만일 어떤 것을 props 또는 state에서 끌어올 수 있다면, 그것은 상태에 있으면 안됩니다. 예를 들어, celsiusValue
그리고 fahrenheitValue
두 개를 저장하는 것 대신에, 우리는 마지막으로 수정된 temperature
와 그 scale
을 저장합니다. 다른 입력란의 값은 render
메소드 안에서 temperature
와 scale
을 이용해 계산될 수 있습니다. 이렇게 구현하면 유저 입력값에서 어떠한 정밀함의 손실도 없이 다른 필드에 반올림을 수행할 수 있습니다.
UI에서 잘못된 점을 보았을 때, 여러분은 상태를 업데이트하는 것에 대한 책임을 갖는 컴포넌트를 찾을 때까지, props를 검사하고 tree를 위로 이동시키기 위해 [React Developer Tools]을 사용할 수 있습니다. 이 툴은 소스의 버그를 추적하는것을 가능하게 해줄 것입니다.