React/공식 문서

Context API

favor 2026. 5. 27. 18:10

Context를 사용해 데이터를 깊게 전달하기

보통의 경우, 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달
ㄴ 그러나 중간에 많은 컴포넌트를 거쳐야 하거나, 애플리케이션의 많은 컴포넌트에서 동일한 정보가 필요한 경우에는 props를 전달하는 것이 번거롭고 불편할 수 O
Context는 부모 컴포넌트가 깊이에 상관 없이, 트리 아래에 있는 모든 컴포넌트에 정보를 명시적으로 사용할 수 있게 해줌

 

✔️ Props 전달하기의 문제점

Props 전달하기 : UI 트리를 통해 해당 데이터를 사용하는 컴포넌트에 명시적으로 데이터를 전달하는 방법

 

Props drilling : 데이터가 필요한 여러 컴포넌트의 가장 가까운 공통 조상이 트리 상 높이 위치하여 높게까지 state를 끌어올리는 것

 

✔️ Context: Props 전달하기의 대안

Context는 부모 컴포넌트가 그 아래의 트리 전체에 데이터를 전달할 수 있도록 해줌

 

크기 조정을 위해 level prop을 전달하는 코드

// App.js

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

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Section>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Section>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Section>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
// Section.js

export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}
// Heading.js

export default function Heading({ level, children }) {
  switch (level) {
    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);
  }
}

 

<Section>
  <Heading level={3}>About</Heading>
  <Heading level={3}>Photos</Heading>
  <Heading level={3}>Videos</Heading>
</Section>

^ 각각의 <Heading>에 level prop을 전달

<Section level={3}>
  <Heading>About</Heading>
  <Heading>Photos</Heading>
  <Heading>Videos</Heading>
</Section>

^ <Section> 컴포넌트에 level prop을 전달

 

위와 같이 변경하려면, <Heading> 컴포넌트가 가장 가까운 <Section>의 레벨을 알아야 함

ㄴ 자식에게 트리 위 어딘가에 있는 데이터를 요청하는 방법이 필요

 

Props만으로는 불가능 --> Context 활용

  1. Context 생성
  2. 데이터가 필요한 컴포넌트에서 context를 사용
  3. 데이터를 지정하는 컴포넌트에서 context를 제공 

 

* Context는 부모가 트리 내부 전체에, 심지어 멀리 떨어진 컴포넌트에조차 데이터를 제공할 수 있도록 함

 

✔️ 1단계: Context 생성하기

 먼저 context를 생성, 이후 컴포넌트에서 사용할 수 있도록 파일에서 내보내야 함

 

// LevelContext.js

import { createContext } from 'react';

export const LevelContext = createContext(1);

 

createContext의 유일한 인자는 기본값

ㄴ 객체를 포함한 모든 종류의 값 전달 가능 

 

✔️ 2단계: Context 사용하기

React에서 useContext 훅과 생성한 Context를 가져옴 

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

 

지금은 Heading 컴포넌트가 props에서 level을 읽어오지만, level prop을 제거하고 위에서 가져온 context인 LevelContext에서 값을 읽도록 바꿈 

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

 

Heading이 아닌 Section이 대신 level을 받도록 JSX 업데이트

<Section level={4}>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
</Section>

 

✔️ 3단계: Context 제공하기

// Section.js

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext value={level}>
        {children}
      </LevelContext>
    </section>
  );
}

 

이제 level prop을 각 Heading 컴포넌트에 전달할 필요 X

대신 위의 가장 가까운 Section에게 제목 레벨을 확인함

 

  1. level prop을 <Section>에 전달
  2. Section은 자식을 <LevelContext value={level}>로 감싸줌
  3. Heading은 useContext(LevelContext)를 사용해 가장 근처의 LevelContext의 값을 요청

 

✔️ 같은 컴포넌트에서 context를 사용하며 제공하기

export default function Page() {
  return (
    <Section level={1}>
      ...
      <Section level={2}>
        ...
        <Section level={3}>
          ...

^ 지금은 각각의 섹션에 level을 수동으로 지정해야 함

 

Context를 통해 위의 컴포넌트에서 정보를 읽을 수 있으므로, 각 Section은 위의 Section에서 level을 읽고 자동으로 level+1을 아래로 전달할 수 있음 

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

 

✔️ Context로 중간 컴포넌트 지나치기

Context를 사용하면 주변에 적응하고 렌더링되는 위치에 따라 자신을 다르게 표시하는 컴포넌트를 작성 가능

 

✔️ Context를 사용하기 전에 고려할 것

어떤 props를 여러 레벨 깊이로 전달해야 한다고 해서 해당 정보를 context에 넣어야 하는 것은 아님

 

context를 사용하기 전 고려해볼 몇 가지 대안

  1. Props 전달하기로 시작하기
    • 사소한 컴포넌트들이 아니라면 여러 개의 props가 여러 컴포넌트를 거쳐 가는 것은 그리 이상한 일이 X
    • 데이터의 흐름이 props를 통해 분명해져 코드를 유지보수 하기에도 좋음
  2. 컴포넌트를 추출하고 JSX를 children으로 전달하기
    • 컴포넌트를 추출하는 것을 잊은 겨웅 多
    • ex) posts처럼 직접 사용하지 않는 props를 <Layout posts={posts} />와 같이 전달 가능

 

✔️ Context 사용 예시 

  • 테마 지정하기
    • 사용자가 모양을 변경할 수 있는 애플리케이션의 경우에 context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있음
  • 현재 계정
    • 로그인한 사용자를 알아야 하는 컴포넌트가 많을 수 있음
    • Context에 놓으면 트리 어디에서나 편하게 알아낼 수 있음
  • 라우팅
    • 대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context를 사용
    • 이것이 모든 링크의 활성화 여부를 알 수 있는 방법
  • 상태 관리
    • 애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 됨
    • reducer를 context와 함께 사용하는 것은 복잡한 state를 관리하고 번거로운 작업 없이 멀리 있는 컴포넌트까지 값을 전달하는 방법

 

 

 


Reducer와 Context로 앱 확장하기

Reducer를 사용하면 컴포넌트의 state 업데이트 로직 통합 가능
Context를 사용하면 다른 컴포넌트들에 정보 전달 가능
Reducer와 context를 함께 사용하면 복잡한 화면의 state를 관리 가능

 

✔️ Reducer와 context를 결합하기

Reducer는 이벤트 핸들러를 짧고 간결하게 유지하는 데 도움이 됨

 

// App.js

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Day off in Kyoto</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

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

let nextId = 3;
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 }
];
// AppTask.js

import { useState } from 'react';

export default function AddTask({ onAddTask }) {
  const [text, setText] = useState('');
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        onAddTask(text);
      }}>Add</button>
    </>
  )
}
// TaskList.js

import { useState } from 'react';

export default function TaskList({
  tasks,
  onChangeTask,
  onDeleteTask
}) {
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task
            task={task}
            onChange={onChangeTask}
            onDelete={onDeleteTask}
          />
        </li>
      ))}
    </ul>
  );
}

function Task({ task, onChange, onDelete }) {
  const [isEditing, setIsEditing] = useState(false);
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            onChange({
              ...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 => {
          onChange({
            ...task,
            done: e.target.checked
          });
        }}
      />
      {taskContent}
      <button onClick={() => onDelete(task.id)}>
        Delete
      </button>
    </label>
  );
}

^ 현재 task state 및 dispatch 함수는 최상위 TaskApp 컴포넌트에서만 사용 가능

ㄴ 다른 컴포넌트가 작업 목록을 읽거나 변경하려면 현재 state와 해당 state를 변경하는 이벤트 핸들러를 명시적으로 props로 전달해야 함

 

Reducer와 context를 결합하는 방법

  1. Context를 생성
  2. State와 dispatch 함수를 context에 넣음
  3. 트리 안에서 context를 사용

 

✔️ 1단계: Context 생성

useReducer 훅은 현재 tasks와 업데이트할 수 있는 dispatch 함수를 반환

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

 

트리를 통해 전달하려면, 두 개의 별개의 context를 생성해야 함

  • TaskContext는 현재 tasks 리스트를 제공
  • TaskDispatchContext는 컴포넌트에서 action을 dispatch하는 함수를 제공

 

// TasksContext.js

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

^ 두 context에 모두 기본값으로 null을 전달, 실제 값은 TaskApp 컴포넌트에서 제공될 것

 

✔️ 2단계: State와 dispatch 함수를 context에 넣기

이제 TaskApp 컴포넌트에서 두 context를 모두 가져올 수 있음

useReducer()에서 반환된 tasks 및 dispatch를 가져와 아래 트리 전체에 제공

import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        ...
      </TasksDispatchContext>
    </TasksContext>
  );
}

 

✔️ 3단계: 트리 안에서 context 사용하기

// TaskList.js

import { useState, useContext } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

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

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useContext(TasksDispatchContext);
  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>
  );
}

 

✔️ 하나의 파일로 합치기

반드시 이런 방식으로 작성하지 않아도 되지만, reducer와 context를 모두 하나의 파일에 작성하면 컴포넌트들을 조금 더 정리 가능

 

Reducer를 같은 파일로 옮기고 TasksProvider 컴포넌트를 새로 선언

ㄴ 해당 컴포넌트는 모든 것을 하나로 묶는 역할을 함

  1. Reducer로 state를 관리
  2. 두 context를 모두 하위 컴포넌트에 제공
  3. children을 prop으로 받기 때문에 JSX를 전달 가능

 

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

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

 

TasksContext.js에서 context를 사용하기 위한 use 함수들도 내보낼 수 있음

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

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

 

이 함수를 사용하여 컴포넌트에서 context를 읽을 수 있음

const tasks = useTasks();
const dispatch = useTasksDispatch();

 

TasksProvider는 tasks를 화면의 한 부분으로 tasks를 관리

useTasks로 tasks를 읽을 수 있고, useTasksDispatch로 컴포넌트들에서 tasks를 업데이트할 수 있음

'React > 공식 문서' 카테고리의 다른 글

useReducer  (0) 2026.05.21
상태 구조 설계와 공유  (0) 2026.05.07
불변 업데이트 패턴  (0) 2026.04.28
렌더링과 State 업데이트  (0) 2026.04.08
이벤트 처리와 State  (0) 2026.04.02