---
title: "Tailwind CSS v4 마이그레이션 + 다크/라이트/시스템 테마 — Tauri v2 + React 19"
category: integration-issues
date: 2026-03-30
tags:
  - tailwind-css-v4
  - tauri-v2
  - react-19
  - dark-mode
  - theme-toggle
  - css-custom-properties
  - react-flow
  - vite-6
  - component-splitting
severity: medium
component: src/app (frontend — React + Tailwind + Tauri webview)
symptoms:
  - 모놀리식 App.tsx + App.css에 모든 스타일/로직 집중
  - 다크 모드 미지원, 테마 전환 불가
  - Tailwind v4 미적용 (raw CSS 사용)
  - 컴포넌트 단위 개발 불가
---

# Tailwind CSS v4 마이그레이션 + 다크/라이트/시스템 테마

## 문제

Kiok (AI 에이전트 경험 메모리) Tauri v2 데스크톱 앱의 프론트엔드가 모놀리식 구조 — `App.tsx` 하나에 모든 UI 로직, `App.css`에 raw CSS. 테마 전환 불가, 컴포넌트 재사용 불가, 스타일 토큰 체계 없음.

## 근본 원인

세 가지 문제가 얽혀 있었음:
1. CSS 유틸리티 프레임워크 없이 raw CSS → 디자인 토큰 관리 불가
2. 단일 파일 구조 → 컴포넌트 분리 시 스타일 공유 방법 없음
3. 테마 인프라(CSS 변수 + 클래스 전환) 부재 → 다크모드 구현 불가

## 해결

### 1. Tailwind v4 설치 (Vite 플러그인)

v4는 `tailwind.config.js` 불필요. PostCSS 설정도 불필요.

```bash
pnpm add tailwindcss @tailwindcss/vite
```

```ts
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
  plugins: [react(), tailwindcss()],
});
```

### 2. CSS 변수 + `@theme` 매핑 패턴

핵심 패턴: `:root`/`.dark`에 CSS 변수 정의 → `@theme`에서 Tailwind 유틸리티로 매핑.

```css
/* index.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

:root {
  --c-canvas: #F4F4F4;
  --c-surface: #FFFFFF;
  --c-ink: #191F28;
  --c-accent: #3182F6;
  --c-accent-soft: #EBF4FF;
}

.dark {
  --c-canvas: #0c0c0e;
  --c-surface: #141416;
  --c-ink: #e8e8ec;
  --c-accent: #5ba0f8;
  --c-accent-soft: #1a2744;
}

@theme {
  --color-canvas: var(--c-canvas);
  --color-surface: var(--c-surface);
  --color-ink: var(--c-ink);
  --color-accent: var(--c-accent);
  --color-accent-soft: var(--c-accent-soft);
  --font-sans: 'Pretendard Variable', Pretendard, system-ui, sans-serif;
}
```

이렇게 하면 `bg-canvas`, `text-ink`, `bg-accent-soft` 등을 JSX에서 직접 사용. `.dark` 클래스가 CSS 변수를 스왑하므로 **`dark:` 접두사가 각 요소에 필요 없음**.

### 3. `useTheme` 훅 (3상태: light/dark/system)

```ts
export type Theme = "light" | "dark" | "system";

function apply(theme: Theme) {
  const isDark = theme === "dark" ||
    (theme === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
  document.documentElement.classList.toggle("dark", isDark);
}

export function useTheme() {
  const [theme, set] = useState<Theme>(
    () => (localStorage.getItem("kiok-theme") as Theme) ?? "system",
  );

  const setTheme = useCallback((t: Theme) => {
    set(t);
    if (t === "system") localStorage.removeItem("kiok-theme");
    else localStorage.setItem("kiok-theme", t);
    apply(t);
  }, []);

  useEffect(() => apply(theme), []);

  useEffect(() => {
    if (theme !== "system") return;
    const mq = matchMedia("(prefers-color-scheme: dark)");
    const handler = () => apply("system");
    mq.addEventListener("change", handler);
    return () => mq.removeEventListener("change", handler);
  }, [theme]);

  return { theme, setTheme } as const;
}
```

- `"system"` 선택 시 localStorage 키 자체를 제거 (FOUC 스크립트와 동기화)
- `matchMedia` 리스너는 `"system"` 모드일 때만 등록, cleanup 필수

### 4. FOUC 방지 (인라인 스크립트)

```html
<!-- index.html <head> 안, React 번들 로드 전 -->
<script>!function(){var t=localStorage.getItem("kiok-theme"),d=t==="dark"||(t!=="light"&&matchMedia("(prefers-color-scheme:dark)").matches);d&&document.documentElement.classList.add("dark")}()</script>
```

React 하이드레이션 전에 동기 실행. `useTheme`의 `apply()` 로직과 반드시 동기화해야 함.

### 5. React Flow 스타일 오버라이드

React Flow는 자체 DOM을 생성하므로 Tailwind 유틸리티가 아닌 CSS 변수로 오버라이드:

```css
.react-flow__controls {
  background: var(--c-surface) !important;
  border: 1px solid var(--c-line) !important;
}
```

엣지 스타일도 CSS 변수 참조:
```ts
style: { stroke: "var(--c-edge)", strokeWidth: 1.5 }
```

CSS 변수가 스왑되면 React Flow도 자동으로 테마 반영 — 리렌더 불필요.

### 6. 컴포넌트 분리

| 파일 | 역할 |
|------|------|
| `App.tsx` | 상태 오케스트레이터, Tauri IPC |
| `Sidebar.tsx` | 에이전트 목록, 검색, 뷰 토글, 테마 스위처 |
| `GraphCanvas.tsx` | ReactFlow 래퍼, 레이아웃/엣지 알고리즘 |
| `ExperienceNode.tsx` | 커스텀 노드 컴포넌트 |
| `useTheme.ts` | 테마 훅 |
| `index.css` | Tailwind + 토큰 + RF 오버라이드 |

## 예방 전략

1. **`tailwind.config.js` 완전 제거** — v4에서 남아있으면 이중 적용 또는 무시됨
2. **CSS 변수를 한 파일에 집중** — 여러 CSS에 분산 시 로드 순서에 따라 우선순위 꼬임
3. **FOUC 스크립트와 `useTheme` 로직 동기화** — 하나만 수정하면 깜빡임 발생
4. **`@xyflow/react/dist/style.css` import 누락 주의** — Tailwind 전환 중 실수로 삭제하기 쉬움
5. **`dark:` 접두사와 CSS 변수 혼용 금지** — 하나만 선택. CSS 변수 기반이면 `dark:` 불필요

## 검증 체크리스트

- [ ] `pnpm exec tsc --noEmit` — 타입 에러 0
- [ ] `pnpm build` — CSS 번들에 커스텀 변수 포함 확인
- [ ] 앱 새로고침 5회 — FOUC 없음 확인
- [ ] 라이트/다크/시스템 토글 — 모든 컴포넌트 색상 전환 확인
- [ ] OS 다크모드 토글 (시스템 모드일 때) — 즉시 반영 확인
- [ ] React Flow 캔버스 — 노드/엣지/컨트롤 테마 적용 확인

## 참고

- 기존 `docs/solutions/` 에 관련 문서 없음 (첫 문서)
- `PROGRESS.md` 기술 스택 섹션에 Tailwind v4 추가 필요 (stale)
- 디자인 가이드: Pretendard 폰트, 중립 그레이 팔레트, AI느낌 장식 금지 (auto memory [claude])
