Lightning CSS로 CSS 빌드 속도 극한까지 올리기
프론트엔드 프로젝트의 CSS 빌드 파이프라인을 생각하면 머리가 복잡해지곤 합니다. 벤더 프리픽스를 자동으로 붙여주는 Autoprefixer, 최신 CSS 문법을 변환해주는 postcss-preset-env, 결과물을 압축해주는 cssnano… 이 모든 걸 PostCSS 위에 플러그인 형태로 쌓아올리다 보면 설정 파일이 점점 길어지고 빌드 시간도 함께 느려지는 경험, 한 번쯤 해보셨을 겁니다.
이번 포스팅에서는 이 모든 기능을 하나의 도구에 통합하면서도 압도적인 성능을 자랑하는 Lightning CSS에 대해서 알아보겠습니다.
Lightning CSS란?
Lightning CSS는 Rust로 작성된 CSS 파서(parser), 변환기(transformer), 번들러(bundler), 압축기(minifier)입니다. 원래 Parcel CSS라는 이름으로 Parcel 번들러의 일부였는데, 독립적인 프로젝트로 분리되면서 지금의 이름을 갖게 되었습니다.
내부적으로는 Mozilla가 Firefox 브라우저를 위해 만든 cssparser와 selectors 크레이트를 사용합니다.
실제 브라우저가 CSS를 파싱하는 것과 동일한 방식으로 모든 규칙, 속성, 값을 완전히 파싱하기 때문에 정확도가 매우 높습니다.
기존에는 CSS를 처리하려면 여러 도구를 조합해야 했습니다. 벤더 프리픽싱에는 Autoprefixer, 최신 문법 변환에는 postcss-preset-env, 압축에는 cssnano, 번들링에는 별도의 플러그인이 필요했죠. Lightning CSS는 이 모든 기능을 단일 도구로 통합합니다.
얼마나 빠를까?
Lightning CSS가 주목받는 가장 큰 이유는 역시 성능입니다. 싱글 스레드만으로 초당 270만 줄 이상의 CSS를 처리할 수 있다고 하는데요, 실제 벤치마크 결과를 보면 그 차이가 확 와닿습니다.
Bootstrap 4 CSS를 압축하는 데 cssnano는 약 545ms가 걸리지만, Lightning CSS는 단 4ms면 끝납니다. 무려 130배 이상 빠른 셈이죠. Tailwind CSS처럼 더 큰 파일에서는 cssnano가 약 2.2초 걸리는 것에 비해 Lightning CSS는 43ms만에 완료합니다.
벤더 프리픽싱도 마찬가지입니다. Autoprefixer가 96ms 걸리는 작업을 Lightning CSS는 12ms 안에 끝내버립니다.
이런 수준의 성능 차이가 나는 이유는 크게 두 가지입니다. 우선 Rust로 작성되어 네이티브 코드로 컴파일되기 때문에 JavaScript 기반 도구보다 근본적으로 빠릅니다. 이 부분은 Babel을 대체하는 SWC와 비슷한 맥락이라고 볼 수 있는데요, SWC가 JavaScript 트랜스파일링 영역에서 그랬던 것처럼 Lightning CSS는 CSS 처리 영역에서 Rust의 성능 이점을 보여주고 있습니다.
또한 PostCSS는 CSS를 AST(추상 구문 트리)로 파싱한 뒤 각 플러그인이 순차적으로 트리를 순회하면서 변환합니다. 플러그인이 5개면 트리를 5번 돌아야 하는 거죠. 반면 Lightning CSS는 단일 패스로 파싱, 변환, 압축을 모두 수행하기 때문에 불필요한 중간 과정이 없습니다.
설치
Lightning CSS는 npm 패키지로 제공되며, Node.js API와 CLI 두 가지 방식으로 사용할 수 있습니다.
$ bun add -D lightningcss lightningcss-cli
$ npm add -D lightningcss lightningcss-cli
CLI만 필요하다면 lightningcss-cli만 설치해도 되고, 프로그래밍 방식으로 사용하려면 lightningcss 패키지를 설치하면 됩니다.
CLI로 사용하기
가장 간단하게 Lightning CSS를 체험하는 방법은 CLI입니다.
예를 들어 아래와 같이 최신 CSS 문법을 사용하는 스타일시트가 있다고 해볼게요.
.card {
color: oklch(70% 0.25 145);
&:hover {
color: oklch(80% 0.2 145);
}
& .title {
font-size: 1.2rem;
}
}
CSS Nesting과 oklch() 색상 함수를 사용하고 있는데, 아직 이 문법을 지원하지 않는 브라우저가 있을 수 있습니다.
Lightning CSS로 변환하면 지정한 브라우저 타겟에 맞게 호환 가능한 코드로 바꿔줍니다.
$ bunx lightningcss --targets '>= 0.25%' src/style.css
위 명령을 실행하면 CSS Nesting이 풀려서 일반 선택자로 변환되고, oklch() 색상도 구형 브라우저가 이해할 수 있는 형태로 폴백이 추가됩니다.
압축까지 함께 하고 싶다면 --minify 플래그를 추가합니다.
$ bunx lightningcss --minify --targets '>= 0.25%' src/style.css -o dist/style.css
여러 CSS 파일을 하나로 합치고 싶다면 --bundle 플래그를 사용합니다.
이때 CSS 표준의 @import 규칙을 따라 파일을 인라인으로 합쳐줍니다.
$ bunx lightningcss --bundle --minify --targets '>= 0.25%' src/style.css -o dist/style.css
Node.js API로 사용하기
빌드 스크립트나 커스텀 도구에서 프로그래밍 방식으로 Lightning CSS를 사용할 수도 있습니다.
import { transform, browserslistToTargets } from "lightningcss";
import browserslist from "browserslist";
import { readFileSync } from "fs";
const targets = browserslistToTargets(browserslist(">= 0.25%"));
const { code, map } = transform({
filename: "style.css",
code: readFileSync("src/style.css"),
minify: true,
sourceMap: true,
targets,
});
console.log(code.toString());
transform() 함수가 핵심인데, CSS 코드를 받아서 변환과 압축을 수행하고 결과 코드와 소스맵을 반환합니다.
browserslistToTargets() 함수를 사용하면 Browserslist 쿼리 문자열을 Lightning CSS가 이해하는 타겟 형식으로 변환할 수 있어서, 기존에 사용하던 브라우저 타겟 설정을 그대로 재활용할 수 있습니다.
이 기능을 사용하려면 browserslist 패키지를 별도로 설치해야 합니다.
$ bun add -D browserslist
여러 파일을 번들링하려면 transform() 대신 bundle() 함수를 사용합니다.
import { bundle, browserslistToTargets } from "lightningcss";
import browserslist from "browserslist";
const targets = browserslistToTargets(browserslist(">= 0.25%"));
const { code, map } = bundle({
filename: "src/style.css",
minify: true,
sourceMap: true,
targets,
});
bundle() 함수는 진입점 파일에서 @import로 참조된 다른 CSS 파일을 자동으로 찾아서 하나의 파일로 합쳐줍니다.
최신 CSS 문법 변환
Lightning CSS의 핵심 기능 중 하나는 최신 CSS 문법을 지원하지 않는 브라우저를 위해 자동으로 폴백 코드를 생성해주는 것입니다. PostCSS에서 postcss-preset-env 플러그인이 담당하던 역할을 Lightning CSS가 기본으로 제공하는 셈이죠.
몇 가지 대표적인 변환을 살펴보겠습니다.
CSS Nesting
CSS Nesting으로 선택자를 중첩해서 작성하면, Lightning CSS가 타겟 브라우저에 맞춰 풀어줍니다.
/* 입력 */
.card {
background: white;
& .title {
font-weight: bold;
}
&:hover {
background: #f0f0f0;
}
}
/* 출력 (구형 브라우저 타겟 시) */
.card {
background: white;
}
.card .title {
font-weight: bold;
}
.card:hover {
background: #f0f0f0;
}
색상 함수
oklch(), oklab(), lab(), lch() 같은 최신 색상 함수와 color-mix(), 상대 색상 문법도 변환합니다.
/* 입력 */
.button {
background: oklch(60% 0.25 30);
border-color: color-mix(in oklch, var(--brand), transparent 30%);
}
타겟 브라우저가 이 함수들을 지원하지 않으면, 가장 가까운 RGB나 HSL 값으로 자동 변환됩니다.
미디어 쿼리 범위
미디어 쿼리도 최신 범위 문법을 사용할 수 있습니다.
/* 입력 */
@media (width >= 768px) {
.container {
max-width: 1200px;
}
}
@media (400px <= width <= 768px) {
.container {
padding: 1rem;
}
}
/* 출력 */
@media (min-width: 768px) {
.container {
max-width: 1200px;
}
}
@media (min-width: 400px) and (max-width: 768px) {
.container {
padding: 1rem;
}
}
이 밖에도 논리적 속성(logical properties), light-dark() 함수, 커스텀 미디어 쿼리 등 다양한 최신 CSS 기능의 폴백을 지원합니다.
벤더 프리픽싱
Lightning CSS는 Autoprefixer를 내장하고 있다고 봐도 무방합니다. 브라우저 타겟을 지정하면 필요한 벤더 프리픽스를 자동으로 추가해줍니다.
/* 입력 */
.box {
user-select: none;
}
/* 출력 (구형 브라우저 포함 시) */
.box {
-webkit-user-select: none;
user-select: none;
}
반대로 더 이상 필요하지 않은 프리픽스는 자동으로 제거해주기도 합니다.
기존 코드에 불필요하게 남아있던 -webkit-, -moz- 프리픽스를 타겟에 맞춰 정리해주는 거죠.
Vite에서 사용하기
Vite를 사용하고 있다면 별도의 플러그인 설치 없이 바로 Lightning CSS를 활성화할 수 있습니다. Vite 4.4부터 Lightning CSS를 실험적으로 지원하기 시작했고, 이후 버전에서 안정화되었습니다.
import { defineConfig } from "vite";
import browserslist from "browserslist";
import { browserslistToTargets } from "lightningcss";
export default defineConfig({
css: {
transformer: "lightningcss",
lightningcss: {
targets: browserslistToTargets(browserslist(">= 0.25%")),
},
},
build: {
cssMinify: "lightningcss",
},
});
css.transformer를 "lightningcss"로 설정하면 개발 서버에서도 Lightning CSS가 CSS 변환을 담당하고, build.cssMinify를 "lightningcss"로 설정하면 프로덕션 빌드 시 압축도 Lightning CSS가 처리합니다.
이렇게 설정하면 기존에 PostCSS로 처리하던 벤더 프리픽싱, 문법 변환, 압축을 모두 Lightning CSS가 대신하게 되므로, postcss.config.js 파일과 관련 PostCSS 플러그인들을 제거할 수 있습니다.
CSS Modules
Lightning CSS는 CSS Modules도 기본으로 지원합니다. CSS 클래스명을 자동으로 해시화하여 스코프를 격리해주는 기능인데요, 별도의 로더 없이도 사용할 수 있습니다.
import { bundle } from "lightningcss";
const { code, map, exports } = bundle({
filename: "style.module.css",
cssModules: true,
minify: true,
});
console.log(exports);
// { card: { name: "card_abc123", ... }, title: { name: "title_def456", ... } }
cssModules: true 옵션을 켜면 클래스명이 고유한 해시로 변환되고, exports 객체를 통해 원래 클래스명과 변환된 클래스명의 매핑을 얻을 수 있습니다.
커스텀 변환
기본 제공 기능으로 부족할 때는 Visitor API를 사용해서 커스텀 변환을 구현할 수 있습니다.
import { transform } from "lightningcss";
const { code } = transform({
filename: "style.css",
code: Buffer.from(`
.banner {
color: theme(primary);
}
`),
visitor: {
Function: {
theme(fn) {
const name = fn.arguments[0].value;
const colors = {
primary: { type: "rgb", r: 59, g: 130, b: 246, alpha: 1 },
danger: { type: "rgb", r: 239, g: 68, b: 68, alpha: 1 },
};
return colors[name];
},
},
},
});
위 예제는 theme(primary) 같은 커스텀 함수를 만나면 미리 정의된 색상 값으로 치환하는 변환입니다.
Visitor API를 활용하면 PostCSS 플러그인을 작성하듯 자신만의 CSS 변환 로직을 만들 수 있습니다.
PostCSS에서 마이그레이션
기존에 PostCSS를 사용하고 있던 프로젝트라면 어떻게 Lightning CSS로 전환할 수 있을까요?
우선 자신의 프로젝트에서 사용하는 PostCSS 플러그인 목록을 확인합니다.
postcss.config.js 파일을 열어보면 대략 이런 모습일 겁니다.
module.exports = {
plugins: [
require("postcss-import"),
require("postcss-preset-env")({ stage: 2 }),
require("autoprefixer"),
require("cssnano")({ preset: "default" }),
],
};
이 중에서 Lightning CSS가 대체할 수 있는 플러그인은 다음과 같습니다.
postcss-import→--bundle옵션 또는bundle()함수postcss-preset-env→ 자동 문법 변환 (타겟 기반)autoprefixer→ 자동 벤더 프리픽싱 (타겟 기반)cssnano→--minify옵션
대부분의 일반적인 PostCSS 플러그인은 Lightning CSS의 기본 기능으로 대체할 수 있습니다. 다만 Tailwind CSS 같은 특수한 PostCSS 플러그인은 Lightning CSS로 대체할 수 없으므로, 이런 경우에는 PostCSS와 Lightning CSS를 함께 사용하거나 해당 도구의 자체 빌드 시스템을 활용해야 합니다.
다른 빌드 도구와의 통합
Lightning CSS는 Vite 외에도 다양한 빌드 도구와 통합할 수 있습니다.
우선 Parcel 번들러에는 Lightning CSS가 기본으로 내장되어 있어서 별도의 설정이 필요 없습니다. Parcel이 원래 Lightning CSS의 고향이었으니 당연한 일이겠죠.
Rspack이나 webpack을 사용하는 프로젝트에서는 lightningcss-loader를 통해 Lightning CSS를 적용할 수 있습니다.
Next.js에서도 Lightning CSS 도입을 검토하고 있어서 앞으로 더 많은 프레임워크에서 기본 CSS 도구로 채택될 가능성이 높습니다.
마치며
Lightning CSS는 CSS 빌드 도구의 세대교체를 보여주는 프로젝트입니다. PostCSS 기반으로 여러 플러그인을 조합하던 시대에서 Rust로 작성된 단일 도구가 모든 걸 처리하는 시대로 넘어가고 있는 셈이죠.
JavaScript 영역에서 Babel이 SWC로 대체되고, ESLint와 Prettier가 Oxc로 대체되고, Webpack이 Vite로 대체되는 흐름과 맥을 같이 합니다. 성능이 중요한 빌드 도구를 Rust나 Go 같은 저수준 언어로 다시 작성하는 것이 이제는 거스를 수 없는 흐름이 된 것 같습니다.
새 프로젝트를 시작한다면 PostCSS 플러그인 여러 개를 조합하는 것보다 Lightning CSS를 먼저 고려해보시는 것을 추천드립니다. 기존 프로젝트도 Vite를 사용하고 있다면 설정 몇 줄로 전환할 수 있으니 한번 시도해보시면 좋겠습니다.
Lightning CSS에 대해서 더 자세히 알고 싶으시다면 아래 공식 문서를 참고하세요.
This work is licensed under
CC BY 4.0