frontend/react

[React] React.js 강좌 25. Context API로 전역 상태 관리하기

mirabo01 2025. 12. 4. 10:02

React를 쓰다 보면 언젠가 꼭 맞닥뜨리게 되는 고민이 있습니다.
“이 상태를 어디에 두는 게 맞을까?”
처음에는 각 컴포넌트 내부에서 useState로 관리하면 되지만,
점점 컴포넌트가 깊어지고, props로 데이터를 계속 내려주다 보면
코드가 복잡해지고 관리가 어려워집니다.

이때 등장하는 게 바로 Context API입니다.
상태를 전역으로 공유할 수 있게 도와주는,
React 기본 내장 기능이자 상태 관리의 시작점이라고 할 수 있습니다.


props drilling이 문제였던 시절

예전에 프로젝트를 할 때,
로그인한 사용자의 정보를 여러 페이지에서 보여줘야 했습니다.
헤더, 사이드바, 마이페이지 등 거의 모든 컴포넌트가 이 정보를 필요로 했죠.

처음엔 상위 컴포넌트에서 props로 내려주었는데,
중간에 연결만 담당하는 컴포넌트가 너무 많아졌습니다.
예를 들어 App → Layout → Header → UserMenu 식으로요.

이런 구조는 불필요한 리렌더링도 유발합니다.
사용자 정보가 바뀌면 전혀 관계없는 Layout까지 다시 렌더링되니까요.

이 문제를 해결한 게 바로 Context입니다.
한마디로, props 없이 전역적으로 데이터에 접근할 수 있는 통로를 만들어주는 개념입니다.


Context API의 기본 구조

Context를 사용하는 패턴은 아주 단순합니다.
createContext로 Context를 만들고,
Provider로 감싼 뒤,
useContext로 값을 가져옵니다.

아래 코드를 보면 바로 감이 잡힙니다.

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

// 1. Context 생성
const UserContext = createContext();

// 2. Provider 컴포넌트 정의
export function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (name) => setUser({ name });
  const logout = () => setUser(null);

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

// 3. Context를 사용하는 훅
export function useUser() {
  return useContext(UserContext);
}

이제 어디서든 useUser()를 호출하면
user 상태와 login/logout 함수를 바로 쓸 수 있습니다.


실제 적용 예시 – 로그인 상태 전역 관리

위의 구조를 App 전체에 적용해보겠습니다.

import { UserProvider } from './UserContext';
import Header from './Header';
import Main from './Main';

function App() {
  return (
    <UserProvider>
      <Header />
      <Main />
    </UserProvider>
  );
}

이제 Header 컴포넌트에서 전역 상태를 바로 불러올 수 있습니다.

import { useUser } from './UserContext';

function Header() {
  const { user, login, logout } = useUser();

  return (
    <header>
      {user ? (
        <>
          <span>{user.name}님 반갑습니다!</span>
          <button onClick={logout}>로그아웃</button>
        </>
      ) : (
        <button onClick={() => login('기범')}>로그인</button>
      )}
    </header>
  );
}

이 구조의 가장 큰 장점은
어디서든 상태를 접근할 수 있고, props가 사라진다는 점입니다.
또한 불필요한 리렌더링도 줄어듭니다.


Context를 남용하면 생기는 문제

하지만 Context는 전역 상태라고 해서
모든 걸 여기에 넣는 건 좋지 않습니다.

예전에 제가 실수했던 게,
모든 UI 상태를 Context에 몰아넣은 적이 있었습니다.
모달 열림 여부, 입력 폼 값, 필터 조건 등…
결국 작은 변경에도 앱 전체가 리렌더링되면서 성능이 떨어졌습니다.

Context는 **“여러 곳에서 동시에 필요한 데이터”**에만 써야 합니다.
사용자 정보, 언어 설정, 테마(다크/라이트 모드) 같은 전역 상태가 그 예입니다.

일반적인 UI 상태는 여전히 로컬 useState로 관리하는 게 낫습니다.


Context와 Reducer를 함께 쓰면 더 깔끔해진다

상태가 조금 복잡해질 경우,
Context 안에서 useReducer를 쓰는 게 훨씬 구조적입니다.

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'TOGGLE':
        return { ...state, dark: !state.dark };
      default:
        return state;
    }
  }, { dark: false });

  return (
    <ThemeContext.Provider value={{ state, dispatch }}>
      {children}
    </ThemeContext.Provider>
  );
}

이렇게 하면 reducer를 따로 관리할 수 있어
상태 변경 로직이 명확해집니다.
Redux를 쓰지 않아도 충분히 유연한 구조가 됩니다.


프로젝트에서 적용해보며 느낀 점

Context를 본격적으로 사용하기 시작한 건
관리 도구를 만들던 시점이었습니다.
로그인한 사용자 정보, 권한(role), 접근 가능한 메뉴 목록을
여러 컴포넌트에서 공유해야 했기 때문이죠.

그때 Context를 도입하고 나서
props를 거의 쓰지 않아도 되는 구조가 만들어졌습니다.
게다가 전역 상태를 한 곳에서만 관리하니
협업할 때도 “이 상태 어디서 바꿔요?” 같은 질문이 사라졌습니다.

물론 나중에는 더 복잡한 상태를 위해
Zustand나 Redux Toolkit으로 넘어갔지만,
Context는 여전히 React의 기반이자 출발점이라고 생각합니다.


Context API는 “작은 프로젝트의 전역 상태 관리”에는 충분히 강력합니다.
불필요한 외부 라이브러리 없이도,
데이터 흐름을 한눈에 파악할 수 있는 장점이 있습니다.

다음 글에서는 이 전역 상태 개념을 확장해서,
Zustand로 더 구조적인 상태 관리를 하는 방법을 다뤄보겠습니다.
Context보다 조금 더 가볍고 직관적인 방식으로
프로젝트의 상태 흐름을 관리할 수 있는 방법을 함께 살펴보겠습니다.