Skip to content

PostCSS 아키텍처

Frontend / CSS
  • css
  • postcss
  • build-tools

해당 글은 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의 처리 과정에서 주요 역할을 맡는 구성 요소를 더 자세히 살펴보겠습니다.

주요 구성 요소

토크나이저

lib/tokenize.js

토크나이저(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를 사용합니다. 파서에는 명확한 인터페이스를 제공하면서, 토큰 목록 전체가 아닌 일부 토큰만 보관해 메모리 사용량을 줄입니다.

파서

lib/parse.js, lib/parser.js

파서는 입력 CSS의 구문 분석을 담당하는 핵심 구성 요소입니다. 파서는 추상 구문 트리(Abstract Syntax Tree, AST)를 생성하며, 플러그인은 이후 이 구조를 변환할 수 있습니다.

파서는 토크나이저와 함께 동작하며 원본 문자열 대신 토큰을 처리합니다. 원본 문자열을 직접 다루면 처리가 매우 비효율적이기 때문입니다.

주로 토크나이저가 제공하는 nextToken과 back 메서드로 토큰을 하나 또는 여러 개 얻고, 이를 이용해 AST를 구성하는 Node를 만듭니다.

PostCSS가 생성하는 노드에는 여러 종류가 있지만 모두 기본 Node 클래스를 상속합니다.

프로세서

lib/processor.js

프로세서(Processor)는 플러그인을 초기화하고 구문 변환을 실행하는 단순한 구성 요소입니다.

외부에 공개하는 API 메서드는 몇 개뿐이며, 자세한 설명은 API 문서에서 확인할 수 있습니다.

스트링기파이어

lib/stringify.js, lib/stringifier.js

스트링기파이어(Stringifier)는 수정된 AST를 일반 CSS 문자열로 변환하는 기본 클래스입니다. 전달받은 노드부터 AST를 순회하며 각 노드에 해당하는 메서드를 호출해 문자열 표현을 만듭니다.

가장 핵심적인 메서드는 Stringifier.stringify입니다. 시작 노드와 세미콜론 표시 여부를 인자로 받습니다. 자세한 내용은 stringifier.js에서 살펴볼 수 있습니다.

API 참고 문서

더 자세한 API 설명은 API 문서에서 확인할 수 있습니다.