종종 여러 컴포넌트에 동일한 변경 데이터 변경을 반영하고 싶은 필요가 있습니다. 이럴 때 공통 조상에 state를 끌어올리는 걸 권장합니다. 어떻게 하는 지 살펴봅시다.
이 섹션에서는 주어진 온도에서 물의 끊음 여부를 계산하는 온도 계산기를 작성합니다.
먼저 BoilingVerdict
컴포넌트입니다. 이 컴포넌트는 prop
으로 celsius
온도를 받고, 물이 충분히 끓었는지 표시 합니다.
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
로 넣습니다.
input
의 입력값으로 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
컴포넌트를 추출 합니다. c
나 f
값을 넣을 수 있는 scale
prop
을 추가합니다.
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
컴포넌트에 두개의 온도 입력 컴포넌트 TemperatureInput
를 추가 하여 랜더링 하도록 합니다.
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
두 개의 온도 입력 컴포넌트 TemperatureInput
를 가지고 있지만, 구 중 하나에서 온도를 입력하면 다른 온도 입력 컴포넌트는 업데이트되지 않습니다.
이것은 최초 요구 사항인 값의 동기화 조건에 충족되지 않습니다.
또한 Calculator
에서 BoilingVerdict
도 표시할 수 없습니다. Calculator
는 현재 온도가 TemperatureInput
안에 존재 하기 때문에 현재 온도를 알 수 없습니다.
섭씨와 화씨를 변환하는 두 가지 함수를 작성합니다.
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
두 함수는 숫자를 변환합니다. 문자열 temperature
과 변환 함수를 인자로 받아 문자열을 반환하는 또 다른 함수를 작성할 것입니다. 다른 input
기반으로 한 input
값을 계산하는데 사용합니다.
아래 함수는 유효하지 않은 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();
}
예를 들어, tryConvert('abc', toCelsius)
는 빈 문자열을 반환하고, tryConvert('10.22', toFahrenheit)
는 50.396
을 반환합니다.
두 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;
// ...
우리는 두 입력이 서로 동기화되기를 바랍니다. Celsius
입력을 업데이트 할 때, 화씨 입력은 변환 된 온도를 반영해야하며 반대의 경우도 마찬가지입니다.
React에서 공유 상태는 필요로하는 컴포넌트의 가장 가까운 공통 조상으로 이동하여 처리합니다. 이것을 lifting state up라고합니다. TemperatureInput
컴포넌트에서 로컬 state
를 삭제하고 Calculator
로 옮깁니다.
만약 Calculator
가 공유되는 state
를 가지면, 이는 두 개의 input
에서 사용할 수 있는 현재 온도에 대한 신뢰 가능한 소스가 됩니다. 이를 통해 서로에게 일관된 값을 가질 수 있도록 지시할 수 있습니다. 양쪽 TemperatureInput
컴포넌트의 props
가 같은 부모 Calculator
컴포넌트에서 오므로, 두 input은
항상 동기화 상태입니다.
이제 단계별로 어떻게 동작하는 지 살펴봅시다.
먼저 TemperatureInput
컴포넌트의 this.state.temperature
를 this.props.temperature
로 변경합니다. 추후에 Calculator
에서 props
를 전달해야할 필요가 있지만 지금은 this.props.temperature
값이 존재한다고 가정해봅시다.
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
// ...
props는 읽기 전용 입니다. temperature
가 로컬 state
일 때는 this.setState()
를 호출해 변경하면 되었습니다. 하지만 이제는 temperature
가 부모로부터 props
으로 전달받기 때문에, TemperatureInput
이 값을 변경 할 수 없습니다.
React에서는 이 문제를 해결하기 위해 보통 “제어되는” 컴포넌트를 만듭니다. DOM <input>
이 value
와 onChange
prop
을 받는 것처럼, TemperatureInput
컴포넌트도 부모 Calculator
컴포넌트로부터 temperature
와 onTemperatureChange
prop
을 받게 만들 수 있습니다.
이제 TemperatureInput
가 그 온도를 업데이트하고 싶을 때 this.props.onTemperatureChange
를 호출합니다.
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
// ...
노트
사용자 지정 컴포넌트의 props 이름이temperature
onTemperatureChange
인것은 특별한 의미가 없습니다.
이 속성은value
나onchange
같은 이름으로 지정할 수도 있습니다.
onTemperatureChange
prop
은 부모 Calculator
컴포넌트에서 temperature
prop
과 함께 전달 됩니다. 이 함수는 자체 로컬 state
를 수정하여 변경사항을 제어하므로 두 input
을 새 값으로 새로 렌더링합니다. 새로운 Calculator
class는 곧바로 살펴봅시다.
Calculator
변경을 하기전에, TemperatureInput
컴포넌트의 변경사항을 다시한번 살펴봅시다. 로컬 state
를 컴포넌트에서 제거하고 this.state.temperature
를 읽어오는 대신 this.props.temperature
를 읽어옵니다. 변경사항이 생겼을 때 this.setState()
를 호출하는 대신 Calculator
에서 전달한 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>
);
}
다시 Calculator
컴포넌트로 돌아와서.
현재 input
의 temperature
와 scale
을 로컬 state
에 저장합니다. 이 state
는 input
으로부터 “끌어올려지며”, 두개의 값 모두를 “신뢰 가능한 소스”로 제공합니다. 두 input
을 렌더링하기 위해 알아야하는 모든 데이터를 최소한으로 표현한 것입니다.
예를 들어, 섭씨 input
에 37
을 입력하면, Calculator
컴포넌트의 상태는 아래와 같습니다.
{
temperature: '37',
scale: 'c'
}
그리고 화씨 input을 212로 수정하면, Calculator 컴포넌트의 상태는 아래와 같습니다.
{
temperature: '212',
scale: 'f'
}
양쪽 값을 모두 저장할 수도 있지만 이는 불필요합니다. 가장 최근에 변경된 input
값과 그것이 나타내는 scale
을 저장하는 것만으로 충분합니다. 현재의 temperature
와 scale
만으로도 다른 input
값을 추론할 수 있습니다.
input
은 그 값을 같은 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>
);
}
}
이제 어떤 input
을 편집하던지 Calculator
안의 this.state.temperature
와 this.state.scale
이 업데이트됩니다. input
중 하나가 값을 그대로 가져오므로 모든 사용자 입력이 보존되고 다른 input
값은 그 값을 기반으로 다시 계산합니다.
input
을 수정할 때 무슨 일이 일어나는 지 다시 살펴봅시다.
<input>
이 바뀔 때마다 onChange
로 정의된 함수를 호출합니다. 이 케이스에서는 TemperatureInput
컴포넌트의 handleChange
메서드가 이 역할을 수행합니다.TemperatureInput
컴포넌트 안의 handleChange
메서드는 새로운 값으로 this.props.onTemperatureChange()
를 호출합니다. onTemperatureChange
를 포함한 props
는 부모 컴포넌트인 Calculator
에서 제공합니다.Calculator
는 섭씨 TemperatureInput
의 onTemperatureChange
는 Calculator
의 handleCelsiusChange
이고, 화씨 TemperatureInput
의 onTemperatureChange
메서드는 Calculator
의 handleFahrenheitChange
메서드로 정의합니다. 따라서 수정한 입력에 따라 두 Calculator
메서드 중 하나가 호출됩니다.Calculator
컴포넌트는 React에게 새로운 입력 값과 막 수정된 input
의 현재 scale
로 this.setState()
를 호출해 스스로를 다시 렌더링하도록 요청합니다.Calculator
컴포넌트의 render
메서드를 호출하여 UI
가 어떻게 보여야하는 지 알아냅니다. 두 입력 값은 현재 온도와 활성 scale
에 따라 다시 계산됩니다. 온도 변환은 여기에서 수행합니다.Calculator
에서 계산한 새 props
로 개별 TemperatureInput
의 render
메서드를 호출합니다. 이를 통해 UI
가 어떻게 보여야하는 지 알아냅니다.React DOM
은 이상적인 입력 값과 매치하는 DOM
을 업데이트합니다. 방금 수정한 input
은 현재 값을 받고, 다른 input
은 변환 후 온도를 업데이트합니다.모든 업데이트는 같은 단계를 통하기 때문에 input
이 동기화 상태를 유지합니다.
React 애플리케이션에서 변경되는 모든 데이터에 대한 단일 "진실"이 있어야합니다. 일반적으로 state는 렌더링을 위해 필요한 컴포넌트에 먼저 추가 합니다. 그런 다음 다른 컴포넌트에서도 필요 하다면 가장 가까운 공통 조상까지 들어 올릴 수 있습니다. 다른 구성 요소간에 상태를 동기화하려는 대신 top-down data flow(하향식 데이터 흐름)에 의존해야합니다 .
state
를 올리는 것은 two-way(양방향)
바인딩 접근방식보다 더 많은 boilerplate
코드가 포함되지만 그 이점으로 디버깅이 쉬워집니다. 특정 컴포넌트에 모든 state
가 “존재”하고 해당 컴포넌트만 변경될 수 있기 때문에, 버그가 발생하는 상황이 크게 줄어듭니다. 또한 유저 입력을 거부하거나 변형하는 커스텀 로직을 구현할 수도 있습니다.
props
나 state
에서 파생될 수 있는 게 있다면, 그건 state
여서는 안됩니다. 예를 들어, celsiusValue
와 fahrenheitValue
를 둘 다 저장하는 대신 마지막으로 수정된 temperature
와 그 scale
만 보관합니다. 다른 input
값은 render()
메서드에서 그 값들을 가지고 계산할 수 있습니다. 이를 통해 유저 입력에서 정밀도를 잃지 않고 다른 필드에서 반올림을 하거나 지울 수 있습니다.
UI
에서 문제가 발생하면 React Developer Tools를 사용해서 state
업데이트를 담당하는 컴포넌트를 찾을 때까지 prop
을 검사하고 트리를 위로 옮길 수 있습니다. 이제 버그를 소스에서 추적할 수 있습니다.
잘보았습니다~