lifecarelog
프론트엔드

next-themes 없이 다크모드 깜빡임(FOUC) 없애는 법

다크모드 새로고침 때 화면이 번쩍하는 FOUC를, 라이브러리 없이 head 인라인 스크립트 몇 줄로 없애는 방법을 정리했어요.

5분 읽기

다크모드를 켜 둔 사용자가 새로고침을 하면, 화면이 잠깐 하얗게 번쩍하고 나서야 어두워질 때가 있어요. 이 깜빡임을 FOUC(Flash of Unstyled Content)라고 부르는데, 다크모드에서 특히 눈에 거슬려요. 혹시 이런 화면 본 적 없나요?

왜 한 프레임 깜빡일까요

리액트는 서버가 보낸 HTML을 먼저 그리고, 그다음에 hydration을 하면서 자바스크립트를 붙여요. 테마를 useEffect 안에서 결정하면 순서가 이렇게 돼요. 기본(밝은) 화면을 한 번 그리고, hydration이 끝난 뒤에야 어두운 테마로 바뀌어요. 그 사이 한 프레임이 깜빡임으로 보이는 거예요.

기본 화면 페인트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
#다크모드#FOUC#Next.js#프론트엔드

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.