useContext

김동현·2026년 3월 17일

useContext

useContext는 컴포넌트에서 context를 읽고 구독할 수 있게 해주는 React Hook이에요.

const value = useContext(SomeContext)

레퍼런스 {/reference/}

useContext(SomeContext) {/usecontext/}

컴포넌트의 최상위 레벨에서 useContext를 호출해서 context를 읽고 구독하세요.

import { useContext } from 'react';

function MyComponent() {
  const theme = useContext(ThemeContext);
  // ...

아래에서 더 많은 예제를 볼 수 있어요.

매개변수 {/parameters/}

  • SomeContext: 이전에 createContext로 생성한 context예요. context 자체는 정보를 보관하지 않아요, 단지 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낼 뿐이에요.

💡 부연 설명: context는 일종의 "통로"라고 생각하면 돼요. 실제 데이터는 Provider가 제공하고, useContext는 그 통로를 통해 데이터를 받아오는 거예요.

반환값 {/returns/}

useContext는 호출하는 컴포넌트에 대한 context 값을 반환해요. 이 값은 트리에서 호출하는 컴포넌트 위에 있는 가장 가까운 SomeContext.Provider에 전달된 value로 결정돼요. 그런 provider가 없다면, 반환되는 값은 해당 context를 위해 createContext에 전달한 defaultValue가 될 거예요. 반환된 값은 항상 최신 상태예요. React는 context가 변경되면 그 context를 읽는 컴포넌트를 자동으로 리렌더링해요.

주의사항 {/caveats/}

  • 컴포넌트의 useContext() 호출은 같은 컴포넌트에서 반환된 provider의 영향을 받지 않아요. 해당하는 <Context.Provider>useContext() 호출을 하는 컴포넌트보다 위에 있어야 해요.
  • React는 다른 value를 받은 provider부터 시작해서 특정 context를 사용하는 모든 자식을 자동으로 리렌더링해요. 이전 값과 다음 값은 Object.is 비교로 비교돼요. memo로 리렌더링을 건너뛰더라도 자식이 새로운 context 값을 받는 것을 막지는 못해요.
  • 빌드 시스템이 출력물에 중복 모듈을 생성하면(symlink로 인해 발생할 수 있어요), context가 깨질 수 있어요. context를 통해 무언가를 전달하는 것은 context를 제공하는 데 사용하는 SomeContext와 읽는 데 사용하는 SomeContext=== 비교로 판단했을 때 정확히 같은 객체일 때만 작동해요.

💡 부연 설명: 이건 주로 모듈 번들링 설정 문제로 발생하는데, 같은 context가 두 개의 다른 객체로 인식되면 제대로 작동하지 않아요.


사용법 {/usage/}

트리 깊숙이 데이터 전달하기 {/passing-data-deeply-into-the-tree/}

컴포넌트의 최상위 레벨에서 useContext를 호출해서 context를 읽고 구독하세요.

import { useContext } from 'react';

function Button() {
  const theme = useContext(ThemeContext);
  // ... 

useContext는 전달한 context에 대한 context 값을 반환해요. context 값을 결정하기 위해 React는 컴포넌트 트리를 검색하고 해당 특정 context에 대해 위에 있는 가장 가까운 context provider를 찾아요.

Button에 context를 전달하려면, 그것 또는 부모 컴포넌트 중 하나를 해당 context provider로 감싸세요:

function MyPage() {
  return (
    <ThemeContext value="dark">
      <Form />
    </ThemeContext>
  );
}

function Form() {
  // ... 내부에 버튼을 렌더링해요 ...
}

provider와 Button 사이에 얼마나 많은 컴포넌트 레이어가 있는지는 중요하지 않아요. Form 안 어디에서든 ButtonuseContext(ThemeContext)를 호출하면, "dark"를 값으로 받을 거예요.

useContext()는 항상 그것을 호출하는 컴포넌트 위에 있는 가장 가까운 provider를 찾아요. 위쪽으로 검색하며 useContext()를 호출하는 컴포넌트 안의 provider는 고려하지 않아요.

💡 부연 설명: 자기 자신이나 자식 컴포넌트의 Provider는 보지 않고, 오직 부모/조상 컴포넌트의 Provider만 찾는다는 뜻이에요!

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext value="dark">
      <Form />
    </ThemeContext>
  )
}

function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}
.panel-light,
.panel-dark {
  border: 1px solid black;
  border-radius: 4px;
  padding: 20px;
}
.panel-light {
  color: #222;
  background: #fff;
}

.panel-dark {
  color: #fff;
  background: rgb(23, 32, 42);
}

.button-light,
.button-dark {
  border: 1px solid #777;
  padding: 5px;
  margin-right: 10px;
  margin-top: 10px;
}

.button-dark {
  background: #222;
  color: #fff;
}

.button-light {
  background: #fff;
  color: #222;
}

context를 통해 전달된 데이터 업데이트하기 {/updating-data-passed-via-context/}

시간이 지남에 따라 context를 변경하고 싶을 때가 많아요. context를 업데이트하려면, state와 결합하세요. 부모 컴포넌트에서 state 변수를 선언하고, 현재 state를 provider에 context 값으로 전달하세요.

function MyPage() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext value={theme}>
      <Form />
      <Button onClick={() => {
        setTheme('light');
      }}>
        Switch to light theme
      </Button>
    </ThemeContext>
  );
}

이제 provider 안의 모든 Button은 현재 theme 값을 받을 거예요. provider에 전달하는 theme 값을 업데이트하기 위해 setTheme을 호출하면, 모든 Button 컴포넌트가 새로운 'light' 값으로 리렌더링될 거예요.

context를 통해 값 업데이트하기 {/updating-a-value-via-context/}

이 예제에서, MyApp 컴포넌트는 state 변수를 보관하고 있고, 그것을 ThemeContext provider에 전달해요. "Dark mode" 체크박스를 체크하면 state가 업데이트돼요. 제공된 값을 변경하면 그 context를 사용하는 모든 컴포넌트가 리렌더링돼요.

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext value={theme}>
      <Form />
      <label>
        <input
          type="checkbox"
          checked={theme === 'dark'}
          onChange={(e) => {
            setTheme(e.target.checked ? 'dark' : 'light')
          }}
        />
        Use dark mode
      </label>
    </ThemeContext>
  )
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}
.panel-light,
.panel-dark {
  border: 1px solid black;
  border-radius: 4px;
  padding: 20px;
  margin-bottom: 10px;
}
.panel-light {
  color: #222;
  background: #fff;
}

.panel-dark {
  color: #fff;
  background: rgb(23, 32, 42);
}

.button-light,
.button-dark {
  border: 1px solid #777;
  padding: 5px;
  margin-right: 10px;
  margin-top: 10px;
}

.button-dark {
  background: #222;
  color: #fff;
}

.button-light {
  background: #fff;
  color: #222;
}

value="dark""dark" 문자열을 전달하지만, value={theme}는 JavaScript theme 변수의 값을 JSX 중괄호로 전달한다는 점을 주목하세요. 중괄호를 사용하면 문자열이 아닌 context 값도 전달할 수 있어요.

context를 통해 객체 업데이트하기 {/updating-an-object-via-context/}

이 예제에는 객체를 보관하는 currentUser state 변수가 있어요. { currentUser, setCurrentUser }를 하나의 객체로 결합하고 value={} 안의 context를 통해 전달해요. 이렇게 하면 LoginButton 같은 아래의 모든 컴포넌트가 currentUsersetCurrentUser를 모두 읽을 수 있고, 필요할 때 setCurrentUser를 호출할 수 있어요.

import { createContext, useContext, useState } from 'react';

const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <CurrentUserContext
      value={{
        currentUser,
        setCurrentUser
      }}
    >
      <Form />
    </CurrentUserContext>
  );
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <LoginButton />
    </Panel>
  );
}

function LoginButton() {
  const {
    currentUser,
    setCurrentUser
  } = useContext(CurrentUserContext);

  if (currentUser !== null) {
    return <p>You logged in as {currentUser.name}.</p>;
  }

  return (
    <Button onClick={() => {
      setCurrentUser({ name: 'Advika' })
    }}>Log in as Advika</Button>
  );
}

function Panel({ title, children }) {
  return (
    <section className="panel">
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  return (
    <button className="button" onClick={onClick}>
      {children}
    </button>
  );
}
label {
  display: block;
}

.panel {
  border: 1px solid black;
  border-radius: 4px;
  padding: 20px;
  margin-bottom: 10px;
}

.button {
  border: 1px solid #777;
  padding: 5px;
  margin-right: 10px;
  margin-top: 10px;
}

💡 부연 설명: context로 값 하나만 전달하는 게 아니라 객체로 여러 값을 묶어서 전달할 수 있어요. 이렇게 하면 관련된 데이터와 함수를 함께 관리할 수 있어서 편리해요!

여러 context {/multiple-contexts/}

이 예제에는 두 개의 독립적인 context가 있어요. ThemeContext는 문자열인 현재 테마를 제공하고, CurrentUserContext는 현재 사용자를 나타내는 객체를 보관해요.

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <ThemeContext value={theme}>
      <CurrentUserContext
        value={{
          currentUser,
          setCurrentUser
        }}
      >
        <WelcomePanel />
        <label>
          <input
            type="checkbox"
            checked={theme === 'dark'}
            onChange={(e) => {
              setTheme(e.target.checked ? 'dark' : 'light')
            }}
          />
          Use dark mode
        </label>
      </CurrentUserContext>
    </ThemeContext>
  )
}

function WelcomePanel({ children }) {
  const {currentUser} = useContext(CurrentUserContext);
  return (
    <Panel title="Welcome">
      {currentUser !== null ?
        <Greeting /> :
        <LoginForm />
      }
    </Panel>
  );
}

function Greeting() {
  const {currentUser} = useContext(CurrentUserContext);
  return (
    <p>You logged in as {currentUser.name}.</p>
  )
}

function LoginForm() {
  const {setCurrentUser} = useContext(CurrentUserContext);
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const canLogin = firstName.trim() !== '' && lastName.trim() !== '';
  return (
    <>
      <label>
        First name{': '}
        <input
          required
          value={firstName}
          onChange={e => setFirstName(e.target.value)}
        />
      </label>
      <label>
        Last name{': '}
        <input
        required
          value={lastName}
          onChange={e => setLastName(e.target.value)}
        />
      </label>
      <Button
        disabled={!canLogin}
        onClick={() => {
          setCurrentUser({
            name: firstName + ' ' + lastName
          });
        }}
      >
        Log in
      </Button>
      {!canLogin && <i>Fill in both fields.</i>}
    </>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, disabled, onClick }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button
      className={className}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}
label {
  display: block;
}

.panel-light,
.panel-dark {
  border: 1px solid black;
  border-radius: 4px;
  padding: 20px;
  margin-bottom: 10px;
}
.panel-light {
  color: #222;
  background: #fff;
}

.panel-dark {
  color: #fff;
  background: rgb(23, 32, 42);
}

.button-light,
.button-dark {
  border: 1px solid #777;
  padding: 5px;
  margin-right: 10px;
  margin-top: 10px;
}

.button-dark {
  background: #222;
  color: #fff;
}

.button-light {
  background: #fff;
  color: #222;
}

provider를 컴포넌트로 추출하기 {/extracting-providers-to-a-component/}

앱이 커지면, 앱의 루트에 가까운 곳에 context들의 "피라미드"가 생길 것으로 예상돼요. 그건 전혀 잘못된 게 아니에요. 하지만 중첩이 미적으로 마음에 들지 않는다면, provider를 하나의 컴포넌트로 추출할 수 있어요. 이 예제에서 MyProviders는 "배관공사"를 숨기고 필요한 provider 안에 전달된 children을 렌더링해요. themesetTheme state는 MyApp 자체에서 필요하므로, MyApp은 여전히 그 state 부분을 소유하고 있어요.

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <MyProviders theme={theme} setTheme={setTheme}>
      <WelcomePanel />
      <label>
        <input
          type="checkbox"
          checked={theme === 'dark'}
          onChange={(e) => {
            setTheme(e.target.checked ? 'dark' : 'light')
          }}
        />
        Use dark mode
      </label>
    </MyProviders>
  );
}

function MyProviders({ children, theme, setTheme }) {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <ThemeContext value={theme}>
      <CurrentUserContext
        value={{
          currentUser,
          setCurrentUser
        }}
      >
        {children}
      </CurrentUserContext>
    </ThemeContext>
  );
}

function WelcomePanel({ children }) {
  const {currentUser} = useContext(CurrentUserContext);
  return (
    <Panel title="Welcome">
      {currentUser !== null ?
        <Greeting /> :
        <LoginForm />
      }
    </Panel>
  );
}

function Greeting() {
  const {currentUser} = useContext(CurrentUserContext);
  return (
    <p>You logged in as {currentUser.name}.</p>
  )
}

function LoginForm() {
  const {setCurrentUser} = useContext(CurrentUserContext);
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const canLogin = firstName !== '' && lastName !== '';
  return (
    <>
      <label>
        First name{': '}
        <input
          required
          value={firstName}
          onChange={e => setFirstName(e.target.value)}
        />
      </label>
      <label>
        Last name{': '}
        <input
        required
          value={lastName}
          onChange={e => setLastName(e.target.value)}
        />
      </label>
      <Button
        disabled={!canLogin}
        onClick={() => {
          setCurrentUser({
            name: firstName + ' ' + lastName
          });
        }}
      >
        Log in
      </Button>
      {!canLogin && <i>Fill in both fields.</i>}
    </>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, disabled, onClick }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button
      className={className}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}
label {
  display: block;
}

.panel-light,
.panel-dark {
  border: 1px solid black;
  border-radius: 4px;
  padding: 20px;
  margin-bottom: 10px;
}
.panel-light {
  color: #222;
  background: #fff;
}

.panel-dark {
  color: #fff;
  background: rgb(23, 32, 42);
}

.button-light,
.button-dark {
  border: 1px solid #777;
  padding: 5px;
  margin-right: 10px;
  margin-top: 10px;
}

.button-dark {
  background: #222;
  color: #fff;
}

.button-light {
  background: #fff;
  color: #222;
}

💡 부연 설명: Provider가 많아지면 JSX가 복잡해 보일 수 있는데, 이렇게 별도의 컴포넌트로 추출하면 코드가 훨씬 깔끔해져요!

context와 reducer로 확장하기 {/scaling-up-with-context-and-a-reducer/}

더 큰 앱에서는 context를 reducer와 결합해서 어떤 state와 관련된 로직을 컴포넌트 밖으로 추출하는 것이 일반적이에요. 이 예제에서 모든 "배선"은 TasksContext.js에 숨겨져 있고, 여기에는 reducer와 두 개의 별도 context가 포함되어 있어요.

이 예제의 전체 설명을 읽어보세요.

// App.js
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}
// TasksContext.js
import { createContext, useContext, useReducer } from 'react';

const TasksContext = createContext(null);

const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        {children}
      </TasksDispatchContext>
    </TasksContext>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  { id: 0, text: 'Philosopher's Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];
// AddTask.js
import { useState, useContext } from 'react';
import { useTasksDispatch } from './TasksContext.js';

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useTasksDispatch();
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        }); 
      }}>Add</button>
    </>
  );
}

let nextId = 3;
// TaskList.js
import { useState, useContext } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }

💡 부연 설명: Context와 Reducer를 함께 사용하는 패턴은 복잡한 상태 관리에 아주 유용해요. Redux 같은 외부 라이브러리 없이도 강력한 상태 관리를 할 수 있답니다!


대체 기본값 지정하기 {/specifying-a-fallback-default-value/}

React가 부모 트리에서 특정 context의 provider를 찾을 수 없다면, useContext()가 반환하는 context 값은 그 context를 생성할 때 지정한 기본값과 같을 거예요:

const ThemeContext = createContext(null);

기본값은 절대 변경되지 않아요. context를 업데이트하고 싶다면, 위에서 설명한 대로 state와 함께 사용하세요.

종종 null 대신 기본값으로 사용할 수 있는 더 의미 있는 값이 있어요. 예를 들어:

const ThemeContext = createContext('light');

이렇게 하면 실수로 해당 provider 없이 어떤 컴포넌트를 렌더링해도 깨지지 않아요. 또한 테스트에서 많은 provider를 설정하지 않고도 컴포넌트가 테스트 환경에서 잘 작동하는 데 도움이 돼요.

아래 예제에서 "Toggle theme" 버튼은 어떤 테마 context provider 밖에 있기 때문에 항상 밝은 테마이고, 기본 context 테마 값은 'light'예요. 기본 테마를 'dark'로 편집해보세요.

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <>
      <ThemeContext value={theme}>
        <Form />
      </ThemeContext>
      <Button onClick={() => {
        setTheme(theme === 'dark' ? 'light' : 'dark');
      }}>
        Toggle theme
      </Button>
    </>
  )
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  );
}
.panel-light,
.panel-dark {
  border: 1px solid black;
  border-radius: 4px;
  padding: 20px;
  margin-bottom: 10px;
}
.panel-light {
  color: #222;
  background: #fff;
}

.panel-dark {
  color: #fff;
  background: rgb(23, 32, 42);
}

.button-light,
.button-dark {
  border: 1px solid #777;
  padding: 5px;
  margin-right: 10px;
  margin-top: 10px;
}

.button-dark {
  background: #222;
  color: #fff;
}

.button-light {
  background: #fff;
  color: #222;
}

트리의 일부에 대해 context 재정의하기 {/overriding-context-for-a-part-of-the-tree/}

트리의 일부를 다른 값을 가진 provider로 감싸서 그 부분에 대한 context를 재정의할 수 있어요.

<ThemeContext value="dark">
  ...
  <ThemeContext value="light">
    <Footer />
  </ThemeContext>
  ...
</ThemeContext>

필요한 만큼 provider를 중첩하고 재정의할 수 있어요.

테마 재정의하기 {/overriding-a-theme/}

여기서 Footer 안의 버튼은 밖의 버튼들("dark")과 다른 context 값("light")을 받아요.

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext value="dark">
      <Form />
    </ThemeContext>
  )
}

function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
      <ThemeContext value="light">
        <Footer />
      </ThemeContext>
    </Panel>
  );
}

function Footer() {
  return (
    <footer>
      <Button>Settings</Button>
    </footer>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      {title && <h1>{title}</h1>}
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}
footer {
  margin-top: 20px;
  border-top: 1px solid #aaa;
}

.panel-light,
.panel-dark {
  border: 1px solid black;
  border-radius: 4px;
  padding: 20px;
}
.panel-light {
  color: #222;
  background: #fff;
}

.panel-dark {
  color: #fff;
  background: rgb(23, 32, 42);
}

.button-light,
.button-dark {
  border: 1px solid #777;
  padding: 5px;
  margin-right: 10px;
  margin-top: 10px;
}

.button-dark {
  background: #222;
  color: #fff;
}

.button-light {
  background: #fff;
  color: #222;
}

자동으로 중첩된 제목 {/automatically-nested-headings/}

context provider를 중첩할 때 정보를 "축적"할 수 있어요. 이 예제에서 Section 컴포넌트는 섹션 중첩의 깊이를 지정하는 LevelContext를 추적해요. 부모 섹션에서 LevelContext를 읽고, 자식에게 1 증가된 LevelContext 숫자를 제공해요. 결과적으로 Heading 컴포넌트는 얼마나 많은 Section 컴포넌트 안에 중첩되어 있는지에 따라 <h1>, <h2>, <h3>, ... 태그 중 어느 것을 사용할지 자동으로 결정할 수 있어요.

이 예제의 자세한 설명을 읽어보세요.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
// Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext value={level + 1}>
        {children}
      </LevelContext>
    </section>
  );
}
// Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 0:
      throw Error('Heading must be inside a Section!');
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
// LevelContext.js
import { createContext } from 'react';

export const LevelContext = createContext(0);
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

💡 부연 설명: 이 예제는 context가 단순히 데이터를 전달하는 것 이상의 일을 할 수 있다는 걸 보여줘요. 중첩 레벨을 자동으로 추적해서 적절한 HTML 태그를 선택하는 똑똑한 컴포넌트를 만들 수 있어요!


객체와 함수를 전달할 때 리렌더링 최적화하기 {/optimizing-re-renders-when-passing-objects-and-functions/}

객체와 함수를 포함한 모든 값을 context를 통해 전달할 수 있어요.

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  function login(response) {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }

  return (
    <AuthContext value={{ currentUser, login }}>
      <Page />
    </AuthContext>
  );
}

여기서 context 값은 두 개의 속성을 가진 JavaScript 객체이고, 그 중 하나는 함수예요. MyApp이 리렌더링될 때마다 (예: 라우트 업데이트 시), 이것은 다른 함수를 가리키는 다른 객체가 될 거예서, React는 useContext(AuthContext)를 호출하는 트리 깊숙이 있는 모든 컴포넌트도 리렌더링해야 해요.

작은 앱에서는 문제가 되지 않아요. 하지만 currentUser 같은 기본 데이터가 변경되지 않았다면 리렌더링할 필요가 없어요. React가 그 사실을 활용할 수 있도록, login 함수를 useCallback으로 감싸고 객체 생성을 useMemo로 감쌀 수 있어요. 이것은 성능 최적화예요:

import { useCallback, useMemo } from 'react';

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  const login = useCallback((response) => {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }, []);

  const contextValue = useMemo(() => ({
    currentUser,
    login
  }), [currentUser, login]);

  return (
    <AuthContext value={contextValue}>
      <Page />
    </AuthContext>
  );
}

이 변경의 결과로, MyApp이 리렌더링되어야 하더라도, currentUser가 변경되지 않는 한 useContext(AuthContext)를 호출하는 컴포넌트는 리렌더링할 필요가 없어요.

useMemouseCallback에 대해 더 읽어보세요.

💡 부연 설명: context 값으로 객체나 함수를 전달할 때는 항상 메모이제이션을 고려해야 해요. 매번 새로운 객체/함수를 만들면 그걸 사용하는 모든 자식 컴포넌트가 불필요하게 리렌더링될 수 있거든요!


문제 해결 {/troubleshooting/}

컴포넌트가 provider의 값을 보지 못해요 {/my-component-doesnt-see-the-value-from-my-provider/}

이런 일이 발생할 수 있는 몇 가지 일반적인 방법이 있어요:

  1. useContext()를 호출하는 곳과 같은 컴포넌트 (또는 아래)에서 <SomeContext.Provider>를 렌더링하고 있어요. <SomeContext.Provider>useContext()를 호출하는 컴포넌트 위와 밖으로 이동하세요.
  2. <SomeContext.Provider>로 컴포넌트를 감싸는 것을 깜빡했거나, 생각했던 것과 다른 트리 부분에 넣었을 수 있어요. React DevTools를 사용해서 계층 구조가 올바른지 확인하세요.
  3. 제공하는 컴포넌트에서 본 SomeContext와 읽는 컴포넌트에서 본 SomeContext가 두 개의 다른 객체인 빌드 문제가 발생했을 수 있어요. 예를 들어 symlink를 사용하면 발생할 수 있어요. window.SomeContext1window.SomeContext2 같은 전역 변수에 할당한 다음 콘솔에서 window.SomeContext1 === window.SomeContext2인지 확인해서 검증할 수 있어요. 같지 않다면, 빌드 도구 레벨에서 그 문제를 해결하세요.

기본값이 다른데도 context에서 항상 undefined를 받아요 {/i-am-always-getting-undefined-from-my-context-although-the-default-value-is-different/}

트리에 value가 없는 provider가 있을 수 있어요:

// 🚩 작동하지 않아요: value prop이 없어요
<ThemeContext>
   <Button />
</ThemeContext>

value를 지정하는 것을 깜빡하면, value={undefined}를 전달하는 것과 같아요.

실수로 다른 prop 이름을 사용했을 수도 있어요:

// 🚩 작동하지 않아요: prop은 "value"라고 불러야 해요
<ThemeContext theme={theme}>
   <Button />
</ThemeContext>

이 두 경우 모두 콘솔에 React의 경고가 표시될 거예요. 해결하려면 prop을 value라고 부르세요:

// ✅ value prop 전달하기
<ThemeContext value={theme}>
   <Button />
</ThemeContext>

createContext(defaultValue) 호출의 기본값위에 일치하는 provider가 전혀 없을 때만 사용된다는 점에 주목하세요. 부모 트리 어딘가에 <SomeContext value={undefined}> 컴포넌트가 있다면, useContext(SomeContext)를 호출하는 컴포넌트는 context 값으로 undefined받을 거예요.

💡 부연 설명: value prop을 명시적으로 undefined로 설정하면 기본값이 사용되지 않아요! Provider가 있으면 무조건 그 값을 사용하니까요.


Sitemap

모든 문서 페이지 개요

profile
프론트에_가까운_풀스택_개발자

0개의 댓글