Next.js App Router에서 "useTheme must be used within a ThemeProvider" 에러 해결하기

박은정·2026년 2월 11일

NextJS 프로젝트

목록 보기
1/2

🚨 문제 상황

Next.js 14+ App Router로 포트폴리오를 만들다가 다크모드 기능을 구현했는데, 이런 에러가 발생했다.

Runtime Error
useTheme must be used within a ThemeProvider

분명히 layout.tsx에서 ThemeProvider로 감싸고, 그 안에 Navigation을 넣었는데 왜 에러가 날까?

// src/app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        <ThemeProvider>
          <Navigation />  {/* useTheme() 사용 */}
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

코드만 보면 Navigation이 ThemeProvider 안에 있으니까 문제없어 보인다. 근데 왜 Context를 못 찾는 걸까?


🔍 원인 분석

1. App Router의 기본 동작

Next.js App Router에서 layout.tsx는 기본적으로 서버 컴포넌트다.

// layout.tsx → 서버 컴포넌트 (기본값)
// "use client" 선언이 없으면 서버에서 실행됨

2. 서버 컴포넌트와 클라이언트 컴포넌트의 관계

서버 컴포넌트에서 클라이언트 컴포넌트를 import하면, Next.js는 이들을 별도로 처리한다.

layout.tsx (서버 컴포넌트)
    │
    ├── ThemeProvider ("use client")  →  클라이언트로 전송
    │
    └── Navigation ("use client")     →  클라이언트로 전송 (별도로!)

핵심 문제: 서버 컴포넌트에서 여러 클라이언트 컴포넌트를 import하면, 이들이 독립적으로 hydration된다.

3. 실제 실행 순서

1. layout.tsx가 서버에서 HTML 구조 생성
2. 클라이언트에서 hydration 시작
3. Navigation 컴포넌트 마운트 → useTheme() 호출
4. 이 시점에 ThemeProvider의 Context가 아직 준비 안 됨
5. context === undefined → 💥 에러!

React Context는 컴포넌트 트리 구조를 따라 전파되는데, 서버 컴포넌트를 거치면서 이 트리 관계가 보장되지 않는 것이다.

4. 시각적으로 이해하기

우리가 기대한 구조:

ThemeProvider (Context 제공)
    └── Navigation (Context 사용) ✅

실제로 일어난 일:

서버 컴포넌트 (layout.tsx)
    ├── ThemeProvider (독립적으로 hydration)
    └── Navigation (독립적으로 hydration) 
         └── useTheme() 호출 시점에 Context 없음 ❌

✅ 해결 방법

방법 1: Navigation을 page.tsx로 이동 (권장)

layout.tsx에서 Navigation을 제거하고, page.tsx에서 렌더링한다.

수정 전 - layout.tsx:

// ❌ 문제가 되는 코드
import { ThemeProvider } from "@/components/ThemeProvider";
import Navigation from "@/components/Navigation";

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        <ThemeProvider>
          <Navigation />
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

수정 후 - layout.tsx:

// ✅ Navigation import 제거
import { ThemeProvider } from "@/components/ThemeProvider";

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

수정 후 - page.tsx:

// ✅ Navigation을 여기서 렌더링
import Navigation from "@/components/Navigation";
import Hero from "@/components/Hero";

export default function Home() {
  return (
    <main>
      <Navigation />
      <Hero />
      {/* ... */}
    </main>
  );
}

이렇게 하면 page.tsx의 내용이 ThemeProvider의 children으로 들어가고, Navigation은 확실히 Context 안에서 실행된다.

방법 2: Wrapper 클라이언트 컴포넌트 만들기

모든 페이지에서 Navigation을 쓴다면, Wrapper 컴포넌트를 만드는 방법도 있다.

// src/components/ClientLayout.tsx
"use client";

import { ThemeProvider } from "./ThemeProvider";
import Navigation from "./Navigation";

export default function ClientLayout({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider>
      <Navigation />
      {children}
    </ThemeProvider>
  );
}
// src/app/layout.tsx
import ClientLayout from "@/components/ClientLayout";

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        <ClientLayout>
          {children}
        </ClientLayout>
      </body>
    </html>
  );
}

이 방법은 ThemeProvider와 Navigation이 같은 클라이언트 컴포넌트 안에 있어서 Context 관계가 보장된다.


📌 핵심 정리

상황결과
서버 컴포넌트에서 여러 클라이언트 컴포넌트 importContext 관계 보장 안 됨 ❌
클라이언트 컴포넌트 안에서 다른 클라이언트 컴포넌트 사용Context 관계 보장됨 ✅

기억할 점:

Context를 공유하는 컴포넌트들은 같은 클라이언트 컴포넌트 트리 안에 있어야 한다.


🔗 참고 자료


마무리

이 에러는 Next.js App Router를 처음 사용할 때 자주 마주치는 문제다. 서버 컴포넌트와 클라이언트 컴포넌트의 경계를 이해하면 해결할 수 있다.

비슷한 에러를 만났다면 "이 Context Provider와 Consumer가 같은 클라이언트 트리에 있는가?"를 먼저 확인해보자!

profile
새로운 것을 도전하고 배운것을 정리하려 합니다.

0개의 댓글