image

다크모드 적용 (emotion)

태그
EmotionReact
상세설명emotion으로 다크모드 적용하는 방법
작성일자2023.10.18

Emotion으로 다크모드 적용해보았다.

theme 색상적용

먼저 styles 폴더 하위에 theme 전용 파일인 Theme.ts를 만들고 라이트 / 다크 모드에 대한 색상 값을 지정해줬다.

// styles/Theme.ts
import { Theme } from "@emotion/react";

interface VariantType {
  [key: string]: string;
}

export const colors: VariantType = {
  white: "#fff",
};

export const lightTheme: Theme = {
  mode: {
    text: "#202020",
    background: "#F5F7FC",
    backgroundMain: colors.white,
    borderColor: "rgba(17, 17, 17, 0.15)",
  },
};

export const darkTheme: Theme = {
  mode: {
    text: "#D9D9D9",
    background: "#32323D",
    backgroundMain: "#3f3f4d",
    borderColor: "#666565",
  },
};

ThemeProvider

그 다음으로 theme 을 사용하기 위해 ThemeProvide r를 페이지에 적용해야 하는데 최 상단인 _app.tsx에서 <Component {...pageProps} />를 <Layout></Layout>로 감싸는 방식으로 진행했기 때문에 Layout compoenet에서 ThemeProvider를 적용했다.

Layout 전체를 ThemeProvider 로 감싸고 styles 폴더에서 라이트 / 다크 모드에 대한 색상 값을 지정한 lightTheme / darkTheme를 colorTheme 상태 값에 알맞게 지정해줬다.

theme={colorTheme === lightTheme ? lightTheme : darkTheme}

Header에서 라이트 / 다크 모드를 변경할 버튼을 배치 할 예정이기 때문에 Header component 에 colorTheme={colorTheme} setColorTheme={setColorTheme} 값을 넘겼다.

// components/Layout.tsx
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
import { PropsWithChildren } from "react";
import Header from "./Header";
import Nav from "./Navbar";
import { ThemeProvider } from "@emotion/react";
import { darkTheme, lightTheme } from "@/styles/Theme";
import { useState } from "react";

export default function Layout({ children }: PropsWithChildren) {
  const [colorTheme, setColorTheme] = useState(lightTheme);
  return (
    <ThemeProvider theme={colorTheme === lightTheme ? lightTheme : darkTheme}>
      <div css={layout}>
        <Header colorTheme={colorTheme} setColorTheme={setColorTheme} />
        <section className="bottom-area">
          <Nav />
          <div className="right-area">
            <section className="content-container">{children}</section>
          </div>
        </section>
      </div>
    </ThemeProvider>
  );
}

라이트 / 다크 모드 버튼 & localStorage 저장

먼저 styles/Theme에서 darkTheme, lightTheme를 가지고 온 후 colorTheme이 darkTheme / lightTheme 일 경우의 버튼 스타일을 지정해준다.

 <button
    onClick={toggleColorTheme}
    css={css`color: ${colorTheme === lightTheme ? "#202020" : "#D9D9D9"}`}
>
   {colorTheme === lightTheme ? (
      <BsSun css={{ fontSize: "2.4rem" }} />
    ) : (
      <BsMoon css={{ fontSize: "2.4rem" }} />
    )}
</button>

그리고 사용자가 라이트, 다크 모드 변경 시 브라우저를 나가더라도 다시 진입 시 해당 값이 유지되기 위해서 localStorage에 colorTheme값을 저장해야 된다. setMode 함수를 만들어 Key 명이 theme,

Value 값이 mode 에 따라 light / dark 로 저장되게 하였다.

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

이제 onClick={toggleColorTheme} 통해 colorTheme 이 변할 수 있도록 toggleColorTheme 함수를 만들고 setMode에도 Theme을 보낸다.

const toggleColorTheme = () => {
    colorTheme === lightTheme ? setMode(darkTheme) : setMode(lightTheme);
    colorTheme === lightTheme ? setColorTheme(darkTheme): setColorTheme(lightTheme);
 };

마지막으로 useLayoutEffect를 활용하여 페이지에 진입 시 localStorage에 theme이 있다면 해당 값을 colorTheme에 적용한다.

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

useLayoutEffect 를 사용한 이유

새로 고침 시 화면 깜빡임 문제를 해결하기 위함이다.

useEffect와 useLayoutEffect의 차이점

  • useEffect는 DOM이 화면에 그려진 이후에 호출됩니다.그렇기 때문에 useEffect를 이용해서 Layout을 변경할 경우, 새로 고침 시 화면이 깜빡이는 문제가 발생할 수 있습니다. 이전 상태가 그려진 이후에, Layout이 바뀐다.
  • useLayoutEffect는 이러한 문제를 해결하기 위해 등장했습니다. useLayoutEffect는 DOM이 화면에 그려지기 전에 호출됩니다. 따라서, useLayoutEffect를 이용해 Layout을 변경할 경우, Layout이 변경된 이후에 DOM이 그려지기 때문에 새로 고침 시 깜빡임이 발생하지 않습니다.
  • 전체 코드

    // components/Header.tsx
    /** @jsxImportSource @emotion/react */
    import { css } from "@emotion/react";
    import { useLayoutEffect, useState } from "react";
    import { darkTheme, lightTheme } from "@/styles/Theme";
    import { BsSun, BsMoon } from "react-icons/bs";
    
    export default function Header({ colorTheme, setColorTheme }: any) {
      // darkmode save in localStorage
      const setMode = (mode: any) => {
        mode === lightTheme
          ? window.localStorage.setItem("theme", "light")
          : window.localStorage.setItem("theme", "dark");
        setColorTheme(mode);
      };
    
      const toggleColorTheme = () => {
        colorTheme === lightTheme ? setMode(darkTheme) : setMode(lightTheme);
        colorTheme === lightTheme
          ? setColorTheme(darkTheme)
          : setColorTheme(lightTheme);
      };
    
      useLayoutEffect(() => {
        const localTheme = window.localStorage.getItem("theme");
        if (localTheme !== null) {
          if (localTheme === "dark") {
            setColorTheme(darkTheme);
          } else {
            setColorTheme(lightTheme);
          }
        }
      }, [setColorTheme]);
    
      return (
        <header css={headerArea}>
          <h3 css={{ fontSize: "1.8rem", minWidth: "22rem" }}>
            사이트
          </h3>
          <div className="header-right">
            <button
              onClick={toggleColorTheme}
              css={css`color: ${colorTheme === lightTheme ? "#202020" : "#D9D9D9"}`}
            >
              {colorTheme === lightTheme ? (
                <BsSun css={{ fontSize: "2.4rem" }} />
              ) : (
                <BsMoon css={{ fontSize: "2.4rem" }} />
              )}
            </button>
          </div>
        </header>
      );
    }
    
    const headerArea = (theme: ThemeType) => css`
      display: flex;
      justify-content: space-between;
      width: 100%;
      height: 6rem;
      align-items: center;
      padding: 0 2rem;
      border-bottom: 1px solid ${theme.mode.borderColor};
    `;

    css에서 theme 적용 방법

    css 에서는 const reset = (theme: ThemeType) => css`` 이렇게 theme 을 가지고 와서 theme 에 있는 mode에 따라 선언된 값을 가지고 오면 된다. background: ${theme.mode.background}; 이렇게 선언 시 라이트 모드에서 background 색상은 "#F5F7FC”, 다크 모드에서는 "#32323D”가 들어가진다.

    import { css } from "@emotion/react";
    import { colors } from "./Theme";
    import { ThemeType } from "@/InterfaceGather";
    
    export const reset = (theme: ThemeType) => css`
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        font-family: "Pretendard";
        font-weight: 400;
      }
      html {
        font-size: 62.5%;
      }
      body {
        width: 100%;
        height: 100%;
        font-size: 1.4rem;
        color: ${theme.mode.text};
        background: ${theme.mode.background};
      }
      input,
      select {
        border: 1px solid ${theme.mode.borderColor};
        color: ${theme.mode.text};
        background: ${theme.mode.backgroundMain};
      }
    `;

    Typescript 사용 시 theme 타입 지정

    Typescript 사용 시 theme 타입 지정이 필요해 Interface 파일을 만들어 theme 색상에 대한 타입을 지정 한 후 Theme이라는 Emotion의 기본 테마에 대한 TypeScript 인터페이스를 확장했다.

    //Interface.ts
    
    // Theme
    export interface ThemeType {
      mode: {
        text: string;
        background: string;
        backgroundMain: string;
        borderColor: string;
      };
    }
    
    // emotion.d.ts
    declare module "@emotion/react" {
      export interface Theme extends ThemeType {}
    }