리액트에서 Pure CSS(다른 라이브러리를 사용하지 않은 CSS)만을 사용해 웹 페이지에서 다크 모드 기능을 구현하는 방법에 대해 알아보자. 🌞🌛
다크 모드는 UI 디자인의 한 부분으로, 배경 색상은 어둡게 하고 그 외 텍스트나 UI 요소는 밝게 표시하는 디스플레이 설정이다.
웹 뿐만 아니라 모바일이나 OS와 같은 다양한 플랫폼에서 제공되고 있으며, 시스템 설정에 따라 자동으로 적용되거나 사용자 설정에 의해 직접 적용될 수도 있다.
만약 사이트가 다크 모드가 지원되는지 간단하게 확인해보려면 맥에서 시스템 설정 > 화면 모드로 들어가서 라이트 모드/다크 모드를 각각 눌러서 모드 전환을 해보면 된다.

다크 모드를 사용하면, 어두운 환경에서도 시각적인 피로를 줄여 준다거나 배터리 소모 면에서도 어두운 픽셀이 더 적은 전력을 소비하기 때문에 여러모로 사용자에게 이점을 제공해 줄 수 있다.
이처럼 최근 수년간 다크 모드는 사용자 경험을 향상시키기 위한 중요한 기능으로써, 주요 트렌드 중 하나로 자리 잡아가고 있다.
그럼 이제 본격적으로 CSS의 표준 문법을 이용해서 추가적인 도구 없이도 다크 모드를 사용자에게 어떻게 제공할 수 있는지 알아보도록 하자.
prefers-color-schemeCSS 미디어 특성 중 하나로, 사용자가 라이트 모드(light mode)를 선호하는지 다크 모드(dark mode)를 선호하는지와 관련된 정보를 감지하기 위해 사용된다.
사용자는 OS 시스템 설정이나 User agent 설정에 따라 원하는 색상 테마를 선택할 수 있으며, 이 값이 prefers-color-scheme에 의해 불러와진다. 이를 활용해 사용자 색상 선호도에 따라 자동으로 테마를 반영시킬 수 있다.
만약 선호하는 값이 없는 걸로 나타나면 기본 값은 "light" 이다.
@media (prefers-color-scheme: light) {
body {
background-color: #fff;
color: #333;
}
}
@media (prefers-color-scheme: dark) {
body {
background-color: #333;
color: #fff;
}
}
위 코드는 사용자가 라이트 모드를 선호하는 경우 HTML body 요소에 흰 배경에 차콜색 글씨를 적용하고, 다크 모드를 선호하는 경우 차콜색 배경에 흰 글씨를 적용한다는 코드이다.
color-scheme 프로퍼티요소가 어떤 컬러 스키마를 지원하는지 브라우저에게 알려주는 역할을 하는 속성이다.
브라우저는 이 정보를 기반으로 UI 요소들의 스타일을 자동으로 조정한다.
light : 라이트 모드 지원dark : 다크 모드 지원light dark : 사용자 선호도에 따라 둘 다 지원only light | only dark : 각각 라이트 모드/다크 모드 만을 지원함을 명시적으로 선언normal : 브라우저 기본 스키마 사용:root {
color-scheme: light dark;
}
위 코드는 문서가 라이트 모드와 다크 모드를 모두 지원함을 브라우저에 알리며, 전체 페이지에서 기본 UI 요소 스타일을 사용자가 선호하는 컬러 스키마로 적용하게 된다.
light-dark() 함수앞서 설명한 2가지 기능에 비해서는 비교적 새로운 기능으로, 사용자가 선호하는 시스템 테마 모드에 맞는 값을 선택할 수 있도록 도와준다.
예를 들면 다음과 같이 사용된다.
property: light-dark(라이트_모드에서_사용될_값, 다크_모드에서_사용될_값);
이를 통해 별도 미디어 쿼리 없이도 간편하게 라이트/다크 모드를 구현할 수 있지만, 최신 기능인 만큼 구형 장치나 브라우저 등에서는 지원이 안된다는 단점이 있다. 따라서 지금은 '이런 함수도 있구나' 정도로만 알아 두면 좋을 것 같다.
body {
background-color: light-dark(white, black);
color: light-dark(black, white);
}
위 코드는 HTML body 요소 내에서 라이트 모드 및 다크 모드에서 각각 다른 속성값을 지정하고 있다.
함수 인자는 순서대로 라이트 모드에서의 값, 다크 모드에서의 값을 뜻한다.
이제 리액트와 리코일을 사용하는 환경에서 어떻게 다크 모드 전환 기능을 구현할 수 있는지 순서대로 진행해 보자.
/* :root 선택자를 사용해 전역 CSS 변수를 선언 */
:root {
/* 브라우저에게 '라이트 모드와 다크 모드를 모두 지원한다'고 알림 */
color-scheme: light dark;
/* 기본적으로 라이트 모드일 땐 강조 색에 #333, 배경 색에 #eee 적용 */
--color-primary: #333;
--color-background: #eee;
}
@media (prefers-color-scheme: dark) {
:root {
/* 다크 모드일 땐 강조 색에 #eee, 배경 색에 #333 적용 */
--color-primary: #eee;
--color-background: #333;
}
}
.example-class {
/* CSS 변수 사용 */
background-color: var(--color-background);
color: var(--color-primary);
}
<div className="example-class">
<p>Text Content</p>
</div>
지금까지의 코드가 적용된 모습은 아래와 같다. 시스템 설정에서 선호 모드를 변경하면 그에 맞춰 라이트 모드와 다크 모드에 정의된 스타일이 자동으로 적용되어진다.

이번에는 사용자 시스템의 선호 모드 설정에만 의존하는 대신, 사용자가 직접 모드를 선택할 수 있도록 하는 기능도 구현해 보자.
현재 상태 관리를 위해 Recoil 라이브러리를 사용중이기에 해당 라이브러리를 기준으로 설명하고 있으며, 작성되는 코드는 Context API 혹은 Redux 등 프로젝트에서 어떤 라이브러리를 사용하느냐에 따라 달라질 수 있다.
import { atom } from "recoil";
export const themeState = atom<"system" | "light" | "dark">({
key: "themeState",
default: "system",
});
import { themeState } from "@/state/atoms/themeState";
import { useEffect } from "react";
import { useRecoilState } from "recoil";
const useTheme = () => {
const [theme, setTheme] = useRecoilState(themeState);
useEffect(() => {
const root = window.document.documentElement;
if (theme === "system") {
const sysTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
root.classList.remove("light", "dark");
root.classList.add(sysTheme);
} else {
root.classList.remove("light", "dark");
root.classList.add(theme);
}
}, [theme]);
return { theme, setTheme };
};
export default useTheme;
import useTheme from "@/hooks/useTheme";
const ToggleTheme = () => {
const { theme, setTheme } = useTheme();
const changeTheme = (selected: "light" | "dark" | "system") => {
setTheme(selected);
};
return (
<div>
<button onClick={() => changeTheme("light")}>Light</button>
<button onClick={() => changeTheme("dark")}>Dark</button>
<button onClick={() => changeTheme("system")}>System</button>
<p>current theme: {theme}</p>
</div>
);
};
export default ToggleTheme;
/* 기존에 @media (prefers-color-scheme: dark) 내용을 지우고 아래 클래스로 이동 */
.dark {
--color-primary: #eee;
--color-background: #333;
}
.example-class {
background-color: var(--color-background);
color: var(--color-primary);
/* CSS 클래스 방식으로 변경 시 prefers-color-scheme 에서는 자동 적용되던 */
/* 트랜지션 효과가 사라져, 아래처럼 명시적으로 추가 */
transition: backgound-color 0.5s, color 0.5s;
}
<>
<div className="example-class">
<p>Text Content</p>
</div>
<ToggleTheme />
</>
여기까지 코드가 반영된 모습은 다음과 같다. 이렇게 하면 사용자가 원하는 대로 테마를 제공할 수 있으며, 동시에 시스템 설정을 존중하는 옵션도 제공 가능하다.
다만 지금까지의 내용은 가장 기본적인 설정만 담겨 있으므로 각자의 프로젝트의 요구 사항에 맞게 테마 적용 범위를 더욱 늘리거나, 로컬 스토리지 등의 저장소를 활용해 사용자 설정 값을 저장하는 등의 구현을 추가해 볼 수 있다.

Can I Use 사이트에서 지금까지 살펴본 CSS 기능들의 브라우저 호환성을 살펴 볼 수 있다.
(2024년 7월 기준)
prefers-color-scheme과 color-scheme는 대부분 주요 최신 브라우저(Chrome, Firefox, Safari, Edge) 및 모바일 브라우저(Android Chrome, iOS Safari)에서도 지원되는 모습을 보이며, 전체 글로벌 사용자 중 95% 이상이 이 기능을 지원하는 브라우저를 사용하는 것으로 예상되어 현재 웹 사이트에서 안정적으로 도입 가능한 기능이라 볼 수 있을 것 같다.
하지만 으레 그렇듯 소수의 해당 기능을 지원하지 않는 브라우저 환경을 대비해 스타일 기본 값(Fallback)을 제공할 수 있도록 유의하자.
또한 추가적으로 알아 보았던 light-dark 함수 같은 경우엔 아직 글로벌 사용자 지원 비율이 70%에 못 미치는 것으로 나타나기 때문에 아직 프로덕션 환경에는 도입하기 이르다고 생각이 든다.