쿠키를 이용한 다크모드 구현

Next.js
2024-03-11

image

깃허브 링크

쿠키를 이용하여 Next.js 13 환경에서 다크모드를 구현했다.

웹 스토리지를 사용하지 않은 이유

Next.js의 서버 컴포넌트에서는 브라우저의 API를 사용할 수 없다. 즉, 웹 스토리지를 이용할 수 없기 때문에 웹 스토리지 값을 이용하려면 클라이언트 컴포넌트에서 다크 모드를 구현해야 한다. 하지만 이러면 웹 페이지가 완전히 불러오기 전에 한 번 깜빡이는 플리킹 현상이 발생한다.

image

컴포넌트 설계

쿠키가 없을 때 (첫 방문 시)

서버 컴포넌트 : 라이트모드를 기본 테마로 설정한다.

클라이언트 컴포넌트 : interactive하게 테마를 설정하고 쿠키에 값을 추가한다.

쿠키가 있을 때 (이후 방문 시)

서버 컴포넌트 : 쿠키 값을 받아와서 테마를 설정한다.

클라이언트 컴포넌트 : interactive하게 테마를 설정하고 쿠키를 재설정한다.

Next.js로 쿠키를 이용한 다크모드 구현

/app/layout.tsx : 서버 컴포넌트에서 초기 테마를 설정한다.

import { cookies } from "next/headers"; import { CookiesProvider } from "next-client-cookies"; import "./globals.css"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={getThemeInCookie() === "dark" ? "ui-dark" : ""}> <body> <CookiesProvider value={cookies().getAll()}>{children}</CookiesProvider> </body> </html> ); } export const getThemeInCookie = () => { return cookies().get("theme")?.value as CookieTheme; }; type CookieTheme = 'light' | 'dark' | undefined
import { cookies } from "next/headers"; import { CookiesProvider } from "next-client-cookies"; import "./globals.css"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={getThemeInCookie() === "dark" ? "ui-dark" : ""}> <body> <CookiesProvider value={cookies().getAll()}>{children}</CookiesProvider> </body> </html> ); } export const getThemeInCookie = () => { return cookies().get("theme")?.value as CookieTheme; }; type CookieTheme = 'light' | 'dark' | undefined

CookiesProvider : 클라이언트 컴포넌트에서 쿠키 값을 사용할 수 있게 해주는 라이브러리

next/headercookies 를 이용하여 서버 컴포넌트에서 쿠키 값을 불러와서,

  • 쿠키 값이 없거나, 쿠키 객체의 값이 lightclassName 을 설정하지 않는다.
  • 쿠키 객체의 값이 darkclassNameui-dark를 설정한다.

/hooks/useTheme.ts : interactive하게 테마를 설정하는 hooks

"use client"; import { useEffect, useState } from "react"; import { useCookies } from "next-client-cookies"; const useTheme = (cookieTheme: CookieTheme) => { const cookies = useCookies(); const [theme, setTheme] = useState<Theme>(""); // 클라이언트 컴포넌트가 마운트될 때, 브라우저의 쿠키 값을 state에 추가한다. useEffect(() => { if (cookieTheme) setTheme(cookieTheme); }, []); const handleThemeToggle = () => { // 브라우저 완전 초기 접근시 if (!cookieTheme && !theme) { setMode("dark"); return; } if (theme === "light") { setMode("dark"); return; } if (theme === "dark") { setMode("light"); return; } }; const setMode = (type: "dark" | "light") => { setTheme(type); // 다크모드로 설정하면 className에 ui-dark를 추가한다. // 라이트모드로 설정하면 className에 ui-dark를 삭제한다. const html = document.getElementsByTagName("html")[0]; html.classList.toggle("ui-dark"); // 쿠키 재설정하기 cookies.remove("theme"); cookies.set("theme", type, { expires: 3600, }); return; }; return { theme, handleThemeToggle }; }; export type Theme = "" | "light" | "dark"; export type CookieTheme = undefined | "light" | "dark"; export default useTheme;
"use client"; import { useEffect, useState } from "react"; import { useCookies } from "next-client-cookies"; const useTheme = (cookieTheme: CookieTheme) => { const cookies = useCookies(); const [theme, setTheme] = useState<Theme>(""); // 클라이언트 컴포넌트가 마운트될 때, 브라우저의 쿠키 값을 state에 추가한다. useEffect(() => { if (cookieTheme) setTheme(cookieTheme); }, []); const handleThemeToggle = () => { // 브라우저 완전 초기 접근시 if (!cookieTheme && !theme) { setMode("dark"); return; } if (theme === "light") { setMode("dark"); return; } if (theme === "dark") { setMode("light"); return; } }; const setMode = (type: "dark" | "light") => { setTheme(type); // 다크모드로 설정하면 className에 ui-dark를 추가한다. // 라이트모드로 설정하면 className에 ui-dark를 삭제한다. const html = document.getElementsByTagName("html")[0]; html.classList.toggle("ui-dark"); // 쿠키 재설정하기 cookies.remove("theme"); cookies.set("theme", type, { expires: 3600, }); return; }; return { theme, handleThemeToggle }; }; export type Theme = "" | "light" | "dark"; export type CookieTheme = undefined | "light" | "dark"; export default useTheme;

/components/Toggle.tsx : 토글 버튼이 있는 뷰 컴포넌트

import { CookieTheme, Theme } from "@/hooks/useTheme"; import DarkModeIcon from "./DarkModeIcon"; import LightModeIcon from "./LightModeIcon"; type Props = { cookieTheme: CookieTheme; theme: Theme; handleThemeToggle: () => void; }; export default function Toggle(props: Props) { const { cookieTheme, theme, handleThemeToggle } = props; // 쿠키가 있고 클라이언트 컴포넌트가 렌더링 되기 전 if (cookieTheme && !theme) return ( <button className="themeToggle" onClick={handleThemeToggle}> {cookieTheme === "dark" ? <DarkModeIcon /> : <LightModeIcon />} </button> ); return ( <button className="themeToggle" onClick={handleThemeToggle}> {theme === "dark" ? <DarkModeIcon /> : <LightModeIcon />} </button> ); }
import { CookieTheme, Theme } from "@/hooks/useTheme"; import DarkModeIcon from "./DarkModeIcon"; import LightModeIcon from "./LightModeIcon"; type Props = { cookieTheme: CookieTheme; theme: Theme; handleThemeToggle: () => void; }; export default function Toggle(props: Props) { const { cookieTheme, theme, handleThemeToggle } = props; // 쿠키가 있고 클라이언트 컴포넌트가 렌더링 되기 전 if (cookieTheme && !theme) return ( <button className="themeToggle" onClick={handleThemeToggle}> {cookieTheme === "dark" ? <DarkModeIcon /> : <LightModeIcon />} </button> ); return ( <button className="themeToggle" onClick={handleThemeToggle}> {theme === "dark" ? <DarkModeIcon /> : <LightModeIcon />} </button> ); }

“쿠키가 있고 클라이언트 컴포넌트가 렌더링 되기 전”을 따로 구별하는 이유는 cookieTheme은 서버 컴포넌트에 의해 설정되고 이후에 바뀌지 않는 값이기 때문이다.

즉, cookieTheme에 의해 뷰 컴포넌트는 리렌더링 되지 않기 때문에, theme 값이 이용가능해지면 뷰 컴포넌트는 리렌더링을 위해 theme값에 의존해야 한다.

cookieTheme에만 의존하는 렌더링

image

theme에만 의존하는 렌더링

image

cookieTheme & theme

image

관련 포스트

post thumbnail

데스크톱, 모바일 호환되는 가로 슬라이더 구현

Next.js 14, Tailwindcss, Typescript 터치 이벤트는 일반적으로 터치 스크린을 가진 디바이스에서 이용가능하다. 하지만, 터치 스크린을 갖는 디바이스를 포함

2024-03-12
post thumbnail

Next.js + Vercel TTFB 문제 2

에서 Edge runtime으로 TTFB 문제를 해결했었는데, Express를 Next.js로 이미그레이션 후에는 Edge runtime을 사용할 수 없어서 다른 방법으로 문제를

2024-03-19
post thumbnail

Next.js + Vercel TTFB 문제

로컬 환경에서는 애플리케이션의 TTFB(Time To First Byte)가 70~150ms정도 걸리는데, 배포 환경에서는 TTFB가 느리면 1500ms, 빠르면 400ms정도가

2024-03-12
post thumbnail

[번역] Next.js 14

Next.js 14는 다음과 같은 가장 중점으로 둔 릴리스이다. - Turbopack: App & Pages Router에서 5,000번의 테스트 통과 - 로컬 서버 시작이 53%

2024-03-12

도로모의 기술 블로그

# Contact : jyw966@naver.com

Copyright © doromo. All Rights Reserved.