🏋️♂️ State 끌어올리기
때로 같은 데이터에 대해 여러 개의 컴포넌트가 반응해야 합니다. 이럴때는 가까운 조상 컴포넌트에게 state 를 공유하는 방법을 추천합니다. 이 섹션에서 state를 끌어올릴 때 어떻게 작동하는지 알아보겠습니다.
이번 섹션에서는 온도를 줬을 때 물이 끓는 온도인지 알려주는 온도 계산기를 만들겠습니다. BoilingVerdict
라는 컴포넌트로 시작하겠습니다. 이것은 celsius
온도를 props 로 받아서 물이 끓는지 보여줍니다.
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
를 렌더링 합니다.
⌨️ 두 번째 입력 값 추가하기
현재 섭씨 입력 필드만 있지만 화씨 입력을 제공하고 두 입력을 동기화 하도록 하는 새로운 요구사항이 생겼습니다. Calculator
에서 TemperatureInput
으로 분리해서 시작하겠습니다. “c”
또는 “f”
로 될 수 있는 scale
이라는 새로운 props 를 추가하겠습니다.
Calculator
가 두 가지 온도 입력 필드 렌더링을 하도록 하겠습니다.
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
이제 두 가지 입력 필드가 있습니다. 하지만 둘 중 어떤 것도 온도를 입력했을 때 다른 입력 필드의 업데이트가 일어나지 않습니다. 이것은 동기화하려는 요구사항과 맞지 않습니다. 또한 TemperatureInput
안에 숨겨져 있는 현재 온도를 Calculator
는 알 수 없기 때문에 Calculator
의 BoilingVerdict
를 보여줄 수 없습니다.
🔄 변환 함수 작성하기
일단 섭씨 에서 화씨로, 화씨에서 섭씨로 바꾸는 두 가지 함수를 만들겠습니다.
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’
을 리턴합니다.
🏗️ State 끌어올리기
현재 두 가지 TemperatureInput
컴포넌트 모두 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;
// ...
하지만 우리는 두 가지가 동기화되길 원합니다. 섭씨 입력 필드가 업데이트 됐을 때 화씨 입력 필드에도 변환된 온도가 반영되어야 합니다. 그 반대의 경우도 마찬가지 입니다. React에서 state를 공유하는 일은 각 컴포넌트가 필요로 할 때 가까운 조상에게 끌어올려서 해결할 수 있습니다. 이런 방법을 “state 끌어올리기 ( lifting state up
)” 로 불립니다. TemperatureInput
의 state를 지우고 대신 Calculator
로 옮기겠습니다.
Calculator
가 공유할 state를 가지고 있다면 그것은 두 가지 입력의 현재 온도의 “진실의 근원 ( source of truth
)” 가 됩니다. 이것은 두 입력 모두 일치하는 값을 가지도록 지시할 수 있습니다. 두 가지 TemperatureInput
컴포넌트 모두 같은 Calculator
컴포넌트로 부터 props 를 받기 때문에 항상 동기화 됩니다.
동작하는 방법을 순서대로 보겠습니다.
먼저 TemperatureInput
컴포넌트의 this.state.temperature
를 this.props.temperature
로 바꾸겠습니다. 지금은 this.props.temperature
의 데이터가 존재한다고 가정하겠습니다. 나중에 Calculator
에서 전달할 것 입니다.
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
// ...
알다시피 props 은 읽기 전용 입니다. temperature
가 state 였을 때 TemperatureInput
은 this.setState()
를 호출해서 변경할 수 있었습니다. 하지만 지금은 temperature
가 부모로부터 props 로 받기 때문에 TemperatureInput
는 더 이상 temperature
를 제어할 수 없습니다.
React에서 대게 컴포넌트를 “제어" 가능하게 만들어서 해결합니다. DOM의 <input>
이 value
와 onChange
를 props 로 받는 것처럼 TemperatureInput
도 temperature
와 onTemperatureChange
를 Calculator
로 부터 props 로 받을 수 있습니다. 이제 TemperatureInput
가 온도 업데이트를 원할 때 this.props.onTemperatureChange
를 호출합니다.
handleChange(e) {
// Before: this.setState({ temperature: e.target.value });
this.props.onTemperatureChange(e.target.value);
// ...
onTemperatureChange
props 는 temperature
props 와 같이 Calculator
에서 제공됩니다. 이것은 자신의 state가 바뀔 때 마다 두 가지 입력이 재 렌더링 (re-rendering) 이 일어납니다. Calculator
의 변경사항을 보기전에 TemperatureInput
컴포넌트의 변경사항을 먼저 요약하겠습니다. TemperatureInput
의 state를 지웠습니다. 그리고 this.state.temperature
를 this.props.temperature
로 바꿨습니다. this.setState()
호출 대신 변화를 원할 때 Calculator
로 받은 this.props.onTemperatureChange()
를 호출합니다.
이제 Calculator
컴포넌트를 보겠습니다. 우리는 현재 입력한 temperature
와 scale
을 state 에 저장할 것 입니다. 이것은 입력 필드에서 끌어올린 state 입니다. 그리고 TemperatureInput
컴포넌트에게 진실의 근원으로 작동합니다. 또한 두 입력창을 렌더링 하기 위해서 알아야 하는 모든 데이터를 최소한으로 표현한 것이기도 합니다.
예로 섭씨 입력창에 37을 입력하면 Calculator
컴포넌트의 state 는 이렇게 됩니다.
{
temperature: '37',
scale: 'c'
}
화씨로 212를 입력하면 이렇게 됩니다.
{
temperature: '212',
scale: 'f'
}
두 가지 입력 값을 저장할 수 있습니다. 하지만 이것은 중요하지 않습니다. 가장 최근에 바뀐 값과 그 값을 나타내는 scale
을 저장하면 충분합니다. 그렇다면 현재 temperature
와 scale
에 기반하는 다른 입력 값을 추론할 수 있습니다. 입력 값이 같은 state 에서 계산되기 때문에 입력은 동기화 상태로 있습니다.
이제 Calculator
안에 this.state.temperature
와 this.state.scale
가 업데이트 됩니다. 두 개의 입력 중 하나에 값을 입력하면 다른 입력창에 계산이 된 값이 표시됩니다.
입력 값을 수정 했을 때 무슨 일이 일어나는지 요약해보겠습니다.
- React가 DOM의
<input>
에서onChange
로 명시된 함수를 호출합니다. 위 예제의 경우는TemperatureInput
컴포넌트 안에 있는handleChange
메소드 입니다. TemperatureInput
컴포넌트 안handleChange
메소드는 새로운 값을 인자로this.props.onTemperatureChange()
호출합니다. props 는Calculator
부모 컴포넌트에서 왔습니다.Calculator
에서 섭씨TemperatureInput
의onTemperatureChange
는handleCelsiusChange
메소드, 화씨TemperatureInput
의onTemperatureChange
는handleFahrenheitChange
메소드를 명시했습니다. 따라서 입력 필드에 따라서 두 메소드 중 하나를 호출됩니다.- 메소드를 살펴보면
Calculator
컴포넌트는 React에게 새로운 입력 값과 함께this.setState()
를 호출해서 재 렌더링 (re-rendering) 요청합니다. - React는 UI의 모양을 보여주기 위해서
Calculator
컴포넌트의render
메소드를 호출합니다. 두 가지 입력 값 모두temperature
와scale
에 기반해서 다시 계산됩니다. 온도 변환은 여기서 일어납니다. - React가 각
TemperatureInput
컴포넌트의render
메소드를Calculator
에 명시된 새로운 props 과 함께 호출합니다. 여기서 그들의 UI가 어떤 모습인지 알 수 있습니다. - React가 섭씨 온도를 props 로
BoilingVerdict
의render
메소드를 호출합니다. - React DOM이 입력 값과 맞는
BoilingVerdict
컴포넌트의 결과 값을 DOM에 업데이트합니다. 작성한 입력창과 다른 입력창은 계산이 끝난 온도를 업데이트합니다.
입력창이 동기화 되있기 때문에 모든 업데이트는 같은 순서로 작동합니다.
🤔 교훈
React 애플리케이션 안에서 변경이 일어나는 데이터에 대해서는 “진실의 근원” 을 하나만 두어야 합니다. 보통 state 는 렌더링에 필요할 때 처음 추가됩니다. 만약 다른 컴포넌트도 필요로 할 때 가장 가까운 공통조상에게 올려줍니다. 다른 컴포넌트 사이에 state 를 동기화하려고 하지 말고 하향식 데이터 흐름 에 의존해야 합니다.
state 를 끌어올리는 작업은 양방향 바인딩 접근 방식보다 더 많은 “보일러 플레이트” 코드를 유발하지만, 버그를 찾고 격리하기 더 쉽게 만든다는 장점이 있습니다. 어떤 state 든 간에 특정 컴포넌트 안에서 존재하기 마련이고 그 컴포넌트가 자신의 state 를 스스로 변경할 수 있기 때문에 버그가 존재할 수 있는 범위가 크게 줄어듭니다. 또한 사용자의 입력을 거부하거나 변형하는 자체 로직을 구현할 수도 있습니다.
props 나 state 중 어떤거든 파생할 수 있다면 이것은 state 가 되지 않을 것입니다. 예로 celsiusValue
와 fahrenheitValue
를 저장하는 것 대신 마지막에 작성한 temperature
와 scale
만 저장할 수 있습니다. 다른 입력 값은 render()
안에서 항상 계산될 수 있습니다. 이를 통해 사용자 입력값의 정밀도를 유지한 채 다른 필드의 입력값에 반올림을 지우거나 적용할 수 있게 됩니다.
UI에서 무언가 잘못된 부분이 있을 경우, React Developer Tools를 이용하여 props를 검사하고 state를 갱신할 수 있는 컴포넌트를 찾을 때까지 트리를 따라 탐색해보세요. 이렇게 함으로써 소스 코드에서 버그를 추적할 수 있습니다.
📚 정리 및 참고 자료
- React에서 state를 공유하려면 공통 조상 컴포넌트까지 state를 올려서 해결할 수 있습니다.
- 조상 컴포넌트에서
Event handler
를 props로 전달하면 하위 컴포넌트에서 상위 컴포넌트의 state를 제어를 할 수 있습니다. - React 애플리케이션 안에서 변경이 일어나는 데이터에 대해서는 “진실의 근원” 을 하나만 두어야 합니다
- 보일러 플레이트란 반드시 작성해야하지만 반복적인 코드를 의미합니다.
- React 보일러 플레이트 예시