[React.js] 다크모드 (Emotion.js + Next.js + TypeScript)

https://velog.io/@ongddree/블로그만들기-다크모드-구현
게시물을 통해 참고를 하였고, 이 게시물은 다른 프로젝트에 구현한 것을 회고합니다.


🎯 TL;DR

  • 다크모드는 프로젝트 시작 후 빠르게 구현하는게 좋습니다. (꿀팁🍯)

  • 헤더에 라이트모드/다크버튼의 토글을 통해 테마를 변경한다.

  • 페이지를 이동, 새로고침 해도 적용한 테마가 유지되게 한다. (Context API + localStorage)


1️⃣ theme 색상 지정

// styles/theme.ts
export const lightTheme = {
  MAIN: 'black',
  SUB: 'white',
  BACKGROUND: '#fdfdff',
};

export const darkTheme = {
  MAIN: 'white',
  SUB: 'black',
  BACKGROUND: '#202124',
};

export type ColorTheme = typeof lightTheme;
  1. 프로젝트의 특성에 맞게 MAIN, SUB는 글자의 색상, BACKGROUND는 배경색을 지정합니다. (더 필요한 색상이 있으면 추가합니다!)

  2. typeof를 통해 ColorTheme type을 export 해줍니다.


2️⃣ useDarkMode() 구현

  • Next.js에서는 렌더링이 일어나기 전에 localStorage, window 등에 접근하면 에러가난다. 그래서 useEffect내부에서 window.localStorage.getItem('theme')를 통해 새로고침 or 첫 렌더링시 테마를 결정하게 된다. (default value는 lightTheme이다.)

  • 처음으로 localStorage.setItem을 하는 시기는 라이트모드 -> 다크모드를 처음했을때다.

// hooks/useDarkMode.ts

import { useEffect, useState } from 'react';
import { lightTheme, darkTheme, ColorTheme } from '../styles/theme';

export const useDarkMode = () => {
  // 1. 초기 colorTheme은 lightTheme를 가진다.
  const [colorTheme, setColorTheme] = useState<ColorTheme>(lightTheme);
  
  // 4. state의 값도 변경 + local 저장 값도 변경
  const setMode = (mode: ColorTheme) => {
    mode === lightTheme
      ? window.localStorage.setItem('theme', 'light')
      : window.localStorage.setItem('theme', 'dark');
    setColorTheme(mode);
  };

  // 3. 사용자가 toggleColorTheme을 하면 setMode를 통해 기존의 colorTheme과 반대 값을 저장한다.
  const toggleColorTheme = () => {
    colorTheme === lightTheme ? setMode(darkTheme) : setMode(lightTheme);
  };

  // 2. 마운트 되면 localStorage에 'theme'이 있는지 찾는다.
  // - 새로고침시 다크모드/라이트모드 바로 적용
  // - 페이지 로드가 처음이면 이 과정은 무시된다
  useEffect(() => {
    const localTheme = window.localStorage.getItem('theme');
    if (localTheme !== null) { // localTheme이 존재한다면
      if (localTheme === 'dark') {
        setColorTheme(darkTheme);
      } else {
        setColorTheme(lightTheme);
      }
    }
  }, []);

  return { colorTheme, toggleColorTheme };
};

3️⃣ darkmode 상태 관리

props를 계속 넘겨주는 방식을 피하기 위해서 Context API를 사용합니다.
페이지를 이동해도 테마를 유지할 수 있도록 ThemeContext.Provider를 통해서 하위 컴포넌트들이 Context를 통해 테마를 접근할 수 있도록합니다.

Next.js에서는 최상단이 _app이기 때문에 여기서 설정을 하게 됩니다.

// pages/_app.tsx

import React, { createContext } from 'react';
import type { AppProps } from 'next/app';
import { Global } from '@emotion/react';
import { lightTheme, darkTheme, ColorTheme } from '../styles/theme';

// createContext 타입지정
interface ContextProps { 
  colorTheme: ColorTheme;
  toggleColorTheme: () => void;
}

// Context 생성
export const ThemeContext = createContext<ContextProps>({
  colorTheme: lightTheme, // 초기 값으로 lightTheme를 넣어줍니다.
  toggleColorTheme: () => { // light || dark mode를 토글합니다.
    return null
  },
})

function MyApp({ Component, pageProps }: AppProps) {
  // ❗️useDarkMode hook을 통해 theme과 toggleTheme return;
 const { theme, toggleTheme } = useDarkMode();
  
  return (
    // Provider은 context의 변화를 알리는 역할을 합니다.
    // toggleTheme를 통해 theme이 변경되면 하위 컴포넌트들은 모두 리렌더링됩니다.
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
        <Component {...pageProps} />
    </ThemeContext.Provider>
  )
}

export default MyApp

4️⃣ 다크모드 사용 및 토글 버튼

  • useDarkMode를 통해 생성되고 ThemeContext로 전파가 된 ~colorTheme, toggleColorThemeuseContext를 사용해서 구독합니다.

  • 버튼을 클릭하면 toggleColorTheme을 작동합니다.

  • 가져온 colorTheme를 통해 css를 적용합니다.

// components/Header/HeaderBtns/DarkModeToggle/index.tsx

import React, { ReactElement, useContext } from 'react';
import { ThemeContext } from '../../../../pages/_app';
import styled from '@emotion/styled';
import { lightTheme, ColorTheme } from '../../../../styles/theme';

interface ToggleProps {
  colorTheme: ColorTheme;
}

const DarkModeToggle = () => {
  // 1. useContext를 통해서 colorTheme, toggleColorTheme를 구독한다
  const { colorTheme, toggleColorTheme } = useContext(ThemeContext);

  return (
    // 2. 버튼을 클릭하면 toggleColorTheme을 작동한다
    <ToggleButton onClick={toggleColorTheme} colorTheme={colorTheme}>
      {colorTheme === lightTheme ? '다크 모드' : '라이트 모드'}
    </ToggleButton>
  );
}

// 3. colorTheme을 prop으로 가져와 css를 적용한다.
const ToggleButton = styled('button')<ToggleProps>`
  display: flex;
  color: ${({ colorTheme }) => colorTheme.MAIN};
  cursor: pointer;
  background: ${({ colorTheme }) => colorTheme.BACKGROUND};
  box-shadow: 3px 3px 10px rgb(0 0 0 / 20%);

  &:hover {
    filter: brightness(${({ colorTheme }) => (colorTheme === lightTheme ? '0.9' : '1.13')});
  }
`;

export default DarkModeToggle;

좋은 웹페이지 즐겨찾기