다크모드 적용하기 (next.js app router, vanilla-extract, data-theme)

김지환·2024년 6월 7일

Next.js app router와 vanilla-extract를 활용해서 개발 중에 다크모드를 개발하고자했다.

Next.js에서 다크모드를 개발할 때에는 주의할 점이 있다.
여러가지 CSS 라이브러리에서 제공하는 ThemeProvider를 활용해서 다크모드를 구현했다면,
서버 컴포넌트에서는 Theme의 State를 알지 못해
사용자가 웹 페이지의 기본 화면 모드와 다른 화면 모드를 사용할 때
최초 진입 혹은 새로고침 시 화면이 깜박이는 현상이 있다.

이는 화면 모드를 어떠한 상태에 의존적이게 만들지 않고 CSS 변수를 사용해서 해결할 수 있다.

data-theme, CSS 변수를 활용한 테마 생성.

다음과 같이 CSS 변수를 사용해서 테마 정보를 생성한다.
vanilla extract에서는 css 변수를 생성할 때 vars라는 키워드 내부 객체로 선언해야 한다.

//global.css.ts
import { globalStyle } from "@vanilla-extract/css";

globalStyle("body[data-theme='dark']", {
  vars: {
    "--text-color": "#E0E0E0",
    "--text-color-rgb": "224 224 224",
    "--bg-color": "#1f1f1f",
    "--content-bg-color": "#2A2A2A ",
    "--border-color": "#424242",
    "--tile-bg-color": "rgba(68, 71, 70, 1.00)",
  },
});

globalStyle("body[data-theme='light']", {
  vars: {
    "--text-color": "#212121",
    "--text-color-rgb": "33 33 33",
    "--bg-color": "#FFFFFF ",
    "--content-bg-color": "#F5F5F5",
    "--border-color": "#BDBDBD",
    "--tile-bg-color": "rgba(225, 227, 225, 1.00)",
  },
});

document.body.dataset은 data-* 속성을 자바스크립트에서 접근할 수 있는 객체다.
document.body.dataset.theme = 'light' 와 같이 설정한다면,
data-theme = 'light'로 설정된다.
data-* 관련 mdn 자료 바로가기

새로고침 시 혹은 웹 사이트 최초 진입 시 깜박임을 제거하려면
브라우저 랜더링 전에 스크립트 파일을 실행해서 body의 data-theme속성에 현재 적용해려는 theme을 지정해줘야한다.

theme 결정 script

아래와 같은 구조로 theme을 지정하고자한다.
사용자가 이전에 설정한 화면 theme가 로컬스토리지에 저장되어 있으면 해당 theme를 불러온다.
로컬스토리지에 저장되어 있지 않다면, 접속한 기기 OS에서 사용중인 화면 theme를 불러온다.

//layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html>
      <body>
         <script
          dangerouslySetInnerHTML={{
            __html: `
            const localTheme = window.localStorage.getItem('theme');
            if(localTheme) document.body.dataset.theme = localTheme;
            else{
              const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
              if(mediaQueryList.matches) document.body.dataset.theme = 'dark';
              else document.body.dataset.theme = 'light';
            }
            `,
          }}
        />
     	...
      </body>
    </html>
  );
}

우선, localStorage에 theme이라는 key로 저장된 테마가 있는지 확인한다.
만약 있다면, body의 data-theme에 해당 속성을 적용한다.

localStorage에 저장된 theme이 없다면, OS의 prefer-color-scheme을 읽어 그에 맞는 테마를 적용한다.

mediaQueryList는 아래와 같은 형태이다.
아래의 예시는 OS의 테마가 light로 설정되어 있고, OS가 dark 모드로 설정되어 있니?를 묻기 때문에 matches가 false인 것을 확인할 수 있다.

이렇게 data-theme을 활용해 각 theme에 맞는 CSS 변수를 생성하였다면
스타일링이 필요한 컴포넌트에서 다음과 같이 스타일해주면 된다.

import { style } from "@vanilla-extract/css";

export const Body = style({
  color: "var(--text-color)",
  background: "var(--bg-color)",
});
profile
세상의 문제 해결을 즐기는 프론트엔드 개발자

0개의 댓글