[블로그만들기] 다크모드 구현(typescript)

🛠 개발환경 : nextJS, typescript, emotionJS

✨ 다크모드 기능 구현

다크모드 기능을 적용할 것이다.

  • 라이트모드(밝은 배경의 어두운 텍스트)와 다크모드(어두운 배경의 밝은 텍스트)로 스타일을 분리한다.
  • 사용자가 버튼을 클릭하여 원하는 테마로 변경할 수 있다.
  • 페이지를 이동해도 적용한 테마가 유지된다.

💄 라이트모드/다크모드 스타일 지정

먼저 라이트모드와 다크모드 스타일을 지정한다. 이 때 mainFont, subFont 등 객체의 key값이 같아야 한다.

styles.theme.tsx

export const lightTheme = {
  MAIN: "#6868AD",
  SUB: "#dbd7ff",
  BACKGROUND: "#fdfdff",
  SUBBACKGROUND: "rgb(242, 240, 253)",
}

export const darkTheme : Theme = {
  MAIN: "#dbd7ff",
  SUB: "#6868AD",
  BACKGROUND: "#202124",
  SUBBACKGROUND: "#30373e",
}

그리고 타입을 지정해준다. 이 때typeof 를 쓰면, 지정한 객체(lightTheme)의 프로퍼티 타입들을 참조해 타입을 선언할 수 있다.

  • 예시
let s = "hello";
let n: typeof s;
  • 적용
export type Theme = typeof lightTheme;

✨ 다크모드 상태관리 - themeProvider

페이지를 이동해도 적용된 테마가 유지될 수 있도록 theme providertheme context를 활용해 다크모드 상태관리를 진행할 것이다.

일반적으로 react에서 데이터는 부모에서 자식 방향으로 props를 통해 전달한다. 그러나, 부모에서 자식이 아니라 증조에서 증손자로 전달해야되는 상황이라면 과정이 굉장히 번거로워진다. 이 때 context를 활용하면 단계마다 props를 넘겨주지 않아도 전역적으로 값을 공유할 수 있게 된다. 우리는 App 최상위에 ThemeContext.Provider를 추가해서 하위 컴포넌트들이 Context를 통해 테마에 접근할 수 있도록 만들 것이다.

React.createContext

  • 공식문서 예시
const MyContext = React.createContext(defaultValue);

여기서 defalutvalue 는 컴포넌트가 적절한 provider를 찾지 못했을 때 쓰이는 값이다.

  • 적용
//타입 지정
interface ContextProps {
  theme: Theme
  toggleTheme: () => void
}

//객체 생성
export const ThemeContext = createContext<ContextProps>({
  theme: lightTheme,
  toggleTheme: () => {
    return null
  },
})

Context.Provider

ThemeContext 객체 생성을 마쳤다. 이제 Context 객체를 구독하고 있는 컴포넌트들이 Context Provider로부터 현재 값을 읽을 것이다.

  • 공식 문서 예시
<MyContext.Provider value={/* 어떤 값 */}>

provider는 context의 변화를 알리는 역할, 그리고 value prop을 받아서 하위 컴포넌트에게 전달하는 역할을 한다. 따라서 provider의 value가 바뀐다면, context를 구독하고 있는 컴포넌트들은 모두 리렌더링된다.

  • 적용
// _app.tsx

import type { AppProps } from 'next/app';
import React, { createContext } from 'react';
import { Global } from '@emotion/react';
import { GlobalStyle } from '@styles/global-styles';
import { lightTheme, darkTheme, Theme } from '@styles/theme';
import { useDarkMode } from '@hooks/useDarkMode';
import DarkModeToggle from '@components/Home/DarkModetoggle';

interface ContextProps {
  theme: Theme
  toggleTheme: () => void
}

//contextX객체 생성
export const ThemeContext = createContext<ContextProps>({
  //테마와 테마를 변경하는 함수
  theme: lightTheme,
  toggleTheme: () => {
    return null
  },
})

//useDarkMode hook을 통해 theme과 toggleTheme return;
 const { theme, toggleTheme } = useDarkMode();

function MyApp({ Component, pageProps }: AppProps) {
  const { theme, toggleTheme } = useDarkMode()
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>8
        <Global
          styles={GlobalStyle(theme === lightTheme ? lightTheme : darkTheme)}
        />
        <Component {...pageProps} />
        <DarkModeToggle />
    </ThemeContext.Provider>
  )
}

export default MyApp

✨ 다크모드 상태값 저장 : localStorage 그리고 nextJS

// useDarkMode.ts

import { useEffect, useState } from "react";
import { lightTheme, darkTheme, Theme } from "../styles/theme";

export const useDarkMode = () => {
  const [theme, setTheme] = useState<Theme>(lightTheme);

  const setMode = (mode: Theme) => {
    mode === lightTheme
      ? window.localStorage.setItem("theme", "light")
      : window.localStorage.setItem("theme", "dark");
    setTheme(mode);
  };

  const toggleTheme = () => {
    theme === lightTheme ? setMode(darkTheme) : setMode(lightTheme);
  };

  useEffect(() => {
    const localTheme = window.localStorage.getItem("theme");
    if (localTheme !== null) {
      if (localTheme === "dark") {
        setTheme(darkTheme);
      } else {
        setTheme(lightTheme);
      }
    }
  }, []);

  return { theme, toggleTheme };
};

현재 테마(theme)와 테마를 변경하는 함수(toggleTheme)를 리턴해주는 hook을 생성했다. nextJS는 서버 사이드 렌더링이기 때문에 window, localStorage, alert 등에 직접 접근하면 undefined를 뱉어낸다. 그러나 렌더링이 일어난 후 실행되는 useEffect의 성질을 이용하면 localStorage를 사용할 수 있다. localStorage에 기본적으로 라이트테마를 저장해두고, 토글이 클릭될 때마다 테마가 변경된다. 이후에 유저가 페이지를 이동하거나 새로고침을 했을 때에도 localStorage를 참조해 테마를 유지한다.

다크모드 토글


import LightModeIcon from '@mui/icons-material/LightMode';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import styled from '@emotion/styled';
import React, { ReactElement, useContext } from 'react';
import { ThemeContext } from '@pages/_app';
import { lightTheme, Theme } from '@styles/theme';
import { MEDIA_QUERY_END_POINT } from '@constants/.';

interface ToggleProps {
  theme: Theme;
}

export default function DarkModeToggle(): ReactElement {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <ToggleButton onClick={toggleTheme} theme={theme}>
      {theme === lightTheme ? (
        <>
          <Emoji>
            <DarkModeIcon aria-label="darkMoon" />
          </Emoji>
          <ModeContent>다크 모드</ModeContent>
        </>
      ) : (
        <>
          <Emoji>
            <LightModeIcon aria-label="lightSun" />
          </Emoji>
          <ModeContent>라이트 모드</ModeContent>
        </>
      )}
    </ToggleButton>
  );
}

useContext를 통해 Provider 가 전달했던 theme과 toggleTheme을 구독한다. 그리고 button 컴포넌트의 onClick 이벤트에 테마를 변경하는 toggleTheme 함수를 연결해준다.

다크모드 적용 방식 1. global

다크모드 적용방식은 2가지가 있다. 첫 번째는 globalstyle을 활용해 app에서 스타일을 내려주는 방법이다.

//global-style.ts
import { css } from "@emotion/react";
import { Theme } from "../styles/theme";

export const GlobalStyle = (props: Theme) =>
  css`
    body {
      background: ${props.BACKGROUND};
      color: ${props.MAIN_FONT};
    }
  `;
//app.ts
import type { AppProps } from "next/app"
import React, { createContext } from "react"
import { Global } from "@emotion/react"
import { GlobalStyle } from "@styles/global-styles"
import { lightTheme, darkTheme, Theme } from "@styles/theme"
import { useDarkMode } from "@hooks/useDarkMode"
import DarkModeToggle from "@components/Home/DarkModetoggle"


export const ThemeContext = createContext<ContextProps>({
  theme: lightTheme,
  toggleTheme: () => {
    return null
  },
})

function MyApp({ Component, pageProps }: AppProps) {
  const { theme, toggleTheme } = useDarkMode()
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>8
        <Global
          styles={GlobalStyle(theme === lightTheme ? lightTheme : darkTheme)}
        />
        <Component {...pageProps} />
        <DarkModeToggle />
    </ThemeContext.Provider>
  )
}

export default MyApp

다크모드 적용 방식 2. useContext를 활용해 각 컴포넌트에 적용

두 번째는 useContext를 활용해서 각 컴포넌트에서 context를 구독하는 방식이다. 팀 프로젝트에서는 컴포넌트와 스타일 지정한 변수가 많아서 2번째 방식을 선택했다.

import { useContext } from 'react';
import { Theme } from '@styles/theme';
import { ThemeContext } from '@pages/_app';

interface ThemeProps {
  theme: Theme;
}

export const ListCard = () => {
  const { theme } = useContext(ThemeContext);
  
    return (
    <Card theme={theme}/>
   )
   
   const Card = styled.article<ThemeProps>`
  background: ${({ theme }) => theme.CARD_BACKGROUND};
  }
`;
 }

📸 기능 구현 화면

참고링크1
참고링크2

좋은 웹페이지 즐겨찾기