Panda CSS 완벽 가이드 — 제로 런타임 CSS-in-JS·토큰·Recipes·RSC

Panda CSS 완벽 가이드 — 제로 런타임 CSS-in-JS·토큰·Recipes·RSC

이 글의 핵심

Panda CSS는 빌드 시점에 정적 CSS와 타입 안전 API를 생성하는 «제로 런타임» 스타일 도구입니다. 토큰·프리셋으로 디자인 시스템을 코드로 고정하고, Recipes·Patterns로 반복 UI를 추상화하며, React Server Component와도 잘 맞습니다. 이 글은 설정부터 실무 컴포넌트 설계·Tailwind와의 선택 기준까지 한 흐름으로 연결합니다.

이 글의 핵심

Panda CSS는 CSS-in-JS의 개발 경험(코드로 스타일을 표현하고 타입 추론을 받는 경험)과 런타임 비용을 줄이려는 요구를 동시에 만족시키기 위해 등장한 도구입니다. 런타임에 <style>을 삽입하거나 JavaScript로 규칙을 계산하는 대신, 빌드 타임에 정적 CSS와 타입 정의를 생성하고 애플리케이션은 클래스명과 CSS 파일만 로드합니다.

이 글에서 다루는 내용은 다음과 같습니다.

  • 핵심 개념: 제로 런타임, 코드 생성, 토큰 중심 API
  • 설정과 프리셋: panda.config, 확장·프리셋 병합
  • Recipes와 Patterns: 변형(variant)이 있는 컴포넌트 스타일과 레이아웃 추상화
  • 토큰과 디자인 시스템: 색·간격·타이포를 코드 계약으로 고정
  • RSC와 호환성: 서버 컴포넌트·클라이언트 경계를 깨지 않는 사용 패턴
  • Tailwind vs Panda CSS: 선택 기준과 공존 전략
  • 실전 컴포넌트 라이브러리: 버튼·카드 등 재사용 단위 설계 예시

아래 예제는 Panda CSS 최신 안정 계열의 일반적인 관용구를 기준으로 합니다. 설치된 패키지 버전에 따라 import 경로·설정 키가 다를 수 있으므로, 적용 시 공식 문서와 프로젝트의 package.json을 함께 대조하시기 바랍니다.


1. Panda CSS의 핵심 개념

1.1 제로 런타임(Zero Runtime)이 의미하는 것

전통적인 런타임 CSS-in-JS(예: 테마 프로바이더가 스타일을 주입하는 방식)는 사용자 기기에서 스타일 계산·삽입 비용이 발생할 수 있습니다. Panda는 이 비용을 빌드 파이프라인으로 이동합니다. 개발자는 TypeScript/JavaScript로 스타일 의도를 작성하지만, 배포 산출물에는 미리 생성된 클래스정적 스타일시트가 포함됩니다.

그 결과 다음과 같은 특징이 생깁니다.

  • 초기 로드: 스타일 엔진이 애플리케이션 번들을 부풀리지 않는 편이 일반적입니다.
  • 예측 가능성: 생성 규칙이 빌드 시점에 확정되어, 프로덕션에서의 스타일 «깜빡임»이나 주입 순서 이슈를 줄이기 쉽습니다.
  • 타입 안전성: 토큰·variant 이름이 잘못되면 컴파일 단계에서 걸러지도록 설계할 수 있습니다.

1.2 코드 생성(Codegen) 워크플로

Panda를 도입하면 보통 설정 파일(panda.config.ts 등)과 소스의 스타일 호출을 기준으로 styled-system 디렉터리(이름은 설정에 따라 다름)가 생성됩니다. 이 디렉터리에는 css 함수, 조건(condition), 레시피 타입 등 프로젝트에 맞춘 API 표면이 만들어집니다.

왜 이 단계가 필요한가에 대한 짧은 답은, «토큰과 레시피 정의를 한곳에서 읽어, 일관된 타입과 런타임 최소 헬퍼만 남기기 위해서»입니다. 팀이 커질수록 «문자열 클래스 나열»이 아니라 계약된 토큰 집합으로 말하는 편이 유지보수에 유리합니다.

1.3 유틸리티·토큰·시맨틱의 균형

Panda는 단순히 유틸리티 나열 도구가 아니라, 토큰 계층시맨틱 토큰(예: colors.fg.default)을 두어 디자인 시스템을 코드로 표현하는 것을 권장합니다. 이는 «디자이너가 정의한 스케일»과 «컴포넌트가 참조하는 의미」를 분리해, 테마 변경·다크 모드·브랜드 확장 시 수정 범위를 줄이는 데 도움이 됩니다.


2. 설정과 프리셋

2.1 최소 설치와 진입점

프로젝트에는 보통 @pandacss/dev가 devDependency로 들어가고, PostCSS 또는 Vite·Webpack 플러그인 체인에 Panda가 연결됩니다. 포인트는 «소스를 감시해 CSS를 emit하고, codegen을 실행한다»는 것입니다.

대표적인 초기화 흐름은 다음과 같습니다.

# 패키지 매니저에 맞게 선택 (예: pnpm)
pnpm add -D @pandacss/dev postcss
pnpm panda init

panda initpanda.config.ts, PostCSS 설정, package.json 스크립트 등을 추가합니다. 모노레포에서는 어느 패키지가 소스 루트인지를 명확히 해야 include/exclude가 어긋나지 않습니다.

2.2 defineConfig로 루트 구성 잡기

panda.config.ts는 Panda의 단일 진실 공급원에 가깝습니다. 여기서 프리셋, 테마(토큰), 조건(미디어 쿼리·다크 모드), 글로벌 CSS, 레시피 등을 선언합니다.

import { defineConfig } from '@pandacss/dev';

export default defineConfig({
  // 소스에서 Panda가 스캔할 파일 범위
  include: ['./src/**/*.{ts,tsx,js,jsx}'],
  exclude: [],

  // 기본 프리셋 + 팀 확장
  theme: {
    extend: {
      tokens: {
        colors: {
          brand: { value: '#2563eb' },
        },
      },
    },
  },

  outdir: 'styled-system',
});

주의할 점: include가 너무 넓으면 빌드가 느려지고, 너무 좁으면 사용한 스타일이 추출되지 않습니다. 디자인 시스템 패키지를 별도로 둔 경우, 그 패키지 경로를 반드시 포함해야 합니다.

2.3 프리셋(Preset)으로 팀 규칙 공유하기

프리셋은 재사용 가능한 Panda 설정 묶음입니다. 조직 단위로 «브랜드 토큰 + 공통 레시피 + 린트에 가까운 금지 규칙»을 프리셋으로 배포하면, 여러 앱이 동일한 스타일 계약을 공유할 수 있습니다.

import { definePreset } from '@pandacss/dev';

export const acmePreset = definePreset({
  name: '@acme/panda-preset',
  theme: {
    extend: {
      tokens: {
        fonts: {
          body: { value: 'Inter, system-ui, sans-serif' },
        },
      },
    },
  },
});

루트 설정에서는 presets: [acmePreset] 형태로 합성합니다. 프리셋 병합 순서는 토큰 충돌 해결에 영향을 주므로, 팀 문서에 «어느 프리셋이 우선인지»를 명시하는 것이 좋습니다.


3. Recipes와 Patterns

3.1 Recipe: variant 기반 컴포넌트 스타일

Recipe는 버튼·배지처럼 도메인 컴포넌트에 붙는 변형 세트를 한곳에 모읍니다. size, variant, tone 같은 축을 정의하고, 각 조합에 해당하는 스타일을 선언합니다. 이는 단순 유틸 클래스 나열을 줄이고, 디자인 시스템의 «허용된 조합»을 코드로 강제하는 데 유리합니다.

개념적으로는 다음 요소로 이해할 수 있습니다.

  • base: 모든 변형에 공통으로 적용되는 기본 스타일
  • variants: 축마다 허용 값과 스타일
  • compoundVariants: 여러 축이 동시에 만족될 때만 적용되는 규칙
  • defaultVariants: 생략 시 기본값

Recipe를 정의한 뒤에는 생성된 button 레시피 헬퍼로 클래스를 조합합니다(프로젝트의 codegen 이름에 따름).

// 예: panda.config.ts 내부 또는 별도 파일에서 defineRecipe 사용
import { defineRecipe } from '@pandacss/dev';

export const buttonRecipe = defineRecipe({
  className: 'btn',
  base: {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontWeight: 'semibold',
    borderRadius: 'md',
  },
  variants: {
    size: {
      sm: { textStyle: 'sm', px: '3', py: '1.5' },
      md: { textStyle: 'md', px: '4', py: '2' },
    },
    variant: {
      solid: {},
      outline: { borderWidth: '1px', borderColor: 'border' },
    },
  },
  defaultVariants: {
    size: 'md',
    variant: 'solid',
  },
});

실무 팁: Recipe 축이 늘어날수록 조합 폭발이 생깁니다. 디자인 단계에서 «허용 조합표»를 만들고, compoundVariants로 금지 조합을 막거나, 아예 별도 Recipe로 쪼개는 편이 낫습니다.

3.2 Pattern: 반복 레이아웃 추상화

Pattern은 스택·그리드·센터링처럼 자주 쓰는 레이아웃 패턴을 이름 붙여 재사용하는 레이어입니다. 매번 display:flex, gap, alignItems를 나열하는 대신, 팀이 합의한 레이아웃 프리미티브로 표현합니다.

패턴을 쓰면 다음이 좋아집니다.

  • 간격 스케일 고정: gap이 임의 값이 아니라 토큰으로만 들어가게 유도
  • 접근성·정렬 규칙 공유: 아이콘+텍스트 정렬 같은 세부를 한곳에서 관리
// 개념 예: 패턴 정의(프로젝트 설정 방식에 맞게 배치)
import { definePattern } from '@pandacss/dev';

export const stackPattern = definePattern({
  description: '세로 스택',
  properties: {
    gap: { type: 'token', value: 'spacing' },
    align: { type: 'property', value: 'alignItems' },
  },
  defaultValues: {
    gap: '4',
    align: 'stretch',
  },
  // 구현 세부는 Panda 버전/프리셋에 따릅니다.
});

Pattern과 Recipe의 역할 분담을 「레이아웃 vs 컴포넌트 외형」으로 나누면, 디자인 시스템 라이브러리 구조가 명확해집니다.


4. 토큰과 디자인 시스템

4.1 토큰 계층 설계

Panda에서 토큰은 색·간격·라운드·폰트·그림자 등의 스케일을 의미합니다. 잘 설계된 토큰 계층은 다음을 만족합니다.

  1. 원시 토큰(raw): 브랜드 팔레트, 기본 스케일
  2. 시맨틱 토큰: bg.canvas, fg.muted, border.subtle처럼 «의미」로 소비
  3. 컴포넌트 토큰(선택): 특정 컴포넌트만의 높이·패딩 등

이 글은 «정답 레시피」를 강제하지 않지만, 실무에서는 시맨틱 토큰을 컴포넌트가 직접 참조하게 두는 편이 테마 전환에 유리합니다.

4.2 텍스트 스타일과 타이포 스케일

타이포그래피는 UI 일관성의 핵심입니다. Panda에서는 textStyle 등으로 폰트 크기·행간·자간을 묶어 재사용할 수 있습니다. 페이지와 컴포넌트가 임의의 font-size를 쓰기보다, 정해진 스타일 토큰만 참조하게 하면 디자인 감사(audit)가 쉬워집니다.

4.3 다크 모드·테마: 조건(Conditions) 활용

테마는 단순히 색 두 벌이 아니라 대비·포커스 링·보더 가시성까지 함께 바뀌어야 합니다. Panda의 조건_dark, _hover 같은 셀렉터 계열을 일관되게 다루는 데 쓰입니다. 토큰 값을 CSS 변수로 노출해 런타임에 테마만 바꾸는 전략도 흔합니다.

함정: 토큰만 늘리고 사용처 통제가 없으면 «토큰 폭발»이 납니다. 린트·코드 리뷰·Figma 토큰 동기화 정책을 병행하는 것이 좋습니다.


5. RSC(React Server Components)와 호환성

5.1 왜 Panda가 RSC 담론에 자주 등장하는가

서버 컴포넌트는 클라이언트 번들을 키우지 않고 UI를 준비하려는 모델입니다. 이 관점에서 Panda의 제로 런타임 접근은 방향성이 맞습니다. 스타일이 빌드 시 생성된 클래스로 끝나면, 서버에서 HTML을 만들 때도 동일한 클래스명을 안정적으로 쓸 수 있습니다.

5.2 서버 vs 클라이언트 경계

실무에서의 규칙은 단순합니다.

  • 서버 컴포넌트: 가능한 한 css() 호출만으로 클래스 문자열을 만들고 마크업에 부착
  • 클라이언트 컴포넌트: 상태·이벤트·애니메이션처럼 브라우저 API가 필요한 경우에 한함

문제는 스타일이 아니라 어떤 코드가 클라이언트 번들로 끌려가느냐입니다. Panda 자체보다는 프레임워크의 'use client' 규칙서드파티 컴포넌트가 경계를 흔듭니다.

5.3 Next.js App Router에서의 체크리스트

  1. Codegen 산출물 경로가 TypeScript 경로 별칭과 맞는지
  2. 글로벌 CSS import 위치(app/layout 등)가 프레임워크 규칙에 맞는지
  3. 조건부 스타일을 클라이언트 훅에만 의존하지 않는지

이 세 가지를 빌드 산출물과 실제 DOM에서 검증하면, RSC 환경에서의 불일치 대부분을 줄일 수 있습니다.


6. Tailwind CSS vs Panda CSS

6.1 철학의 유사점과 차이점

Tailwind는 널리 쓰이는 유틸리티 퍼스트 접근과 강력한 생태계가 강점입니다. Panda는 유틸리티를 배제하기보다, 토큰·타입·레시피·코드 생성을 중심에 두어 «디자인 시스템을 코드베이스에 장착»하는 경험에 초점을 둡니다.

관점TailwindPanda CSS
설정tailwind.config 중심panda.config + codegen
스타일 표현클래스 문자열 위주css 객체 + 레시피·패턴
타입 안전플러그인·제3자 도구에 의존 가능생성 타입으로 강하게 유도 가능
런타임유틸 클래스는 정적 CSS로 purge제로 런타임 지향
학습 곡선자료·예제 풍부DS·TS 친화적 팀에 유리

6.2 선택 기준(실무 의사결정)

  • Tailwind가 유리한 경우: 레퍼런스·플러그인·템플릿 생태계가 최우선이거나, 팀이 이미 숙련됨
  • Panda가 유리한 경우: 토큰·variant·패턴을 엄격히 관리해야 하고, 모노레포에서 DS 패키지를 코드로 공유해야 함
  • 공존: 레거시는 Tailwind, 신규 DS는 Panda처럼 경계를 나누는 점진적 도입이 현실적입니다.

7. 실전 컴포넌트 라이브러리

7.1 라이브러리 구조 제안

디자인 시스템 패키지를 만든다면 다음 구조가 읽기 쉽습니다.

  • tokens/ — 브랜드·시맨틱 토큰
  • recipes/ — 버튼·입력·배지
  • patterns/ — 스택·그리드·인라인 클러스터
  • components/ — React/Vue 등 래퍼(프로젝트별)

핵심은 «스타일 계약」과 «프레임워크 래퍼」를 분리하는 것입니다. 스타일은 Panda, 상호작용 상태는 각 프레임워크가 담당합니다.

7.2 버튼 컴포넌트 예시

아래는 개념을 보여주는 React 예시입니다. 실제 import 경로는 codegen 결과에 맞추어야 합니다.

// Button.tsx — 개념 예시 (경로·API는 프로젝트 생성물에 맞게 조정)
import { type RecipeVariantProps } from '../styled-system/types';
import { button } from '../styled-system/recipes';

type ButtonVariants = RecipeVariantProps<typeof button>;

export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  ButtonVariants & { loading?: boolean };

export function Button({ loading, className, children, ...rest }: ButtonProps) {
  const classes = button(rest);
  return (
    <button type="button" className={className ? `${classes} ${className}` : classes} disabled={loading || rest.disabled}>
      {loading ? '…' : children}
    </button>
  );
}

설명: button(rest)는 variant props를 받아 최종 클래스 문자열을 조합합니다. 소비자는 타입 안전한 props만 넘기면 되고, 잘못된 조합은 가능한 한 컴파일 타임에 걸립니다. className을 합치는 이유는 소비자가 레이아웃·접근성 목적으로 추가 클래스를 붙일 수 있게 하기 위입니다.

7.3 카드·표면(surface) 패턴

카드는 배경·테두리·그림자·라운드의 조합입니다. 토큰만 잘 정의했다면 Recipe보다는 작은 조합 유틸로도 충분한 경우가 많습니다. 팀에서 카드 변형이 많아지면 Recipe로 승격하세요.

// Card.tsx — 개념 예시
import { css } from '../styled-system/css';

const cardClass = css({
  bg: 'bg.surface',
  color: 'fg.default',
  borderWidth: '1px',
  borderColor: 'border.subtle',
  borderRadius: 'lg',
  boxShadow: 'sm',
  p: '4',
});

export function Card(props: React.PropsWithChildren<{ className?: string }>) {
  return <div className={[cardClass, props.className].filter(Boolean).join(' ')}>{props.children}</div>;
}

실무에서 자주 나는 문제는 «토큰 이름은 있는데 실제 값이 테마에 없음»입니다. DS 저장소에 스냅샷 테스트스토리북 시각 회귀를 붙이면 초기 품질이 올라갑니다.


8. 문제 해결과 모범 사례

8.1 생성물이 갱신되지 않을 때

  • include 경로에 파일이 빠졌는지
  • Watch 모드가 켜져 있는지(개발 서버 설정)
  • 캐시 디렉터리가 꼬이지 않았는지

를 순서대로 확인합니다.

8.2 토큰 이름은 엄격하게, 사용은 단순하게

토큰 네이밍은 일관된 규칙(예: bg.*, fg.*, border.*)을 문서화하고, 컴포넌트에서는 시맨틱 토큰만 참조하게 합니다. 이렇게 해야 브랜드 색이 바뀌어도 컴포넌트 수정 범위가 작아집니다.

8.3 코드 리뷰에서 볼 질문

  1. 이 스타일은 토큰으로 표현 가능한가
  2. variant 축이 늘어나 금지 조합을 만들지 않는가
  3. 서버·클라이언트 경계를 불필요하게 흔들지 않는가

9. 맺음말

Panda CSS는 제로 런타임이라는 기술적 약속과 함께, 토큰·레시피·패턴이라는 디자인 시스템 도구를 코드베이스에 녹여 넣도록 설계되어 있습니다. 도구를 «유행」이 아니라 팀의 계약·배포·접근성 목표에 맞춰 선택하고, Tailwind와의 관계도 대체가 아닌 역할 분담으로 보는 것이 장기적으로 유리합니다.

배포 전에는 git add, commit, pushnpm run deploy를 실행하는 워크플로를 프로젝트 규칙에 맞춰 유지하시기 바랍니다.


참고