테마 토글 버튼(Daisy UI)

  • 렌더링 최적화를 포함해, 테마를 토글하기 위한 모든 기능이 래핑되어 있는 컴포넌트입니다.
  • tailwind CSS의 테마 정보를 html 의 data-theme 속성을 읽는 방식으로 설정한 상태입니다. 테마의 상태변화를 react가 아닌, effect를 사용하여 해결했습니다. 단순 색상 스타일의 변화이기 때문에 re-rendering 작업은 필요없기 때문입니다. 이로인해 렌더링 최적화를 이루었습니다.
  • OS의 다크모드를 감지하여 자동으로 테마가 변경될 수 있도록 설정했습니다.
  • 사용자가 토글을 진행하면, 사용자가 설정한 테마를 적용할 수 있습니다. 단 새로고침시, OS에서 설정한 테마로 초기화됩니다.
  • 추가적으로, tailwindCSS 테마의 영향을 받지 않는 컴포넌트를 위한 테마 전역상태를 변경할 수 있습니다. 여기서는 zustand를 사용하여 테마 변경 시 전역 상태 또한 같이 변경되도록 했습니다.
'use client'

import { THEME_DARK, THEME_LIGHT } from '@/constants'
import { useThemeStore } from '@/stores'
import { ChangeEventHandler, useCallback, useEffect, useRef } from 'react'

const ToggleTheme = () => {
  const toggleThemeRef = useRef<HTMLInputElement>(null)
  const changeGlobalThemeState = useThemeStore((state) => state.change)

  const setToggleStateByTheme = (theme: string) => {
    switch (theme) {
      case THEME_LIGHT:
        toggleThemeRef.current!.checked = true
        break
      case THEME_DARK:
        toggleThemeRef.current!.checked = false
        break
      default:
        toggleThemeRef.current!.checked = true
    }
  }

  const setTheme = useCallback(
    (theme: string) => {
      document.documentElement.dataset.theme = theme
      setToggleStateByTheme(theme)
      changeGlobalThemeState(theme)
    },
    [changeGlobalThemeState],
  )

  const toggleTheme: ChangeEventHandler<HTMLInputElement> = (e) => {
    const checked = e.target.checked

    const newTheme = checked ? THEME_LIGHT : THEME_DARK
    setTheme(newTheme)
  }

  useEffect(() => {
    setTheme(document.documentElement.dataset.theme ?? THEME_LIGHT)

    const colorSchemePreferenceChangeEventHandler = (event: MediaQueryListEvent) => {
      const newTheme = event.matches ? THEME_DARK : THEME_LIGHT
      setToggleStateByTheme(newTheme)
      setTheme(newTheme)
    }

    window
      .matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', colorSchemePreferenceChangeEventHandler)

    return () => {
      window
        .matchMedia('(prefers-color-scheme: dark)')
        .removeEventListener('change', colorSchemePreferenceChangeEventHandler)
    }
  }, [setTheme])

  return (
    <label className="btn btn-square btn-ghost swap swap-rotate">
      {/* this hidden checkbox controls the state */}
      <input ref={toggleThemeRef} type="checkbox" onChange={toggleTheme} />

      {/* sun icon */}
      <svg className="swap-on h-6 w-6 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
        <path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
      </svg>

      {/* moon icon */}
      <svg className="swap-off h-6 w-6 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
        <path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
      </svg>
    </label>
  )
}
export default ToggleTheme