React 학습 원페이지

React Context 완전 정리

Props Drilling부터 createContext, Provider, useContext, useState 연동, RadioGroup, Tabs, Controlled / Uncontrolled 패턴까지 한 번에 정리합니다.

Context = 여러 컴포넌트가 공통으로 사용할 데이터를 props로 단계별 전달하지 않고 공유하는 방법

1. Context가 왜 필요한가?

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>
}

Props Drilling 구조

App │ user ▼ Page │ user ▼ Header │ user ▼ UserName
실제로 user가 필요한 것은 UserName뿐인데 Page와 Header도 단지 전달을 위해 user를 받아야 합니다. 이것을 Props Drilling이라고 합니다.

2. Context를 사용하면

App │ │ UserContext.Provider ▼ Page │ ▼ Header │ ▼ UserName │ └── useContext(UserContext)

중간의 Page, Header는 user를 받을 필요가 없습니다. 실제 필요한 UserName이 Context에서 직접 가져옵니다.

3. Context 기본 구조

createContext()
공유할 데이터 통로를 만든다.
Provider
하위 컴포넌트에게 값을 공급한다.
value
Context로 어떤 값을 공유할지 정한다.
useContext()
공유된 값을 실제 컴포넌트에서 꺼낸다.
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>
}

4. 객체도 전달할 수 있다

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>
  )
}

5. useState와 Context를 같이 쓰는 이유

실무에서는 단순 값 하나보다 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>
  )
}

상태 변경 흐름

App │ ├─ useState │ theme = "light" │ setTheme() │ ▼ ThemeContext.Provider │ value={{ theme, setTheme }} ▼ ThemeButton │ ├─ useContext(ThemeContext) └─ setTheme("dark") │ ▼ App state 변경 │ ▼ App 재렌더링 │ ▼ Provider value 변경 │ ▼ Context를 사용하는 컴포넌트 재렌더링
중요: Context가 state를 만드는 것이 아닙니다. state는 useState가 만들고, Context는 그 state와 함수를 다른 컴포넌트에 전달합니다.

6. 실전 예제: RadioGroup + Radio

그룹형 컴포넌트는 Context를 이해하기 가장 좋은 예제입니다.

<RadioGroup
  value={gender}
  onChange={setGender}
>
  <Radio value="male">남자</Radio>
  <Radio value="female">여자</Radio>
</RadioGroup>

Context 생성

const RadioGroupContext = createContext(null)

RadioGroup

function RadioGroup({ value, onChange, children }) {
  return (
    <RadioGroupContext.Provider
      value={{ value, onChange }}
    >
      {children}
    </RadioGroupContext.Provider>
  )
}

Radio

function Radio({ value, children }) {
  const group = useContext(RadioGroupContext)

  return (
    <label>
      <input
        type="radio"
        checked={group.value === value}
        onChange={() => group.onChange(value)}
      />
      {children}
    </label>
  )
}

실행 원리

gender = "male" │ ▼ RadioGroup value="male" │ ▼ Context = { value: "male", onChange: setGender } │ ├─ Radio value="male" │ "male" === "male" → true │ └─ Radio value="female" "male" === "female" → false
여자를 클릭하면 결국 setGender('female')가 실행되고 App이 재렌더링됩니다. 이후 Context value도 female로 바뀌고 각 Radio의 checked가 다시 계산됩니다.

7. 실전 예제: Tabs + Tab

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인지 판단하는 핵심입니다.

8. Context + Controlled / Uncontrolled

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>
  )
}

Uncontrolled

<Tabs defaultValue="home">
  <Tab value="home">홈</Tab>
  <Tab value="product">상품</Tab>
</Tabs>

Controlled

<Tabs value={tab} onChange={setTab}>
  <Tab value="home">홈</Tab>
  <Tab value="product">상품</Tab>
</Tabs>

9. Context의 범위

Context는 완전한 전역변수가 아니라 Provider 아래쪽 트리에서만 접근할 수 있습니다.

UserContext.Provider ├── Header → 사용 가능 └── Main └── Profile → 사용 가능

같은 Context Provider가 중첩되면 자신과 가장 가까운 Provider의 값을 사용합니다.

<UserContext.Provider value="홍길동">
  <ComponentA />

  <UserContext.Provider value="김철수">
    <ComponentB />
  </UserContext.Provider>
</UserContext.Provider>
ComponentA → 홍길동 ComponentB → 김철수

10. createContext(null)을 많이 쓰는 이유

const TabContext = createContext(null)

Provider 없이 Tab을 단독 사용했을 때 오류를 쉽게 잡을 수 있습니다.

function useTabContext() {
  const context = useContext(TabContext)

  if (!context) {
    throw new Error(
      'Tab 관련 컴포넌트는 Tabs 내부에서 사용해야 합니다.'
    )
  }

  return context
}
실무에서는 이런 식으로 Custom Hook + Context 조합을 많이 사용합니다.

11. 언제 Context를 쓰고 언제 props를 쓰나?

상황추천 방식이유
부모 → 바로 자식props가장 단순하고 명확함
ThemeContext많은 하위 컴포넌트가 공유
로그인 사용자 정보Context여러 화면에서 공통 사용
RadioGroup + RadioContext그룹 공통 상태 공유
Tabs + TabContext선택값과 onChange 공유
CheckboxGroup + CheckboxContext그룹 공통 로직 공유
복잡한 앱 전역 상태Zustand / Redux 고려상태관리 자체가 복잡해짐

12. props / state / Context 차이

props
부모가 자식에게 데이터를 전달한다.
state
컴포넌트가 상태를 만들고 변경한다.
Context
트리 안의 여러 컴포넌트가 공통 데이터를 공유한다.
Redux / Zustand
앱 규모의 복잡한 상태를 관리한다.
Context = Redux 대체품이라고 단순하게 보면 안 됩니다. Context의 핵심은 값 전달 / 공유이고 Redux나 Zustand는 상태관리 구조에 더 초점이 있습니다.

13. Context 최종 공식

// 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)
createContext ↓ 공유 통로 생성 ↓ Provider ↓ value={{ ... }} 전달 ↓ useContext ↓ 공유 데이터 사용 ↓ state 변경 ↓ Provider value 변경 ↓ Context를 사용하는 컴포넌트 재렌더링

14. 추천 연습 순서

Context를 제대로 익히려면 아래 순서가 가장 이해하기 쉽습니다.

순서연습 컴포넌트핵심
1RadioGroup + Radiovalue / onChange 공유
2Tabs + Tabactive 계산
3CheckboxGroup + Checkbox배열 상태 공유
4Accordion + Item열림 상태 공유
5Form + Field검증 상태와 메시지 공유