지난 다크모드에 대해 알아보자 글 작성 이후 다크모드 구현한 내용을 정리해보았습니다.
이전 프로젝트에는 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 라이브러리는 브라우저에서 페이지가 로드될 때 스타일을 구문 분석해 적용한다.(런타임)
다크모드가 설정이 되면 html 태그에 다크모드 class 지정하는 방식으로 다크모드를 구현한다. css selector를 이용해 다크모드이면 다른 색상을 사용하는 방식. JS를 사용해 html 요소에 'dark' 클래스를 추가/제거하여 다크모드와 라이트모드를 설정한다.
사용자 지정 CSS 속성을 사용한다. :root
의사 클래스는 문서 트리의 루트 요소를 선택한다. 이를 전역 CSS 변수를 선언하는데 활용한다.
사용자 지정 속성은 두 개의 붙임표로 시작하는 속성의 이름과 함께, 유효한 CSS 값이라면 아무거나 그 값으로 지정해 선언합니다. 다른 일반적인 속성과 마찬가지로 사용자 지정 속성도 아래와 같이 규칙 집합 내에 작성합니다.
:root { --main-bg-color: brown; }
위에서 언급했듯, 사용자 지정 속성의 값을 사용할 때에는 일반적인 값의 자리에 var() 함수를 지정하고, 그 매개변수로는 사용자 지정 속성의 이름을 제공합니다.
element { background-color: var(--main-bg-color); }
(+) 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 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>
);
}