Back to all articles
CSSSassFrontend
·

CSS 전처리기는 아직도 쓸 이유가 있을까?

Sass가 메운 CSS의 빈틈을 모던 CSS가 어디까지 따라잡았는지, 아직 남은 영역은 무엇인지 런타임/컴파일타임 관점으로 정리해요.

CSS 전처리기는 아직도 쓸 이유가 있을까?

CSS 전처리기는 아직도 쓸 이유가 있을까? thumbnail

CSS 전처리기는 CSS를 더 편하게 쓰기 위해 먼저 가공해 주는 도구예요. 개발자는 SCSS, Sass, Less, Stylus 같은 문법으로 작성하고, 빌드 단계에서 최종 CSS로 변환해요. 브라우저가 직접 이해하는 것은 결국 변환된 CSS예요.

전처리기는 원래 CSS의 빈틈을 메우려고 생겼어요. 그런데 그 빈틈이 지난 몇 년 사이에 꽤 많이 줄었어요. 그래서 "전처리기가 있으면 편하다"가 아니라 "어떤 기능이 필요한데, 그게 파이프라인의 어느 단계에서 해결되는가" 를 먼저 따져야 해요. 이 글은 그 기준을 정리해 보려는 거예요.

전처리기는 CSS를 더 잘 쓰기 위한 우회로예요

전처리기의 장점은 반복을 줄이고 구조를 나누기 쉬워진다는 데 있어요.

  • 변수로 색상과 간격을 재사용해요.
  • 믹스인으로 반복되는 선언 묶음을 줄여요.
  • 중첩으로 선택자 구조를 읽기 쉽게 써요.
  • 함수와 연산으로 토큰을 계산해요.
  • partial과 @use로 파일을 나눠 관리해요.
@use './tokens' as *;
$radius: 16px;
@mixin card($padding: 16px) {
  padding: $padding;
  border-radius: $radius;
  border: 1px solid var(--border);
}
.panel {
  @include card(20px);
  background: var(--surface);
  .title {
    font-weight: 600;
  }
}

이런 문법은 CSS 자체보다 표현력이 좋아 보여요. 특히 디자인 토큰이 많고, 비슷한 컴포넌트가 많은 코드베이스에서 효과가 커요. 참고로 예전에 파일을 나눌 때 쓰던 @import는 Dart Sass 1.80.0부터 deprecated예요. 이제는 위 예시처럼 @use / @forward 모듈 시스템을 쓰는 게 표준이에요.

핵심 축은 런타임과 컴파일타임이에요

전처리기와 모던 CSS를 비교할 때 가장 먼저 잡아야 할 축이 이거예요. Sass 변수는 컴파일타임에 값이 결정되고, CSS custom properties는 런타임에 살아있어요. 둘은 경쟁 관계가 아니라 서로 다른 도구예요.

// 컴파일타임: 값이 빌드 결과에 그대로 박혀요
$brand: #4f46e5;
.button { background: $brand; }   // → background: #4f46e5;

// 런타임: 브라우저가 값을 '살아있는 상태'로 들고 있어요
:root { --brand: #4f46e5; }
.button { background: var(--brand); }

// 그래서 이건 Sass 변수로는 표현할 수 없어요
[data-theme='dark'] { --brand: #a5b4fc; }  // 같은 .button이 테마에 따라 바뀜

Sass 변수는 컴파일이 끝나면 사라져요. 브라우저는 그게 변수였는지도 몰라요. 반면 custom properties는 cascade 안에 존재해서 미디어 쿼리, :hover, container query, JavaScript로 언제든 바꿀 수 있어요. 그래서 테마 전환·다크 모드·컴포넌트별 오버라이드 같은 런타임 관심사는 custom properties가 맞고, 빌드 시점에 고정하고 싶은 값(반복문 범위, 계산에 쓰는 상수, 절대 안 바뀌어야 하는 설정)은 Sass 변수가 맞아요.

색상: 전처리기의 가장 큰 명분이 옮겨간 자리

한때 Sass를 쓰는 가장 강력한 이유 중 하나가 lighten() / darken() / mix() 같은 색상 함수였어요. 그런데 이 전역 함수들도 Dart Sass 1.80.0부터 deprecated예요. 모듈 버전(color.scale(), color.adjust(), color.mix())으로 옮겨야 하고, 전역 함수는 Dart Sass 3.0.0에서 제거될 예정이에요.

이유가 흥미로워요. Sass의 색상 함수는 컴파일 시점에 색을 하나로 고정해 버려요. 런타임에 바뀌는 --brand 토큰에서 파생색을 만들어내지 못한다는 뜻이에요. 반대로 네이티브 CSS는 이걸 런타임에 해내요.

:root {
  --brand: oklch(55% 0.2 275);
}
.button {
  background: var(--brand);

  /* hover 색을 토큰에서 즉석 계산 — 파생 변수를 미리 만들 필요가 없어요 */
  &:hover { background: color-mix(in oklch, var(--brand), black 12%); }

  /* relative color syntax: 원본 채널을 직접 조정 */
  border: 1px solid oklch(from var(--brand) calc(l - 0.12) c h);
}

color-mix()는 2026년 기준 모든 주요 브라우저에서 안정적으로 쓸 수 있어요. relative color syntax(from 키워드)는 최신 에버그린 브라우저에서 동작하지만 Baseline "widely available" 도달은 2027년 예정이라, 지금은 progressive enhancement로 얹는 게 안전해요. 핵심은, 토큰 하나에서 hover·disabled·border 변형을 런타임에 파생시키는 건 전처리기로는 못 하던 일이라는 거예요. 이 영역은 CSS가 따라잡은 정도가 아니라 넘어섰어요.

중첩은 비슷해 보이지만 같지 않아요

네이티브 CSS 중첩은 이제 모든 주요 브라우저에서 쓸 수 있어요. 얼핏 Sass와 똑같아 보이지만, &의 의미가 달라서 그대로 옮기면 깨지는 지점이 있어요.

/* 네이티브 중첩 — 여기까지는 Sass와 거의 같아요 */
.card {
  padding: 1rem;
  & .title { font-weight: 600; }       /* .card .title (자손) */
  &:hover { transform: translateY(-2px); } /* .card:hover (동일 요소) */
}

문제는 BEM에서 자주 쓰던 &__title 패턴이에요.

/* Sass에서는 문자열 결합으로 .card__title 이 돼요 */
.card {
  &__title { font-weight: 600; }
}

네이티브 CSS에서 &는 **문자열이 아니라 '부모 선택자 참조'**예요. 그래서 &__title 같은 결합은 동작하지 않고, .card__title처럼 클래스를 직접 써야 해요. 또 하나, Sass 중첩은 컴파일 단계에서 평평하게 펼쳐지지만 네이티브 중첩은 cascade 안에서 살아있는 규칙이라 특이도 계산에도 영향을 줘요. 그래서 "Sass 코드를 그대로 붙여넣으면 되겠지"라고 접근하면 미묘하게 어긋나요.

구조와 우선순위도 이제 CSS가 직접 다뤄요

전처리기로 우회하던 또 다른 영역은 "구조와 우선순위 관리"였어요. 여기도 네이티브 기능이 상당히 채웠어요.

가장 큰 건 cascade layer(@layer)예요. 특이도 싸움을 레이어 순서로 정리해 줘요.

@layer reset, base, components, utilities;

@layer components {
  .btn { background: var(--brand); }          /* 특이도 (0,1,0) */
}
@layer utilities {
  .bg-transparent { background: transparent; } /* 특이도는 같지만 */
}
/* utilities가 뒤 레이어라 특이도와 무관하게 이깁니다. */

예전에는 유틸리티 클래스가 항상 이기게 하려고 !important나 선택자 깊이로 억지를 부렸는데, 이제는 레이어 순서로 깔끔하게 해결돼요. Tailwind 4도 내부적으로 @layer를 쓰고요.

여기에 더해서, 컴포넌트 스타일링에서 전처리기가 완전히 풀어주지 못했던 부분들도 네이티브로 들어왔어요. @scope는 스타일이 적용될 범위를 하한선까지 지정할 수 있고(@scope (.card) to (.card-content)), container query는 뷰포트가 아니라 부모 컨테이너 크기에 반응해요. :has()는 부모·형제 상태에 따라 스타일을 바꾸는 관계 선택자로 2026년 기준 사실상 완전히 지원돼요. BEM식 네이밍 규율로 감당하던 스코프 문제가 상당 부분 언어 차원으로 흡수된 셈이에요.

그래도 전처리기가 이기는 영역이 있어요

이쯤 되면 "전처리기 필요 없는 거 아닌가" 싶지만, 아직 네이티브 CSS가 못 하는 게 있어요. 바로 반복적인 규칙 생성이에요.

@use 'sass:map';

$space: (1: 4px, 2: 8px, 3: 16px, 4: 24px);

@each $key, $value in $space {
  .p-#{$key}   { padding: $value; }
  .gap-#{$key} { gap: $value; }
}
// → .p-1 { padding: 4px } .gap-1 { gap: 4px } ... 규칙 자체를 반복 생성

@each / @for / @if로 맵을 순회하며 유틸리티 클래스나 간격 스케일을 찍어내는 건 네이티브 CSS에 대응물이 없어요. CSS엔 반복문이 없거든요. 또 컴파일타임 검증이라는 장점도 있어요. Sass 변수 이름을 오타 내면 빌드가 에러로 잡아주지만, custom property는 조용히 fallback으로 넘어가서 문제를 늦게 발견하게 돼요.

한 가지 짚어둘 건, 이 격차마저 좁혀지는 중이라는 점이에요. 네이티브 CSS @function(@function --half(--value) { result: calc(var(--value) / 2); })과 if()가 등장하면서 Sass의 함수·조건 로직도 흡수되고 있어요. 다만 2026년 현재 @function은 Chromium 계열(Chrome/Edge 139+, 전역 사용률 약 67%)에서만 동작하는 실험적 기능이고, Firefox·Safari는 진행 중이라 fallback 없이는 못 써요. 게다가 @function값을 반환할 뿐 선택자나 규칙을 생성하지는 못하고, 규칙을 재사용하는 @mixin / @apply는 아직 어느 브라우저에도 없어요. 그래서 "데이터로부터 규칙을 대량 생성"하는 일은 당분간 전처리기 영역으로 남아요.

"전처리기냐 아니냐"는 잘못된 질문일 수 있어요

사실 이분법 자체가 프레임을 좁혀요. 실제 파이프라인에는 세 종류의 도구가 있어요.

  • 전처리기(Sass): 자체 언어로 작성하고 SCSS → CSS로 컴파일해요. 변수·믹스인·반복문 같은 표현력이 강점이에요.
  • 후처리/플러그인(PostCSS): 거의 표준 CSS를 입력으로 받아 변환해요. autoprefixer, 중첩 폴리필처럼 필요한 것만 골라 끼워요.
  • 트랜스파일러(Lightning CSS, esbuild 내장 등): 진짜 CSS를 파싱해서 최신 문법을 구형 브라우저용으로 낮추고 압축해요. Rust/Go 기반이라 아주 빨라요.

2026년의 흔한 선택은 이래요. 네이티브 CSS로 작성하고, 중첩·모던 색상을 구형 브라우저에서 돌리기 위해 Lightning CSS나 PostCSS로 다운레벨만 하는 것. Sass의 반복 생성이 꼭 필요한 게 아니라면 전처리기 없이도 충분한 경우가 많아요. 즉 선택지는 "Sass냐 아니냐"가 아니라 "어느 단계에서 무엇을 처리할까"예요.

언제 쓰고 언제 안 쓰나요

그래서 프로젝트 크기보다 필요의 성격으로 판단하는 게 맞아요.

  • 런타임 테마·토큰이 핵심이면 → custom properties면 충분해요. 전처리기가 없어도 돼요.
  • 데이터로부터 규칙을 대량으로 찍어내야 하면 → Sass 반복문이 여전히 가장 낫아요.
  • 그냥 중첩·모던 색상을 구형 브라우저에서 돌리고 싶은 거면 → 풀 전처리기 대신 Lightning CSS나 PostCSS면 돼요.
  • 이미 큰 Sass 코드베이스가 있으면 → 유지하되, Dart Sass 3.0 전에 @import@use, lighten()color.scale()로 마이그레이션하고(sass-migrator module), 런타임 관심사는 custom properties로 옮기는 걸 검토해요.

특히 다음 상황에서는 한 번 더 고민해야 해요.

  • CSS custom properties만으로도 충분한데 Sass 변수부터 넣으려는 경우
  • 중첩을 과하게 써서 선택자 깊이가 깊어지는 경우
  • 믹스인이 너무 많아져서 어떤 CSS가 최종으로 나오는지 추적하기 어려운 경우

편해 보인다는 이유만으로 넣으면 오히려 복잡해져요. 전처리기는 도구지 목적이 아니에요.

정리

  1. 전처리기는 CSS를 강력하게 만드는 게 아니라 작성 과정을 편하게 만드는 도구예요. 브라우저는 어차피 변환된 CSS만 봐요.
  2. 판단의 핵심 축은 런타임 vs 컴파일타임이에요. 테마·토큰 같은 런타임 관심사는 custom properties, 빌드 시점에 고정할 값은 Sass 변수예요.
  3. 색상 파생(color-mix(), relative color syntax)과 구조 관리(@layer, @scope, container query, :has())는 네이티브 CSS가 따라잡거나 넘어섰어요. Sass의 lighten()·@import은 Sass 안에서도 deprecated고요.
  4. 반면 반복적인 규칙 생성은 아직 전처리기 영역이에요. @function·if()가 이 격차를 좁히고 있지만 아직 실험적이라 fallback이 필요해요.
  5. "전처리기냐 아니냐"보다 파이프라인의 어느 단계에서 무엇을 처리할지로 접근하는 게 맞아요.

참고