개발조각

[모두의 이력서_10-11일차] 다크모드(React Context API + Next.js + TS + styled-component) 본문

모두의 이력서

[모두의 이력서_10-11일차] 다크모드(React Context API + Next.js + TS + styled-component)

개발조각 2023. 4. 5. 18:24
728x90
반응형

다크모드는 나중에 구현할까 했지만 프로젝트 규모가 더 커지기 전에 빨리 구현하는 게 좋겠다는 판단이 들어 구현하게 되었습니다. :)

 

  • 프로젝트 규모가 더 커지기 전에 빠르게 구현
  • 토글을 통해 변경
  • 페이지 이동, 새로고침, 페이지를 나갔다 들어봐도 적용한 테마가 유지 되도록 localStorage를 사용

 

1. context API를 사용한 이유


다크모드 구현 방법은 대표적으로 contex tAPI, 상태관리(redux, recoil)를 사용해서 구현할 수 있습니다.

하지만 context API를 선택한 이유는 light모드, dark모드만 만들 생각인데 상태관리를 쓸 필요가 있을까 해서 context API를 사용하게 되었습니다.

(사실은 context API를 사용해보고 싶어서 사용해보았습니다.)

 

 

2. theme 설정


기존에 styled-component를 초기세팅을 했을 때 theme를 설정했습니다.

초기세팅 시에는 라이트모드, 다크모드를 생각 안 하고 세팅한 거라 이번에는 라이트모드, 다크모드 세팅에 맞게 수정했습니다.

Next에서 styled-component 초기 세팅하는 법을 알고 싶으면 아래 링크로👇

https://development-piece.tistory.com/309

 

[모두의 이력서_7일차] styled-components + Next.js + TS

파일구조 global.ts // 전역스타일 지정 theme.ts // 공통적으로 사용할 스타일 지정 styled.d.ts // theme파일에 들어갈 변수들의 타입을 정의 1. styled-components 설치하기 styled-components를 타입스크립트에서

development-piece.tistory.com

 

// theme.ts
import { DefaultTheme } from "styled-components";

export type colorsType = typeof colors;
export type fontSizesType = typeof fontSizes;
export type commonType = typeof common;
export type deviceType = typeof device;

const colors = {
  // main
  main: "#FF6678",

  // gray
  grayText: "#767676",

  lightTheme: {
    text: "#333",
    bg: "#fff",
    lightBg: "#F8F8F8",
    border: "#E1E2E3",
  },

  darkTheme: {
    text: "#E8E8E8",
    bg: "#1e1f21",
    lightBg: "#353638",
    border: "#292a2d",
  },
};

const fontSizes = {...};
const common = {...};
const device = {...};

const theme: DefaultTheme = {
  colors,
  fontSizes,
  device,
  common,
};

export default theme;

저는 이와 같이 세팅해 주었습니다.

${({theme}) => theme.color.lightTheme.text} 하면 나옵니다.

 

3. context API 설정


context
App 안에서 전역적으로 사용되는 데이터를 여러 컴포넌트끼리 공유 할 수 있는 방법을 제공해 줍니다.
그래서 수많은 컴포넌트들이 필요한 전역적인 데이터를 전달하기에 굉장히 편리합니다.

그렇다고 prop 대신 context를 사용해서는 안됩니다.
context를 사용하면 컴포넌트를 재사용하기 어려워질 수 있기 때문에 꼭 필요할 때만 사용해야 됩니다.

 

context폴더 구조

src
|- context
	|- themeContext.ts
	|- type.ts

 

type.ts

import { Dispatch, SetStateAction } from "react";

export interface IsDark {
  isDark: boolean;
  setIsDark: Dispatch<SetStateAction<boolean>>;
}

 

타입스크립트를 사용하기 때문에 value에 전송해 줄 값을 미리 지정했습니다.

 

themeContext.ts

import { createContext } from "react";

import { IsDark } from "./type";

export const ThemeContext = createContext<IsDark | null>(null);

상태 관리를 하기 위해 createContext를 생성해 주었습니다.

 

_app.tsx

import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ThemeProvider } from "styled-components";

import Header from "../components/layout/header/Header";
import Float from "../components/layout/Float"; // 다크모드 버튼, top 버튼 

import theme from "../styles/theme";
import Global from "../styles/global";
import { ThemeContext } from "../context/themeContext";

const _app = ({ Component }: AppProps) => {
  const [isDark, setIsDark] = useState(false);

  return (
    <>
      <main className={notoSansKr.className}>
        <ThemeContext.Provider value={{ isDark, setIsDark }}>
          <ThemeProvider theme={theme}>
            <Global mode={isDark ? "darkTheme" : "lightTheme"} />
            <Header />
            <Float />
            <Component />
          </ThemeProvider>
        </ThemeContext.Provider>
      </main>
    </>
  );
};

export default _app;

(코드가 길어서 context에서 사용되는 것만 편집했습니다.)

 

ThemeContextProvider는 value라는 prop를 받고, value안에 전달하고자 하는 데이터를 집어넣어 주면 됩니다.

ThemeContextProvider로 감싸고 있는 모든 하위 컴포넌트는 value 집어넣은 데이터를 접근할 수 있습니다.

 

여기서는 [isDark, seIsDark]의 useState를 이용해서

isDark가 true이면 다크모드, false면 라이트모드로 변할 수 있도록 했습니다.

 

4. localstorage 설정하기


페이지 이동, 새로고침, 페이지를 나갔다 들어봐도 적용한 테마가 유지되도록

다크모드이면 localStorage에 담고, 라이트모드면 localStorage를 제거하도록 만들었습니다.

 

localStorage 추가, 읽기, 삭제

setItem() - key, value 추가

localStorage.setItem(key, value)

getItem() - value 읽어 오기

localStorage.getItem(key)

removeItem() - item 삭제

localStorage.removeItem(key);

 

_app에서는 localStorage에 넣어줄 value가 있는지 확인을 해줄거기 때문에 아래와 같이 넣어주었습니다.

(저는 여기서는 key값은 "isDark", value값은 "Y"로 넣어줄 겁니다.)

// _app.tsx
const derkMode = localStorage.getItem("isDark")
const [isDark, setIsDark] = useState(derkMode);

 

하지만.... Next에서 localStorage를 사용하면 다음과 같은 서버 에러가 뜹니다.

서버에러가 뜨는 이유

간단하게
Next.js는 SSR이기 때문에
자세하게
Next.js는 client-side를 렌더하기 전 server-side 렌더를 수행합니다.
Next.js에서 제공하는 Server Side Rendering(SSR)에서는 window, document 같은 브라우저 전역 객체를 사용할 수 없어 window 객체는 client-side에만 존재하게 됩니다.
=> 따라서, 페이지가 client에 로드되고 window 객체가 정의될 때까지 localStorage에 접근할 수 없습니다.

 

해결방법

window 객체의 유무를 살피고 window 함수에 접근한다면 에러메시지를 발생시키지 않으면 됩니다.

typeof window !== 'undefined'

 

const derkMode =
    typeof window !== "undefined" ? localStorage.getItem("isDark") : null;
const [isDark, setIsDark] = useState(derkMode ? true : false);

저는 이렇게 써주었습니다.

 

_app.tsx 최종 코드

import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ThemeProvider } from "styled-components";

import Header from "../components/layout/header/Header";
import Float from "../components/layout/Float"; // 다크모드 버튼, top 버튼 

import theme from "../styles/theme";
import Global from "../styles/global";
import { ThemeContext } from "../context/themeContext";

const _app = ({ Component }: AppProps) => {
  const derkMode =
    typeof window !== "undefined" ? localStorage.getItem("isDark") : null;
  const [isDark, setIsDark] = useState(derkMode ? true : false);

  return (
    <>
      <main className={notoSansKr.className}>
        <ThemeContext.Provider value={{ isDark, setIsDark }}>
          <ThemeProvider theme={theme}>
            <Global mode={isDark ? "darkTheme" : "lightTheme"} />
            <Header />
            <Float />
            <Component />
          </ThemeProvider>
        </ThemeContext.Provider>
      </main>
    </>
  );
};

export default _app;

 

5. 라이트, 다크모드 버튼 생성

라이트모드removeItem을 사용해요 key값을 제거해 주었고,

다크모드setItem을 사용하여 key: "isDark", value:"Y"를 넣어주었습니다.

(라이트모드: isDark false, 다크모드: isDark true)

 

component > layout > Float.tsx

import { useContext } from "react";

import { ThemeContext } from "../../context/themeContext";
import { IsDark } from "../../context/type";

import * as styled from "../../styles/components/layout/Float";

import { Moon, Sun } from "@styled-icons/heroicons-solid";
import { ChevronUp } from "@styled-icons/boxicons-regular";

const Float = () => {
  const { isDark, setIsDark } = useContext(ThemeContext) as IsDark;

  const handlerIsDarkClick = () => {
    if (isDark) {
      localStorage.removeItem("isDark");
    } else {
      localStorage.setItem("isDark", "Y");
    }
    setIsDark(!isDark);
  };

  return (
    <styled.FloatCon mode={isDark ? "darkTheme" : "lightTheme"}>
      <button onClick={handlerIsDarkClick}>
        {isDark ? <Sun /> : <Moon />}
      </button>
      <button>
        <ChevronUp />
      </button>
    </styled.FloatCon>
  );
};

export default Float;

 

styled-icons 사용해 보기(부가적 내용)

이번에는 react-icons가 아닌 styled-icons를 사용해 보았습니다.

 

설치하기

npm i styled-icons

 

사용방법

사용방법은 react-icons랑 사용방법이 똑같습니다.

import {Zap} from '@styled-icons/octicons'

const App = () => <RedZap />

색상 변경은 svg에서 변경하시면 됩니다.

 

아이콘은 아래 링크로 들어가셔서 사용하시면 됩니다.

https://styled-icons.dev/

 

Styled Icons - a Styled Components icon library

Import icons from the following icon packs as Styled Components: Bootstrap, Boxicons, Crypto Icons, Entypo, Eva Icons, Evil Icons, Feather, FluentUI System, Font Awesome, Foundation, Heroicons, Icomoon, Ionicons, Material Design, Octicons, Open Iconic, Rem

styled-icons.dev

 

6. 라이트, 다크모드에 따라 스타일 적용

 

최상단 gif를 보시면 토글버튼에서도 다크모드 라이트 모드에 따라 스타일이 변경되시는 걸 보실 수 있습니다.

저는 styled-component의 props를 사용해서 라이트, 다크 모드에 따른 스타일 변경사항을 제어했습니다.

 

이런 식으로 전달을 해주고

<styled.FloatCon mode={isDark ? "darkTheme" : "lightTheme"}>

 

스타일 파일에 props로 전달받은 mode의 타입을 정의를 하고

 

export const FloatCon = styled.div<{ mode: "darkTheme" | "lightTheme" }>`

mode에서 받은 값을 넣어주시면 됩니다.

background: ${({ theme, mode }) => theme.colors[mode].bg};

globals, header, footer 등등 테마가 적용될 곳에 다 적용하시면 됩니다.

 

최종코드

component > layout > Float.tsx

import { useContext } from "react";

import { ThemeContext } from "../../context/themeContext";
import { IsDark } from "../../context/type";

import * as styled from "../../styles/components/layout/Float";

import { Moon, Sun } from "@styled-icons/heroicons-solid";
import { ChevronUp } from "@styled-icons/boxicons-regular";

const Float = () => {
  const { isDark, setIsDark } = useContext(ThemeContext) as IsDark;

  const handlerIsDarkClick = () => {
    if (isDark) {
      localStorage.removeItem("isDark");
    } else {
      localStorage.setItem("isDark", "Y");
    }
    setIsDark(!isDark);
  };

  return (
    <styled.FloatCon mode={isDark ? "darkTheme" : "lightTheme"}>
      <button onClick={handlerIsDarkClick}>
        {isDark ? <Sun /> : <Moon />}
      </button>
      <button>
        <ChevronUp />
      </button>
    </styled.FloatCon>
  );
};

export default Float;

Float.ts

import styled from "styled-components";
import { SpinLeft, SpinRight } from "../../common/animation";

export const FloatCon = styled.div<{ mode: "darkTheme" | "lightTheme" }>`
  position: fixed;
  bottom: 20px;
  right: 20px;
  display: flex;
  gap: 8px;

  button {
    width: 36px;
    height: 36px;
    border: 1px solid ${({ theme, mode }) => theme.colors[mode].border};
    border-radius: 50%;
    background: ${({ theme, mode }) => theme.colors[mode].bg};
    transition: all 0.3s;

    &:hover {
      background: ${({ theme, mode }) => theme.colors[mode].lightBg};
    }
  }

  button:first-of-type {
    animation: ${({ mode }) => (mode === "darkTheme" ? SpinRight : SpinLeft)} 1s;

    svg {
      width: 20px;
      transform: rotation;
    }
  }
`;

 

참고자료


context API 설명 및 사용법 + 라이트, 다크모드 실습

https://www.youtube.com/watch?v=LwvXVEHS638&list=PLZ5oZ2KmQEYjwhSxjB_74PoU6pmFzgVMO&index=6 

 

contextAPI로 라이트, 다크 모드 만들기

https://velog.io/@gparkkii/reactdarkmode#--%EB%8B%A4%ED%81%AC%EB%AA%A8%EB%93%9C-%ED%85%8C%EB%A7%88-%EC%8A%A4%ED%83%80%EC%9D%BC-%EC%84%B8%ED%8C%85

 

리액트 다크모드 구현하기 feat. styled-components & context API

사용자 경험을 최상으로 이끌어주는 디자인 트렌드 다크모드 UI.애플, 구글, 인스타그램, 페이스북 등 세계적인 브랜드들이 이 다크모드 기능을 애용하기 시작하며 UI/UX에 필수적인 기능 중 하나

velog.io

https://velog.io/@minbr0ther/React.js-%EB%8B%A4%ED%81%AC%EB%AA%A8%EB%93%9C-Emotion.js-Next.js-TypeScript

 

[React.js] 다크모드 (Emotion.js + Next.js + TypeScript)

다크모드를 구현 가즈아앗~!

velog.io

 

localStorage 사용법

https://hianna.tistory.com/697

 

[Javascript] localStorage 사용법 (읽기, 쓰기, 삭제, 키목록 등)

이번에는 localStorage 사용법을 정리해보았습니다. localStorage란? localStorage에 아이템 추가, 읽기 localStorage에 객체, 배열 저장하기 localStorage에 값 삭제하기 localStorage에 값 전체 삭제하기 localStorage의

hianna.tistory.com

 

Next에서 localStorage 오류 해결

https://brick-house.tistory.com/18

 

Next js에서 sessionStorage is not defined 문제 해결 방법

개발 중에 발견한 문제에 대한 해결책을 간단하게 적어두려 한다. 우선 Next Js는 SSR이라는 점을 꼭 기억해야 한다!! 무턱대고 sessionStorage 혹은 localStorage (그 외 window 함수..)를 사용하려고 하면 다

brick-house.tistory.com

https://velog.io/@qhflrnfl4324/%EB%82%91%EA%B9%A1%ED%8C%9C08-js-cookie

 

🍊 낑깡팜_08 : "localStorage is not defined" in Next.js with js-cookie

🔸 How to Fix "localStorage is not defined" in Next.js

velog.io

 

728x90
반응형
Comments