목차
상태 관리 전략
리액트에서 상태 끌어올리기는 로컬 상태를 관리 확장하는 데 사용되는 주요한 전략 중 하나다. 상태는 필요에 따라 끌어올리거나 내릴 수 있다.
상태 끌어올리기는 하위 컴포넌트 두 개 이상이 같은 상태를 공유해야 할 때 해당 상태를 공통 부모 컴포넌트로 옮기는 방법이다. 예를 들어, 자식 컴포넌트인 B와 C가 사용하는 상태를 공통 부모 컴포넌트인 A로 끌어올리면, A에서 상태를 관리하고 B와 C에 props로 전달해 줄 수 있다. 이렇게 하면 상태가 변경될 때마다 모든 자식 컴포넌트가 동기화된다.
상태 끌어내리기는 부모 컴포넌트가 자식 컴포넌트의 상태를 대신 관리하는 경우다. 하지만 이 방법은 중간에 다른 컴포넌트가 많아지면 비효율적이 될 수 있다.
Props Drilling
상태를 끌어올려 부모 컴포넌트에서 관리할 때 상태가 필요한 자식 컴포넌트가 트리 깊숙한 곳에 있다고 생각해 보자. 예를 들어 App 컴포넌트의 상태를 손자 컴포넌트인 D에 전달해야 할 때 App → B → C → D의 구조로 전달하게 된다. 이때 B와 C는 상태를 사용하지 않으면서 D에 전달만 하기 위해 props를 받아야 한다. 이처럼 중간 컴포넌트들이 불필요하게 props를 전달하는 과정을 Props Drilling이라고 한다.
Props Drilling은 코드를 복잡하게 만들고 유지보수를 어렵게 만든다. 컴포넌트 구조가 조금만 바뀌어도 관련된 props 전달 경로를 모두 수정해야 하기 때문이다.
컨텍스트 사용해 공유
Props Drilling 문제를 해결하기 위한 리액트의 공식적인 해법이 컨텍스트(Context)다. Context는 컴포넌트 트리를 따라 props를 일일이 전달하지 않고도 필요한 데이터를 깊숙한 하위 컴포넌트로 바로 텔레포트(teleport) 시켜주는 역할을 한다.
Context는 주로 다음과 같은 전역 상태 관리에 사용된다.
- 테마 관리: 다크/라이트 모드처럼 앱 전체의 스타일을 변경할 때
- 인증 관리: 로그인한 사용자 정보, 토큰 등 여러 컴포넌트에서 필요한 데이터를 공유할 때
- 언어 설정: 다국어 지원이 필요한 앱에서 현재 언어 정보를 관리할 때
Context는 다음 3단계 방법으로 사용한다.
1단계: createContext()로 컨텍스트 객체 생성
// src/contexts/ThemeContext.js
import { createContext } from 'react'
export const ThemeContext = createContext()
2단계: <Provider>로 값 제공
값을 공유할 컴포넌트들을 Context.Provider로 감싸고 value 속성으로 데이터를 전달한다.
// src/App.js
import { ThemeContext } from '@/contexts/ThemeContext'
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
)
}
3단계: useContext() 훅으로 값 사용
하위 컴포넌트에서 useContext() 훅을 사용해서 전달받은 값을 쉽게 읽을 수 있다.
// src/components/Toolbar.js
import { useContext } from 'react'
import { ThemeContext } from '@/contexts/ThemeContext'
export default function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext)
// ... theme 값을 자유롭게 사용할 수 있다.
}
컨텍스트 관리
컨텍스트 관련 로직이 복잡해지면 별도의 파일로 분리하고 커스텀 훅을 만들어 관리하는 것이 좋다. 아래 예시처럼 관리하면 컴포넌트에서 useContext 대신 useTheme 커스텀 훅을 호출하기만 하면 된다. 이렇게 하면 코드가 훨씬 깔끔하고 안전해진다.
// src/hooks/useTheme.js
import { useContext } from 'react'
import { ThemeContext } from '@/contexts/ThemeContext'
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme는 ThemeProvider 안에서 사용되어야 합니다.')
}
return context
}
오늘 하루를 돌아보며
이틀에 걸쳐 컨텍스트로 상태를 공유하는 방법에 대해 배웠다. 그리고 지금까지 배운 것들을 복습할 수 있는 할 일 목록 관리하기 실습도 진행했다. 리액트에서 상태를 공유하는 다양한 방법이 있는데, 무조건 한 가지 방법만 사용하는 것이 아니라 컴포넌트의 관계와 상태의 복잡성에 따라 적절한 전략을 선택하는 것이 중요하다는 생각이 들었다.
필요할 떄는 별도 파일로 분리하거나, 하나의 파일로 합칠 수도 있고, 부모 컴포넌트에서 관리할 수도 있고, 자식 컴포넌트에서만 사용하면 부모 컴포넌트로 끌어올리지 않을 수도 있다. 상태 관리를 위해 reduce와 context를 적절한 때에 사용하는 것도 중요하다. 필요하다면 커스텀 훅을 작성할 수도 있을 것이다.
언제 어떤 전략을 선택해야 하는가는 많이 시도하고 많이 고민해야 알 수 있는 것 같다. 경험이 쌓일수록 상태 관리도 더 잘하게 될 것 같다.