Vite에서 사용하기

Vite 환경에서 @vapor-ui/core 테마 시스템을 설정하고, 화면 깜빡임(FOUC)을 방지하기 위해 스크립트를 수동으로 설정하는 방법을 안내합니다.

Vite에서 사용하기

Vite는 클라이언트 사이드 렌더링(CSR)을 기본으로 동작합니다. 이 때문에 동적 테마를 적용할 때 화면 깜빡임(FOUC, Flash of Unstyled Content) 현상이 발생할 수 있습니다.

이 문서는 Vite 환경에서 @vapor-ui/core를 설정하고, FOUC 현상을 방지하는 방법을 안내합니다.

왜 깜빡임(FOUC)이 발생할까요?

Vite는 최소한의 index.html을 브라우저에 먼저 보냅니다. 브라우저는 이 HTML과 기본 CSS로 첫 화면(주로 라이트 모드)을 그립니다. 그 후 React 앱의 자바스크립트가 로드되고 실행되면서 ThemeProvider가 사용자의 저장된 테마(예: 다크 모드)를 뒤늦게 DOM에 적용합니다. 이 시차 때문에 "라이트 모드 → 다크 모드로 화면이 바뀌는" 깜빡임이 발생합니다.

설정 방법

FOUC를 방지하려면, React가 실행되기 전에 localStorage의 테마 설정을 읽어 <html> 태그에 적용하는 스크립트를 index.html에 직접 추가해야 합니다.

향후 지원 안내

현재는 스크립트를 수동으로 추가해야 하지만, 추후 이 과정을 자동화하는 @vapor-ui/vite-plugin을 제공할 예정입니다.

1단계: 패키지 설치

먼저 @vapor-ui/core를 설치합니다.

npm install @vapor-ui/core

2단계: index.html에 FOUC 방지 스크립트 추가

public/index.html 또는 프로젝트 루트의 index.html 파일을 열고, <head> 태그 안에 아래의 <script> 코드를 복사하여 붙여넣으세요. 이 스크립트는 React보다 먼저 실행되어 깜빡임을 방지합니다.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vapor UI with Vite</title>

        <!-- 💡 @vapor-ui/core FOUC 방지 스크립트 시작 -->
        <script>
            (function () {
                // 이 스크립트는 React 실행 전에 동작하여 FOUC를 방지합니다.
                // 아래 설정을 자신의 프로젝트에 맞게 수정하세요.
                const defaultConfig = {
                    appearance: 'light',
                    radius: 'md',
                    scaling: 1.0,
                };
                const storageKey = 'my-vite-app-theme'; // ThemeProvider와 동일한 storageKey를 사용해야 합니다.

                // --- 내부 로직 (수정 불필요) ---
                const root = document.documentElement;
                let currentThemes = defaultConfig;

                try {
                    const storedItem = localStorage.getItem(storageKey);
                    if (storedItem) {
                        const storedSettings = JSON.parse(storedItem);
                        currentThemes = Object.assign({}, defaultConfig, storedSettings);
                    }
                } catch (e) {
                    // localStorage가 비활성화된 경우 기본값을 사용합니다.
                }

                // 1. Color Theme 적용
                if (currentThemes.appearance === 'dark') {
                    root.classList.add('vapor-dark-theme');
                } else {
                    root.classList.add('vapor-light-theme');
                }

                // 2. Radius Theme 적용
                const radiusMap = { none: 0, sm: 0.5, md: 1, lg: 1.5, xl: 2, full: 3 };
                const radiusFactor = radiusMap[currentThemes.radius] ?? 1;
                root.style.setProperty('--vapor-radius-factor', radiusFactor.toString());

                // 3. Scaling Theme 적용
                const scaleFactor = currentThemes.scaling ?? 1;
                root.style.setProperty('--vapor-scale-factor', scaleFactor.toString());
            })();
        </script>
        <!-- 스크립트 끝 -->
    </head>
    <body>
        <div id="root"></div>
        <script type="module" src="/src/main.tsx"></script>
    </body>
</html>

중요: 위 스크립트의 storageKey 값은 3단계에서 ThemeProvider에 전달할 storageKey와 반드시 일치해야 합니다.

3단계: ThemeProvider 설정

애플리케이션의 진입점(src/main.tsx)에서 ThemeProvider로 앱 전체를 감싸고, 스타일 시트를 임포트합니다.

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import { ThemeProvider, createThemeConfig } from '@vapor-ui/core';
// 필수: 스타일 시트를 임포트합니다.
import '@vapor-ui/core/styles.css';

import App from './App.tsx';

// index.html의 스크립트와 동일한 설정을 사용합니다.
const themeConfig = createThemeConfig({
    appearance: 'light',
    radius: 'md',
    scaling: 1.0,
    storageKey: 'my-vite-app-theme', // index.html의 storageKey와 일치해야 합니다.
});

ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
        <ThemeProvider config={themeConfig}>
            <App />
        </ThemeProvider>
    </React.StrictMode>,
);

4단계: 컴포넌트에서 테마 사용하기

설정이 완료되면 useTheme 훅을 사용하여 어떤 컴포넌트에서든 현재 테마 값을 읽거나 변경할 수 있습니다.

// src/components/ThemeToggleButton.tsx
import { useTheme } from '@vapor-ui/core';

export function ThemeToggleButton() {
    const { appearance, setTheme } = useTheme();

    const toggleTheme = () => {
        setTheme({
            appearance: appearance === 'light' ? 'dark' : 'light',
        });
    };

    return <button onClick={toggleTheme}>Toggle Theme (Current: {appearance})</button>;
}

이제 Vite 프로젝트에서도 화면 깜빡임 없이 동적 테마를 사용할 수 있습니다.