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