🔎 React 공식문서 자료
React_State 끌어올리기
종종 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야 할 필요가 있다. 이럴 때는 가장 가까운 공통 조상으로 state를 끌러올리는 것이 좋다.
⚙️ 예시 코드 - 1
BoilingVeridict
컴포넌트 생성celsius
prop를 받아서 이 온도가 물이 끓기에 충분하지 여부를 출력function BoilingVerdict(props) {
if (props.celsius >= 100) {
// props.celsius가 100 이상인 경우
return <p>The water would boil.</p>;
}
// props.celsius가 100 미만인 경우
return <p>The water would not boil.</p>;
}
⚙️ 예시 코드 - 2
Calculatro
컴포넌트 생성<input>
을 렌더링하고 그 값을 this.state.temperature
에 저장BoilingVerdict
컴포넌트를 렌더링class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
// ✨ 제어 컴포넌트
// React 값에 의해 폼 제어
this.state = {temperature: ''};
}
handleChange(e) {
// ✨ 제어 컴포넌트
// setState()로 값을 업데이트
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} />
// ⚙️ parseFloat
// 주어진 값을 부동소수점 실수로 파싱해서 반환
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
새 요구사항으로써 섭씨 입력 필드뿐만 아니라 화씨 입력 필드를 추가하고 두 필드 간에 동기화 상태를 유지하도록 해보자!
⚙️ 예시 코드 - 1
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>
);
}
}
⚙️ 예시 코드 - 2
Calculator
가 분리된 두 개의 온도 입력 필드를 렌더링하도록 변경class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
이제 두 개의 입력 필드를 갖게 됐다. 그러나 둘 중 하나에 온도를 입력하더라도 다른 하나는 갱신되지 않는 문제가 발생한다. 이것은 두 입력 필드 간에 동기화 상태를 유지하고자 했던 원래 원래 요구사항과는 맞지 않다.
또한, Calculator
에서 BoilingVerdict
도 보여줄 수 없는 상황이다. 현재 입력된 온도 정보가 TemperatureInput
안에 숨겨져 있으므로 Calculator
는 그 값을 알 수 없다.
⚙️ 예시 코드 - 1
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
이 두 함수는 숫자를 변환한다. 이제 temperature
문자열과 변환 함수를 인수로 취해서 문자열을 반환하는 또 다른 함수를 생성한다. 그리고 그것을 한 입력값에 기반해 나머지 입력 값을 계산하는 용도로 사용할 것이다.
⚙️ 예시 코드 - 2
temperature
값에 대해서는 빈 문자열을 반환하고 값을 소수점 세 번째 자리로 반올림하여 출력// tryConvert는 두 개의 매개변수를 사용
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
// Number.isNaN() 메서드는 전달된 값이 NaN인지 아닌지를 검사
// 오직 숫자인 값에만 동작, NaN인 경우 동작 X
if (Number.isNaN(input)) {
// NaN인 경우 빈 문자열을 반환
return '';
}
const output = convert(input);
// Math.round() 메서드는 입력값을 반올림한 수와 가장 가까운 정수 값을 반환
// 즉, 우리가 수학에서 배운 반올림과 같다!
const rounded = Math.round(output * 1000) / 1000;
// toString() 메서드는 문자열을 반환
return rounded.toString();
}
⚙️ 예시 코드 - 1
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를 끌어올림으로써 이뤄낼 수 있다. 이렇게 하는 방법을 state 끌어올리기라고 부른다. 이제 TemperatureInput
이 개별적으로 가지고 있던 지역 state를 지우는 대신 Calculator
로 그 값을 옮겨놓을 것이다.
Clculator
가 공유될 state를 소유하고 있으면 이 컴포넌트는 두 입력 필드의 현재 온도에 대한 진리의 원천(source of truth) 이 된다. 이를 통해 두 입력 필드가 서로 간에 일관된 값을 유지하도록 만들 수 있다. 두 TemperatureInput
컴포넌트의 prop가 같은 부모인 Calculator
로 부터 전달되기 때문에 두 입력 필드는 항상 동기화된 상태를 유지할 수 있다.
⚙️ 예시 코드 - 2
TemperatureInput
컴포넌트에서 this.state.temperature
를 this.props.temperature
로 대체Calculator
로 부터 보내주어야 함 render() {
// Before: const temperature = this.state.temperature;
// ✨ props는 읽기 전용!!!
const temperature = this.props.temperature;
// ...
props는 읽기 전용이다.
temperature
가 지역 state였을 때는 그 값을 변경하기 위해서 그저 TemperatureInput
의 this.setState()
를 호출하는 걸로 충분했다. 그러나 이제 temperature
가 부모로부터 prop로 전달되기 때문에 TemperatureInput
은 그 값을 제어할 능력이 없다.
React에서는 보통 이 문제를 컴포넌트를 제어가능하게 만드는 방식으로 해결한다. DOM <input>
이 value
와 onChange
prop를 건네받는 것과 비슷한 방식으로, 사용자 정의된 TemperatureInput
역시 temperature
와 onTemperatureChange
props 자신의 부모인 Calculator
로 부터 받을 수 있다.
⚙️ 예시 코드 - 3
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와 함께 제공될 것이다. 이를 이용해 자신의 지역 state를 수정해서 변경사항을 처리하므로, 변경된 새 값을 전달받은 두 입력 필드는 모두 리렌더링될 것이다.
Calculator
의 변경사항을 보기 전에 TemperatureInput
컴포넌트에 대한 변경사항을 요약하면 다음과 같다. 이 컴포넌트의 지역 state를 제거했으며 this.state.temperature
대신에 this.props.temperature
를 읽어오도록 변경했다. state를 변결하고 싶을 경우 this.setState()
대신에 Calculator
로부터 받은 this.props.onTemperatureChange()
를 호출하도록 만들었다.
⚙️ 예시 코드 - 3
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
// input값을 가져옴
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
의 현재 입력값을 이 컴포넌트의 지역 state에 저장한다. 이것은 개발자가 입력 필드들로부터 끌어올린 state이며 그들에 대한 진리의 원천(source of truth) 으로 작용할 것이다. 또한, 두 입력 필드를 렌더링하기 위해서 알아야 하는 모든 데이터를 최소한으로 표현하는 것이기도 한다.
⚙️ 예시 코드 - 4
// 섭씨 입력 필드에 37을 입력하면 `Calculator` 컴포넌트의 sate는 다음과 같다.
{
temperature: '37',
scale: 'c'
}
// 이후에 화씨 입력 필드의 값을 212로 수정하면 Calculator의 state는 다음과 같다.
{
temperature: '212',
scale: 'f'
}
두 입력 필드에 모두 값을 저장하는 일도 가능했지만 결국은 불필요한 작업이었다...
가장 최근에 변경된 입력값과 그 값이 나타내는 단위를 저장하는 것만으로도 충분하다. 그러고 나면 현재의 temperature
와 scale
에 기반해 다른 입력 필드의 값을 추론할 수 있다.
두 입력 필드의 값이 동일한 state로부터 계산되기 때문에 이 둘은 항상 동기화된 상태를 유지하게 된다.
⚙️ 예시 코드 - 5
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
이 갱신된다. 입력 필드 중 하나는 있는 그대로의 값을 받으므로 사용자가 입력한 값이 보존되고, 다른 입력 필드의 값은 항상 다른 하나에 기반해 재계산된다.
입력 값을 변경할 경우 일어나는 과정은 다음과 같다.
React는 DOM <input>
의 onChange에 지정된 함수를 호출한다. 위 예시의 경우 TemperatureInput
의 hanldeChange
메서드에 해당한다.
TemperatureInput
컴포넌트의 handleChange
메서드는 새로 입력된 값고 함께 this.props.onTemperatureChange()
를 호출한다. onTemperatureChange
를 포함한 이 컴포넌트의 props는 부모 컴포넌트인 Calculator
로부터 제공받은 것이다.
이전 렌더링 단계에서, Calculator
는 섭씨 TemperatureInput
의 onTemperatureChange
를 Calculator
의 handleCelsiusChange
메서드로, 화씨 TemperatureInput
의 onTemperatureChange
를 Calculator
의 handleFahrenheitChange
메서드로 지정해 놓는다. 따라서 어떤 입력 필드를 수정하느냐에 따라 Calculator
의 두 메서드 중 하나가 호출된다.
이들 메서드는 내부적으로 Calculator
컴포넌트가 새 입력값, 그리고 현재 수정한 입력 필드의 입력 단위와 함께 this.setState()
를 호출하게 함르호써 React에게 자신을 다시 렌더링하도록 요청한다.
React는 UI가 어떻게 보여야 하는지 알아내기 위해 Calculator
컴포넌트의 render
메서드를 호출한다. 두 입력 필드의 값은 현재 온도와 활성화된 단위를 기반으로 재계산된다. 온도의 변환이 이 단께에서 수행된다.
React는 Calculator
가 전달한 새 props와 함께 각 TemperatureInput
컴포넌트의 render
메서드를 호출한다. 그러면서 UI가 어떻게 보여아 할지를 파악한다.
React는 BoilingVerdict
컴포넌트에게 섭씨온도를 props로 건네면서 그 컴포넌트의 render
메서드를 호출한다.
React DOM은 물의 끓는 여부와 올바른 입력값을 일치시키는 작업과 함께 DOM을 갱신한다. 값을 변경한 입력 필드는 현재 입력값을 그대로 받고, 다른 입력 필드는 변환된 온도 값으로 갱신된다.
입력 필드의 값을 변경할 때만다 동일한 정차를 거치고 두 입력 필드는 동기화된 상태로 유지된다.
state를 끌어올리는 작업은 양방향 바인딩 접근 방식보다 더 많은 보일러 플레이트 코드를 유발하지만, 버그를 찾고 격리하기 더 쉽게 만든다는 장점이 있다.
사용자의 입력을 거부하거나 변경하는 자체 로직을 구현할 수 있다.
어떤 값이 props 또는 state로부터 계산될 수 있다면, 아마도 그 값을 state에 두어서는 안 된다.