[TYPESCRIPT] Context API와 TypeScript — 전역 상태를 안전하게 설계하는 방법

 

React에서 Context API는 전역 상태나 공통 데이터를 전달할 때 자주 사용됩니다. 하지만 TypeScript 없이 Context를 사용하면 값 구조를 추측해야 하고, Provider 누락·타입 불일치로 인한 런타임 오류가 쉽게 발생합니다.

 

 

Context API가 필요한 상황

Context는 다음과 같은 경우에 적합합니다.

  • 로그인 사용자 정보
  • 테마(dark / light)
  • 언어(i18n)
  • 전역 설정 값

반대로, 자주 변경되거나 복잡한 비즈니스 상태에는 React Query, Zustand 같은 상태 관리 라이브러리가 더 적합합니다.

 

Context에 타입이 반드시 필요한 이유

타입 없이 Context를 만들면 다음과 같은 문제가 발생합니다.

  • Context 값 구조를 IDE에서 알 수 없음
  • Provider를 빼먹어도 컴파일 단계에서 감지 불가
  • value 변경 시 사용처 전체를 수동으로 확인

TypeScript를 사용하면 Context는 “전역 상태의 타입 계약”이 됩니다.

 

Context 타입 먼저 정의하기

가장 중요한 원칙은 Context 생성 전에 타입을 먼저 정의하는 것입니다.

type AuthContextValue = {
  user: User | null;
  login: (user: User) => void;
  logout: () => void;
};

Context의 역할과 책임이 이 타입 하나에 모두 드러납니다.

 

createContext의 올바른 사용 패턴

Context 생성 시 가장 흔한 실수는 임의의 기본값을 넣는 것입니다.

// X 권장하지 않음
createContext({} as AuthContextValue);

이 방식은 타입 안전성을 깨뜨립니다. 실무에서는 undefined 기반 Context가 가장 안전합니다.

import { createContext } from "react";

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

이렇게 하면 Provider 없이 사용했을 때 즉시 오류를 발생시킬 수 있습니다.

 

Provider 컴포넌트 구현

import { useState } from "react";

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const value: AuthContextValue = {
    user,
    login: setUser,
    logout: () => setUser(null),
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

핵심 포인트:

  • Context value는 명시적으로 타입 지정
  • State와 Context 타입 분리
  • children은 React.ReactNode

 

커스텀 훅으로 안전하게 사용하기

Context를 직접 useContext로 사용하는 것은 권장되지 않습니다. 대신 커스텀 훅으로 감싸는 것이 실무 표준입니다.

import { useContext } from "react";

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return ctx;
}

이 훅 하나로 다음이 보장됩니다.

  • Provider 누락 즉시 오류 발생
  • 반환 타입은 항상 AuthContextValue
  • 사용부 코드 단순화

 

Context 사용 예시

function Profile() {
  const { user, logout } = useAuth();

  if (!user) {
    return <div>Not logged in</div>;
  }

  return (
    <div>
      <span>{user.name}</span>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Context 값이 명확한 타입으로 보장되기 때문에 불필요한 null 체크나 타입 단언이 사라집니다.

 

여러 Context를 다룰 때의 패턴

Context가 많아질수록 다음 원칙이 중요합니다.

  • Context 하나당 책임 하나
  • 전역 상태와 UI 상태 분리
  • Provider 중첩은 App 레벨에서 관리
<AuthProvider>
  <ThemeProvider>
    <App />
  </ThemeProvider>
</AuthProvider>

이 구조는 가독성과 확장성 면에서 가장 안정적입니다.

 

실무에서 자주 하는 실수

  • createContext 기본값에 더미 객체 사용
  • Context 타입과 실제 value 구조 불일치
  • Provider 없이 useContext 직접 사용
  • Context에 너무 많은 책임을 부여

대부분 “타입을 중심으로 설계하지 않았기” 때문에 발생합니다.

 

실무 권장 정리

  • Context 타입부터 정의
  • createContext는 undefined 기반
  • 반드시 커스텀 훅으로 감싸서 사용
  • Context는 전역 설정/상태에만 사용

 


 

Context API와 TypeScript를 함께 사용하면 전역 상태는 더 이상 불안한 공유 변수가 아니라 명확한 타입 계약을 가진 설계 요소가 됩니다.

  • Context = 전역 계약
  • 타입이 설계를 이끈다
  • 커스텀 훅은 필수