์์ ์ฌ์ง์ ๋งฅ ๊ธฐ์ค์ ์์คํ ์ค์ ์ ๋๋ค. ์ฌ์ฉ์๋ ์ด๋ ๊ฒ ๋ณด์ฌ์ง ๋ทฐ๋ฅผ light ๋๋ dark๋ชจ๋๋ก ์ ํ์ ํ ์ ์์ต๋๋ค. ์ ๋ธ๋ก๊ทธ๋ฅผ ๋ฐฉ๋ฌธํ๋ ์ฌ๋๋ค์๊ฒ ๋ฏธ๋ฆฌ ์ฌ์ฉ์๊ฐ ์ ์ฉํ ์ค์ ์ ๋ง์ถ์ด ๋ทฐ๋ฅผ ์ ๊ณตํ๋ค๋ฉด ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๋ฌ ํ ์ ์์ ๊ฒ์ ๋๋ค.
๋คํฌ ๋ชจ๋ ๊ธฐ๋ณธ ๋ก์ง
- ์ด๋ฏธ ๋ธ๋ก๊ทธ ํ์ด์ง์ ๋ฐฉ๋ฌธํ ์ ์ด ์๋ค๋ฉด โ localstorage ์ theme ๊ฐ ์ ์ฉ.
- ์ฒ์ ๋ฐฉ๋ฌธํ์๊ณ , ์ฌ์ฉ์์ ์ด์ ์ฒด์ ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ๋คํฌ๋ชจ๋๊ฐ ๋์ด ์๋ ๊ฒฝ์ฐ โ ์ฌ์ฉ์์ ์ด์ ์ฒด์ ๊ธฐ๋ณธ ๊ฐ์ผ๋ก ์ ์ฉ.
์ด์
ํ์ง๋ง ๋คํฌ๋ชจ๋๊ฐ ์ ์ฉ์ด ๋์ง ์๋ ํ์์ด ๋ฐ์ํฉ๋๋ค.์ด๋ ๋น๋ ํ ์ ์ ํ์ผ์ด ์์ฑ๋์์๋ productionํ๊ฒฝ ์์๋ localStorage์ ์ ๊ทผํ์ง ๋ชปํ์ฌ ์ฌ์ฉ์์ ์ธํฐ๋ ์ ์ ๋ฐ๋ผ ํ ๋ง๊ฐ ๋ฐ๋์ง ์๋ ์ด์์ ๋๋ค.
์ด์์ ์์ธ
gatsby์์๋ hydrate ๊ณผ์ ์ ๊ฑฐ์น๋๋ฐ ์ฆ ๋น๋ ํ ์๋ฒ์ธก์์ ์ ์ ํ์ผ์ ์ ๊ณตํ๊ณ ํด๋ผ์ด์ธํธ์ธก์์ ์ ์ ํ์ผ์ ๋ ๋๋ง ํ ๋ฆฌ์กํธ์ ์ํ๋ฅผ ๊ทธ๋ ค๋ด ๋ค์ ๋์ ์ธ ์ฑ์ ๋ ๋๋ง ํฉ๋๋ค.
dark mode ๋ฅผ ์ง์ํ๊ธฐ ์ํด์ ์ต์ด ๋ ๋๋ง์ด ๋๊ธฐ ์ ์ local storage์ ์ ๊ทผํ๊ฑฐ๋ ๋๋ ์ฌ์ฉ์์ ํ๊ฒฝ์ ์ ๊ทผํ์ฌ theme ๊ฐ์ ๊ฐ์ ธ์์ผ ํ๋๋ฐ ๋จผ์ ๋ ๋๋ง์ด ์ผ์ด๋๊ณ rehydrate์ด ์ผ์ด๋๊ธฐ ๋๋ฌธ์, ๊น๋นก์์ด ์๊ธฐ๊ฑฐ๋ ์ํ๋ ๊ฐ์ ๋ฏธ๋ฆฌ ๊ฐ์ ธ์ค์ง ๋ชปํ๋ ์ด์๊ฐ ์๊น๋๋ค.
ํด๊ฒฐ ์ ๊ทผ
HTML ํ์ด์ง๊ฐย ๋ธ๋ผ์ฐ์ ์ ์ํดย ๊ตฌ๋ฌธ ๋ถ์๋๊ณ ๋ ๋๋ง๋๊ธฐ ์ ์ ์คํํ ย ํ ๋ง๋ฅผ ์ ํํ๋ ์ฝ๋๊ฐ ํ์ ํฉ๋๋ค.
gatsby์์ ์ง์ ์ ์ผ๋ก html ์ ์ ๊ทผํ ์ ์์ผ๋ฏ๋ก, Customizing html.js ํ์ผ์ ์ด์ฉํฉ๋๋ค.
/src/html.js
HTMLํ์ด์ง๊ฐ ๋ ๋๋ง ๋ ๋ ์ด ์ฝ๋๋ฒ ์ด์ค๋ ๊ฐ์ด ํ์ฑ์ด ๋๋ฉด์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง๋ก ์ ๊ทผ์ ํ๊ณ ,
window ์ ์ญ ๊ฐ์ฒด์ theme์ ์ธํ
ํฉ๋๋ค. ์ต์ด ๋ ๋๋ง์ theme ์ ์ ์ ์๊ฒ <body>
ํ๊ทธ์ class๋ก theme์ ์ง์ ํฉ๋๋ค. window.matchMedia('(prefers-color-scheme)')
์ ์ด์ฉํ์ฌ ์ฌ์ฉ์ ์ด์์ฒด์ ๊ธฐ๋ณธ์ค์ ์ theme์ ์ ๊ทผ ํ window๊ฐ์ฒด์ theme์ ์ธํ
ํฉ๋๋ค.
๋ฆฌ์กํธ ๊ธฐ๋ฐ์ฝ๋์์๋ context API
์ UseReducer
ํ
์ ์ด์ฉํ์ฌ theme์ ์ ์ญ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์๊ฒ ํ์์ต๋๋ค.
useReducer์ ๋งค๊ฐ๋ณ์๋ก ์ต์ด๊ฐ์ ๊ฐ์ ธ์ฌ์ ์์์ ์ ์ฅํ window.__theme
๊ฐ์ฒด ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
๋ฆฌ๋์๋ฅผ ํ์ฉํ์ฌ ์ก์ ํ์ ์ ๋ฐ๋ผ theme์ค์์น ๋ฒํผ ํ ๊ธ์ window ๊ฐ์ฒด ํจ์๋ฅผ ์คํํ์ฌ theme์ ์ ์ฅํฉ๋๋ค.
html.js์์ ์ง์ ํ bodyํ๊ทธ์ class name์ ๊ธฐ์ค์ผ๋ก css variable์ ์ด์ฉํ์ฌ theme์ ์ง์ ํฉ๋๋ค. css variable์ ์ด์ฉํ๋ฉด ์ดํ๋ฆฌ์ผ์ด์ ์คํ ์ค์๋ ๋ณ์๊ฐ์ ๋ณ๊ฒฝ์ ๋์ ์ผ๋ก ์คํ์ผ์ ๋ฐ๋ก ๋ณ๊ฒฝ์ ํ ์ ์๋ ์ด์ ์ด ์์ต๋๋ค.
์ ์ฒด ์ฝ๋ ์ ๋๋ค.
context API ์ ์ ํฉ๋๋ค.
import { useReducer, createContext, Dispatch, useContext } from 'react'
import { ThemeProvider } from '@emotion/react'
import GlobalStyles from '../styles/GlobalStyles'
import theme from '../styles/theme'
type State = {
isDark?: string
theme: string
}
type Action = { type: 'TOGGLE_DARK_MODE'; theme: string }
type SampleDispatch = Dispatch<Action>
export const GlobalStateContext = createContext<State | null>(null)
export const GlobalDispatchContext = createContext<SampleDispatch | null>(null)
declare global {
interface Window {
__theme: string
__setPreferredTheme: (toggleTheme: string) => void
}
}
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'TOGGLE_DARK_MODE': {
const toggledTheme = window.__theme === 'dark' ? 'light' : 'dark'
window.__setPreferredTheme(toggledTheme)
return {
theme: toggledTheme,
}
}
default:
return state
}
}
const initializeState = () => {
if (typeof window !== 'undefined') {
return {
theme: window.__theme,
}
} else {
return { theme: 'light' }
}
}
const initialState: State = {
theme: 'light',
}
const GlobalContextProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState, initializeState)
if (state) {
return (
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>
<ThemeProvider
theme={state.theme === 'light' ? theme.light : theme.dark}
>
<GlobalStyles />
{children}
</ThemeProvider>
</GlobalDispatchContext.Provider>
</GlobalStateContext.Provider>
)
}
}
export default GlobalContextProvider
export function useThemeState() {
const state = useContext(GlobalStateContext)
if (!state) throw new Error('Cannot find GlobalStateProvider')
return state
}
export function useThemeDispatch() {
const dispatch = useContext(GlobalDispatchContext)
if (!dispatch) throw new Error('Cannot find GlobalStateProvider') // ์ ํจํ์ง ์์๋ ์๋ฌ๋ฅผ ๋ฐ์
return dispatch
}
src/html.js
import React from 'react'
import PropTypes from 'prop-types'
export default function HTML(props) {
return (
<html {...props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{props.headComponents}
</head>
<body {...props.bodyAttributes} className="light">
<script
dangerouslySetInnerHTML={{
__html: `
(() => {
let preferredTheme;
try {
preferredTheme = localStorage.getItem('theme');
} catch (err) {}
const setTheme = (newTheme) => {
window.__theme = newTheme;
preferredTheme = newTheme;
document.body.className = newTheme;
}
window.__setPreferredTheme = (newTheme) => {
setTheme(newTheme);
try {
localStorage.setItem('theme', newTheme);
} catch (err) {}
}
let darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkQuery.addListener(e => {
window.__setPreferredTheme(e.matches ? 'light' : 'dark');
})
setTheme(preferredTheme || (darkQuery.matches ? 'dark' : 'light'));
})();
`,
}}
/>
{props.preBodyComponents}
<div
key={`body`}
id="___gatsby"
dangerouslySetInnerHTML={{ __html: props.body }}
/>
{props.postBodyComponents}
</body>
</html>
)
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array,
}
src/header.tsx
- ๋คํฌ๋ชจ๋ ์ค์์น ๋ฒํผ์ด ์๋ component
button์ onClick ์ด๋ฒคํธ๋ setToggleTheme
์ ํจ์์ ์ผ์ธ
TOGGLE_DARK_MODE
์ก์
์ ๋์คํจ์น ํฉ๋๋ค. ์ฆ ๋ฆฌ๋์์ ์ฝ์๋์ด์ง
TOGGLE_DARK_MODE
์ก์
์ ์คํ์์ผ theme์ ์ ํํฉ๋๋ค.
useThemeState()
๋ฅผ ์ด์ฉํ์ฌ ์ ์ญ ์ํ์ธ theme ์ํ๋ฅผ ๋ฐ์์์ ์ํ ๊ฐ์ ๋ฐ๋ผ ์์ด์ฝ์ ๋ณ๊ฒฝํฉ๋๋ค.
const Header = ({ siteTitle }: HeaderProps): React.ReactElement => {
const dispatch = useThemeDispatch()
const state = useThemeState()
const setToggleTheme = () => dispatch({ type: 'TOGGLE_DARK_MODE', theme: '' })
return (
<header>
<button onClick={setToggleTheme}>
<Twemoji
emoji={state?.theme === 'dark' ? 'โ๏ธ' : '๐'}
css={css`
width: 24px;
`}
/>
</button>
</header>
)
}
export default Header
์ฐธ๊ณ ์๋ฃ