-
[ Project ] Next.js 13 다크모드 적용기Project 2023. 8. 30. 18:08
- 구현 방향
현재 프로젝트에 Styled-component를 사용하고 있다.
해당 스택 기준으로 다크 모드를 구현할 수 있는 방법은 두 가지로 추려지는데
첫 번째로는 Theme Provider 사용,
두 번째로는 Css variable 사용하는 방법이 있다.
그 중 벨로퍼님의 의견을 참고하여 Css variable 방법을 채택하여 구현했다.
- 스타일 설정
디자이너 없이 진행하고 있는 프로젝트라서,
dark mode, light mode 색상 선택이나 디자인 적인 부분을 생각하고 기획하는데 시간이 은근 쓰였다.
단순히 디자이너의 영역이라고 생각하면 스트레스였지만, 프론트엔드 개발자로서 나쁠 것 없는 역량이라 생각하며 . . 😊
객체에 각 모드별 스타일 색상을 정의하고 Css variable로 변환해주는 코드를 정의했다(출처 : 벨로퍼님).
// src/styles/theme.ts interface ThemeVariables { bg_element : string; bg_element2 : string; bg_element3 : string; bg_element4: string; text1: string; text2: string; text3: string; border: string; borderRadius: string; } type Theme = 'light' | 'dark' type VariableKey = keyof ThemeVariables; type ThemedPalette = Record<VariableKey, string>; // 각 모드별 스타일 색상 객체 const themeVariableSets: Record<Theme, ThemeVariables> = { light: { bg_element : "#FFFFFF", bg_element2 : "#EEEEEE", bg_element3 : '#f8f9fa', bg_element4: "#1877FF", // (버튼) text1: "#000000", text2: "#FFFFFF", // (버튼) text3: "#1877FF", border: "#EEEEEE", borderRadius: '7px', }, dark: { bg_element : "#121212", bg_element2 : "#1E1E1E", bg_element3 : '#1B1B1B', bg_element4: "#BB86FC", text1: "#E1E1E1", text2 : "#000000", text3 : "#BB86FC", border: "#555555", borderRadius: '7px', }, }; const buildCssVariables = (variables: ThemeVariables) => { const keys = Object.keys(variables) as (keyof ThemeVariables)[]; return keys.reduce( (acc, key) => acc.concat(`--${key.replace(/_/g, '-')}: ${variables[key]};`, '\n'), '', ); }; export const themes = { light: buildCssVariables(themeVariableSets.light), dark: buildCssVariables(themeVariableSets.dark), }; const cssVar = (name: string) => `var(--${name.replace(/_/g, '-')})`; const variableKeys = Object.keys(themeVariableSets.light) as VariableKey[]; export const themedPalette: Record<VariableKey, string> = variableKeys.reduce( (acc, current) => { acc[current] = cssVar(current); return acc; }, {} as ThemedPalette, );
styled-components GlobalStyle로 적용한다.
import { createGlobalStyle } from "styled-components"; import { themes } from "./theme"; import reset from "styled-reset"; export const GlobalStyle = createGlobalStyle` ${reset}; // case : prefers-color-scheme: light body { ${themes.light} transition: 0.125s all ease-in; } // case : prefers-color-scheme: dark @media (prefers-color-scheme: dark) { body { ${themes.dark} } } // case : 유저가 light mode로 변경했을 경우 body[data-theme='light'] { ${themes.light}; } // case : 유저가 dark mode로 변경했을 경우 body[data-theme='dark'] { ${themes.dark}; } `;
- 다크모드 세부 구현 사항
1. 처음에는 유저의 시스템 테마에 따라서 다크모드 상태 설정.
2. 유저가 한 번이라도 다크모드 상태를 변경하면, 이후에는 시스템 테마에 따른 색상이 아닌 유저가 변경한 상태로 유지.
이제 두 가지 세부 구현 사항을 위해 custom hook으로 구현했다.
// src/hook/useTheme import { useState, useEffect, useMemo, useCallback } from "react"; type ThemeKey = 'light' | 'dark' | 'init'; type ReturnType = { theme: ThemeKey; isDarkMode: boolean; setTheme: (theme: ThemeKey) => void; toggleTheme: () => void; }; const useTheme = (): ReturnType => { const [theme, setTheme] = useState<ThemeKey>('init'); const isDarkMode = theme === 'dark' // 초기 다크모트 세팅 useEffect(() => { // 유저의 시스템테마 const preferDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const initalTheme = (localStorage?.getItem('theme') || (preferDarkMode ? 'dark' : 'light')) as ThemeKey; localStorage.setItem("theme", initalTheme); document.body.dataset.theme = initalTheme; setTheme(initalTheme); }, []); // 초기 이후 사용자가 다크모드 상태를 변경할 경우 useEffect(() => { if(theme === 'init') return localStorage.setItem("theme", theme); document.body.dataset.theme = theme; }, [theme]); const toggleTheme = () => { setTheme((prev) => (prev === "light" ? "dark" : "light")); } return { theme, isDarkMode, setTheme, toggleTheme }; }; export default useTheme;
📌 두 번의 useEffect 사용, 각 역할에 대해서
1. 유저의 시스템 테마에 따라서 다크모드 상태 설정.
-> 첫 번째 useEffect에서는 초기 로드 시 진행되는 테마 세팅이다.
localStorage에 테마가 저장되어 있다면 그 값으로 테마가 설정이 되고, localStorage에 테마가 저장되어 있지 않다면
유저의 시스템 테마로 테마가 설정된다.
2. 유저가 한 번이라도 다크모드 상태를 변경하면, 이후에는 시스템 테마에 따른 색상이 아닌 유저가 변경한 상태로 유지.
-> 두 번째 useEffect에서는 의존성 배열에 theme 상태를 설정함으로써 유저가 토글 버튼을 통해 테마를 변경할 때 작동하는 코드이다.
유저의 액션으로 변경된 테마를 localStorage에 저장하여 새로고침해도 설정한 테마가 유지되도록 구현했다.
- useTheme hook 적용
const [theme, setTheme] = useState<ThemeKey>('init');
초기값을 'init'으로 한 이유는 initialTheme에 값이 할당되는 동안 토글버튼이 보이지 않게 하기 위해서 'init'으로 설정했다.
type ThemeKey = 'light' | 'dark' | 'init'; 세 가지 상태가 존재하는데
즉 'init'은 모드 상태를 초기 할당하는 중에 있는 상태이다.
// components/Toggle import { DarkModeSwitch } from 'react-toggle-dark-mode'; import useTheme from '@/hooks/useTheme'; const Toggle = () => { const { isDarkMode, toggleTheme, theme } = useTheme(); return ( theme !== 'init' ? <DarkModeSwitch style={{ }} checked={isDarkMode} onChange={toggleTheme} size={30} /> : null ) } export default Toggle
다크모드 변환 GIF
참고 블로그
'Project' 카테고리의 다른 글
[ Project ] FormData ( File과 Json data를 함께 ) (0) 2023.10.19 [ Project ] 컴포넌트 설계에 대한 고민 (Feat. 합성 컴포넌트) (1) 2023.09.10 [ Project ] Axios Interceptor 활용하기 (0) 2023.09.06 [ Project ] RTK-Query에 Axios 입히기 (0) 2023.09.04 [ Project ] React 재사용 버튼 컴포넌트 만들기 (0) 2023.09.01