CSS 전처리기 (Sass) 입문
SCSS의 변수·중첩·믹스인·extend·모듈(@use/@forward)을 사용하고 컴파일 결과가 표준 CSS가 되는 흐름을 익힙니다.
우리는 지금까지 순수 CSS(Vanilla CSS)만으로 웹 페이지를 스타일링하고, 고급 기법으로 코드 효율을 높이는 방법을 학습했습니다.
CSS custom property와 calc(), native CSS nesting은 표준 CSS의 표현력을 크게 높였습니다.
그래도 규모가 커지면 반복되는 선언 묶음, 빌드 시점 계산, 파일 간 재사용 규칙을 일관되게 관리해야 할 때가 있습니다.
이때 사용할 수 있는 도구가 CSS 전처리기(CSS Preprocessor)입니다.
Sass는 CSS와 호환되는 문법에 변수, 믹스인, 함수, 모듈 같은 빌드 시점 기능을 더한 스타일시트 언어입니다.
개발자가 작성한 SCSS는 Dart Sass 컴파일러를 거쳐 브라우저가 읽는 표준 CSS가 됩니다. 브라우저는 SCSS 자체가 아니라 컴파일 결과만 해석합니다.
가장 널리 사용되는 CSS 전처리기에는 Sass(Syntactically Awesome Style Sheets), Less, Stylus 등이 있습니다.
이 장에서는 Sass를 중심으로, 표준 CSS와 가장 유사한 문법을 가진 SCSS(Sassy CSS)를 사용해 전처리기의 핵심 기능을 학습합니다.
-
.scss소스변수, mixin,
@use같은 빌드 시점 문법을 작성한다. -
Dart Sass
compile또는--watch로 SCSS를 변환한다. -
.css산출물브라우저가 이해하는 표준 선택자와 값만 남는다.
-
브라우저
CSS를 불러와 cascade를 계산하고 화면을 렌더링한다.
sass와 --watch의 책임은 SCSS를 CSS로 변환하는 데까지다. 파일 bundling과 asset URL 처리는 별도 build tool의 책임이다.전처리기란 무엇이며 왜 필요한가?
CSS 전처리기(CSS Preprocessor): CSS를 확장하는 스크립트 언어입니다.
개발자가 전처리기 문법으로 코드를 작성하면, 전처리기가 이를 일반적인 CSS 코드로 변환(컴파일)해 줍니다.
브라우저는 이 변환된 CSS 파일을 해석하여 웹 페이지를 렌더링합니다.
필요성변수(Variables): 반복되는 값을 이름으로 관리하고 빌드 시점 계산이나 모듈 설정에 사용할 수 있습니다. Sass 변수는 컴파일 결과에서 실제 값으로 치환됩니다.
중첩(Nesting): 관련 선택자를 가까이 작성하고 부모 선택자 &를 활용할 수 있습니다. 생성되는 선택자가 길어지지 않도록 얕게 유지합니다.
믹스인(Mixins): 반복적으로 사용되는 CSS 속성 묶음을 함수처럼 정의하여 필요할 때마다 호출하여 사용할 수 있습니다.
확장(Extend): 같은 의미를 공유하는 선택자를 하나의 규칙으로 묶어 선언 중복을 줄일 수 있습니다.
부분 파일(Partials)과 모듈(Modules): 재사용 전용 SCSS 파일을 partial로 나누고, @use로 namespace를 가진 모듈을 불러옵니다. 여러 모듈의 공개 API를 하나의 진입점으로 묶을 때는 @forward를 사용합니다.
연산과 함수(Operations and Functions): sass:math, sass:color 같은 built-in module을 불러와 숫자와 색상을 명시적으로 계산할 수 있습니다.
Sass (SCSS 문법) 설치 및 사용 준비
현재 Sass의 주 구현체는 Dart Sass입니다.
이 교재에서는 Node.js 프로젝트에 Sass 팀의 sass npm 패키지를 설치해 사용합니다. 이 패키지는 Dart Sass 기반 JavaScript 구현과 명령줄 인터페이스를 함께 제공합니다.
Ruby Sass, Node Sass, LibSass는 유지보수가 종료되었으므로 신규 프로젝트에서 사용하지 않습니다.
설치 과정 (Node.js 및 npm 필요)Node.js 설치 확인: 터미널/명령 프롬프트에서 node -v와 npm -v를 입력하여 버전이 표시되는지 확인합니다.
(설치되어 있지 않다면 Node.js 공식 홈페이지에서 설치)
mkdir my-sass-project
cd my-sass-projectnpm init -y(package.json 파일이 생성됩니다.)
npm install --save-dev sass # 프로젝트 내 설치 (프로젝트에서만 사용)
npx sass --version프로젝트 로컬 의존성으로 설치하면 이 프로젝트가 사용할 Sass 버전이 package.json과 lockfile에 기록됩니다. 이후 예제에서는 로컬 실행 파일을 npx sass로 호출합니다.
SCSS 파일(*.scss)을 표준 CSS 파일(*.css)로 컴파일하는 기본적인 명령입니다.
npx sass input.scss output.css자동 컴파일 (Watch Mode): 파일을 저장할 때마다 자동으로 컴파일되도록 watch 모드를 사용하는 것이 개발에 훨씬 편리합니다.
npx sass --watch input.scss:output.css
# 예시: npx sass --watch scss/main.scss:css/style.css이 명령을 실행하면 터미널은 계속 대기 상태가 되고, input.scss 파일에 변경이 감지될 때마다 output.css를 자동으로 업데이트합니다.
Sass (SCSS) 핵심 기능
| 기능 | 선택할 때 | 확인할 점 |
|---|---|---|
| 변수 | 반복되는 빌드 시점 값, 계산, 모듈 설정에 이름을 붙일 때 | $variable은 값으로 치환되어 사라진다. 런타임 테마에는 CSS custom property를 쓴다. |
| 중첩 | 관련 선택자와 & 상태를 가까이 둘 때 |
얕게 유지하고 생성된 선택자의 길이와 specificity를 확인한다. |
@mixin |
인자를 받는 의미 있는 선언 묶음이 반복될 때 | @include 위치마다 선언이 출력되므로 한 값의 별칭으로 남용하지 않는다. |
@extend |
같은 의미의 선택자들이 하나의 규칙을 공유할 때 | 선택자 목록이 확장된다. placeholder는 확장될 때만 출력된다. |
| partial + module | 파일을 나누고 소비자에게 공개할 API를 정할 때 | @use namespace와 @forward 진입점을 사용한다. |
| 계산 · 함수 | 숫자나 색상을 빌드 시점에 변환할 때 | math.div(), color.adjust(…, $space: hsl)처럼 module API를 명시한다. |
- 변수
- 선택 반복되는 빌드 시점 값, 계산, 모듈 설정
- 확인
$variable은 사라진다. 런타임 테마는 CSS custom property로 둔다. - 중첩
- 선택 관련 선택자와
&상태를 가까이 둘 때 - 확인 얕게 유지하고 생성된 선택자와 specificity를 본다.
@mixin- 선택 인자를 받는 의미 있는 선언 묶음이 반복될 때
- 확인
@include위치마다 선언이 출력된다. @extend- 선택 같은 의미의 선택자가 규칙을 공유할 때
- 확인 선택자 목록이 확장되고 placeholder는 사용될 때만 출력된다.
- partial + module
- 선택 파일을 나누고 공개 API를 정할 때
- 확인
@usenamespace와@forward진입점을 쓴다. - 계산 · 함수
- 선택 숫자와 색상을 빌드 시점에 변환할 때
- 확인
math.div()와sass:colormodule API를 쓴다.
이제 SCSS의 주요 기능들을 살펴봅시다.
변수 (Variables)
Sass 변수는 $foo: value; 형식으로 선언하며 컴파일 시점에 평가되어 출력 CSS에서는 사라집니다.
반면 CSS custom property는 출력 CSS에 남아 cascade와 상속에 참여하고 요소별로 다른 값을 가질 수 있습니다. 빌드 시점 상수·계산·모듈 설정에는 Sass 변수를, 런타임 테마나 요소별 값에는 CSS custom property를 사용합니다.
/* style.scss */
$primary-color: #3498db;
$text-color: #333;
$font-stack: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
$base-padding: 15px;
/* style.scss */
body {
font-family: $font-stack;
color: $text-color;
background-color: #f0f2f5;
}
.button {
background-color: $primary-color;
padding: $base-padding;
color: white;
border: none;
border-radius: 5px;
}중첩 (Nesting)
Sass 중첩은 관련 선택자를 가까이 두되 DOM의 전체 계층을 그대로 복제하지 않습니다.
표준 CSS에도 native nesting이 있지만 selector list의 specificity 등 일부 동작은 Sass가 펼쳐 쓰는 결과와 다를 수 있습니다. 여기서는 컴파일된 선택자까지 함께 확인합니다.
/* SCSS */
.site-nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
display: inline-block;
margin-right: 10px;
}
a {
display: inline-block;
padding: 8px 12px;
text-decoration: none;
color: #333;
&:hover { /* 부모 선택자(&) */
background-color: lightgray;
}
}
}컴파일된 CSS:
/* CSS */
.site-nav ul {
margin: 0;
padding: 0;
list-style: none;
}
.site-nav li {
display: inline-block;
margin-right: 10px;
}
.site-nav a {
display: inline-block;
padding: 8px 12px;
text-decoration: none;
color: #333;
}
.site-nav a:hover {
background-color: lightgray;
}&는 중첩된 규칙에서 부모 선택자를 참조하므로 :hover, ::before 같은 상태나 가상 요소를 붙일 때 유용합니다. 중첩은 보통 한두 단계로 제한하고, 생성된 CSS 선택자가 지나치게 구체적이지 않은지 확인합니다.
믹스인 (Mixins)
함께 반복되는 선언 묶음을 @mixin으로 정의하고 @include로 삽입합니다. 인자를 사용하면 같은 패턴에 필요한 값만 바꿀 수 있습니다.
/* SCSS */
@mixin cluster($gap: 12px) {
display: flex;
align-items: center;
justify-content: center;
gap: $gap;
}
@mixin surface($padding: 16px, $radius: 8px) {
padding: $padding;
border: 1px solid #d9d9d9;
border-radius: $radius;
}
.toolbar {
@include cluster(16px);
}
.card {
@include surface(20px, 8px);
background-color: white;
}컴파일된 CSS:
/* CSS */
.toolbar {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.card {
padding: 20px;
border: 1px solid #d9d9d9;
border-radius: 8px;
background-color: white;
}믹스인은 한 속성의 축약 이름을 만드는 용도보다, 의미가 있는 선언 묶음과 그 인자를 재사용할 때 적합합니다. 브라우저 호환용 vendor prefix 처리는 이 믹스인의 책임으로 두지 않고 프로젝트의 CSS 빌드 단계에서 별도로 다룹니다.
확장 (@extend)
@extend는 DOM 상속이나 CSS 속성 상속이 아니라, 확장 대상이 적용되는 모든 선택자 규칙에 현재 선택자를 추가합니다. 같은 의미 관계를 가진 선택자가 규칙을 공유할 때 사용합니다.
/* SCSS */
.message {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.message--success {
@extend .message; /* .message가 쓰인 규칙에 현재 선택자를 추가 */
background-color: #d4edda;
color: #155724;
border-color: #c3e6cb;
}
.message--error {
@extend .message;
background-color: #f8d7da;
color: #721c24;
border-color: #f5c6cb;
}/* CSS */
.message, .message--success, .message--error { /* 선택자들이 그룹화됨 */
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.message--success {
background-color: #d4edda;
color: #155724;
border-color: #c3e6cb;
}
.message--error {
background-color: #f8d7da;
color: #721c24;
border-color: #f5c6cb;
}-
@extend는placeholder선택자(%placeholder-name)와 함께 사용될 때 더 강력합니다.placeholder는@extend될 때만 CSS로 컴파일되어 불필요한 클래스를 생성하지 않습니다.%common-button-style { padding: 10px 15px; border-radius: 5px; cursor: pointer; } .btn-submit { @extend %common-button-style; background-color: blue; color: white; }
부분 파일과 모듈 (@use, @forward)
재사용 전용 파일은 이름 앞에 언더스코어를 붙인 partial로 만들 수 있습니다(예: _variables.scss). partial은 자체 CSS 파일로 컴파일되지 않으며, 불러올 때 언더스코어와 확장자를 생략합니다.
Sass의 @import와 전역 built-in 함수 호출은 deprecated 상태입니다. 새 코드에서는 @use로 모듈을 한 번 불러와 namespace를 통해 사용하고, @forward로 여러 파일의 공개 API를 하나의 진입점에서 내보냅니다.
/* styles/_variables.scss */
$font-size-base: 16px;
$color-main: #3498db;
/* styles/_mixins.scss */
@mixin button-style {
padding: 10px 20px;
border-radius: 5px;
}
/* styles/_index.scss: 공개 진입점 */
@forward 'variables';
@forward 'mixins';
/* style.scss */
@use 'styles';
body {
font-size: styles.$font-size-base;
color: styles.$color-main;
}
.my-button {
@include styles.button-style;
background-color: styles.$color-main;
color: white;
}@forward는 소비자가 사용할 변수와 믹스인을 모아 공개하고, @use 'styles'는 그 API를 styles. namespace 아래에 둡니다. 이름의 출처가 드러나므로 전역 이름 충돌도 줄어듭니다.
연산 (Operations)
Sass는 숫자와 색상 값을 빌드 시점에 계산할 수 있습니다. built-in 기능은 명시적인 모듈 namespace로 호출합니다.
/* SCSS */
@use 'sass:color';
@use 'sass:math';
$base-font-size: 16px;
$line-height: 1.5;
$margin-unit: 10px;
body {
font-size: $base-font-size;
line-height: $line-height;
}
.box {
width: 100px + 50px; /* 150px */
height: math.div(200px, 2); /* 100px */
padding: $margin-unit * 2; /* 20px */
margin: $margin-unit - 5px; /* 5px */
font-size: $base-font-size * 1.2; /* 19.2px */
}
/* 색상 연산 */
$color-primary: #3498db;
$color-darker: color.adjust($color-primary, $lightness: -10%, $space: hsl);
$color-lighter: color.adjust($color-primary, $lightness: 15%, $space: hsl);
.header {
background-color: $color-darker;
color: white;
}
.footer {
background-color: $color-lighter;
}숫자 나눗셈에는 deprecated된 / 문법 대신 math.div()를 사용합니다. 기존 darken()·lighten()과 같은 고정 HSL 조정을 재현하려면 color.adjust()에 $space: hsl을 명시합니다.