Props Drilling부터 createContext, Provider, useContext, useState 연동, RadioGroup, Tabs, Controlled / Uncontrolled 패턴까지 한 번에 정리합니다.
React의 기본 데이터 전달 방식은 부모 → 자식 props입니다. 하지만 실제 데이터를 쓰는 컴포넌트가 아주 아래에 있으면 중간 컴포넌트들이 필요하지 않은 props를 계속 전달해야 합니다.
function App() {
const user = '홍길동'
return <Page user={user} />
}
function Page({ user }) {
return <Header user={user} />
}
function Header({ user }) {
return <UserName user={user} />
}
function UserName({ user }) {
return <span>{user}</span>
}
중간의 Page, Header는 user를 받을 필요가 없습니다. 실제 필요한 UserName이 Context에서 직접 가져옵니다.
import { createContext, useContext } from 'react'
const UserContext = createContext()
function App() {
const user = '홍길동'
return (
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
)
}
function UserName() {
const user = useContext(UserContext)
return <span>{user}</span>
}
const user = {
name: '홍길동',
age: 30,
grade: 'VIP'
}
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
function UserInfo() {
const { name, age, grade } = useContext(UserContext)
return (
<div>
<p>{name}</p>
<p>{age}</p>
<p>{grade}</p>
</div>
)
}
실무에서는 단순 값 하나보다 state와 state 변경 함수를 같이 공유하는 경우가 많습니다.
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext()
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider
value={{ theme, setTheme }}
>
<Page />
</ThemeContext.Provider>
)
}
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext)
function handleClick() {
setTheme(theme === 'light' ? 'dark' : 'light')
}
return (
<button onClick={handleClick}>
현재 테마: {theme}
</button>
)
}
그룹형 컴포넌트는 Context를 이해하기 가장 좋은 예제입니다.
<RadioGroup
value={gender}
onChange={setGender}
>
<Radio value="male">남자</Radio>
<Radio value="female">여자</Radio>
</RadioGroup>
const RadioGroupContext = createContext(null)
function RadioGroup({ value, onChange, children }) {
return (
<RadioGroupContext.Provider
value={{ value, onChange }}
>
{children}
</RadioGroupContext.Provider>
)
}
function Radio({ value, children }) {
const group = useContext(RadioGroupContext)
return (
<label>
<input
type="radio"
checked={group.value === value}
onChange={() => group.onChange(value)}
/>
{children}
</label>
)
}
setGender('female')가 실행되고 App이 재렌더링됩니다. 이후 Context value도 female로 바뀌고 각 Radio의 checked가 다시 계산됩니다.const TabContext = createContext(null)
function Tabs({ value, onChange, children }) {
return (
<TabContext.Provider value={{ value, onChange }}>
{children}
</TabContext.Provider>
)
}
function Tab({ value, children }) {
const group = useContext(TabContext)
const active = group.value === value
return (
<button
className={active ? 'active' : ''}
onClick={() => group.onChange(value)}
>
{children}
</button>
)
}
<Tabs value={tab} onChange={setTab}>
<Tab value="home">홈</Tab>
<Tab value="product">상품</Tab>
</Tabs>
const active = group.value === value가 현재 선택된 Tab인지 판단하는 핵심입니다.function Tabs({
value,
defaultValue = 'home',
onChange,
children
}) {
const [internalValue, setInternalValue] = useState(defaultValue)
const selectedValue = value ?? internalValue
function handleChange(nextValue) {
if (value === undefined) {
setInternalValue(nextValue)
}
onChange?.(nextValue)
}
return (
<TabContext.Provider
value={{
value: selectedValue,
onChange: handleChange
}}
>
{children}
</TabContext.Provider>
)
}
<Tabs defaultValue="home">
<Tab value="home">홈</Tab>
<Tab value="product">상품</Tab>
</Tabs>
<Tabs value={tab} onChange={setTab}>
<Tab value="home">홈</Tab>
<Tab value="product">상품</Tab>
</Tabs>
Context는 완전한 전역변수가 아니라 Provider 아래쪽 트리에서만 접근할 수 있습니다.
같은 Context Provider가 중첩되면 자신과 가장 가까운 Provider의 값을 사용합니다.
<UserContext.Provider value="홍길동">
<ComponentA />
<UserContext.Provider value="김철수">
<ComponentB />
</UserContext.Provider>
</UserContext.Provider>
const TabContext = createContext(null)
Provider 없이 Tab을 단독 사용했을 때 오류를 쉽게 잡을 수 있습니다.
function useTabContext() {
const context = useContext(TabContext)
if (!context) {
throw new Error(
'Tab 관련 컴포넌트는 Tabs 내부에서 사용해야 합니다.'
)
}
return context
}
| 상황 | 추천 방식 | 이유 |
|---|---|---|
| 부모 → 바로 자식 | props | 가장 단순하고 명확함 |
| Theme | Context | 많은 하위 컴포넌트가 공유 |
| 로그인 사용자 정보 | Context | 여러 화면에서 공통 사용 |
| RadioGroup + Radio | Context | 그룹 공통 상태 공유 |
| Tabs + Tab | Context | 선택값과 onChange 공유 |
| CheckboxGroup + Checkbox | Context | 그룹 공통 로직 공유 |
| 복잡한 앱 전역 상태 | Zustand / Redux 고려 | 상태관리 자체가 복잡해짐 |
// 1. Context 생성
const MyContext = createContext(null)
// 2. Provider로 값 공급
<MyContext.Provider
value={{ value, onChange }}
>
{children}
</MyContext.Provider>
// 3. 하위 컴포넌트에서 꺼내기
const { value, onChange } = useContext(MyContext)
// 4. 사용
const active = selectedValue === value
onChange(value)
Context를 제대로 익히려면 아래 순서가 가장 이해하기 쉽습니다.
| 순서 | 연습 컴포넌트 | 핵심 |
|---|---|---|
| 1 | RadioGroup + Radio | value / onChange 공유 |
| 2 | Tabs + Tab | active 계산 |
| 3 | CheckboxGroup + Checkbox | 배열 상태 공유 |
| 4 | Accordion + Item | 열림 상태 공유 |
| 5 | Form + Field | 검증 상태와 메시지 공유 |