React 기반 웹 사이트에서의 다크모드 구현 방법은 ThemeProvider 를 사용하거나, CSS Variable 를 사용하는 방법이 있다.
ThemeProvider?
App 최상위에 ThemeProvider 를 추가하면 내부 Context 를 통해 하위 컴포넌트들이 테마에 접근할 수 있도록 해준다. styled component 에서는 props.theme 를 통해, emotion.js 에서는 css prop 으로 접근할 수 있다.
CSS Variable?
CSS 사용자 지정 변수란 CSS 속성을 미리 정의해 놓고 필요할 때, "var(변수명)" 형식으로 참조하여 사용할 수 있는 변수를 의미한다.
- CSS 속성을 미리 정의
:root { --bg-page1: '#F8F9FA' }
- 변수 참조
.content { background-color : var(--bg-page1); }
구현 방법 선택의 가장 큰 기준은 SSR
이냐 CSR
이냐의 차이이다.
SSR
일 경우 화면 깜박임이 발생할 수 있다.
만약 OS 설정이 다크모드인 사용자가 처음 방문한다면 라이트 모드로 보여줬다가 OS 설정을 감지하고 화면이 깜빡이면서 다크 모드로 보여질 것이다.
또한 사용자가 재방문해서 브라우저 스토리지에 저장된 사용자 테마가 있을 경우에도 테마 설정값을 가져오기 전까지 웹사이트를 표시하지 않도록 처리해야한다.
스크립트를 head
태그에 넣는다면 렌더링을 차단하면서 자바스크립트를 파싱하며 실행 딜레이가 발생할 것이고 body
태그가 끝나는 시점에 넣으면 렌더링을 차단하지는 않지만 스크립트를 실행하기 전까지는 흰 화면을 봐야한다.
테마를 설정하는 시점이 스타일시트를 불러온 후, DOM 트리가 구성되기 전이어야 한다. 그러기 위해서는 body
태그의 첫번째 child element로 script
태그를 추가해 자바스크립트 코드를 작성해야한다. (아직 SSR 이 아니라서 잘 모르겠지만 염두해 둬야 될 것같다.)
지금 진행중인 프로젝트는 CSR
에다가 ThemeProvider
를 사용하고 있었지만 추후 SSR
이 될 것을 고려하여 CSS Variable 을 사용하기로 했다.
사용자의 시스템이 라이트 테마를 사용하는지 다크 테마를 사용하는지 알아내기 위해 prefers-color-scheme
CSS 미디어 쿼리 또는 javascript 를 사용할 수 있다.
@media (prefers-color-scheme: light) {
body {
--color-text: black;
--color-background: white;
}
}
@media (prefers-color-scheme: dark) {
body {
--color-text: white;
--color-background: black;
}
}
다만 이때 시스템의 선호 테마가 없을 수도 있기 때문에 light를 기본으로 하고 dark 를 위한 미디어쿼리만 작성한다.
body {
--color-text: black;
--color-background: white;
}
@media (prefers-color-scheme: dark) {
body {
--color-text: white;
--color-background: black;
}
}
body {
color: var(--color-text);
background: var(--color-background);
}
const preferDark = systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const mode = preferDark ? 'light' : 'dark';
// body[data-theme] 속성 설정
function setTheme(theme) {
mode = theme
document.body.dataset.theme = theme
}
setTheme(mode)
브라우저 상에서 해당 페이지가 열려있는 상태에서 시스템 테마가 변경되었다는 것을 감지해 리렌더링 되어야하기 때문에 미디어 쿼리에 대해 이벤트 리스너를 달아주어야 한다.
window.matchMedia('(prefers-color-scheme: dark)').addListener(function (e) {
setTheme(e.matches ? 'dark' : 'light')
});
프로젝트 내에서 사용할 스타일을 emotion.js
에서 제공하는 Global
Component 를 사용하여 글로벌하게 제공해 줄 수 있다.
//App.tsx
import { Global, css } from '@emotion/react'
const App = () => {
return (
<>
{/* 기존 코드 중략*/}
<Global styles={globalStyles} />
</>
);
};
//globalStyles.ts
const globalStyles: Interpolation<Theme> = css`
body {
--bg-page1: '#F8F9FA',
}
@media (prefers-color-scheme: dark) {
body {
--bg-page1: '#121212',
}
}
body[data-theme='light'] {
--bg-page1: '#F8F9FA'
}
body[data-theme='dark'] {
--bg-page1: '#121212',
}
`;
객체 {bg_page1: '#F8F9FA'}
를 선언해두고 이를 가공해서 css variable로 정의하기 위해 --bg-page1:'#F8F9FA'
인 구조와, 컴포넌트 내에서 theme.bg_page1 처럼 참조해서 사용하기 위해 theme.bg_page1
가 var(--bg-page1)
을 참조할 수 있는 구조로 만들어 두면 편할 것 같다.
다크모드 테마 상태 관리, 변경 및 유지, 새로 고침시에도 테마를 유지하는 내용은 다음 블로그에 계속..