Files
2nd/10_Wiki/Topics/Coding/Frontend_CSS_Migration_Tailwind.md
T
2026-05-10 22:08:15 +09:00

6.6 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
frontend-css-migration-tailwind CSS Migration — CSS-in-JS → Tailwind / native Coding draft B conceptual 2026-05-09 2026-05-09
frontend
css
vibe-coding
language applicable_to
CSS / TS
Frontend
CSS migration
Tailwind 4
Panda CSS
vanilla extract
CSS-in-JS death
runtime cost

CSS Migration

CSS-in-JS (Emotion, styled-components) 가 cost 큰. Tailwind 4 / native CSS / Panda CSS 가 modern.

📖 핵심 개념

  • CSS-in-JS = runtime cost (parse, hydrate).
  • Tailwind = build-time, 0 runtime.
  • Panda CSS / Vanilla Extract = atomic CSS-in-JS.
  • Native CSS = modern feature 가 충분.

💻 코드 패턴

옛 (Emotion / styled-components)

import styled from '@emotion/styled';

const Button = styled.button<{ primary?: boolean }>`
  padding: 8px 16px;
  background: ${({ primary }) => primary ? 'blue' : 'gray'};
  color: white;
  border-radius: 4px;
`;

<Button primary>Click</Button>

→ Runtime parsing. Hydration cost. SSR slow.

Tailwind

<button className='px-4 py-2 bg-blue-500 text-white rounded'>
  Click
</button>

// 또는 cva
import { cva } from 'class-variance-authority';

const button = cva('px-4 py-2 rounded', {
  variants: {
    color: { blue: 'bg-blue-500 text-white', gray: 'bg-gray-500 text-white' },
  },
});

<button className={button({ color: 'blue' })}>Click</button>

→ 0 runtime. Build-time only.

Tailwind 4 (Lightning CSS)

@import "tailwindcss";

@theme {
  --color-primary: oklch(0.7 0.15 270);
}

→ JS config 없음. CSS-native.

Panda CSS (atomic CSS-in-JS)

import { css } from '../styled-system/css';

<button className={css({ px: 4, py: 2, bg: 'blue.500', color: 'white' })}>
  Click
</button>

→ Build-time atomic CSS. Type-safe. Tailwind 식 + JS DX.

Vanilla Extract (zero-runtime)

// button.css.ts
import { style } from '@vanilla-extract/css';

export const button = style({
  padding: '8px 16px',
  background: 'blue',
  color: 'white',
});
import { button } from './button.css';
<button className={button}>Click</button>

→ TypeScript + zero runtime.

CSS Modules (legacy 가 OK)

// Button.module.css
.button { padding: 8px 16px; background: blue; }

// Button.tsx
import styles from './Button.module.css';
<button className={styles.button}>Click</button>

→ Scope 자동. Modern alternative 가 native @scope.

Migration: styled-components → Tailwind

Step 1: Add Tailwind.
Step 2: 매 component 의 css = Tailwind class.
Step 3: Remove styled-components.

→ 점진. 1 component 씩.

Migration script

# Twin macro 등 의 도구
npx codemod styled-components-to-tailwind

→ Manual review 가 필요.

Why migrate?

CSS-in-JS cost:
- Bundle size (Emotion: 12 KB).
- Runtime parse.
- Hydration delay.
- SSR overhead.
- Devtools 가 cryptic class.

Tailwind:
- 0 runtime.
- 1 css file.
- Browser cache.
- Devtools 친화 (class).

→ Production performance ↑.

Cons of Tailwind

- Class string 가 길음 (long).
- Learn curve.
- Custom design system = config.
- Dynamic style 어려움 (config 만).

→ shadcn/ui + cva 가 답.

Dynamic style (Tailwind)

// ❌ 동적 class
<div className={`bg-${color}-500`}>...</div>
// → JIT 가 build 시 detect 못 함.

// ✅ 명시
const colorClass = {
  red: 'bg-red-500',
  blue: 'bg-blue-500',
}[color];
<div className={colorClass}>...</div>

Tailwind v4 의 변화

- CSS-native (no JS config).
- Lightning CSS (Rust).
- @theme directive.
- 100x 빠른 build.
- 작은 css output.

Native CSS의 modern features

/* Container query */
.card {
  container-type: inline-size;
}
@container (min-width: 400px) { ... }

/* :has() */
.form:has(:invalid) { ... }

/* @scope */
@scope (.card) {
  h2 { color: blue; }
}

/* CSS variables */
:root { --primary: oklch(0.7 0.15 270); }

→ Tailwind 없이도 CSS 가 powerful.

Theme (Tailwind)

@theme {
  --color-primary: oklch(0.7 0.15 270);
  --font-sans: 'Inter', sans-serif;
  --shadow-card: 0 4px 12px rgba(0,0,0,0.1);
}
<div className='bg-primary font-sans shadow-card'>...</div>

Dark mode

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: black;
  }
}

/* Or class-based */
.dark {
  --color-bg: black;
}
<html className='dark'>

CSS-in-JS 가 적절 한 경우

- 매우 동적 (theme switch runtime).
- Library 가 CSS-in-JS 사용.
- Team 가 익숙.

→ 하지만 modern = native CSS 가 충분.

Astro CSS

---
---
<div class='card'>...</div>

<style>
.card {
  /* Scoped automatically */
  padding: 16px;
}
</style>

→ Component-scoped CSS native.

Production

  • Vercel docs: Tailwind.
  • GitHub: 옛 Sass → 새 design system.
  • Notion: 자체 CSS.
  • Stripe: Sass + design system.
  • shadcn/ui: Tailwind.

Real-world migration

1. Audit current CSS.
2. Pick 1 (Tailwind / Panda / native).
3. Component 별 migrate.
4. Test (visual regression).
5. Remove old.

→ 매 milestone 의 measurable.

Bundle size

Emotion: 12 KB runtime.
Styled-components: 16 KB.
Tailwind: 0 (build).
Panda: 0 (build).
Vanilla Extract: 0.

→ Modern = 0 runtime.

LLM 도움

- Style migration 의 component 별.
- Tailwind class 의 codegen.
- Visual regression 검증.

→ Cursor 의 큰 use case.

Tools

- Tailwind CSS (v4).
- Panda CSS.
- Vanilla Extract.
- StyleX (Meta).
- Lightning CSS.
- PostCSS (legacy).

StyleX (Meta)

import * as stylex from '@stylex/stylex';

const styles = stylex.create({
  base: { padding: 16, background: 'blue' },
});

<button {...stylex.props(styles.base)}>Click</button>

→ Atomic + zero runtime + type-safe.

🤔 의사결정 기준

상황 추천
New project Tailwind 4
Type-safe + JS Panda CSS / Vanilla Extract
매우 dynamic StyleX / Emotion
작은 / static Native CSS
Component library shadcn (Tailwind)
Migration 점진 + visual test

안티패턴

  • CSS-in-JS in 새 project: runtime cost.
  • Dynamic Tailwind class: JIT miss.
  • Tailwind + Sass: 둘 다 사용.
  • Migration big bang: visual regression.
  • No design tokens: drift.

🤖 LLM 활용 힌트

  • Tailwind 4 = modern default.
  • Panda / Vanilla Extract = type-safe alternative.
  • Native CSS 가 충분 (modern feature).
  • CSS-in-JS = legacy migration.

🔗 관련 문서