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를 못 찾는 걸까?
Next.js App Router에서 layout.tsx는 기본적으로 서버 컴포넌트다.
// layout.tsx → 서버 컴포넌트 (기본값)
// "use client" 선언이 없으면 서버에서 실행됨
서버 컴포넌트에서 클라이언트 컴포넌트를 import하면, Next.js는 이들을 별도로 처리한다.
layout.tsx (서버 컴포넌트)
│
├── ThemeProvider ("use client") → 클라이언트로 전송
│
└── Navigation ("use client") → 클라이언트로 전송 (별도로!)
핵심 문제: 서버 컴포넌트에서 여러 클라이언트 컴포넌트를 import하면, 이들이 독립적으로 hydration된다.
1. layout.tsx가 서버에서 HTML 구조 생성
2. 클라이언트에서 hydration 시작
3. Navigation 컴포넌트 마운트 → useTheme() 호출
4. 이 시점에 ThemeProvider의 Context가 아직 준비 안 됨
5. context === undefined → 💥 에러!
React Context는 컴포넌트 트리 구조를 따라 전파되는데, 서버 컴포넌트를 거치면서 이 트리 관계가 보장되지 않는 것이다.
우리가 기대한 구조:
ThemeProvider (Context 제공)
└── Navigation (Context 사용) ✅
실제로 일어난 일:
서버 컴포넌트 (layout.tsx)
├── ThemeProvider (독립적으로 hydration)
└── Navigation (독립적으로 hydration)
└── useTheme() 호출 시점에 Context 없음 ❌
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 안에서 실행된다.
모든 페이지에서 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 관계가 보장된다.
| 상황 | 결과 |
|---|---|
| 서버 컴포넌트에서 여러 클라이언트 컴포넌트 import | Context 관계 보장 안 됨 ❌ |
| 클라이언트 컴포넌트 안에서 다른 클라이언트 컴포넌트 사용 | Context 관계 보장됨 ✅ |
기억할 점:
Context를 공유하는 컴포넌트들은 같은 클라이언트 컴포넌트 트리 안에 있어야 한다.
이 에러는 Next.js App Router를 처음 사용할 때 자주 마주치는 문제다. 서버 컴포넌트와 클라이언트 컴포넌트의 경계를 이해하면 해결할 수 있다.
비슷한 에러를 만났다면 "이 Context Provider와 Consumer가 같은 클라이언트 트리에 있는가?"를 먼저 확인해보자!