상태 끌어올리기와 Context
Article
8강에서 state는 컴포넌트마다 따로 생긴다고 했다. 각자 기억하니 서로 간섭하지 않는다.
그런데 두 컴포넌트가 같은 값을 봐야 한다면 어떻게 할까. Part 5의 마지막 강의다.
형제는 서로를 모른다
온도를 섭씨와 화씨로 동시에 보여주는 화면을 만든다.
function Celsius() {
const [value, setValue] = useState(0);
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
function Fahrenheit() {
const [value, setValue] = useState(32);
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
각자 state를 가지니 서로 모른다. 섭씨를 바꿔도 화씨는 그대로다.
5강에서 "데이터는 아래로만 흐른다"고 했다. 형제끼리 옆으로 흐르는 길은 없다.
- App
- Celsius
- Fahrenheit
상태 끌어올리기
답은 단순하다. 둘 다 볼 수 있는 곳, 즉 공통 부모로 state를 옮긴다.
function App() {
const [celsius, setCelsius] = useState(0);
return (
<>
<Celsius value={celsius} onChange={setCelsius} />
<Fahrenheit value={celsius * 1.8 + 32} onChange={f => setCelsius((f - 32) / 1.8)} />
</>
);
}
function Celsius({ value, onChange }) {
return <input value={value} onChange={e => onChange(Number(e.target.value))} />;
}
Celsius와 Fahrenheit는 이제 state를 갖지 않는다. 값과 콜백을 props로 받을 뿐이다.
- App 의 state유일한 진실
- props 로 내려간다value
- 자식이 콜백을 부른다onChange
- App 이 state 를 바꾼다둘 다 다시 그려진다
5강에서 배운 단방향 흐름 그대로다. 값이 한 곳에만 있으니 어긋날 수가 없다.
이렇게 state를 갖지 않고 props만 받는 컴포넌트를 제어 컴포넌트라 부른다. 10강의 <input>과 같은 구조다.
그런데 너무 깊으면
6강에서 본 prop drilling이다. 테마 색상을 열 단계 아래에서 쓴다면, 중간의 아홉 컴포넌트가 전부 theme을 받아 넘겨야 한다.
- Apptheme 을 가진다
- Layout안 쓰는데 받는다
- Sidebar안 쓰는데 받는다
- Button여기서 쓴다
6강에서 합성으로 한 겹 줄이는 법을 봤다. 그걸로 안 되는 경우에 쓰는 것이 Context다.
Context — 트리 전체에 값을 뿌린다
세 단계로 쓴다.
- 01
만든다
인자는 기본값이다. Provider 없이 쓰면 이 값이 나온다.
const ThemeContext = createContext("light"); - 02
제공한다
이 컴포넌트 아래 모든 자손이 값을 볼 수 있다.
<ThemeContext value={theme}> <Layout /> </ThemeContext> - 03
읽는다
얼마나 깊든 상관없다. props 를 거치지 않는다.
const theme = useContext(ThemeContext);
전부 합치면 이렇다.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext value={theme}>
<Layout />
<button onClick={() => setTheme(t => (t === "dark" ? "light" : "dark"))}>
전환
</button>
</ThemeContext>
);
}
function Layout() {
return <Sidebar />;
}
function Sidebar() {
return <Button />;
}
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>버튼</button>;
}
Layout과 Sidebar는 theme을 모른다. 알 필요가 없어졌다.
Context 의 대가
편해 보이지만 공짜가 아니다.
props
명시적
- 값이 어디서 왔는지 보인다
- 컴포넌트를 따로 떼어 쓰기 쉽다
- 깊어지면 지겹다
Context
암묵적
- 값이 어디서 왔는지 안 보인다
- Provider 없이는 동작하지 않는다
- 값이 바뀌면 읽는 모두가 리렌더링
마지막 항목이 실전에서 아프다. Context 값이 바뀌면 useContext로 읽는 모든 컴포넌트가 다시 그려진다. 자주 바뀌는 값을 넣으면 앱 전체가 흔들린다.
언제 Context 를 쓰나
순서가 있다.
- 01
먼저 props 로 넘긴다
두세 단계라면 그냥 넘긴다. 명시적인 게 낫다.
- 02
합성을 시도한다
6강에서 본 대로 완성된 조각을
children으로 넘기면 중간 단계가 사라진다. - 03
그래도 깊고 넓다면 Context
테마, 로그인 사용자, 언어 설정처럼 앱 전체가 쓰고 자주 안 바뀌는 값.
| Context 에 알맞다 | 알맞지 않다 |
|---|---|
| 테마 (다크 · 라이트) | 입력창의 값 |
| 로그인한 사용자 | 스크롤 위치 |
| 언어 설정 | 마우스 좌표 |
| 라우팅 정보 | 자주 바뀌는 폼 상태 |
기준은 얼마나 자주 바뀌는가다. 자주 바뀌는 값은 가까운 곳에 두고 props로 넘긴다.
커스텀 훅으로 감싼다
Context를 쓸 때 흔한 패턴이다. 16강의 커스텀 훅을 쓴다.
const ThemeContext = createContext(null);
export function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error("useTheme 은 ThemeProvider 안에서만 쓸 수 있다");
}
return context;
}
Provider를 빠뜨렸을 때 null이 조용히 퍼지는 대신 즉시 에러가 난다. 자바스크립트 편 2강에서 말한 대로, 에러는 문제를 정확히 짚어주는 리포트다.
정리하면
- 형제끼리는 값을 나눌 수 없다. 공통 부모로 state를 끌어올린다.
- 필요한 만큼만 올린다. 최상위로 다 올리면
App이 뒤덮인다. - 깊은 전달이 지겨우면 먼저 합성을 시도하고, 그래도 안 되면 Context를 쓴다.
- Context는 값을 전달하는 통로일 뿐 상태 관리 도구가 아니다.
- Context 값이 바뀌면 읽는 모든 컴포넌트가 리렌더링된다. 자주 바뀌는 값을 넣지 않는다.
- 객체를 넣을 때는
useMemo로 주소를 고정한다. - 커스텀 훅으로 감싸 Provider 누락을 즉시 에러로 만든다.
여기까지가 React의 기초다. 1강에서 "화면을 직접 고치지 않는다"로 시작해 값을 어디에 두고 어떻게 나눌지로 닫았다.
남은 Part 6은 부록에 가깝다. React 19에서 새로 생긴 것들을 본다. 지금까지 배운 것만으로도 앱을 만들 수 있지만, 폼과 비동기를 다루다 보면 반복되는 지겨운 코드가 있다. React 19가 그걸 줄여준다.