[react] dark 모드 구현
요즘 유행하는 다크모드를 리액트에서 구현하는 방법입니다. 기본적인 컨셉은 설정된 테마에 따라 body 태그의 클래스를 다르게 변경하는 것입니다. 기본적인 컨셉만 이해하면 누구든지 자유롭게 자신의 사이트 상황에 맞게 적절히 적용할 수 있을 것이라 생각합니다. 아래 방법은 이 블로그에서 사용 중인 다크모드의 구현사례입니다.
1. dark / light 모드에서 사용할 색상 정의
우선 각 테마 별로 사용할 색상을 css 변수를 이용해 전역으로 정의합니다. 그러면 각 컴포넌트에서는 color: var(--textNormal);
과 같이 해당 변수에 접근할 수 있습니다.
/* global.scss */
body {
background-color: var(--bg);
&.light {
--bg: #ffffff;
--textTitle: #444;
--textNormal: #555;
--textDesc: #999;
}
&.dark {
--bg: #282c35;
--textTitle: #eee;
--textNormal: #ddd;
--textDesc: #888;
}
}
2. theme context 정의
테마의 설정 값을 애플리케이션 전역의 상태값으로 사용하기 위해서 context api 를 이용합니다. 그리고 설정한 테마값을 새로고침시에도 유지하기 위해 localStorage
를 사용합니다. (아래 예제는 theme
키에 dark
or light
값을 사용합니다. 선호에 따라 dark
라는 키값에 true
, false
값을 사용하기도 합니다)
// ThemeContext.js
// https://www.gatsbyjs.org/blog/2019-01-31-using-react-context-api-with-gatsby/
import React from 'react'
const defaultState = {
theme: null,
setTheme: () => {},
}
const ThemeContext = React.createContext(defaultState)
// Getting dark mode information from OS!
// You need macOS Mojave + Safari Technology Preview Release 68 to test this currently.
// const supportsDarkMode = () =>
// window.matchMedia("(prefers-color-scheme: dark)").matches === true
export class ThemeProvider extends React.Component {
state = {
theme: null,
}
setTheme = theme => {
document.body.className = theme /* 모바일 브라우져에서 theme-color 를 함께 변경하고자 할 경우, 아래 2줄 주석해제 */
// const meta = document.querySelector('meta[name="theme-color"]')
// meta.content = theme === 'light' ? '#eee' : '#282c35'
localStorage.setItem('theme', theme)
this.setState({theme})
}
componentDidMount() {
this.setTheme(localStorage.getItem('theme') || 'light')
}
render() {
const {children} = this.props
const {theme} = this.state
return (
<ThemeContext.Provider
value={{
theme,
setTheme: this.setTheme,
}}
>
{children}
</ThemeContext.Provider>
)
}
}
export default ThemeContext
ThemeContext
사용을 위해 애플리케이션 전체를 ThemeProvider
컴포넌트로 wrapping
// App.js
import React from 'react'
import {ThemeProvider} from './themeContext'
export default () => (
<ThemeProvider>
<App />
</ThemeProvider>
)
3. 입력컨트롤 정의
이제 dark / light 모드를 제어할 입력 컨트롤이 필요합니다. 이래와 같이 단순하게 Dark, Light 텍스트만 출력하는 형태로 만들어도 동작에는 문제가 없습니다. DarkMode
컴포넌트는 ThemeContext
를 사용해야 하기 때문에 ThemeContext.Consumer
로 wrapping 됩니다
// DarkMode.js
import React from 'react'
import ThemeContext from './themeContext'
export default function DarkMode() {
return (
<ThemeContext.Consumer>
{ctx => (
<div onClick={() => ctx.setTheme(ctx.theme === 'light' ? 'dark' : 'light')}>
{ctx.theme}
</div>
)}
</ThemeContext.Consumer>
)
}
이제 완성된 DarkMode 컴포넌트를 원하는 위치에 삽입하기만 하면 됩니다. 😁🙂
4. 멋진 입력컨트롤 사용
애정을 가지고 운영하는 사이트라면 위의 제시된 DarkMode
컴포넌트를 그대로 사용하지는 않겠죠? 다크모드 컨트롤의 형태는 여러가지가 있는데요. 일단 현 블로그에서 사용 중인 다크모드 컨트롤의 구현은 아래와 같습니다.
Note) 아래 다크모드 컨트롤은 overreacted.io 에서 사용 중인 컨트롤을 그대로 가져온 것입니다.
// DarkMode.js
import React from 'react'
import ThemeContext from './themeContext'
import Toggle from './toggle'
import {moon, sun} from './icon'
export default function DarkMode() {
return (
<ThemeContext.Consumer>
{ctx =>
ctx.theme && ( // ctx.theme 가 null 일 경우에는 렌더링하지 않음(깜빡인 문제 해결)
<Toggle
icons={{
checked: (
<img
src={moon}
alt='moon'
width='16'
height='16'
role='presentation'
style={{pointerEvents: 'none'}}
/>
),
unchecked: (
<img
src={sun}
alt='sun'
width='16'
height='16'
role='presentation'
style={{pointerEvents: 'none'}}
/>
),
}}
checked={ctx.theme === 'dark'}
onChange={e => ctx.setTheme(e.target.checked ? 'dark' : 'light')}
/>
)
}
</ThemeContext.Consumer>
)
}
// icon.js
export const moon = `data:image/png;base64,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`
export const sun = `data:image/png;base64,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`
// toggle.js
/*
* Copyright (c) 2015 instructure-react
* Forked from https://github.com/aaronshaf/react-toggle/
* + applied https://github.com/aaronshaf/react-toggle/pull/90
**/
import './toggle.scss'
import React, {PureComponent} from 'react'
// Copyright 2015-present Drifty Co.
// http://drifty.com/
// from: https://github.com/driftyco/ionic/blob/master/src/util/dom.ts
function pointerCoord(event) {
// get coordinates for either a mouse click
// or a touch depending on the given event
if (event) {
const changedTouches = event.changedTouches
if (changedTouches && changedTouches.length > 0) {
const touch = changedTouches[0]
return {x: touch.clientX, y: touch.clientY}
}
const pageX = event.pageX
if (pageX !== undefined) {
return {x: pageX, y: event.pageY}
}
}
return {x: 0, y: 0}
}
export default class Toggle extends PureComponent {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
this.handleTouchStart = this.handleTouchStart.bind(this)
this.handleTouchMove = this.handleTouchMove.bind(this)
this.handleTouchEnd = this.handleTouchEnd.bind(this)
this.handleTouchCancel = this.handleTouchCancel.bind(this)
this.handleFocus = this.handleFocus.bind(this)
this.handleBlur = this.handleBlur.bind(this)
this.previouslyChecked = !!(props.checked || props.defaultChecked)
this.state = {
checked: !!(props.checked || props.defaultChecked),
hasFocus: false,
}
}
componentWillReceiveProps(nextProps) {
if ('checked' in nextProps) {
this.setState({checked: !!nextProps.checked})
this.previouslyChecked = !!nextProps.checked
}
}
handleClick(event) {
const checkbox = this.input
this.previouslyChecked = checkbox.checked
if (event.target !== checkbox && !this.moved) {
event.preventDefault()
checkbox.focus()
checkbox.click()
return
}
this.setState({checked: checkbox.checked})
}
handleTouchStart(event) {
this.startX = pointerCoord(event).x
this.touchStarted = true
this.hadFocusAtTouchStart = this.state.hasFocus
this.setState({hasFocus: true})
}
handleTouchMove(event) {
if (!this.touchStarted) return
this.touchMoved = true
if (this.startX != null) {
let currentX = pointerCoord(event).x
if (this.state.checked && currentX + 15 < this.startX) {
this.setState({checked: false})
this.startX = currentX
} else if (!this.state.checked && currentX - 15 > this.startX) {
this.setState({checked: true})
this.startX = currentX
}
}
}
handleTouchEnd(event) {
if (!this.touchMoved) return
const checkbox = this.input
event.preventDefault()
if (this.startX != null) {
if (this.previouslyChecked !== this.state.checked) {
checkbox.click()
}
this.touchStarted = false
this.startX = null
this.touchMoved = false
}
if (!this.hadFocusAtTouchStart) {
this.setState({hasFocus: false})
}
}
handleTouchCancel(event) {
if (this.startX != null) {
this.touchStarted = false
this.startX = null
this.touchMoved = false
}
if (!this.hadFocusAtTouchStart) {
this.setState({hasFocus: false})
}
}
handleFocus(event) {
const {onFocus} = this.props
if (onFocus) {
onFocus(event)
}
this.hadFocusAtTouchStart = true
this.setState({hasFocus: true})
}
handleBlur(event) {
const {onBlur} = this.props
if (onBlur) {
onBlur(event)
}
this.hadFocusAtTouchStart = false
this.setState({hasFocus: false})
}
getIcon(type) {
const {icons} = this.props
if (!icons) {
return null
}
return icons[type] === undefined ? Toggle.defaultProps.icons[type] : icons[type]
}
render() {
const {className, icons: _icons, ...inputProps} = this.props
const classes =
'react-toggle' +
(this.state.checked ? ' react-toggle--checked' : '') +
(this.state.hasFocus ? ' react-toggle--focus' : '') +
(this.props.disabled ? ' react-toggle--disabled' : '') +
(className ? ' ' + className : '')
return (
<div
className={classes}
onClick={this.handleClick}
onKeyPress={this.handleClick}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
onTouchCancel={this.handleTouchCancel}
role='presentation'
>
<div className='react-toggle-track'>
<div className='react-toggle-track-check'>{this.getIcon('checked')}</div>
<div className='react-toggle-track-x'>{this.getIcon('unchecked')}</div>
</div>
<div className='react-toggle-thumb' />
<input
{...inputProps}
ref={ref => {
this.input = ref
}}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
className='react-toggle-screenreader-only'
type='checkbox'
aria-label='Switch between Dark and Light mode'
/>
</div>
)
}
}
// toggle.scss
/*
* Copyright (c) 2015 instructure-react
* Forked from https://github.com/aaronshaf/react-toggle/
**/
.react-toggle {
touch-action: pan-x;
display: inline-block;
position: relative;
cursor: pointer;
background-color: transparent;
border: 0;
padding: 0;
-webkit-touch-callout: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent;
}
.react-toggle-screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.react-toggle-track {
width: 50px;
height: 24px;
padding: 0;
border-radius: 30px;
background-color: hsl(222, 14%, 7%);
transition: all 0.2s ease;
}
.react-toggle-track-check {
position: absolute;
width: 17px;
height: 17px;
left: 5px;
top: 0px;
bottom: 0px;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
opacity: 0;
transition: opacity 0.25s ease;
}
.react-toggle--checked .react-toggle-track-check {
opacity: 1;
transition: opacity 0.25s ease;
}
.react-toggle-track-x {
position: absolute;
width: 17px;
height: 17px;
right: 5px;
top: 0px;
bottom: 0px;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
opacity: 1;
transition: opacity 0.25s ease;
}
.react-toggle--checked .react-toggle-track-x {
opacity: 0;
}
.react-toggle-thumb {
position: absolute;
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #fafafa;
box-sizing: border-box;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
transform: translateX(0);
}
.react-toggle--checked .react-toggle-thumb {
transform: translateX(26px);
border-color: #19ab27;
}
.react-toggle--focus .react-toggle-thumb {
box-shadow: 0px 0px 2px 3px #777;
}
.react-toggle:active .react-toggle-thumb {
box-shadow: 0px 0px 5px 5px #777;
}