임동욱 임동욱 07-28
feat: 첫 페인트 전 테마 적용 스크립트 추가
Co-Authored-By: Claude Opus 5 
@be47c4fcaa98aaa06b01672b5cd140ee925a3201
app/layout.tsx
--- app/layout.tsx
+++ app/layout.tsx
@@ -6,7 +6,27 @@
 import { FeedbackProvider } from "@/app/_components/feedback-provider";
 import { SplashGate } from "@/app/_components/splash-gate";
 import { SPLASH_SHOWN_STORAGE_KEY } from "@/lib/constants/splash";
+import {
+  THEME_ATTRIBUTE_NAME,
+  THEME_STORAGE_KEY,
+} from "@/lib/constants/theme";
 import "./globals.css";
+
+// 첫 페인트 전 테마 적용 — <head>는 <body>보다 먼저 파싱되므로 <html>만 조작 가능하다
+// (preventing-flash-before-hydration.md의 Themes 패턴). localStorage에 명시 선택("light"
+// /"dark")이 저장돼 있으면 첫 페인트 전에 <html data-theme>을 부여해 FOUC를 막는다. 저장값이
+// 없거나("system") 손상된 값이면 속성을 부여하지 않는다 — globals.css의
+// `prefers-color-scheme` 미디어 쿼리가 그대로 OS 설정을 따르는 fail-safe 경로다.
+//
+// THEME_STORAGE_KEY/THEME_ATTRIBUTE_NAME은 반드시 'use client' 파일이 아닌
+// lib/constants/theme에서 import한다 — 'use client' 모듈의 일반 값(문자열) export를
+// Server Component가 직접 import하면 클라이언트 레퍼런스 프록시 특성상 undefined로 평가된다
+// (컴포넌트 export만 정상적으로 프록시된다. splash 상수와 동일한 이유로 분리됨).
+const THEME_BOOTSTRAP_SCRIPT = `(function(){try{var t=localStorage.getItem(${JSON.stringify(
+  THEME_STORAGE_KEY
+)});if(t==="light"||t==="dark")document.documentElement.setAttribute(${JSON.stringify(
+  THEME_ATTRIBUTE_NAME
+)},t)}catch(e){}})()`;
 
 // 스플래시 재표시 방지 — <head>는 <body>보다 먼저 파싱되므로 body 하위 요소는 아직 존재하지
 // 않는다(preventing-flash-before-hydration.md의 Themes 패턴과 동일하게 <html>만 조작 가능).
@@ -91,6 +111,7 @@
       className={`${wantedSans.variable} ${geistMono.variable} h-full antialiased`}
     >
       <head>
+        <script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }} />
         <style dangerouslySetInnerHTML={{ __html: SPLASH_HIDE_STYLE }} />
         <script dangerouslySetInnerHTML={{ __html: SPLASH_GATE_SCRIPT }} />
       </head>
Add a comment
List