

https://velog.io/@ongddree/블로그만들기-다크모드-구현
게시물을 통해 참고를 하였고, 이 게시물은 다른 프로젝트에 구현한 것을 회고합니다.
다크모드는 프로젝트 시작 후 빠르게 구현하는게 좋습니다. (꿀팁🍯)
헤더에 라이트모드/다크버튼의 토글을 통해 테마를 변경한다.
페이지를 이동, 새로고침 해도 적용한 테마가 유지되게 한다. (Context API + localStorage)
// 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;
프로젝트의 특성에 맞게 MAIN, SUB는  글자의 색상, BACKGROUND는 배경색을 지정합니다. (더 필요한 색상이 있으면 추가합니다!)
typeof를 통해 ColorTheme type을 export 해줍니다.
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 };
};
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
useDarkMode를 통해 생성되고 ThemeContext로 전파가 된 	~colorTheme, toggleColorTheme를 useContext를 사용해서 구독합니다.
버튼을 클릭하면 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;