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의 차이점
전체 코드
// 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 {} }