종종 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야 할 필요가 있다.
- 이럴 때 가장 가까운 공통의 피상속 컴포넌트로 state를 끌어올리는 것이 좋다.
- 어떻게 할 수 있는지 살펴보자.
- 공식문서 내용을 하나하나 따라하며 원리를 이해해보자.
import { useState } from "react";
import BoilingVerdict from "./BoilingVerdict";
import TemperatureInput from "./TemperatureInput";
function Calculator() {
const [temperatureInfo, setTemperatureInfo] = useState({
temperature: "",
scale: "",
});
const toCelsius = (fahrenheit) => ((fahrenheit - 32) * 5) / 9;
const toFahrenheit = (celsius) => (celsius * 9) / 5 + 32;
const 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();
};
const handleCelsiusChange = (temperature) => {
setTemperatureInfo({ ...temperatureInfo, scale: "c", temperature });
};
const handleFahrenheitChange = (temperature) => {
setTemperatureInfo({ ...temperatureInfo, scale: "f", temperature });
};
const celsius =
temperatureInfo.scale === "f"
? tryConvert(temperatureInfo.temperature, toCelsius)
: temperatureInfo.temperature;
const fahrenheit =
temperatureInfo.scale === "c"
? tryConvert(temperatureInfo.temperature, toFahrenheit)
: temperatureInfo.temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
export default Calculator;
Calculator 컴포넌트로 temperature라는 상태가 끌어올려졌다.
- 각각의 Temperature 태그에 값이 입력될 때마다 Calculator 컴포넌트는 입력받은 값을 삼항연산자로 판별하여 변환하고, 변환된 값은 입력받지 않은 태그의 value로 입력해준다.
- 여기서 중요한 점은 2개의 컴포넌트에서 사용되는 temperature 값을 가장 가까운 공통의 피상속 컴포넌트인 Calculator로 끌어올려서 사용하고 있다는 점이다.
function TemperatureInput({ temperature, scale, onTemperatureChange }) {
const handleChange = (e) => {
onTemperatureChange(e.target.value);
};
return (
<fieldset>
<legend>
온도를 입력하세요.({scale === "c" ? "Celsius" : "Fahrenheit"})
</legend>
<input value={temperature} onChange={handleChange} />
</fieldset>
);
}
export default TemperatureInput;
TemperatureInput 컴포넌트에서는 Calculator에게 props객체로 받은
temperature, scale, onTemperatureChange
프로퍼티들을 구조 분해하여 사용하고 있다.
- 여기서
onTemperatureChange
는 TeperatureInput 컴포넌트의 입력값이 변할 때 호출된다.- onTemperatureChange 함수가 호출될 때 현재 입력받은 e.target.value를 매개변수로 넣어 전달된다.
- onTemperatureChange라는 이름으로 전달받는 함수는 섭씨와 화씨일 때 다른 함수가 담겨 전달된다.
- 섭씨일 때는 Calculator 컴포넌트에 정의된 handleCelsiusChange함수가 담겨있고, 화씨일 때는 handleFahrenheitChange함수가 담겨있다.
- 각 함수가 호출될 때 setTemperatureInfo()를 통해 React에게 자신을 다시 렌더링하도록 요청하는 것을 알 수 있다.
- 즉, input태그에 값이 입력될 때마다 Calculator 컴포넌트에 정의된 로직을 거쳐(렌더링도 됨), 섭씨input에 입력했을 때는 화씨input에 섭씨input에 적힌 숫자를 화씨로 변환된 값이 입력되며(렌더링될 때) 반대도 마찬가지이다.
function BoilingVerdict({ celsius }) {
return celsius >= 100 ? (
<p>The water would boil.</p>
) : (
<p>The water would not boil.</p>
);
}
export default BoilingVerdict;
BoilingVerdict 컴포넌트에서는 Calculator에서 props객체로 내려준 celsius 프로퍼티를 구조 분해해서 사용하고 있다.
- Calculator의 상태값을 내려받아 사용하고 있다!
React 애플리케이션 안에서 변경이 일어나는 데이터에 대해서는 "source of truth"를 하나만 두어야 한다.
- 풀어서 얘기하자면 보통의 경우는 state는 렌더링에 그 값을 필요로 하는 컴포넌트에 먼저 추가되는데, 다른 컴포넌트도 역시 그 값이 필요하게 되면 그 값을 가장 가까운 공통의 피상속 컴포넌트로 끌어올리면 된다는 것이다.
- 다른 컴포넌트 간에 존재하는 state를 동기화 시키려고 노력하는 대신
하향식 데이터 흐름
을 추천한다고 한다.- 물론, state를 끌어올리는 작업은 각 컴포넌트에서 접근하는 양방향 바인딩 접근 방식보다 더 많은 "보일러 플레이트" 코드를 유발하지만, 버그를 찾고 격리하기 더 쉽게 만든다는 장점이 있다.
- 어떤 값이 props 또는 state로부터 계산될 수 있다면, 아마 그 값을 state에 둬서는 안된다.