gatsby 블로그에 다크모드를 추가해보자.
초기 테마 선택
사용자가 블로그에 방문할 때 어떤 테마를 표시할 것인가?
check list
방문자의 운영체제 기본 설정에 맞게 표시
이전 방문에서 저장된 테마 설정에 맞게 표시
블로그 기본 설정값
사이트 방문 중 OS 테마 변경
운영체제 기본 설정
첫 번째 고려사항으로는 방문자의 운영체제 테마 설정과 동일한 테마를 표시하도록 하는 것이다.
prefers-color-scheme
미디어 쿼리를 사용하면 사용자의 운영체제 설정에서 선호하는 테마(lightmode나 darkmode)를 감지할 수 있다. 아래와 같이 현재 브라우저가 다크 모드를 요구하는지 확인한다.
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (darkQuery.matches) {
console.log('다크 모드 선호');
}
저장된 테마 설정
블로그를 한 번 이상 방문한 경우, 이전 방문에서 가장 마지막으로 적용된 테마로 표시한다.
localStorage
API를 사용하면 방문자의 데이터를 저장하고 재방문 시에도 데이터를 불러와 적용할 수 있다. setItem()
으로 테마를 저장하고 getItem()
으로 테마를 불러온다.
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
테마 감지
Dark Mode Flash(다크 모드 플래시)
Gatsby는 빌드 시에 모든 페이지의 HTML을 생성해 사용자 요청에 맞게 제공되는 SSG(Static Site Generation) 렌더링 방식을 사용한다. 문제는 사용자의 디바이스에 도달하기도 전에 미리 생성된 HTML이 제공되고 나서야 사용자 요구에 맞게 변한다는 것이다.
그래서 블로그의 기본 스타일을 Light mode로 지정한 경우, 사용자가 Dark mode로 변경한 뒤 새로고침하면 잠깐 밝은 테마가 나타난 후 어두운 테마로 전환되는 플래시 현상을 경험하게 된다.
이를 해결하려면 브라우저가 HTML 구문을 먼저 분석하고 렌더링 전에 실행할 테마를 선택하는 코드를 추가해주면 된다. gatsby-ssr.js
에 다음과 같은 코드를 추가한다. gatsby-ssr.js
는 gatsby가 컴파일할 때 실행되는 파일이다.
/* gatsby-ssr.js */
const React = require('react');
const ThemeScriptTag = () => {
const codeToRunOnClient = `
// 테마 초기화 코드
`;
return <script dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} />;
};
export const onRenderBody = ({ setPreBodyComponents }) => {
setPreBodyComponents(<ThemeScriptTag />);
};
onRenderBody
: HTML을 빌드하는 동안 gatsby 서버가 렌더링하는 모든 페이지 뒤에 호출되어 html에서 렌더링할 head나 body 요소를 설정할 수 있음setPreBodyComponents
: body 태그 태에서 다른 컴포넌트들보다 먼저 주입된다.
테마 초기화 코드는 전역 네임스페이스 오염을 방지하기 위해 IIFE(즉시 실행 함수)로 작성한다. IIFE는 한 번만 사용되고 사라지기 때문에 전역 변수로서 남아있지 않게 되어 스코프 오염을 줄일 수 있다.
초기화 코드 작성
테마 적용 순서
- 방문한 적이 있으면 localStorage에 저장된 테마를 불러온다.
- 운영체제에 설정된 테마로 불러온다.
- 기본 테마 light mode로 보여준다.
먼저 window 객체 window.__theme
에는 인자로 전달한 theme
값을 받아와 저장한다. 테마가 다크모드라면 body의 클래스를 ‘dark’로 변경하고 theme
를 localStorage에 저장한다.
(function () {
window.__setPreferredTheme = (theme) => {
window.__theme = theme; // 테마 저장
console.log('Change the theme:', theme);
// body 클래스 변경
if (theme === 'dark') {
document.body.className = 'dark';
} else {
document.body.className = '';
}
try {
localStorage.setItem('theme', theme); // localStorage에 테마 저장
} catch (e) {}
};
})();
CSS custom으로 해당 테마에 맞는 속성을 설정해준다. 예시)
body {
/* light mode */
--text: #000;
/* dark mode */
&.dark {
--text: #fff;
}
}
테마에 따라 body 클래스명이 바뀌고 해당 css 속성이 적용된다.
preferredTheme
는 localStorage에 저장된 테마를 받아온다. 그리고 사이트 방문 중 시스템 테마 설정을 변경할 경우에 즉각 업데이트될 수 있도록 addListener
를 추가한다. darkQuery
에 감지한 테마가 다크모드인지를 판별해 저장하고 해당 테마를 window.__setPreferredTheme
인자로 넘겨준다.
OS 테마 변경 시에 화면상 토글 버튼도 변화해야 하므로 그에 해당하는 함수도 생성한다.
(function () {
// ...
let preferredTheme;
try {
preferredTheme = localStorage.getItem('theme'); // localStorage에 저장된 테마 불러오기
} catch (e) {}
// 사이트 방문 중에 OS 테마 설정을 변경한 경우 업데이트
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)'); // 운영 체제 테마 감지
window.__onChangeToggle = () => {}; // OS 테마 변경 시 토글 변경 함수
darkQuery.addListener((e) => {
window.__setPreferredTheme(e.matches ? 'dark' : 'light');
});
})();
시스템 설정을 변경하면 즉시 해당 블로그 테마가 반영된다.
마지막으로 위에서 설정한 테마 변경 순서대로 동작하기 위해 아래 코드를 추가한다.
window.__setPreferredTheme(
preferredTheme || (darkQuery.matches ? 'dark' : 'light'),
);
토글 버튼 생성
이제 다크모드를 제어할 토글 버튼을 생성하는 일만 남았다. Custom hook으로 작성한다.
토글 제어
토글 버튼은 직접 클릭했을 때와 시스템 변경으로 인한 자동 변경 두 가지로 나눌 수 있다.
onChangeTheme
는 토글 버튼을 클릭했을 때 변경된 테마를 window.__setPreferredTheme
에 전달한다. 시스템 설정 변경으로 인한 토글 변화는 useEffect로 처리한다.
import { useState, useCallback, useEffect } from 'react';
// ...
const useTheme = (): useThemeType => {
const getTheme = () => {
if (typeof window === 'undefined') {
return undefined;
}
return window.__theme;
};
const [checked, setChecked] = useState(getTheme() === 'dark');
const onChangeTheme = useCallback(
(e: ChangeEvent) => {
const isChecked = (e.target as HTMLInputElement).checked;
setChecked(isChecked);
const theme = isChecked ? 'dark' : 'light';
window.__setPreferredTheme(theme);
},
[setChecked],
);
useEffect(() => {
window.__onChangeToggle = (theme) => {
setChecked(theme === 'dark');
};
}, []);
return { checked, onChangeTheme };
};
export default useTheme;
참고로 타입스크립트에서 window 객체 속성에 접근하려 할 때는 window interface를 확장해야 한다. 아래 링크에 해당 방법이 나와 있다.
토글 버튼 적용
토글 버튼을 만들고 해당 훅을 적용한다. 토글 버튼을 클릭하여 동작할 때마다 테마가 변경된다.
const ThemeToggle: FunctionComponent<ThemeToggleProps> = ({}) => {
const { checked, onChangeTheme } = useTheme();
return (
<ToggleWrapper>
<ToggleButton
type="checkbox"
checked={checked}
onChange={onChangeTheme}
></ToggleButton>
</ToggleWrapper>
);
};
References
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
https://victorzhou.com/blog/dark-mode-gatsby/
https://www.joshwcomeau.com/react/dark-mode/