ABOUT ME

Today
Yesterday
Total
  • [ 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

     

    참고 블로그

    https://velog.io/@velopert/velog-dark-mode

    https://voyage-dev.tistory.com/134

Designed by Tistory.