어제 오늘 내일

[React] Context API 완벽 가이드: Props Drilling 없이 상태 공유하기 본문

IT/React

[React] Context API 완벽 가이드: Props Drilling 없이 상태 공유하기

hi.anna 2026. 8. 3. 22:16

React로 애플리케이션을 개발하다 보면 여러 컴포넌트가 같은 데이터를 필요로 하는 상황을 자주 마주치게 됩니다. 이럴 때 사용하는 것이 바로 Context API입니다. 이 글에서는 Context API가 왜 필요한지, 어떻게 사용하는지, 그리고 언제 사용해야 하는지 알아보겠습니다.

Props Drilling 문제

React는 기본적으로 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 데이터를 전달합니다. 하지만 컴포넌트 트리가 깊어지면 문제가 생깁니다.

function App() {
  const user = { name: "홍길동", theme: "dark" };
  // App에서 만든 user 데이터를 Layout에 props로 전달
  return <Layout user={user} />;
}

function Layout({ user }) {
  // Layout은 user를 직접 쓰지 않지만, 자식에게 넘기려고 받아둠
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  // Sidebar 역시 user를 사용하지 않고 그냥 아래로 전달만 함
  return <UserProfile user={user} />;
}

function UserProfile({ user }) {
  // 실제로 user가 필요한 곳은 여기 하나뿐
  return <p>{user.name}님 환영합니다</p>;
}

LayoutSidebaruser라는 데이터를 실제로 사용하지 않지만, 하위 컴포넌트에 전달하기 위해 어쩔 수 없이 props를 받아 넘겨줘야 합니다. 이런 현상을 Props Drilling(프롭스 드릴링)이라고 부릅니다. 컴포넌트가 많아질수록 코드가 지저분해지고 유지보수가 어려워집니다.

Context API란?

Context API는 컴포넌트 트리 전체에 데이터를 전달할 수 있는 방법을 제공합니다. 중간 컴포넌트를 거치지 않고, 필요한 컴포넌트에서 바로 데이터를 꺼내 쓸 수 있습니다.
Context API는 크게 세 단계로 사용합니다.

  1. Context 생성createContext
  2. Provider로 데이터 제공<Context.Provider>
  3. Consumer에서 데이터 사용useContext

기본 사용법

1. Context 생성하기

import { createContext } from 'react';

// createContext로 새로운 Context 객체를 생성
// 인자로 넘긴 null은 Provider가 없을 때 사용될 기본값
const UserContext = createContext(null);

2. Provider로 감싸기

Provider는 하위 컴포넌트들에게 데이터를 전달하는 역할을 합니다.

function App() {
  const user = { name: "홍길동", theme: "dark" };

  return (
    // Provider의 value에 담긴 데이터가 하위 컴포넌트 전체에 공유됨
    // 이 안에 있는 어떤 컴포넌트든 user 데이터에 접근 가능
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

3. useContext로 데이터 꺼내 쓰기

import { useContext } from 'react';

function Layout() {
  // 더 이상 user props를 받지 않아도 됨
  return <Sidebar />;
}

function Sidebar() {
  // 중간 컴포넌트는 데이터 전달 책임에서 완전히 해방됨
  return <UserProfile />;
}

function UserProfile() {
  // useContext에 Context 객체를 넘기면 가장 가까운 Provider의 value를 반환
  const user = useContext(UserContext);
  return <p>{user.name}님 환영합니다</p>;
}

이제 LayoutSidebar는 더 이상 user props를 알 필요가 없습니다. UserProfile에서 직접 UserContext에 접근해 데이터를 가져오기 때문입니다.

실전 예제: 테마 전환 기능 만들기

다크 모드/라이트 모드를 전환하는 기능은 Context API가 자주 쓰이는 대표적인 예시입니다.

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

// 테마 관련 데이터를 공유할 Context 생성
const ThemeContext = createContext();

// Provider와 상태 로직을 하나로 묶은 컴포넌트
// children으로 감싼 모든 컴포넌트가 테마에 접근할 수 있게 됨
function ThemeProvider({ children }) {
  // 현재 테마 상태를 관리 (기본값은 'light')
  const [theme, setTheme] = useState('light');

  // 테마를 반대로 전환하는 함수
  const toggleTheme = () => {
    // 이전 상태값(prev)을 기준으로 light ↔ dark 전환
    setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    // theme 값과 toggleTheme 함수를 함께 value로 전달
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemeButton() {
  // Context에서 theme 상태와 toggleTheme 함수를 한 번에 꺼내옴
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    // 버튼 클릭 시 테마 전환 함수 실행
    <button onClick={toggleTheme}>
      현재 테마: {theme} (클릭해서 전환)
    </button>
  );
}

function App() {
  return (
    // ThemeProvider로 감싸면 내부의 모든 컴포넌트가 테마 사용 가능
    <ThemeProvider>
      <ThemeButton />
    </ThemeProvider>
  );
}

이렇게 Provider와 상태 로직을 하나의 컴포넌트로 묶으면, 어디서든 useContext(ThemeContext) 한 줄로 테마 상태와 전환 함수에 접근할 수 있습니다.

커스텀 Hook으로 더 깔끔하게

매번 useContext(ThemeContext)를 호출하는 대신, 커스텀 Hook으로 감싸면 재사용성과 가독성이 좋아집니다.

// Context 사용 로직을 감싼 커스텀 Hook
function useTheme() {
  const context = useContext(ThemeContext);

  // Provider 바깥에서 호출하면 context가 undefined가 됨
  // 이 경우 명확한 에러를 던져 실수를 빠르게 잡아냄
  if (!context) {
    throw new Error('useTheme은 ThemeProvider 안에서 사용해야 합니다');
  }

  return context;
}

// 사용하는 쪽 - useContext 대신 useTheme만 호출하면 됨
function ThemeButton() {
  // 어떤 Context를 쓰는지 신경 쓸 필요 없이 깔끔하게 사용
  const { theme, toggleTheme } = useTheme();
  return <button onClick={toggleTheme}>{theme}</button>;
}

Provider 외부에서 실수로 사용했을 때 명확한 에러 메시지를 보여줄 수 있다는 것도 큰 장점입니다.

언제 Context API를 사용해야 할까?

Context API는 강력하지만 만능은 아닙니다. 다음과 같은 경우에 적합합니다.

  • 전역적으로 필요한 데이터: 로그인한 사용자 정보, 테마, 언어 설정 등
  • 여러 depth를 거쳐 전달되는 데이터: props drilling이 3~4단계 이상 발생할 때
  • 자주 바뀌지 않는 데이터: 너무 자주 변경되는 값은 불필요한 리렌더링을 유발할 수 있습니다

주의할 점: 불필요한 리렌더링

Context의 value가 변경되면 해당 Context를 구독하는 모든 컴포넌트가 리렌더링됩니다. 특히 아래처럼 객체를 매 렌더링마다 새로 생성하면 문제가 될 수 있습니다.

// ❌ 컴포넌트가 렌더링될 때마다 { name: "홍길동" } 객체가 새로 만들어짐
//    참조값이 매번 달라지므로 React는 value가 바뀌었다고 판단 → 불필요한 리렌더링
<UserContext.Provider value={{ name: "홍길동" }}>
// ✅ useMemo로 객체를 메모이제이션해서 참조값을 유지
//    의존성 배열([])이 변하지 않는 한 같은 객체를 재사용 → 리렌더링 방지
const value = useMemo(() => ({ name: "홍길동" }), []);
<UserContext.Provider value={value}>

또한 자주 바뀌는 값(예: 마우스 위치, 스크롤 위치)을 Context로 관리하면 성능 저하가 발생할 수 있으므로, 이런 경우에는 Redux나 Zustand 같은 상태 관리 라이브러리를 고려하는 것이 좋습니다.

Context API vs 상태 관리 라이브러리

Context API는 Redux 같은 전역 상태 관리 라이브러리를 완전히 대체하지는 않습니다. 둘의 차이를 간단히 정리하면 다음과 같습니다.

구분 Context API Redux / Zustand 등
설정 복잡도낮음 (React 내장)상대적으로 높음
적합한 규모소~중규모 앱, 단순한 전역 상태대규모 앱, 복잡한 상태 로직
성능 최적화직접 관리 필요 (memo, useMemo)라이브러리 차원에서 최적화 제공
디버깅 도구별도 없음Redux DevTools 등 지원

간단한 테마 설정이나 인증 정보 정도라면 Context API만으로 충분하지만, 상태 로직이 복잡하고 자주 변경된다면 전용 상태 관리 라이브러리를 검토해보는 것이 좋습니다.

마무리

Context API는 React 애플리케이션에서 props drilling 문제를 해결하는 훌륭한 도구입니다. createContext, Provider, useContext 세 가지 개념만 이해하면 전역 상태를 손쉽게 공유할 수 있습니다. 다만 모든 상태를 Context로 관리하려 하기보다는, 정말 전역적으로 필요한 데이터에 한해 사용하는 것이 좋은 설계 습관입니다.

반응형
Comments