PostCSS 아키텍처
해당 글은 PostCSS Architecture 문서 전체를 한글로 번역한 글입니다.
이 문서는 PostCSS의 아키텍처를 전반적으로 소개합니다. PostCSS 코어에 기여하고 싶거나 도구의 내부 동작을 더 깊이 이해하고 싶은 분들에게 도움이 될 수 있습니다.
목차
개요
이 절에서는 PostCSS의 바탕이 되는 생각을 설명합니다.
PostCSS의 개발을 더 깊이 살펴보기 전에, PostCSS가 어떤 도구인지부터 간단히 짚어보겠습니다.
PostCSS는 다음과 같은 특징이 있습니다.
-
Sass나Less같은 스타일 전처리기가 아닙니다.PostCSS는 자체 문법과 의미 체계를 정의하지 않으며, 그 자체가 하나의 언어도 아닙니다. CSS를 다루는 도구로서 앞서 언급한 도구들과 쉽게 함께 사용할 수 있습니다. 유효한 CSS라면 모두 PostCSS로 처리할 수 있습니다.
-
CSS 구문을 변환하는 도구입니다.
CSS와 비슷한 사용자 정의 문법을 만들고, 플러그인이 이를 해석하고 변환하도록 할 수 있습니다. PostCSS가 다루는 범위는 CSS 명세에만 한정되지 않습니다. CSS처럼 구문을 정의하는 방식을 활용합니다. 예를 들어 사용자 정의 at-rule을 만들 수 있으며, 이런 문법은 PostCSS를 기반으로 도구를 만들 때 유용하게 쓰입니다. PostCSS는 CSS를 다루는 다양한 도구를 만들 수 있도록 프레임워크 역할을 합니다.
-
CSS 생태계에서 중요한 역할을 합니다.
Autoprefixer,Stylelint,CSSnano를 비롯한 여러 유용한 도구가 PostCSS 생태계를 기반으로 만들어졌습니다. 이미 다른 도구를 통해 PostCSS를 사용하고 있을 가능성도 큽니다.node_modules를 한번 확인해보세요. 😃
처리 흐름
PostCSS의 전체 처리 흐름은 이 개요도에서 확인할 수 있습니다.
그림에는 파서(Parser)라는 구성 요소가 등장합니다. 뒤에서 자세히 설명하겠지만, 지금은 CSS와 비슷한 문법을 해석해 객체로 표현하는 역할이라고 이해하면 됩니다.
파서를 작성하는 방법은 몇 가지가 있습니다.
-
문자열을 AST로 변환하는 코드를 하나의 파일에 작성하기
널리 쓰이는 방식으로, Rework의 분석기도 이렇게 작성되었습니다. 다만 코드 규모가 커지면 읽기 어려워지고 상당히 느려집니다.
-
어휘 분석과 구문 분석 단계로 나누기 — 원본 문자열 → 토큰 → AST
PostCSS가 사용하는 방식이자 가장 널리 쓰이는 방식입니다. Babel의 파서인
@babel/parser와CSSTree를 비롯한 많은 파서도 이 방식을 사용합니다. 토큰화와 구문 분석을 분리하는 주된 이유는 성능을 높이고 복잡성을 나누어 다루기 위해서입니다.
두 번째 방식이 PostCSS에 더 적합한 이유를 살펴보겠습니다.
우선 문자열을 토큰으로 바꾸는 작업은 토큰을 구문 분석하는 작업보다 시간이 더 많이 걸립니다. 긴 원본 문자열을 한 글자씩 처리해야 하므로 성능 면에서 비용이 큰 작업이며, 한 번만 수행해야 합니다.
반면 토큰을 AST로 바꾸는 작업은 논리적으로 더 복잡합니다. 두 단계를 분리하면 토크나이저는 속도에 집중해 작성하고, 파서는 가독성에 집중해 작성할 수 있습니다. 속도를 높인 토크나이저는 때로 읽기 어려워질 수 있고, 읽기 쉽게 작성한 파서는 상대적으로 느릴 수 있습니다.
이처럼 단계를 둘로 나누면 성능과 코드 가독성을 개선할 수 있습니다.
이제 PostCSS의 처리 과정에서 주요 역할을 맡는 구성 요소를 더 자세히 살펴보겠습니다.
주요 구성 요소
토크나이저
토크나이저(Tokenizer)는 렉서(Lexer)라고도 부르며, 구문 분석에서 중요한 역할을 합니다.
CSS 문자열을 받아 토큰 목록을 반환합니다.
토큰은 at-rule, comment, word처럼 구문의 일부를 나타내는 간단한 자료 구조입니다. 오류를 더 자세히 설명할 수 있도록 위치 정보를 담기도 합니다.
다음 CSS를 예로 들어보겠습니다.
.className {
color: #fff;
}
원문에서 제시하는 이 CSS의 PostCSS 토큰은 다음과 같습니다.
;[
['word', '.className', 1, 1, 1, 10][('space', ' ')][('{', '{', 1, 12)][
('space', ' ')
][('word', 'color', 1, 14, 1, 18)][(':', ':', 1, 19)][('space', ' ')][
('word', '#FFF', 1, 21, 1, 23)
][(';', ';', 1, 24)][('space', ' ')][('}', '}', 1, 26)]
]
예제에서 각 토큰은 하나의 목록으로 표현되며, space 토큰에는 위치 정보가 없습니다.
word와 같은 토큰 하나를 좀 더 자세히 살펴보겠습니다. 앞서 설명한 대로 토큰은 목록으로 표현되며 다음 구조를 따릅니다.
const token = [
// represents token type
'word',
// represents matched word
'.className',
// This two numbers represent start position of token.
// It is optional value as we saw in the example above,
// tokens like `space` don't have such information.
// Here the first number is line number and the second one is corresponding column.
1,
1,
// Next two numbers also optional and represent end position for multichar tokens like this one. Numbers follow same rule as was described above
1,
10
]
토큰화를 구현하는 방법은 다양하지만 PostCSS가 추구하는 것은 성능과 단순함입니다. 토큰화는 연산이 복잡하고 전체 구문 분석 시간의 상당 부분인 약 90%를 차지합니다. PostCSS의 토크나이저 코드가 깔끔해 보이지 않는 이유도 속도에 맞춰 최적화했기 때문입니다. 클래스 같은 고수준 구조를 사용하면 토크나이저가 크게 느려질 수 있습니다.
PostCSS의 토크나이저는 파서에 nextToken() 메서드를 제공하는 스트리밍·체이닝 형태의 API를 사용합니다. 파서에는 명확한 인터페이스를 제공하면서, 토큰 목록 전체가 아닌 일부 토큰만 보관해 메모리 사용량을 줄입니다.
파서
파서는 입력 CSS의 구문 분석을 담당하는 핵심 구성 요소입니다. 파서는 추상 구문 트리(Abstract Syntax Tree, AST)를 생성하며, 플러그인은 이후 이 구조를 변환할 수 있습니다.
파서는 토크나이저와 함께 동작하며 원본 문자열 대신 토큰을 처리합니다. 원본 문자열을 직접 다루면 처리가 매우 비효율적이기 때문입니다.
주로 토크나이저가 제공하는 nextToken과 back 메서드로 토큰을 하나 또는 여러 개 얻고, 이를 이용해 AST를 구성하는 Node를 만듭니다.
PostCSS가 생성하는 노드에는 여러 종류가 있지만 모두 기본 Node 클래스를 상속합니다.
프로세서
프로세서(Processor)는 플러그인을 초기화하고 구문 변환을 실행하는 단순한 구성 요소입니다.
외부에 공개하는 API 메서드는 몇 개뿐이며, 자세한 설명은 API 문서에서 확인할 수 있습니다.
스트링기파이어
lib/stringify.js, lib/stringifier.js
스트링기파이어(Stringifier)는 수정된 AST를 일반 CSS 문자열로 변환하는 기본 클래스입니다. 전달받은 노드부터 AST를 순회하며 각 노드에 해당하는 메서드를 호출해 문자열 표현을 만듭니다.
가장 핵심적인 메서드는 Stringifier.stringify입니다. 시작 노드와 세미콜론 표시 여부를 인자로 받습니다. 자세한 내용은 stringifier.js에서 살펴볼 수 있습니다.
API 참고 문서
더 자세한 API 설명은 API 문서에서 확인할 수 있습니다.