next-themes 없이 다크모드 깜빡임(FOUC) 없애는 법
다크모드 새로고침 때 화면이 번쩍하는 FOUC를, 라이브러리 없이 head 인라인 스크립트 몇 줄로 없애는 방법을 정리했어요.
다크모드를 켜 둔 사용자가 새로고침을 하면, 화면이 잠깐 하얗게 번쩍하고 나서야 어두워질 때가 있어요. 이 깜빡임을 FOUC(Flash of Unstyled Content)라고 부르는데, 다크모드에서 특히 눈에 거슬려요. 혹시 이런 화면 본 적 없나요?
왜 한 프레임 깜빡일까요
리액트는 서버가 보낸 HTML을 먼저 그리고, 그다음에 hydration을 하면서 자바스크립트를 붙여요. 테마를 useEffect 안에서 결정하면 순서가 이렇게 돼요. 기본(밝은) 화면을 한 번 그리고, hydration이 끝난 뒤에야 어두운 테마로 바뀌어요. 그 사이 한 프레임이 깜빡임으로 보이는 거예요.
첫 페인트 이전으로 당기기
직접 붙여 보니 답은 의외로 단순했어요. 테마 결정을 hydration 이후가 아니라 첫 페인트 이전으로 당기면 돼요. 방법은 <head> 안에 동기 스크립트 한 개를 넣는 거예요. 브라우저는 <head>의 일반 <script>를 만나면 본문을 그리기 전에 먼저 실행하거든요.
그래서 이 스크립트가 localStorage 값이나 시스템 설정을 읽어서 <html>에 data-theme를 미리 박아 둬요. 3~4줄이면 끝나요.
<script
dangerouslySetInnerHTML={{
__html: `
const t = localStorage.getItem('theme')
?? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.dataset.theme = t;
`,
}}
/>돌려 보니 본문이 그려지기 전에 이미 data-theme="dark"가 걸려 있어서, 첫 프레임부터 어두운 화면이 나와요. 깜빡임이 사라져요.
놓치기 쉬운 두 가지
한 가지 걸리는 게 있어요. 서버가 보낸 html에는 이 속성이 없는데 스크립트가 클라이언트에서 속성을 바꾸니까, 리액트가 서버와 클라이언트가 다르다며 경고를 띄워요. Next.js App Router에서는 <html>에 suppressHydrationWarning을 달아 주면 조용해져요. 테스트해 보면 실제 동작에는 영향이 없고 경고만 사라져요.
하나 더. CSS에 color-scheme: dark를 같이 걸어 두면 스크롤바나 기본 폼 요소까지 어두워져요. 이걸 빼먹으면 배경만 어둡고 스크롤바만 하얀 어색한 화면이 남더라고요. 색을 바꾸는 셀렉터는 [data-theme="dark"] 하나로 통일하면 관리가 편해요.
라이브러리 없이도 충분해요
정리하면 단계는 3개예요. <head>에 동기 스크립트 넣기, <html>에 경고 억제 달기, CSS에서 data-theme로 색 바꾸기. 이 정도면 충분해서, 플랜씨(plan-c) 계산기에 다크모드를 붙일 때도 같은 방식으로 처리했어요. 적용해 봤더니 번들에 들어가는 코드가 거의 없어서 가벼웠어요.
next-themes 같은 도구가 편하긴 하지만, 안에서 하는 일도 결국 이 인라인 스크립트예요. 원리를 알고 나면 필요할 때 직접 몇 줄로 끝낼 수 있어요.
- 동기 스크립트
- suppressHydrationWarning
- color-scheme
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.