Css

다크모드 적용 (emotion)

minsun309 2024. 8. 23. 11:26

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 {}
}