쿠키를 이용하여 Next.js 13 환경에서 다크모드를 구현했다.
Next.js의 서버 컴포넌트에서는 브라우저의 API를 사용할 수 없다. 즉, 웹 스토리지를 이용할 수 없기 때문에 웹 스토리지 값을 이용하려면 클라이언트 컴포넌트에서 다크 모드를 구현해야 한다. 하지만 이러면 웹 페이지가 완전히 불러오기 전에 한 번 깜빡이는 플리킹 현상이 발생한다.
서버 컴포넌트 : 라이트모드를 기본 테마로 설정한다.
클라이언트 컴포넌트 : interactive하게 테마를 설정하고 쿠키에 값을 추가한다.
서버 컴포넌트 : 쿠키 값을 받아와서 테마를 설정한다.
클라이언트 컴포넌트 : interactive하게 테마를 설정하고 쿠키를 재설정한다.
/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/header
의 cookies
를 이용하여 서버 컴포넌트에서 쿠키 값을 불러와서,
light
면 className
을 설정하지 않는다.dark
면 className
에 ui-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
값에 의존해야 한다.
Next.js 14, Tailwindcss, Typescript 터치 이벤트는 일반적으로 터치 스크린을 가진 디바이스에서 이용가능하다. 하지만, 터치 스크린을 갖는 디바이스를 포함
2024-03-12에서 Edge runtime으로 TTFB 문제를 해결했었는데, Express를 Next.js로 이미그레이션 후에는 Edge runtime을 사용할 수 없어서 다른 방법으로 문제를
2024-03-19로컬 환경에서는 애플리케이션의 TTFB(Time To First Byte)가 70~150ms정도 걸리는데, 배포 환경에서는 TTFB가 느리면 1500ms, 빠르면 400ms정도가
2024-03-12Next.js 14는 다음과 같은 가장 중점으로 둔 릴리스이다. - Turbopack: App & Pages Router에서 5,000번의 테스트 통과 - 로컬 서버 시작이 53%
2024-03-12# Contact : jyw966@naver.com
Copyright © doromo. All Rights Reserved.