PostCSS Workflow 살펴보기
CSS를 다루다 보면 브라우저 호환성을 위해 접두사를 추가하거나 파일 크기를 줄이기 위해 불필요한 공백을 제거해야 할 때가 있습니다. 이런 작업을 모든 파일에 직접 적용해야 한다면 매우 번거로울 것 입니다.
PostCSS를 사용하면 플러그인을 통해 이러한 작업을 자동화할 수 있습니다. 이번 글에서는 PostCSS의 역할과 CSS를 처리하는 과정을 살펴보고 간단한 플러그인을 직접 만들어보겠습니다.
PostCSS란
PostCSS란 JavaScript로 CSS를 분석하고 변환할 수 있게 해주는 도구입니다. 플러그인을 통해 필요한 CSS 처리 작업을 수행할 수 있습니다.
여기서 플러그인이란 CSS에 적용할 작업을 정의한 코드입니다. 예를 들어 Autoprefixer 플러그인을 사용하면 브라우저 호환성에 맞춰 접두사를 처리할 수 있습니다. 상황에 맞춰 이미 만들어져있는 다양한 플러그인들을 사용하거나 필요에 따라 직접 만들어 사용할 수도 있습니다.
PostCSS 처리 흐름

PostCSS의 처리 과정은 크게 세 단계로 나누어 볼 수 있습니다. 먼저 Parser를 이용해 CSS를 파싱하여 AST(Abstract Syntax Tree, 추상 구문 트리)를 만들고 플러그인을 통해 원하는 작업을 수행합니다. 이후 Stringifier를 통해 수정된 AST를 다시 CSS 문자열로 변환하여 출력합니다.
AST는 Abstract Syntax Tree의 약자로, 추상 구문 트리라고 부릅니다. 코드의 구성 요소를 노드로 표현하고 요소 사이의 관계를 트리 구조로 나타냅니다.
여기서 ‘추상’은 소스 코드의 모든 문자를 그대로 나열하는 대신 문법적인 구조를 중심으로 표현한다는 뜻입니다. 예를 들어 중괄호를 별도 노드로 두는 대신 선언이 규칙 안에 포함된다는 관계로 나타냅니다.
실제 예제를 함께 살펴보며 각 단계에서 어떤 일이 일어나는지 보다 자세히 살펴보겠습니다.
CSS를 AST로 표현하기 (Parser)
다음 CSS에서 color의 값을 변경하고 싶다고 생각해보겠습니다.
.card {
color: black;
padding: 16px;
}
우리는 이 코드를 통해 .card는 선택자이고 중괄호 안에 두 개의 선언이 있다는 것을 바로 이해할 수 있습니다. 하지만 컴퓨터의 입장에서는 그저 CSS 파일의 문자열일 뿐 입니다. 따라서 문자열에서 특정 선언을 다루려면 어디에서 규칙이 시작되고 어떤 부분이 속성과 값인지 구분하는 작업이 필요합니다.
앞의 그림에서 Parser가 이 역할을 맡습니다. PostCSS의 Parser는 내부적으로 토크나이저(Tokenizer) 와 파서(Parser) 로 나눠 구현 되어 있습니다. 토크나이저가 CSS 문자열을 토큰으로 구분하면 파서는 이 토큰을 해석하여 AST를 만듭니다.
CSS 문자열 → 토크나이저 → 토큰 → 파서 → AST
먼저 토크나이저는 문자열을 읽으며 단어, 공백, 기호 등을 구분합니다. 이렇게 구분한 단위를 토큰이라고 합니다. 예제의 color: black; 부분을 토큰의 종류와 내용으로 표시하면 다음과 같습니다.
word "color"
: ":"
space " "
word "black"
; ";"
이 단계에서 color와 black은 모두 word 토큰입니다. 아직 color가 속성 이름이고 black이 그 값이라는 선언 구조가 만들어진 것은 아닙니다. 토크나이저는 문자열을 구분하는 역할만 하고 각 토큰이 문맥에서 어떤 의미를 갖는지는 파서를 이용합니다.
파서는 토큰의 순서와 문맥을 해석해 규칙과 선언을 구성하고 AST를 만듭니다. 앞의 예제를 파싱하면 어떤 구조가 만들어지는지 AST Explorer를 통해 살펴보겠습니다.
AST Explorer를 이용하면 원하는 파서를 선택해 생성된 AST를 확인할 수 있습니다.
앞의 CSS를 postcss 파서로 읽고 규칙과 선언 노드를 펼치면 다음과 같이 표시됩니다.

오른쪽 AST에서 root 아래의 rule 노드는 .card 규칙을 나타냅니다. 선택자는 selector에 저장되고, nodes에는 두 개의 선언 노드가 들어 있습니다.
선언 노드는 decl로 표시됩니다. color: black 선언을 보면 prop에는 color가, value에는 black이 저장되어 있습니다. 이제 JavaScript에서는 문자열에서 color: black을 직접 찾아낼 필요 없이 선언 노드의 prop과 value로 속성과 값을 읽을 수 있습니다.
CSS 문자열을 토큰으로 나누려면 많은 문자를 하나씩 읽어야 합니다. 공식 아키텍처 문서에서는 이 작업이 토큰을 AST로 만드는 작업보다 더 많은 실행 시간이 필요하다고 설명합니다.
반면 파서는 토큰의 순서와 문맥을 해석해야 합니다. 앞의 예제에서 color와 black이 하나의 선언을 이루고, 이 선언이 .card 규칙 안에 속한다는 관계를 구성하는 것입니다. 이런 문법을 처리하는 코드는 논리적으로 복잡하므로 이해하기 쉽게 작성하는 것이 중요합니다.
PostCSS는 두 작업을 분리해 토크나이저는 실행 속도를 중심으로 최적화하고 파서는 코드를 읽기 쉽게 구성했습니다.
또한 실행할 때는 내부적으로 파서가 토크나이저의 nextToken()을 호출해 다음 토큰을 받아가며 AST를 만듭니다. 필요한 토큰을 스트리밍 형태로 전달받으므로 전체 토큰 목록을 한꺼번에 보관할 필요가 없어 메모리를 보다 효율적으로 사용할 수 있습니다.
플러그인으로 AST 수정하기 (Plugin)
앞에서 Parser를 통해 CSS 문자열을 JavaScript로 다룰 수 있는 형태로 바꾸는 과정을 살펴봤습니다. 이제 간단한 플러그인을 통해 AST를 수정하고 실제 CSS 문자열을 변경하는 과정을 살펴보겠습니다.
이번 예제에서는 앞의 예시에서 color: black을 color: #222로 바꾸는 플러그인을 작성해보겠습니다.
const replaceBlack = {
postcssPlugin: 'replace-black',
Declaration(decl) {
if (decl.prop === 'color' && decl.value === 'black') {
decl.value = '#222';
}
},
};
export default replaceBlack;
PostCSS 플러그인은 이름과 노드를 처리할 함수를 가진 JavaScript 객체로 작성할 수 있습니다. 위 코드에서 postcssPlugin은 플러그인의 이름을 지정하는 속성이고 Declaration은 PostCSS가 선언 노드를 처리할 때 호출하는 함수입니다.
Declaration의 매개변수에는 처리할 선언 노드가 전달됩니다. 여기서는 이 노드를 decl이라는 이름으로 받아 속성 이름인 prop과 값인 value를 확인합니다. prop이 color이고 value가 black이면 decl.value에 #222를 넣어 선언의 값을 바꿉니다.
여기에서는 PostCSS의 동작을 살펴보기 위해 간단한 플러그인 예제를 사용합니다. 직접 플러그인을 작성하려면 다음의 공식 문서를 참고해주세요.
이제 AST Explorer를 통해 실행 결과를 살펴보겠습니다. Transform 메뉴에서 postcss를 선택하면 왼쪽 아래에 플러그인 코드를 입력할 수 있습니다.

왼쪽 아래의 플러그인을 실행한 결과는 오른쪽 아래에서 확인할 수 있습니다. color의 값이 black에서 #222로 바뀐 것을 확인할 수 있습니다. 이처럼 플러그인은 CSS의 값을 수정하거나 필요한 선언을 추가할수도 있고 기존 노드를 삭제할 수도 있습니다.
수정한 AST를 CSS로 출력하기 (Stringifier)
앞의 단계에서는 플러그인을 통해 선언 노드의 value를 직접 변경했습니다. 이제 이렇게 변경한 AST를 다시 CSS 문자열로 만드는 과정이 필요합니다. 그리고 이 단계를 Stringifier 라고 부릅니다.
Stringifier는 AST의 노드를 순회하며 각 노드의 정보를 CSS 문법에 맞게 출력합니다. 앞의 예제에서 규칙 노드의 selector에서 .card를 읽고 그 안의 선언 노드에서 prop과 value를 읽습니다. 여기에 중괄호와 콜론, 세미콜론 등을 더하면 다음과 같은 CSS 문자열을 만들 수 있습니다.
.card {
color: #222;
padding: 16px;
}
이렇게 얻은 CSS 문자열은 호출한 쪽에서 파일로 저장하거나 빌드 결과에 사용할 수 있습니다.
PostCSS는 파싱 과정에서 공백과 줄바꿈 같은 서식 정보를 노드의 raws에 함께 보관합니다. Stringifier는 CSS를 출력할 때 이 정보를 활용합니다. 앞의 예제에서 선언의 값만 변경했으므로 기존 들여쓰기와 줄바꿈은 그대로 유지됩니다.
플러그인 직접 실행해보기
앞에서 AST Explorer를 이용하여 플러그인을 적용한 결과를 살펴봤습니다. 이번에는 실제 코드를 통해 플러그인을 작성하고 CSS를 직접 변환해보겠습니다.
실행 준비
먼저 빈 디렉터리에서 PostCSS를 설치합니다.
예제는 Node.js 22.22.0과 PostCSS 8.5.28 환경에서 실행했습니다.
npm init -y
npm install --save-dev postcss@8.5.28
설치가 끝나면 앞에서 작성한 플러그인 코드를 replace-black.mjs에 저장합니다.
플러그인 실행
같은 디렉터리에 transform.mjs를 만들고 다음 코드를 작성합니다. 앞에서 만든 플러그인과 변환할 CSS를 PostCSS에 전달한 뒤 처리 결과를 출력하는 코드입니다.
// transform.mjs
import postcss from 'postcss';
import replaceBlack from './replace-black.mjs';
const css = `.card {
color: black;
padding: 16px;
}`;
const result = await postcss([replaceBlack]).process(css, {
from: 'card.css',
});
console.log(result.css);
postcss([replaceBlack])으로 사용할 플러그인을 등록하고 .process()에 처리할 CSS 문자열을 전달합니다. await로 처리가 끝난 결과를 받은 뒤 result.css에서 출력된 CSS를 읽습니다.
파일을 저장한 뒤 다음의 명령어를 실행합니다.
node transform.mjs
실행하면 다음 CSS가 출력됩니다.
.card {
color: #222;
padding: 16px;
}
실행 결과를 보면 AST Explorer에서 확인했던 것처럼 color의 값이 #222로 바뀐 것을 확인할 수 있습니다.
마치며
이번 글에서는 PostCSS의 역할과 CSS를 변환하는 내부 과정을 살펴보았습니다.
PostCSS가 CSS를 JavaScript로 다룰 수 있는 구조로 바꿔주고 다시 문자열로 변환하는 과정을 맡아준 덕분에 플러그인을 통해 보다 쉽게 CSS 변환을 다룰 수 있다는 것을 알게되었습니다.