Logo
Hyunsu Blog
darkmode

๐Ÿ“†Published :Dec 2, 2021 โ€ข

๐Ÿ“†Updated :Dec 2, 2021 โ€ข

โ˜•๏ธ1min

๊ฐœ์ธ ๋น„์—์„œ ๋‹คํฌ ๋ชจ๋“œ ์ ์šฉ์ด ์•ˆ๋˜๋Š” ์ด์Šˆ

General 2021-12-02 16-18-43

์œ„์˜ ์‚ฌ์ง„์€ ๋งฅ ๊ธฐ์ค€์˜ ์‹œ์Šคํ…œ ์„ค์ •์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์ด๋ ‡๊ฒŒ ๋ณด์—ฌ์งˆ ๋ทฐ๋ฅผ light ๋˜๋Š” dark๋ชจ๋“œ๋กœ ์„ ํƒ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ œ ๋ธ”๋กœ๊ทธ๋ฅผ ๋ฐฉ๋ฌธํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋ฏธ๋ฆฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ ์šฉํ•œ ์„ค์ •์— ๋งž์ถ”์–ด ๋ทฐ๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋ฉด ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ „๋‹ฌ ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‹คํฌ ๋ชจ๋“œ ๊ธฐ๋ณธ ๋กœ์ง

  • ์ด๋ฏธ ๋ธ”๋กœ๊ทธ ํŽ˜์ด์ง€์— ๋ฐฉ๋ฌธํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด โ†’ localstorage ์˜ theme ๊ฐ’ ์ ์šฉ.
  • ์ฒ˜์Œ ๋ฐฉ๋ฌธํ•˜์˜€๊ณ , ์‚ฌ์šฉ์ž์˜ ์šด์˜ ์ฒด์ œ ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ๋‹คํฌ๋ชจ๋“œ๊ฐ€ ๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ โ†’ ์‚ฌ์šฉ์ž์˜ ์šด์˜ ์ฒด์ œ ๊ธฐ๋ณธ ๊ฐ’์œผ๋กœ ์ ์šฉ.

์ด์Šˆ

ํ•˜์ง€๋งŒ ๋‹คํฌ๋ชจ๋“œ๊ฐ€ ์ ์šฉ์ด ๋˜์งˆ ์•Š๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.์ด๋Š” ๋นŒ๋“œ ํ›„ ์ •์ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์—ˆ์„๋•Œ productionํ™˜๊ฒฝ ์—์„œ๋Š” localStorage์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์ธํ„ฐ๋ ‰์…˜์— ๋”ฐ๋ผ ํ…Œ๋งˆ๊ฐ€ ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ์ด์Šˆ์ž…๋‹ˆ๋‹ค.

์ด์Šˆ์˜ ์›์ธ

gatsby์—์„œ๋Š” hydrate ๊ณผ์ •์„ ๊ฑฐ์น˜๋Š”๋ฐ ์ฆ‰ ๋นŒ๋“œ ํ›„ ์„œ๋ฒ„์ธก์—์„œ ์ •์ ํŒŒ์ผ์„ ์ œ๊ณตํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ์ธก์—์„  ์ •์ ํŒŒ์ผ์„ ๋ Œ๋”๋ง ํ›„ ๋ฆฌ์•กํŠธ์™€ ์ƒํƒœ๋ฅผ ๊ทธ๋ ค๋‚ด ๋‹ค์‹œ ๋™์ ์ธ ์•ฑ์„ ๋ Œ๋”๋ง ํ•ฉ๋‹ˆ๋‹ค.
dark mode ๋ฅผ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด์„  ์ตœ์ดˆ ๋ Œ๋”๋ง์ด ๋˜๊ธฐ ์ „์— local storage์— ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ๋˜๋Š” ์‚ฌ์šฉ์ž์˜ ํ™˜๊ฒฝ์— ์ ‘๊ทผํ•˜์—ฌ theme ๊ฐ’์„ ๊ฐ€์ ธ์™€์•ผ ํ•˜๋Š”๋ฐ ๋จผ์ € ๋ Œ๋”๋ง์ด ์ผ์–ด๋‚˜๊ณ  rehydrate์ด ์ผ์–ด๋‚˜๊ธฐ ๋•Œ๋ฌธ์—, ๊นœ๋นก์ž„์ด ์ƒ๊ธฐ๊ฑฐ๋‚˜ ์›ํ•˜๋Š” ๊ฐ’์„ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ•˜๋Š” ์ด์Šˆ๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ์ ‘๊ทผ

HTML ํŽ˜์ด์ง€๊ฐ€ย ๋ธŒ๋ผ์šฐ์ €์— ์˜ํ•ดย ๊ตฌ๋ฌธ ๋ถ„์„๋˜๊ณ  ๋ Œ๋”๋ง๋˜๊ธฐ ์ „์— ์‹คํ–‰ํ• ย ํ…Œ๋งˆ๋ฅผ ์„ ํƒํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ํ•„์š” ํ•ฉ๋‹ˆ๋‹ค.

gatsby์—์„  ์ง์ ‘์ ์œผ๋กœ html ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, Customizing html.js ํŒŒ์ผ์„ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.

/src/html.js

html js โ€” gatsby-starter-blog-sue 2021-11-27 18-47-17

HTMLํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง ๋  ๋•Œ ์ด ์ฝ”๋“œ๋ฒ ์ด์Šค๋„ ๊ฐ™์ด ํŒŒ์‹ฑ์ด ๋˜๋ฉด์„œ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋กœ ์ ‘๊ทผ์„ ํ•˜๊ณ ,

window ์ „์—ญ ๊ฐ์ฒด์— theme์„ ์„ธํŒ… ํ•ฉ๋‹ˆ๋‹ค. ์ตœ์ดˆ ๋ Œ๋”๋ง์‹œ theme ์„ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ <body> ํƒœ๊ทธ์— class๋กœ theme์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. window.matchMedia('(prefers-color-scheme)') ์„ ์ด์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์šด์˜์ฒด์ œ ๊ธฐ๋ณธ์„ค์ •์˜ theme์— ์ ‘๊ทผ ํ›„ window๊ฐ์ฒด์— theme์„ ์„ธํŒ…ํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ ๊ธฐ๋ฐ˜์ฝ”๋“œ์—์„œ๋Š” context API์™€ UseReducer ํ›…์„ ์ด์šฉํ•˜์—ฌ theme์„ ์ „์—ญ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

useReducer์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ตœ์ดˆ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ์‹œ ์œ„์—์„œ ์ €์žฅํ•œ window.__theme ๊ฐ์ฒด ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GlobalContextProvider tsxโ€” gatsby-starter-blog-sue 2021-11-27 19-10-25

๋ฆฌ๋“€์„œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์•ก์…˜ ํƒ€์ž…์— ๋”ฐ๋ผ theme์Šค์œ„์น˜ ๋ฒ„ํŠผ ํ† ๊ธ€์‹œ window ๊ฐ์ฒด ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜์—ฌ theme์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

GlobalContextProvider tsx โ€” gatsby-starter-blog-sue 2021-11-27 19-13-50

html.js์—์„œ ์ง€์ •ํ•œ bodyํƒœ๊ทธ์˜ class name์„ ๊ธฐ์ค€์œผ๋กœ css variable์„ ์ด์šฉํ•˜์—ฌ theme์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. css variable์„ ์ด์šฉํ•˜๋ฉด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์ค‘์—๋„ ๋ณ€์ˆ˜๊ฐ’์˜ ๋ณ€๊ฒฝ์— ๋™์ ์œผ๋กœ ์Šคํƒ€์ผ์„ ๋ฐ”๋กœ ๋ณ€๊ฒฝ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

GlobalStyles tsx โ€” gatsby-starter-blog-sue 2021-11-27 19-16-56

์ „์ฒด ์ฝ”๋“œ ์ž…๋‹ˆ๋‹ค.

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

์ฐธ๊ณ ์ž๋ฃŒ

Hi, I'm Hyunsu ๐Ÿ‘‹

Profile Image

์•ˆ๋…•ํ•˜์„ธ์š”. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž ์ฃผํ˜„์ˆ˜์ž…๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ํ’๋ถ€ํ•˜๊ณ  ๊ฐ€์น˜ ์žˆ๋Š” ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ์ผ์— ๋ฟŒ๋“ฏํ•จ์„ ๋Š๋‚๋‹ˆ๋‹ค.

์˜ต์‹œ๋””์–ธ(Obsidian)์—์„œ ํ˜„์žฌ ๋ธ”๋กœ๊ทธ๋กœ ํ•˜๋‚˜์”ฉ ๊ธ€์„ ์˜ฎ๊ธฐ๋Š” ๊ณผ์ •์— ์žˆ์–ด์š”. โ˜•๏ธ ๐Ÿ‘ฉโ€๐Ÿ’ป โ›ท

Github on ViewReach Me Out