Build Pipeline & CSS Cascade Order — PostCSS·@layer·specificity 충돌 해결
이 문서가 답하는 질문: Panda와 Tailwind가 같은 셀렉터를 emit할 때 누가 이기는가? PostCSS 파이프라인을 어떻게 구성해야 결정적으로 한쪽이 이기게 만들 수 있는가? 한 줄 답 (Pyramid Top): 충돌은 PostCSS 플러그인 순서가 아니라 CSS Cascade Layers (
@layer)의 명시적 정의 순서로 해결한다. layer 안의 specificity와 import 순서는 그 다음 이야기다.
Why — 왜 존재하는가
두 시스템이 같은 페이지에서 작동할 때 다음 3개 충돌이 일어난다.
| 충돌 | 증상 | 원인 |
|---|---|---|
| Preflight 중복 | form control 스타일이 깨짐 | Tailwind preflight(reset)와 Panda preset.base가 동시 적용 |
| 같은 셀렉터 emit | .bg-primary가 한쪽 정의로 덮임 | 두 시스템 모두 @layer utilities에 정의 |
| Specificity wars | !important 폭주 | layer 순서 미정의 → import 순서 의존 → 빌드마다 다름 |
CSS Cascade Layers(2022 표준, 모든 모던 브라우저 지원)는 이 문제를 원천적으로 해결한다 — 어느 layer가 이기는지를 명시적으로 선언할 수 있기 때문이다.
How — 어떻게 동작하는가
핵심 원칙:
@layer선언 순서가 cascade 우선순위. 뒤에 선언된 layer가 이긴다.- layer 안에서는 specificity·순서가 결정. 따라서 같은 layer에 두 시스템이 들어가면 비결정적.
!important는 layer 외부로 빠진다. 한 번 쓰면 layer 시스템이 무너지므로 절대 금지.
What — 구체 사양·수치·예시
권장 layer 구조
src/styles/app.css:
/* 1) Layer 순서 선언 — 뒤가 이김 */
@layer reset, tokens, base, recipes, utilities-panda, utilities-tw, overrides;
/* 2) Reset — 둘 중 하나만 (양쪽 다 켜면 form control 깨짐) */
@layer reset {
@import 'tailwindcss/preflight';
/* OR
@import './panda-reset.css'; */
}
/* 3) Token — Style Dictionary 출력 */
@layer tokens {
@import './tokens.css';
}
/* 4) Base — 디자인 시스템 자체의 element 스타일 */
@layer base {
body { font-family: var(--font-sans); }
}
/* 5) Panda recipe + utility */
@import './styled-system/styles.css' layer(recipes); /* Panda output */
/* 6) Tailwind utility */
@import 'tailwindcss/utilities' layer(utilities-tw);
/* 7) Override (꼭 필요한 경우만) */
@layer overrides {
.legacy-page .button { /* ... */ }
}이 순서면:
- Tailwind utility(
utilities-tw)가 Panda recipe(recipes)보다 이긴다 → 마지막에 className으로 덮기 가능. - Reset은 한 번만 적용.
- Override는 가장 마지막 layer —
!important없이도 이김.
Tailwind v4 @theme 활용
Tailwind v4는 자체적으로 @layer 구조를 갖는다:
@import 'tailwindcss';
@theme {
--color-primary: oklch(0.55 0.18 250);
}이걸 Panda와 함께 쓸 땐:
@layer reset, theme, base, recipes, utilities;
@import 'tailwindcss/theme' layer(theme);
@import 'tailwindcss/preflight' layer(reset);
@import 'tailwindcss/utilities' layer(utilities);
@import './styled-system/styles.css' layer(recipes);PostCSS 플러그인 순서
postcss.config.js:
module.exports = {
plugins: {
'@pandacss/dev/postcss': {}, // Panda가 먼저 styled-system 출력 갱신
'tailwindcss': {}, // 그 다음 Tailwind 출력
'autoprefixer': {},
'cssnano': {}, // 프로덕션만
},
}플러그인 순서는 처리 순서이지 cascade 순서가 아니다. cascade는 위의 @layer 선언이 결정한다.
Vite + Next.js 설정
// next.config.mjs
import nextra from 'nextra'
const withNextra = nextra({/* ... */})
export default withNextra({
webpack(config) {
return config
},
experimental: {
optimizePackageImports: ['styled-system'], // Panda tree-shake
},
})// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
css: {
postcss: './postcss.config.js',
},
})Reset 중복 회피
Panda preset.base와 Tailwind preflight는 거의 같은 일을 하지만 미묘하게 다르다.
| 항목 | Tailwind preflight | Panda preset.base |
|---|---|---|
*, ::before, ::after | box-sizing: border-box, border: 0 | box-sizing: border-box |
body | line-height: 1.5 | line-height: var(--leading-base) |
| Form elements | font: inherit; color: inherit | 비슷하나 다름 |
img | display: block; max-width: 100% | 동일 |
둘 다 켜면: 후행 적용이 이김. <button> 스타일이 한쪽에서 reset 후 다른 쪽이 다시 reset → 미묘한 차이가 누적.
해결: 둘 중 하나만 활성화. Panda 사용자는:
// panda.config.ts
preflight: false, // Tailwind에 위임Tailwind 사용자는:
// panda.config.ts
preflight: true,
// 그리고 tailwind.config.ts에서 preflight 끔
corePlugins: { preflight: false }What-if — 잘못 쓰면 어떻게 깨지는가
@layer선언 없이 두 시스템 import: cascade 순서가 import 순서에 의존 → 다른 빌더(turbopack vs webpack)에서 결과 다름. 항상 명시적 layer.!important한 번 쓰기: layer 외부로 빠지므로 어느 layer도 이기지 못함. 디자인 시스템 라이브러리 코드에!important가 들어가면 사용자 코드에서 override 불가.- PostCSS 플러그인 순서가 cascade 결정한다고 착각: Tailwind가 먼저 처리되든 나중이든 생성된 CSS의 cascade는
@layer가 결정. 처리 순서는 어느 시스템이 어느 셀렉터를 만드는지에만 영향. - CSS Modules와 layer 동시 사용: CSS Modules는 자체 scope,
@layer는 cascade. 둘은 충돌 안 하지만, CSS Modules가 만든 클래스는 어느 layer에도 안 들어감 → 디자인 시스템 토큰을 무시할 위험. - Preflight 중복: form control 스타일이 미묘하게 어긋남. 둘 중 하나만.
Insight — 흥미로운 이야기
CSS Cascade Layers는 2018년 Jen Simmons(Apple Safari 팀)의 제안으로 시작됐다. 당시 CSS의 가장 큰 심리적 부담은 “내 스타일이 왜 안 먹지?”였고, specificity·source order·!important로 얽힌 cascade는 큰 코드베이스에서 예측 불가능했다.
Chris Coyier가 2021년 “The Future of CSS: Cascade Layers”라는 글에서 이를 “BEM·utility-first·CSS-in-JS의 십수 년 논쟁을 종결시킬 수 있는 표준”이라 평한 이유가 여기 있다. 처음으로 *“이 코드는 저 코드를 절대 못 덮는다”*를 표준 문법으로 선언할 수 있게 됐기 때문이다.
흥미로운 반전: Tailwind v3은 @layer base/components/utilities라는 Tailwind 자체 directive를 먼저 만들었고, 이는 표준 @layer와 비슷하지만 다른 것이었다 — Tailwind는 PostCSS 변환으로 layer를 순서로 풀어버렸다. v4에 와서야 표준 @layer directive를 그대로 emit하게 됐다. 즉, Tailwind v3와 Panda를 같이 쓰던 시절엔 v3의 가짜 layer 때문에 진짜 layer를 못 쓰는 우스운 상황이 있었다.
Panda CSS는 처음부터 표준 @layer를 가정하고 설계됐다. @layer reset, base, tokens, recipes, utilities라는 표준 이름은 Panda 문서에서 처음 제안됐고, 다른 zero-runtime CSS-in-JS(vanilla-extract, Stitches)도 비슷하게 따라왔다.
요약
- 충돌 해결의 단일 도구는
@layer선언 순서. - PostCSS 플러그인 순서는 처리 순서일 뿐, cascade 순서가 아니다.
- 권장:
@layer reset, tokens, base, recipes, utilities-panda, utilities-tw, overrides. - Preflight·base reset은 둘 중 하나만 활성화.
!important는 layer 시스템을 무너뜨리므로 절대 금지.