PostCSS 플러그인 활용: Autoprefixer, CSS Nesting, CSS Modules, Webpack·Vite·Tailwind 연동
이 글의 핵심
PostCSS가 CSS를 AST로 바꿔 플러그인을 적용하는 방식, Autoprefixer와 Nesting 설정, CSS Modules, 빌드 도구와 Tailwind 연동까지 실전 설정 예제로 다룹니다.
이 글의 핵심
PostCSS로 CSS를 변환하는 방법을 다룹니다. Autoprefixer, CSS Nesting, CSS Modules, 빌드 도구 연동, Tailwind CSS 통합까지 실전 설정 예제로 정리하고, 각 설정에서 실제로 자주 막히는 지점을 함께 설명합니다.
실무에서 마주치는 문제들
Vendor Prefix를 수동으로 추가해요
-webkit-, -moz- 접두사를 손으로 붙이면 빠뜨리기 쉽고, 더는 필요 없는 접두사를 몇 년째 달고 다니게 됩니다. Autoprefixer는 지원 대상 브라우저 목록을 기준으로 필요한 접두사만 자동으로 붙이고, 불필요해진 접두사는 제거합니다.
최신 CSS를 사용하고 싶어요
oklch() 색상, 미디어 쿼리 범위 문법(width >= 768px) 같은 최신 문법은 구형 브라우저가 해석하지 못합니다. PostCSS 플러그인(postcss-preset-env 등)으로 대상 브라우저가 이해하는 형태로 변환할 수 있습니다.
CSS Nesting이 필요해요
네이티브 CSS Nesting은 2023년부터 Chrome·Safari·Firefox 최신 버전에서 지원되지만, 지원 범위에 그 이전 브라우저가 포함된다면 여전히 평탄화(flatten)가 필요합니다. PostCSS Nesting은 표준 문법으로 작성한 중첩을 일반 선택자로 풀어 줍니다.
PostCSS가 Sass·CSS-in-JS와 다른 점
CSS 전처리기의 진화: LESS → Sass → PostCSS
PostCSS는 2013년 Andrey Sitnik(Autoprefixer 개발자)이 만든 CSS 변환 도구입니다. 당시 CSS 전처리기는 Sass(2006)와 LESS(2009)가 주류였지만, 두 가지 문제가 있었습니다:
- 폐쇄적 문법: 당시 Sass는 Ruby 구현, LESS는 자체 파서라서 새 기능을 추가하려면 언어 자체를 고쳐야 했음
- All-or-nothing: Sass를 쓰려면 nesting·변수·mixin을 한 묶음으로 받아들여야 했고, 필요한 기능 하나만 골라 쓸 수 없었음
PostCSS는 이를 “플러그인 생태계”로 해결했습니다. PostCSS 코어 자체는 CSS를 파싱해 트리로 만들고 다시 문자열로 출력하는 일만 하고, 실제 변환은 전부 플러그인에 위임합니다. 그래서 “PostCSS를 쓴다”는 말은 사실상 “어떤 플러그인 조합을 쓴다”는 뜻이고, 플러그인이 하나도 없으면 입력과 똑같은 CSS가 나옵니다. 이 구조 덕분에 Autoprefixer, Tailwind CSS(v3), CSS Modules 구현, cssnano 같은 서로 성격이 다른 도구가 같은 파이프라인 위에서 동작할 수 있습니다.
PostCSS vs Sass vs CSS-in-JS
| 측면 | PostCSS | Sass | CSS-in-JS |
|---|---|---|---|
| 철학 | 플러그인 조합 | 통합 문법 | JS 안에 CSS |
| 문법 | 표준 CSS + 플러그인 확장 | 독자 문법 (.scss) | JS 템플릿 리터럴/객체 |
| Autoprefixer | ✅ 플러그인 | 별도로 PostCSS 필요 | 라이브러리가 런타임/빌드에 처리 |
| 빌드 속도 | 플러그인 수에 비례 | Dart Sass 기준 준수 | 런타임 생성 방식은 렌더링 비용 발생 |
| 사용 사례 | Tailwind·Bootstrap 빌드, 후처리 | 대형 스타일시트, 디자인 시스템 | React 컴포넌트 스타일링 |
실무에서는 이 셋이 경쟁 관계라기보다 층이 다릅니다. Sass는 “작성용 언어”, PostCSS는 “후처리 파이프라인”에 가깝습니다. 그래서 Sass로 작성하고 컴파일 결과를 PostCSS(Autoprefixer, cssnano)에 통과시키는 조합이 가장 흔합니다. 참고로 Rust로 작성된 Lightning CSS는 PostCSS 플러그인이 아니라 파싱·접두사·최소화를 한 번에 하는 별도 도구입니다. Vite에서 css.transformer: 'lightningcss'로 PostCSS 대신 쓸 수 있고 훨씬 빠르지만, 기존 PostCSS 플러그인은 사용할 수 없다는 트레이드오프가 있습니다.
내부 동작: AST 변환 파이프라인
PostCSS는 CSS를 AST(Abstract Syntax Tree)로 파싱한 뒤, 플러그인들이 순서대로 변환하며, 다시 CSS로 출력합니다.
CSS 입력 → Parser → AST → Plugin 1 → Plugin 2 → ... → Stringifier → CSS 출력
예시 (Autoprefixer):
// AST에서 'display: flex'를 찾음
{
type: 'decl',
prop: 'display',
value: 'flex'
}
// 플러그인이 prefix 추가
{
type: 'decl',
prop: 'display',
value: '-webkit-box' // 추가
},
{
type: 'decl',
prop: 'display',
value: 'flex' // 원본 유지
}
AST는 Root 아래에 Rule(선택자 블록), AtRule(@media 등), Declaration(속성-값 쌍), Comment 노드가 트리로 달린 구조입니다. 플러그인은 Declaration이나 Rule 같은 노드 종류별로 방문자 함수를 등록하고, 노드를 수정·삽입·삭제합니다. 위 예시처럼 Autoprefixer는 원본 선언 앞에 접두사 버전을 추가하고 원본은 그대로 둡니다. 브라우저는 이해하지 못하는 선언을 무시하고 나중에 선언된 값을 우선하므로, 표준 속성을 마지막에 두는 순서가 중요합니다.
파이프라인 구조에서 나오는 가장 중요한 결과는 플러그인 순서가 결과를 바꾼다는 점입니다. 예를 들어 postcss-import가 뒤에 있으면 다른 플러그인은 @import로 불러올 파일 내용을 보지 못하고, cssnano가 Autoprefixer보다 앞에 있으면 최소화된 뒤에 접두사가 붙어 최적화 효과가 줄어듭니다. 원칙은 “파일 합치기(import) → 문법 변환(nesting, preset-env) → 프레임워크(Tailwind) → 접두사 → 최소화” 순서입니다.
핵심 특징
주요 장점:
- Plugins: 수백 개의 플러그인 중 필요한 기능만 조합
- Autoprefixer: Can I Use 데이터 기반 자동 접두사
- CSS Modules: 클래스명을 파일 단위로 스코프 격리해 충돌 방지
- Nesting: 표준 CSS Nesting 문법을 평탄화
- 표준 지향: 앞으로 표준이 될 CSS 문법을 현재 브라우저용으로 변환
Tailwind CSS와의 관계:
- Tailwind v3는 PostCSS 플러그인으로 동작하며
@tailwind,@apply지시자를 PostCSS 단계에서 처리합니다 - Tailwind v4는 자체 엔진을 쓰고, PostCSS 연동은 별도 패키지
@tailwindcss/postcss로 분리되었습니다. v4는@import처리와 접두사 추가까지 내장하므로postcss-import와 Autoprefixer가 필요 없습니다
설치와 postcss.config.js
설치
npm install -D postcss postcss-cli
postcss는 코어 라이브러리이고 postcss-cli는 명령줄에서 파일을 변환하는 도구입니다. Vite, webpack, Next.js처럼 번들러를 쓰는 프로젝트라면 번들러가 PostCSS를 직접 호출하므로 postcss-cli는 필요 없습니다.
postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
},
};
객체 형식의 키는 플러그인 패키지 이름이고 값은 옵션입니다. 이 형식에서는 객체에 적은 순서가 곧 실행 순서입니다. plugins: [require('autoprefixer')()] 같은 배열 형식도 쓸 수 있는데, 조건부로 플러그인을 넣고 빼기에는 배열 형식이 더 명확합니다.
package.json에 "type": "module"이 있는 프로젝트(최근 Vite 템플릿의 기본값)에서는 module.exports가 module is not defined in ES module scope 에러를 냅니다. 이때는 파일 이름을 postcss.config.cjs로 바꾸거나 export default { plugins: { ... } } 형태의 ESM으로 작성하면 됩니다.
package.json
{
"scripts": {
"css": "postcss src/styles.css -o dist/styles.css"
}
}
Autoprefixer로 벤더 프리픽스 자동화
설치
npm install -D autoprefixer
입력
/* src/styles.css */
.container {
display: flex;
user-select: none;
}
출력
/* dist/styles.css */
.container {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
위 출력은 IE10·구형 Android 브라우저까지 포함하는 넓은 대상일 때의 결과입니다. 기본 browserslist(defaults)로 돌리면 display: flex에는 접두사가 붙지 않고, user-select에는 Safari용 -webkit-user-select만 붙습니다. 이처럼 Autoprefixer의 출력은 전적으로 browserslist 설정에 따라 달라집니다. .browserslistrc나 package.json의 browserslist 필드가 없으면 defaults 쿼리가 쓰이며, npx browserslist를 실행하면 현재 설정이 어떤 브라우저 목록으로 해석되는지 확인할 수 있습니다.
Autoprefixer를 쓰면서 가장 흔히 겪는 문제는 “접두사가 안 붙는다”입니다. 대부분은 버그가 아니라 대상 브라우저가 이미 표준 속성을 지원해서 붙일 필요가 없는 경우입니다. 반대로 경고 Browserslist: caniuse-lite is outdated가 뜬다면 접두사 판단에 쓰는 데이터가 오래된 것이므로 npx update-browserslist-db@latest로 갱신해야 합니다. 또 Autoprefixer는 접두사만 다루고 문법 자체를 변환하지는 않습니다. Grid의 gap을 IE용으로 바꾸거나 :is()를 풀어 주는 일은 하지 않으니, 그런 변환이 필요하면 postcss-preset-env를 써야 합니다.
CSS Nesting
설치
npm install -D postcss-nesting
postcss.config.js
module.exports = {
plugins: {
'postcss-nesting': {},
autoprefixer: {},
},
};
입력
.card {
padding: 1rem;
& .title {
font-size: 1.5rem;
}
&:hover {
background: #f0f0f0;
}
}
출력
.card {
padding: 1rem;
}
.card .title {
font-size: 1.5rem;
}
.card:hover {
background: #f0f0f0;
}
중첩 플러그인은 두 가지가 있다는 점을 알아 두어야 합니다. postcss-nesting은 W3C CSS Nesting 표준을 따르고, postcss-nested는 Sass의 중첩 규칙을 따릅니다. 차이가 가장 크게 드러나는 곳은 &-title처럼 부모 선택자에 문자열을 이어 붙이는 BEM 스타일입니다. Sass와 postcss-nested에서는 .card-title이 되지만, 표준 CSS Nesting에서 &는 문자열 치환이 아니라 선택자 참조라서 이 문법이 지원되지 않습니다. Sass에서 옮겨 온 스타일시트에 postcss-nesting을 적용했더니 BEM 클래스가 전부 사라졌다면 이 차이 때문입니다. 기존 코드를 그대로 살려야 하면 postcss-nested를, 언젠가 변환 없이 네이티브 중첩으로 넘어가려면 postcss-nesting을 고르는 것이 합리적입니다.
CSS Modules
설치
npm install -D postcss-modules
postcss.config.js
module.exports = {
plugins: {
'postcss-modules': {
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
};
generateScopedName은 변환된 클래스명의 형식입니다. Button.module.css의 .primary는 Button__primary___a1b2c 같은 이름이 되어 다른 파일의 .primary와 충돌하지 않습니다. 파일 이름과 원래 클래스명이 남으므로 개발자 도구에서 어느 컴포넌트의 스타일인지 알아보기 쉽고, 해시가 붙어 충돌은 막습니다. 프로덕션에서는 [hash:base64:5]만 쓰도록 바꿔 CSS 크기를 줄이는 경우도 많습니다.
주의할 점은 번들러를 쓰는 프로젝트에서는 이 플러그인을 직접 설정할 일이 거의 없다는 것입니다. Vite는 *.module.css 파일을 자동으로 CSS Modules로 처리하고, webpack의 css-loader는 modules 옵션으로 같은 기능을 제공합니다. 이미 번들러가 모듈화를 하는데 postcss-modules를 또 넣으면 클래스명이 두 번 변환되어 JS에서 가져온 이름과 실제 CSS가 맞지 않는 문제가 생깁니다. postcss-modules는 번들러 없이 postcss-cli로 빌드하면서 클래스명 매핑 JSON이 필요한 경우에 씁니다.
Webpack에서 쓰기
npm install -D postcss-loader
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
};
webpack의 use 배열은 오른쪽에서 왼쪽으로 실행됩니다. 따라서 postcss-loader가 가장 먼저 CSS를 변환하고, css-loader가 @import와 url()을 해석하며, 마지막으로 style-loader가 결과를 <style> 태그로 주입합니다. Sass를 함께 쓴다면 ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] 순서가 되어야 Sass 컴파일 결과에 PostCSS가 적용됩니다. postcss-loader는 프로젝트 루트의 postcss.config.js를 자동으로 찾으므로 로더 옵션에 플러그인을 중복으로 적을 필요는 없습니다. 프로덕션에서는 style-loader 대신 MiniCssExtractPlugin.loader로 CSS를 별도 파일로 추출하는 것이 일반적입니다.
Vite에서 쓰기
Vite는 PostCSS를 자동으로 지원합니다. 프로젝트 루트에 설정 파일만 있으면 별도 로더 설치 없이 모든 CSS에 적용됩니다.
// postcss.config.js
module.exports = {
plugins: {
'postcss-nesting': {},
autoprefixer: {},
},
};
Vite 템플릿은 대부분 "type": "module"이므로 위 파일은 postcss.config.cjs로 저장하거나 export default로 바꿔야 합니다. vite.config.js의 css.postcss 옵션에 플러그인을 직접 넣을 수도 있는데, 이 경우 설정 파일은 무시됩니다. 두 곳에 나눠 설정했다가 한쪽 플러그인이 적용되지 않는 일이 흔하니 한 곳으로 모으는 것이 좋습니다. 또 Vite는 개발 서버에서 이미 @import를 처리하기 때문에 Vite 프로젝트에서는 postcss-import가 보통 필요 없습니다.
Tailwind CSS v3와 함께 쓰기
아래는 Tailwind v3 기준 설정입니다. 지금 npm install -D tailwindcss를 실행하면 v4가 설치되므로 v3 방식을 쓰려면 버전을 명시해야 합니다.
npm install -D tailwindcss@3
npx tailwindcss init
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Tailwind v4에서 위 설정을 그대로 쓰면 It looks like you're trying to use tailwindcss directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package 에러가 납니다. v4에서는 npm install -D tailwindcss @tailwindcss/postcss로 설치하고 설정을 다음처럼 바꿉니다. tailwind.config.js와 init 명령도 없어졌고, 설정은 CSS 파일의 @import "tailwindcss";와 @theme 블록으로 옮겨졌습니다.
// postcss.config.mjs (Tailwind v4)
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
Vite 프로젝트라면 v4에서는 PostCSS 대신 @tailwindcss/vite 플러그인을 쓰는 것이 공식 권장이고 빌드도 더 빠릅니다. 기존 v3 프로젝트를 올릴 때는 npx @tailwindcss/upgrade가 설정 파일과 클래스명 변경을 상당 부분 자동으로 처리해 줍니다.
Tailwind를 포함한 전체 설정 예시
Tailwind v3와 함께 쓰는 풀 설정 예시입니다.
postcss.config.js
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
},
};
순서를 보면 앞에서 설명한 원칙이 그대로 적용되어 있습니다. postcss-import가 가장 먼저 파일을 합치고, tailwindcss/nesting이 중첩을 푼 뒤, Tailwind가 유틸리티를 생성하고, Autoprefixer가 접두사를 붙이고, 프로덕션에서만 cssnano가 최소화합니다. tailwindcss/nesting은 내부적으로 postcss-nested(옵션으로 postcss-nesting 선택 가능)를 감싼 플러그인이라, 여기에 postcss-nesting을 또 넣으면 중첩을 두 번 처리하게 됩니다. 중첩 플러그인은 하나만 두고, 반드시 tailwindcss보다 앞에 둬야 합니다. 순서가 반대면 Tailwind가 중첩된 규칙 안의 @apply를 제대로 처리하지 못합니다.
package.json
{
"scripts": {
"css": "postcss src/styles.css -o dist/styles.css",
"css:watch": "postcss src/styles.css -o dist/styles.css --watch"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
browserslist의 last 2 versions는 “모든 브라우저의 최근 두 버전”이라 사용자가 거의 없는 브라우저까지 포함됩니다. 특별한 요구가 없다면 defaults 하나로 시작하고, 실제 방문자 통계를 보고 조정하는 편이 불필요한 접두사를 줄이는 데 유리합니다. 이 설정은 Autoprefixer뿐 아니라 Babel의 @babel/preset-env 같은 다른 도구도 함께 읽으므로, 한 곳에서 관리하면 JS와 CSS의 지원 범위가 자연스럽게 맞춰집니다.
PostCSS 요약
PostCSS는 그 자체로는 아무것도 하지 않는 파서이고, 가치는 어떤 플러그인을 어떤 순서로 배치하느냐에서 나옵니다. 지금 새로 시작하는 프로젝트라면 Autoprefixer와 browserslist 설정은 거의 필수이고, 중첩은 대상 브라우저가 네이티브 지원 범위 안이라면 변환 없이 쓰는 선택지도 생겼습니다. CSS Modules와 @import는 번들러가 이미 처리하는 경우가 많으니 중복 설정을 피하고, Tailwind는 v3와 v4의 연동 방식이 완전히 다르다는 점을 먼저 확인하는 것이 시행착오를 줄이는 길입니다.
같이 보면 좋은 글
자주 묻는 질문 (FAQ)
Q. SCSS와 비교하면 어떤가요?
A. 대체 관계라기보다 역할이 다릅니다. SCSS는 변수·mixin·함수·반복문을 갖춘 작성용 언어이고, PostCSS는 표준 CSS에 필요한 변환만 골라 적용하는 후처리 도구입니다. 복잡한 디자인 토큰 계산이나 mixin이 많다면 SCSS가 편하고, 표준 CSS(커스텀 속성, 네이티브 중첩)만으로 충분하다면 PostCSS만으로 구성하는 편이 단순합니다.
Q. Tailwind CSS와 함께 사용하나요?
A. Tailwind v3는 PostCSS 플러그인으로 동작하므로 PostCSS 설정이 필요합니다. v4는 @tailwindcss/postcss 패키지로 PostCSS와 연동하거나, Vite에서는 @tailwindcss/vite 플러그인을 써서 PostCSS 없이도 쓸 수 있습니다.
Q. 성능은 어떤가요?
A. PostCSS 코어의 파싱은 빠른 편이고, 실제 빌드 시간은 플러그인 수와 각 플러그인의 비용에 좌우됩니다. 대형 프로젝트에서 CSS 빌드가 병목이라면 Rust 기반 Lightning CSS로 접두사·최소화를 대체하는 방법이 있지만, 그 경우 PostCSS 플러그인 생태계는 쓸 수 없습니다.
Q. PostCSS를 따로 설치하지 않았는데도 이미 쓰고 있을 수 있나요?
A. 그럴 수 있습니다. Next.js, Vite, Create React App 등 대부분의 프런트엔드 도구가 내부적으로 PostCSS를 사용하고 있어서, 별도로 설치하지 않았더라도 이미 쓰고 있는 경우가 많습니다.