File name
Commit message
Commit date
File name
Commit message
Commit date
09-16
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use client';
import { useCallback } from 'react';
import {
DEFAULT_THEME_PREFERENCE,
THEME_ATTRIBUTE_NAME,
THEME_PREFERENCE_CYCLE,
THEME_STORAGE_KEY,
type ThemePreference,
} from '@/lib/constants/theme';
// design이 globals.css에서 이 속성이 존재하는 동안만 색상 전환에 트랜지션을 건다(design 계획
// §8.3). prefers-reduced-motion 분기는 그 CSS 쪽 셀렉터가 처리하므로 이 훅은 조건 없이
// 부여/제거만 한다.
const THEME_TRANSITION_ATTRIBUTE_NAME = 'data-theme-transition';
const THEME_TRANSITION_DURATION_MS = 200;
export interface ThemePreferenceState {
cycleThemePreference: () => void;
}
/** `<html data-theme>`에서 현재 선택을 읽는다. 속성이 없거나 알 수 없는 값이면 system이다. */
function readThemePreference(): ThemePreference {
const value = document.documentElement.getAttribute(THEME_ATTRIBUTE_NAME);
return value === 'light' || value === 'dark'
? value
: DEFAULT_THEME_PREFERENCE;
}
/** 순환 순서(THEME_PREFERENCE_CYCLE)에서 다음 값을 계산한다. */
function getNextThemePreference(current: ThemePreference): ThemePreference {
const currentIndex = THEME_PREFERENCE_CYCLE.indexOf(current);
const nextIndex = (currentIndex + 1) % THEME_PREFERENCE_CYCLE.length;
return THEME_PREFERENCE_CYCLE[nextIndex];
}
/** 선택값을 기기에 영속화한다. system은 저장값을 지워 "선택 없음" 상태로 되돌린다. */
function persistThemePreference(next: ThemePreference): void {
try {
if (next === 'system') {
localStorage.removeItem(THEME_STORAGE_KEY);
} else {
localStorage.setItem(THEME_STORAGE_KEY, next);
}
} catch {
// localStorage 접근이 차단된 환경(시크릿 모드 등)에서도 조용히 무시한다 — <html data-theme>만
// 으로 현재 세션 동안은 정상 동작하고, 다음 방문 시 다시 system으로 fail-safe한다.
}
}
/** `<html>`에 선택값을 반영한다. system은 속성 부재로 표현한다(§4-1 규격). */
function applyThemePreference(next: ThemePreference): void {
const root = document.documentElement;
if (next === 'system') {
root.removeAttribute(THEME_ATTRIBUTE_NAME);
} else {
root.setAttribute(THEME_ATTRIBUTE_NAME, next);
}
persistThemePreference(next);
}
/**
* 헤더 테마 토글 버튼의 화면 로컬 로직(ViewModel 대응) — `<html data-theme>` 자체가 SSOT이므로
* React state를 두지 않는다(§4-3). 클릭 시점에 DOM에서 현재값을 읽고 다음 값을 계산해
* 속성/localStorage에 쓴다. `useState`/`useEffect`가 없어 hydration 불일치 표면적이 0이다.
*/
export function useThemePreference(): ThemePreferenceState {
const cycleThemePreference = useCallback(() => {
const root = document.documentElement;
const next = getNextThemePreference(readThemePreference());
// 트랜지션 부여 → 같은 tick에 data-theme 변경 → 200ms 후 트랜지션 속성 제거.
root.setAttribute(THEME_TRANSITION_ATTRIBUTE_NAME, '');
applyThemePreference(next);
window.setTimeout(() => {
root.removeAttribute(THEME_TRANSITION_ATTRIBUTE_NAME);
}, THEME_TRANSITION_DURATION_MS);
}, []);
return { cycleThemePreference };
}