리액트 다크모드 구현하기

DevSeong2·2023년 5월 4일
0

지난 다크모드에 대해 알아보자 글 작성 이후 다크모드 구현한 내용을 정리해보았습니다.

다크모드 구현하기

익숙한 방법이었던 styled-components

이전 프로젝트에는 styled-components를 사용해 제공되는 ThemeProvider와 theme을 정의해서 구현했었다. (feat. recoil)

// Root.tsx
const Root = () => {
  const isDark = useRecoilValue(isDarkAtom);
  return (
    <>
      <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
        <GlobalStyles />
        <Header />
        <Outlet />
      </ThemeProvider>
    </>
  );
};
// theme.ts
import { DefaultTheme } from 'styled-components';

export const lightTheme: DefaultTheme = {
  bgColor: 'white',
  textColor: '#000',
  itemColor: '#E3CAA5',
  accentColor: '#AD8B73',
};

export const darkTheme: DefaultTheme = {
  bgColor: '#0A2647',
  textColor: '#fff',
  itemColor: '#205295',
  accentColor: '#2C74B3', // 379634
};
// styles.ts
export const Title = styled.h1`
  font-size: 48px;
  color: ${(props) => props.theme.accentColor};
`;

ThemeProvider의 props로 전달한 theme이 변경되면 정의한 theme에 따라 바뀌는 쉽고 유지보수가 편한 방법이었다. 모든 컴포넌트는 styled를 사용해 스타일을 정의할 때 이 theme 값을 사용하여 동적으로 스타일링 할 수 있었다.

하지만 이번에는 순수 CSS만으로만 구현하고자 하였고, styled-components를 사용했을 때보다 성능상의 이점을 얻고자 하였다.

CSS-in-JS 라이브러리는 브라우저에서 페이지가 로드될 때 스타일을 구문 분석해 적용한다.(런타임)

그래서 PostCSS

다크모드가 설정이 되면 html 태그에 다크모드 class 지정하는 방식으로 다크모드를 구현한다. css selector를 이용해 다크모드이면 다른 색상을 사용하는 방식. JS를 사용해 html 요소에 'dark' 클래스를 추가/제거하여 다크모드와 라이트모드를 설정한다.

스타일은 어떻게 적용?

사용자 지정 CSS 속성을 사용한다. :root 의사 클래스는 문서 트리의 루트 요소를 선택한다. 이를 전역 CSS 변수를 선언하는데 활용한다.

사용자 지정 속성은 두 개의 붙임표로 시작하는 속성의 이름과 함께, 유효한 CSS 값이라면 아무거나 그 값으로 지정해 선언합니다. 다른 일반적인 속성과 마찬가지로 사용자 지정 속성도 아래와 같이 규칙 집합 내에 작성합니다.

:root {
  --main-bg-color: brown;
}

위에서 언급했듯, 사용자 지정 속성의 값을 사용할 때에는 일반적인 값의 자리에 var() 함수를 지정하고, 그 매개변수로는 사용자 지정 속성의 이름을 제공합니다.

element {
  background-color: var(--main-bg-color);
}

사용자 지정 CSS 속성 사용하기 (변수) - CSS: Cascading Style Sheets | MDN

(+) html에서 root는 항상 html을 가리키는데 html보다 :root 의사 클래스의 우선순위가 더 높다.

공통으로 사용할 속성을 미리 변수로 선언하고 다크모드가 적용되었을 때 속성을 기존의 변수에 덮어쓴다.

:root {
  --color-black: #000;
  --color-white: #fff;
  --color-accent: #03045e;
  --color-bg: #caf0f8;
  --color-bg-darker: #ade8f4;
  --color-bg-sidebar: #fff;
  --color-bg-nav-hover: #e8f8fb;
  --color-bg-actived: #023e8a;
  --color-border: #03045e;
  --color-text: #03045e;
  --color-text-actived: #fff;
  --color-text-hover: #0096c7;
  --color-badge: #fff;
}

html.dark {
  --color-accent: #495057;
  --color-bg: #212529;
  --color-bg-darker: #495057;
  --color-bg-sidebar: #343a40;
  --color-bg-nav-hover: #222;
  --color-bg-actived: #495057;
  --color-border: #f8f9fa;
  --color-text: #dee2e6;
  --color-text-actived: #fff;
  --color-text-hover: #adb5bd;
  --color-badge: #495057;
}

➡ 다크모드가 적용되었을 때 우선순위가 높은 html.dark 변수가 적용된 모습.

다크모드 Context

다크모드는 어플리케이션 전역에서 공통으로 사용되는 값이므로 Context API를 사용한다.

import React, { createContext, useContext, useEffect, useState } from 'react';
import { IChildren } from '../typing/db';

interface ContextProps {
  darkMode: boolean;
  toggleDarkMode: () => void;
}

// 다크모드 Context 생성
export const DarkModeContext = createContext<ContextProps>({
  darkMode: false,
  toggleDarkMode: () => {},
});

// Context 내부 요소들에 value 전달할 Provider 생성
export const DarkModeProvider = ({ children }: IChildren) => {
  const [darkMode, setDarkMode] = useState(false);
  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
    updateDarkMode(!darkMode);
  };

  // 어플리케이션이 실행될 때 localStorage에 다크모드인지 아닌지 
  // 혹은 브라우저가 다크모드인지 아닌지 확인한 다음 다크모드 초기값을 세팅
  useEffect(() => {
    const isDark =
      localStorage.theme === 'dark' ||
      (!('theme' in localStorage) &&
        window.matchMedia('(prefers-color-scheme: dark)').matches);
    setDarkMode(isDark);
    updateDarkMode(isDark);
  }, []);

  return (
    // 자식 컴포넌트에서 다크모드 설정값에 접근하고 값을 변경할 수 있도록
    // darkMode 변수와 toggle 함수를 전달
    <DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
      {children}
    </DarkModeContext.Provider>
  );
};

// 다크모드 값에 따라 document.documentElement 즉, 루트 요소(html)에 'dark' class 값을 추가, 제거
function updateDarkMode(darkMode: boolean) {
  if (darkMode) {
    document.documentElement.classList.add('dark');
    localStorage.theme = 'dark';
  } else {
    document.documentElement.classList.remove('dark');
    localStorage.theme = 'light';
  }
}

// 외부에서 다크모드 컨텍스트를 사용하기 쉽도록 custom hook을 만들어 사용
export const useDarkMode = () => useContext(DarkModeContext);

다크모드 세팅 기억

prefers-color-scheme CSS 미디어 특성은 사용자의 시스템이 라이트 테마나 다크 테마를 사용하는지 탐지하는 데에 사용됩니다.
prefers-color-scheme - CSS: Cascading Style Sheets | MDN

prefers-color-scheme 미디어 쿼리를 사용하면 사용자 운영체제의 다크모드 설정 여부를 알 수 있다. matchMedia() 메서드에 분석할 미디어 쿼리 문자열을 전달하면 반환되는 MediaQueryList 객체의 matches 속성은 반환값으로 참/거짓을 반환하는데 이를 활용. Window.matchMedia() - Web API | MDN
window.matchMedia('(prefers-color-scheme: dark)').matches);
이를 활용해 최초 웹페이지에 접근하면 운영체제의 다크모드 설정 여부를 따르고 이후 localStorage의 theme 값에 따라 다크모드/라이트모드를 적용한다.

실제 적용

커스텀 hooks를 만들어두었기에 쉽게 사용 가능.

import React from 'react';
import { useDarkMode } from '../reducer/DarkModeContext';
import styles from './ToggleTheme.module.css';

export default function ToggleTheme() {
  const { darkMode, toggleDarkMode } = useDarkMode();

  return (
    <div className={styles.theme}>
      <input
        type="checkbox"
        id="toggle"
        hidden
        checked={darkMode}
        onChange={toggleDarkMode}
      />
      <label htmlFor="toggle">
        <span />
      </label>
    </div>
  );
}

결과

Reference

profile
차근차근

0개의 댓글