Sass 통합
Next.js에 Sass를 설치하고 변수·중첩·모듈화를 활용한 SCSS 모듈로 컴포넌트 스타일을 체계화합니다.
이전 글에서 Next.js의 CSS 모듈로 컴포넌트 단위의 스타일을 안전하고 효율적으로 관리하는 방법을 살펴봤습니다. CSS 모듈만으로도 많은 일을 할 수 있지만, CSS 전처리기(pre-processor)인 Sass(Syntactically Awesome Style Sheets)는 CSS만으로는 어려운 기능을 제공해 스타일 작성을 더 효율적이고 체계적으로 만들어 줍니다. Next.js는 Sass 통합을 기본으로 지원하며, .scss나 .sass 확장자의 파일을 CSS 모듈과 함께 쓸 수 있습니다. 이 글에서는 Sass가 무엇이고 왜 필요한지, Next.js 프로젝트에서 어떻게 설정하고 활용하는지 알아봅니다.
Sass란 무엇인가요?
Sass는 CSS의 확장 언어로, 프로그래밍 기능을 CSS에 들여와 스타일시트를 더 유연하게 작성하게 해 줍니다. Sass는 자체 문법을 가지며, 작성한 Sass 코드는 웹 브라우저가 이해하는 표준 CSS로 컴파일되어야 합니다.
중첩을 읽기 전에 실제 선택자를 적어 보기
.btn 안의 &:hover에서 &는 바깥 선택자를 대신하므로 .btn:hover가 됩니다. 반면 같은 위치의 .icon은 기본 후손 중첩이므로 .btn .icon이 됩니다. 버튼 하나에 btn과 primary가 함께 붙었다면 .btn.primary, 또는 .btn 안의 &.primary로 같은 요소를 선택합니다. .btn 안에 그냥 .primary를 넣어 만든 .btn .primary는 별도의 후손 요소를 찾으므로, 그 버튼 자체에는 매칭되지 않습니다.
Sass가 선택자를 만들고 CSS 모듈이 이름을 바꾸어도, 이런 같은 요소와 후손 요소의 관계는 그대로 유지됩니다. 중첩이 깊어질수록 출력 선택자도 길어질 수 있으므로, 들여쓰기 모양보다 생성될 선택자와 실제 DOM 구조를 먼저 맞추어 봅니다. & 치환은 Sass 부모 선택자 안내에서 확인할 수 있습니다.
Sass의 주요 기능
변수(variables)는 $primary-color: #007bff;처럼 색상, 글꼴 크기, 간격 등 반복해서 쓰는 값을 이름으로 정의해 관리하게 해 주며, 유지보수를 쉽게 하고 일관된 디자인을 유지하는 데 도움이 됩니다. 중첩(nesting)은 HTML 구조와 비슷하게 CSS 선택자를 겹쳐 쓰게 해 주어, 코드를 읽기 쉽게 하고 관련된 스타일을 한눈에 파악하게 합니다.
믹스인(mixins)은 @mixin button-style { ... }처럼 다시 쓸 수 있는 CSS 코드 블록을 정의합니다. 여러 곳에서 같은 스타일 패턴을 반복해야 할 때 유용하며, background-color, padding, border-radius 같은 여러 속성을 묶어 한 번에 적용할 수 있습니다. 함수(functions)는 @function calculate-rem($px) { ... }처럼 값을 계산해 돌려주며, 색상 조정에는 sass:color 같은 내장 모듈을 쓸 수 있습니다.
확장(extend)인 @extend .base-button;은 .base-button이 들어 있는 스타일 규칙의 선택자 목록을 넓혀, 현재 선택자도 그 규칙에 매칭되도록 합니다. DOM 부모에게서 속성을 물려받는 CSS 상속이나, 선언을 사용처에 복사하는 믹스인과는 다른 기능입니다. 부분 파일(partials)과 모듈로는 _variables.scss 같은 부분 파일을 만들고, 같은 폴더의 파일에서 @use './variables';로 불러와 variables.$primary-color처럼 이름공간을 거쳐 참조합니다. Sass의 파일 분할과 CSS 모듈의 클래스 이름 지역화는 서로 다른 기능입니다.
Sass의 두 가지 문법
Sass에는 두 가지 문법이 있습니다. SCSS(Sassy CSS)는 CSS와 가장 비슷한 문법으로 중괄호({})와 세미콜론(;)을 쓰며, 대부분의 Sass 사용자가 이 방식을 선호하고 .scss 확장자를 씁니다. 들여쓰기 문법(indented syntax)인 Sass는 중괄호와 세미콜론 없이 들여쓰기와 줄 바꿈으로 구조를 나타내며 .sass 확장자를 씁니다. Next.js에서는 주로 SCSS 문법의 .scss 파일을 씁니다.
왜 Sass를 사용해야 할까요? (이점)
Sass는 기존 CSS의 단점을 보완해 여러 이점을 줍니다. 변수, 믹스인, 중첩으로 반복 작업을 줄여 스타일을 더 빠르게 작성할 수 있고, 공통 변수나 믹스인으로 디자인 시스템을 쉽게 만들어 일관된 스타일을 적용하고 관리할 수 있습니다. 예를 들어 브랜드 색상이 바뀌면 변수 하나만 고치면 됩니다.
믹스인, 함수, @extend로 스타일 코드를 다시 써서 중복을 줄일 수 있고, 중첩 문법은 HTML이나 React 컴포넌트 구조와 비슷하게 스타일을 정리해 읽기 쉽게 만듭니다. 그리고 @use로 다른 파일의 변수, 함수, 믹스인을 명시적으로 가져와 관리할 수 있습니다.
Next.js에서 Sass 통합하기
앞 글과 이 글의 관계부터 정리해 보겠습니다. CSS 모듈은 클래스 이름을 지역화하고, Sass는 작성한 변수, 중첩, 믹스인을 CSS 규칙으로 바꿉니다. .module.scss는 두 기능을 함께 쓰는 파일 이름이며, 일반 .scss라는 확장자만으로 모든 선택자가 지역화되지는 않습니다.
다음 네 실습 파일은 src/app/sass-integration 아래의 별도 페이지입니다. 페이지, 페이지 SCSS, SassStyledButton, 버튼 SCSS를 모두 준비하고 /sass-integration에서 확인합니다. 앞 글의 StyledButton용 CSS나 SCSS 파일을 이 파일로 덮어쓰지 않습니다.
Next.js는 Sass를 공식 지원하므로 설정이 간단하며, 필요한 것은 sass 패키지 설치 하나뿐입니다. 터미널에서 다음 명령을 실행해 sass 컴파일러를 설치합니다.
npm install sass
# 또는
yarn add sass이제 .scss나 .sass 확장자의 파일을 만들어 Sass 문법으로 스타일을 작성하고, CSS 모듈처럼 import해 쓸 수 있습니다.
실습: Sass 변수와 중첩을 사용한 버튼 스타일링
이전 글의 StyledButton 예제를 Sass로 고쳐 써서 Sass의 기능을 활용해 봅시다. 먼저 서버 컴포넌트인 src/app/sass-integration/page.tsx를 만듭니다. 이 페이지는 곧 만들 클라이언트 컴포넌트 SassStyledButton을 가져와 씁니다.
import SassStyledButton from './SassStyledButton'; // 클라이언트 컴포넌트 임포트
import pageStyles from './page.module.scss'; // 페이지 요소에 적용할 Sass 모듈 임포트
export default function SassIntegrationPage() {
return (
<div className={pageStyles.container}>
<h1 className={pageStyles.title}>Sass 통합 예제</h1>
<p className={pageStyles.description}>
아래 버튼들은 Sass 변수, 중첩, 믹스인 등을 사용하여 스타일링되었습니다.
</p>
<div style={{ display: 'flex', gap: '20px', marginTop: '30px', justifyContent: 'center' }}>
<SassStyledButton label="기본 버튼" />
<SassStyledButton label="강조 버튼" variant="primary" />
<SassStyledButton label="경고 버튼" variant="warning" />
</div>
</div>
);
}페이지 코드는 앞 글과 같은 방식이며, import하는 파일의 확장자만 .module.scss로 바뀌었습니다. 버튼은 이번에는 primary 불리언 대신 variant 문자열로 세 종류를 고릅니다. src/app/sass-integration/page.module.scss에는 Sass 문법으로 페이지의 기본 스타일을 정의합니다. SCSS는 CSS의 상위 문법이라서, 아래처럼 변수나 중첩 없이 일반 CSS만 적어도 올바른 SCSS 파일입니다.
.container {
padding: 40px;
max-width: 800px;
margin: 20px auto;
background-color: #f8f8f8;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
text-align: center;
}
.title {
color: #333;
margin-bottom: 15px;
font-size: 2.5em;
}
.description {
color: #666;
font-size: 1.1em;
line-height: 1.6;
}클라이언트 컴포넌트인 src/app/sass-integration/SassStyledButton.tsx는 Sass 모듈을 import해 스타일을 적용하며, variant에 맞는 클래스를 고릅니다. 페이지 스타일에는 Sass 기능이 없으므로, 변수와 믹스인은 이 버튼의 스타일 파일에서 씁니다. 아래 코드의 buttonStyles[variant]처럼 대괄호로 속성을 읽으면, 예를 들어 variant가 'warning'일 때 .warning에 해당하는 이름을 꺼냅니다. default일 때는 빈 문자열을 넣으므로 .btn의 기본 파란색만 적용됩니다.
"use client";
import React from 'react';
import buttonStyles from './SassStyledButton.module.scss'; // 🚨 .scss 확장자 사용
interface SassStyledButtonProps {
label: string;
variant?: 'default' | 'primary' | 'warning'; // 버튼 종류 추가
onClick?: () => void;
}
export default function SassStyledButton({ label, variant = 'default', onClick }: SassStyledButtonProps) {
// variant에 맞는 CSS 모듈 클래스를 선택합니다.
return (
<button
className={`${buttonStyles.btn} ${variant === 'default' ? '' : buttonStyles[variant]}`}
onClick={onClick}
>
{label}
</button>
);
}마지막으로 src/app/sass-integration/SassStyledButton.module.scss에서 Sass의 변수, 중첩, 믹스인으로 버튼 스타일을 정의합니다. 기본 색은 .btn에 있고, primary와 warning만 추가 클래스로 선택합니다.
// Sass 변수 정의
$default-color: #007bff;
$primary-color: #28a745;
$warning-color: #ffc107;
$text-light: white;
$text-dark: #333;
// 재사용 가능한 믹스인 정의
@mixin button-base {
padding: 12px 25px;
font-size: 1.1em;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease, box-shadow 0.3s ease;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
}
.btn {
@include button-base; // 믹스인 포함
// 기본 버튼 스타일 (중첩)
background-color: $default-color;
color: $text-light;
&:hover {
background-color: darken($default-color, 8%); // Sass 함수 사용
}
}
.primary {
background-color: $primary-color;
color: $text-light;
&:hover {
background-color: darken($primary-color, 8%);
}
}
.warning {
background-color: $warning-color;
color: $text-dark; // 경고 버튼은 어두운 텍스트
&:hover {
background-color: darken($warning-color, 8%);
}
}이 코드의 darken()은 폐기 예정인 함수이므로, 새 코드에서는 sass:color 모듈의 color.adjust(..., $lightness: -8%, $space: hsl)로 같은 명도 감소를 표현할 수 있습니다. npm install sass로 패키지를 설치했는지 확인하고, src/app/sass-integration 폴더에 위 파일들을 만든 뒤 개발 서버(npm run dev)를 실행해 http://localhost:3000/sass-integration에 접속합니다.
세 가지 색상의 버튼이 표시됩니다. 개발자 도구에서 생성된 클래스의 규칙과 세 버튼의 기본 색상, hover 색상을 확인합니다. 클래스 이름을 확인한 것만으로 모든 Sass 규칙과 상태가 적용되었다고 판단하지는 않습니다.
Sass를 활용한 스타일 관리 팁
변수와 믹스인은 별도의 부분 파일로 관리하고, 각 사용 파일에서 상대 경로로 @use합니다. _ 접두사 자체가 선택자를 지역화하지는 않으며, Sass의 @import와 전역 내장 함수는 Dart Sass 1.80.0부터 폐기 예정입니다.
중첩은 얕게 유지합니다. DOM 구조를 그대로 깊게 중첩하기보다, 컴포넌트가 맡는 선택자와 반복되는 선언을 구분합니다. 믹스인을 여러 곳에 포함하면 최종 CSS에도 같은 선언이 반복될 수 있습니다. 또한 위 .btn처럼 중첩 규칙이 들어 있는 믹스인을 포함한 뒤에 선언을 두면, Dart Sass 1.92.0부터는 그 선언을 소스 순서대로 중첩 규칙 뒤의 별도 규칙으로 출력합니다. 1.91까지는 같은 배치에 선언 순서 경고를 냈으므로, 출력 순서를 짐작하기 쉽도록 새로 작성할 때는 선언을 먼저 두는 편이 좋습니다.
값과 클래스가 결정되는 시점
SCSS 변수·mixin의 빌드가 완료된 뒤 runtime props가 warning에서 primary로 바뀌어 기존 CSS Module class를 선택합니다. 고정 컴파일 결과와 바뀌는 React·DOM 상태를 추적하면 클릭 때 Sass를 다시 실행하지 않는다는 차이가 드러납니다.
NEXT · 9.2 · Sass와 CSS
Sass 값 계산과 브라우저 값 변경의 시점이 다릅니다
예제의 Sass 색상 변수·믹스인은 CSS를 만들 때 계산됩니다. React는 이미 생성된 기본·강조·경고 클래스를 선택합니다. CSS 변수는 변경 시점을 비교하기 위한 별도 방식입니다.
| 작성 대상 | 결정되는 시점 | 변경을 반영하는 방법 |
|---|---|---|
| Sass 변수 | Sass가 CSS를 만드는 동안 값을 계산 | 소스를 바꾸고 다시 컴파일; 브라우저에는 Sass 변수가 남지 않음 |
| CSS 변수 | 브라우저의 CSS 적용·상속 과정에서 값 결정 | 클래스·속성·JavaScript 등으로 CSS 변수 값을 바꿔 테마에 반영 가능 |
| 모듈 클래스 | 빌드 도구가 파일별 클래스 이름과 매핑 생성 | React에서 이미 생성된 클래스들을 선택하여 기본·강조·경고 상태 표현 |
- Sass 변수
- 결정되는 시점: Sass가 CSS를 만드는 동안 값을 계산변경을 반영하는 방법: 소스를 바꾸고 다시 컴파일; 브라우저에는 Sass 변수가 남지 않음
- CSS 변수
- 결정되는 시점: 브라우저의 CSS 적용·상속 과정에서 값 결정변경을 반영하는 방법: 클래스·속성·JavaScript 등으로 CSS 변수 값을 바꿔 테마에 반영 가능
- 모듈 클래스
- 결정되는 시점: 빌드 도구가 파일별 클래스 이름과 매핑 생성변경을 반영하는 방법: React에서 이미 생성된 클래스들을 선택하여 기본·강조·경고 상태 표현
Sass의 변수·믹스인은 CSS 작성 도구이고, CSS Modules는 클래스 이름을 지역화하는 도구입니다. 예제의 .module.scss는 두 기능을 함께 사용합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 노드 6개와 관계 6개입니다. 위 영역은 Sass 변수·믹스인을 CSS로 계산하는 시점, 아래 영역은 이미 생성된 모듈 클래스 이름을 현재 props로 선택하고 적용하는 시점입니다. CSS 출력은 선택할 이름과 실제 규칙을 각각 제공합니다. variant 입력은 React 선택의 독립 입력입니다. warning에서 primary로 props가 바뀌는 것은 Sass 변수 계산을 다시 실행하는 경로가 아닙니다. CSS 변수의 런타임 변경은 기존 비교표의 별도 방식이며 이 원 예제에는 구현되지 않았습니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 6 | 노드 6개와 관계 6개입니다. 위 영역은 Sass 변수·믹스인을 CSS로 계산하는 시점, 아래 영역은 이미 생성된 모듈 클래스 이름을 현재 props로 선택하고 적용하는 시점입니다. CSS 출력은 선택할 이름과 실제 규칙을 각각 제공합니다. variant 입력은 React 선택의 독립 입력입니다. warning에서 primary로 props가 바뀌는 것은 Sass 변수 계산을 다시 실행하는 경로가 아닙니다. CSS 변수의 런타임 변경은 기존 비교표의 별도 방식이며 이 원 예제에는 구현되지 않았습니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| SCSS 빌드 완료 Sass 변수·mixin이 CSS로 계산되어 클래스 이름과 규칙이 준비됩니다. | 컴파일: 완료 · 산출물: 모듈 class·CSS · runtime CSS 변수: 별도 방식 | runtime에서 선택할 고정 스타일이 생깁니다. |
| warning props 선택 현재 warning props로 React가 이미 생성된 모듈 클래스를 선택합니다. | props: warning · React: 기존 class 선택 · Sass: 재실행 없음 | warning 규칙이 현재 DOM에 적용됩니다. |
| primary props로 변경 props가 primary로 바뀌면 다른 기존 클래스를 선택해 적용합니다. | props: primary · DOM class: 선택 변경 · CSS 산출물: 동일 | runtime의 선택만 바뀌고 빌드는 반복되지 않습니다. |
값과 클래스가 결정되는 시점
SCSS 변수·mixin의 빌드가 완료된 뒤 runtime props가 warning에서 primary로 바뀌어 기존 CSS Module class를 선택합니다. 고정 컴파일 결과와 바뀌는 React·DOM 상태를 추적하면 클릭 때 Sass를 다시 실행하지 않는다는 차이가 드러납니다.
NEXT · 9.2 · Sass와 CSS
Sass 값 계산과 브라우저 값 변경의 시점이 다릅니다
예제의 Sass 색상 변수·믹스인은 CSS를 만들 때 계산됩니다. React는 이미 생성된 기본·강조·경고 클래스를 선택합니다. CSS 변수는 변경 시점을 비교하기 위한 별도 방식입니다.
| 작성 대상 | 결정되는 시점 | 변경을 반영하는 방법 |
|---|---|---|
| Sass 변수 | Sass가 CSS를 만드는 동안 값을 계산 | 소스를 바꾸고 다시 컴파일; 브라우저에는 Sass 변수가 남지 않음 |
| CSS 변수 | 브라우저의 CSS 적용·상속 과정에서 값 결정 | 클래스·속성·JavaScript 등으로 CSS 변수 값을 바꿔 테마에 반영 가능 |
| 모듈 클래스 | 빌드 도구가 파일별 클래스 이름과 매핑 생성 | React에서 이미 생성된 클래스들을 선택하여 기본·강조·경고 상태 표현 |
- Sass 변수
- 결정되는 시점: Sass가 CSS를 만드는 동안 값을 계산변경을 반영하는 방법: 소스를 바꾸고 다시 컴파일; 브라우저에는 Sass 변수가 남지 않음
- CSS 변수
- 결정되는 시점: 브라우저의 CSS 적용·상속 과정에서 값 결정변경을 반영하는 방법: 클래스·속성·JavaScript 등으로 CSS 변수 값을 바꿔 테마에 반영 가능
- 모듈 클래스
- 결정되는 시점: 빌드 도구가 파일별 클래스 이름과 매핑 생성변경을 반영하는 방법: React에서 이미 생성된 클래스들을 선택하여 기본·강조·경고 상태 표현
Sass의 변수·믹스인은 CSS 작성 도구이고, CSS Modules는 클래스 이름을 지역화하는 도구입니다. 예제의 .module.scss는 두 기능을 함께 사용합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 노드 6개와 관계 6개입니다. 위 영역은 Sass 변수·믹스인을 CSS로 계산하는 시점, 아래 영역은 이미 생성된 모듈 클래스 이름을 현재 props로 선택하고 적용하는 시점입니다. CSS 출력은 선택할 이름과 실제 규칙을 각각 제공합니다. variant 입력은 React 선택의 독립 입력입니다. warning에서 primary로 props가 바뀌는 것은 Sass 변수 계산을 다시 실행하는 경로가 아닙니다. CSS 변수의 런타임 변경은 기존 비교표의 별도 방식이며 이 원 예제에는 구현되지 않았습니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 6 | 노드 6개와 관계 6개입니다. 위 영역은 Sass 변수·믹스인을 CSS로 계산하는 시점, 아래 영역은 이미 생성된 모듈 클래스 이름을 현재 props로 선택하고 적용하는 시점입니다. CSS 출력은 선택할 이름과 실제 규칙을 각각 제공합니다. variant 입력은 React 선택의 독립 입력입니다. warning에서 primary로 props가 바뀌는 것은 Sass 변수 계산을 다시 실행하는 경로가 아닙니다. CSS 변수의 런타임 변경은 기존 비교표의 별도 방식이며 이 원 예제에는 구현되지 않았습니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| SCSS 빌드 완료 Sass 변수·mixin이 CSS로 계산되어 클래스 이름과 규칙이 준비됩니다. | 컴파일: 완료 · 산출물: 모듈 class·CSS · runtime CSS 변수: 별도 방식 | runtime에서 선택할 고정 스타일이 생깁니다. |
| warning props 선택 현재 warning props로 React가 이미 생성된 모듈 클래스를 선택합니다. | props: warning · React: 기존 class 선택 · Sass: 재실행 없음 | warning 규칙이 현재 DOM에 적용됩니다. |
| primary props로 변경 props가 primary로 바뀌면 다른 기존 클래스를 선택해 적용합니다. | props: primary · DOM class: 선택 변경 · CSS 산출물: 동일 | runtime의 선택만 바뀌고 빌드는 반복되지 않습니다. |
위 그림은 Sass 값이 계산되는 시점과 브라우저에서 값이 바뀌는 시점이 다르다는 점을 보여 줍니다. 위쪽 영역은 CSS를 만드는 시점입니다. Sass로 작성한 소스의 변수, 믹스인, 중첩이 컴파일되어 계산된 색상과 공통 규칙, 그리고 클래스 이름의 매핑을 담은 모듈 CSS가 됩니다. 아래쪽 영역은 화면에서 props로 선택하는 시점입니다. 현재 variant prop이 default, primary, warning 가운데 하나로 들어오면, React가 공통 btn과 선택한 variant의 이름을 골라 DOM에 클래스로 지정하고, 브라우저가 공통 이동과 variant 색상을 조건에 맞게 적용합니다. 그림의 단계도 SCSS 빌드 완료, warning props 선택, primary props로 변경의 순서입니다.
그림의 설명처럼 warning에서 primary로 props가 바뀌는 것은 Sass 변수 계산을 다시 실행하는 경로가 아닙니다. 아래 표는 세 가지를 비교합니다. Sass 변수는 Sass가 CSS를 만드는 동안 값이 계산되므로, 바꾸려면 소스를 고치고 다시 컴파일해야 하며 브라우저에는 Sass 변수가 남지 않습니다. CSS 변수는 브라우저가 CSS를 적용하고 상속하는 과정에서 값이 정해지므로, 클래스나 속성, JavaScript로 값을 바꿔 테마에 반영할 수 있습니다. 모듈 클래스는 빌드 도구가 파일별 클래스 이름과 매핑을 만들고, React는 이미 만들어진 클래스를 골라 기본, 강조, 경고 상태를 표현합니다. CSS 변수의 런타임 변경은 비교를 위한 별도 방식이며 이 예제에는 구현되어 있지 않습니다.
variant=warning일 때 React가 고르는 것은 이미 만들어진 btn과 warning의 모듈 클래스 이름입니다. SassStyledButton.module.scss를 읽어 보면, 먼저 button-base 믹스인의 공통 padding과 hover 이동 규칙이 .btn 안에 들어가고, warning의 노란 배경과 어두운 글자 규칙이 같은 요소에 더해집니다. 클릭할 때 브라우저가 Sass의 $warning-color를 다시 계산하는 것은 아닙니다.
컴파일 시점의 값과 브라우저에서 바꾸는 값
$warning-color는 생성된 CSS에 실제 색으로 들어가고, Sass 변수의 이름은 남지 않습니다. 반대로 --button-bg 같은 CSS 사용자 정의 속성은 출력 CSS에 남아 브라우저에서 요소마다 바꿀 수 있습니다. 이 예제는 Sass 변수 방식이며, JavaScript로 Sass의 $warning-color라는 이름을 직접 바꾸는 런타임 테마 코드는 아닙니다.
부분 파일에 @use './variables';를 썼다면 그 파일에서는 variables.$primary-color로 값을 읽습니다. 다른 SCSS 파일이 이 파일을 썼다는 이유만으로 같은 이름공간을 자동으로 얻지는 않으므로, 값을 직접 읽는 파일에서도 필요한 모듈을 명시적으로 가져옵니다. _ 접두사는 부분 파일의 관례일 뿐, CSS 모듈의 지역 이름이나 전역 변수 주입을 뜻하지 않습니다.
darken과 color 모듈의 계산 차이
예제의 darken은 이전 함수 사용법을 그대로 남긴 코드입니다. Sass 색상 함수 변경 안내는 제거되기 전의 경고 단계와 Dart Sass 2.0에서의 제거를 구분합니다. 지금 경고가 보이는 결과와, 앞으로 그 함수가 그대로 동작하지 않는 버전의 결과를 혼동하지 마세요.
darken($color, 8%)는 HSL 명도(lightness)에서 8퍼센트포인트를 빼는 기존 동작입니다. HSL 명도가 50%라면 42%가 되고, 현재 값의 8%만큼 줄이는 color.scale(..., $lightness: -8%, $space: hsl)는 46%가 됩니다. 기존 결과를 유지하는 대안은 color.adjust(..., $lightness: -8%, $space: hsl)입니다. 색 공간과, 정해진 양만큼 바꾸는 조정(adjust)과 상대 비율로 바꾸는 조정(scale) 가운데 무엇이 목적인지를 함께 정해야, 함수 이름만 바꾸었다가 다른 결과가 나오는 문제를 피할 수 있습니다. 두 연산의 차이는 Sass 색 함수 안내에서 확인할 수 있습니다.
더 깊이: Next.js 16이 Sass를 컴파일하는 방식과 바꿀 수 있는 설정
Next.js는 설치한 sass 패키지로 .scss와 .sass 파일을 컴파일하며, next.config의 sassOptions로 그 옵션을 넘길 수 있습니다. additionalData에는 모든 Sass 파일에 함께 넣을 코드를 적고, implementation: 'sass-embedded'를 주면 기본 sass 대신 sass-embedded 구현을 씁니다. 반대 방향으로 값을 꺼내는 방법도 있어서, CSS 모듈 파일에서 :export 블록에 Sass 변수를 적으면 그 값을 JavaScript에서 variables.primaryColor처럼 읽을 수 있습니다(Next.js 문서의 Sass).
Next.js 16에서 바뀐 점도 있습니다. 기본 번들러가 된 Turbopack은 node_modules의 Sass 파일 import를 지원하지만, Webpack에서 쓰던 @import '~bootstrap/...' 같은 물결표(~) 접두사는 지원하지 않습니다. 그래서 예전 프로젝트를 옮길 때 이 접두사가 남아 있으면 경로에서 빼거나 turbopack.resolveAlias로 바꿔 줘야 합니다. 또 Next.js 16은 Sass 처리에 쓰는 sass-loader를 16 버전으로 올려, 최신 Sass 문법과 기능을 지원합니다.
정리
- Sass는 변수, 중첩, 믹스인, 함수, 확장, 모듈을 제공하는 CSS 확장 언어이며, 작성한 코드는 표준 CSS로 컴파일됩니다.
- 중첩에서
&는 바깥 선택자를 그대로 붙이고,&없이 중첩한 선택자는 후손 선택자가 됩니다. - Next.js에서는
sass패키지만 설치하면.scss와.sass를 쓸 수 있으며,.module.scss는 Sass와 CSS 모듈의 지역화를 함께 씁니다. - Sass 변수는 CSS를 만들 때 계산되어 브라우저에 남지 않으므로, 런타임에 바꿀 값은 CSS 사용자 정의 속성으로 만듭니다.
- 폐기 예정인
darken()과@import대신sass:color모듈과@use를 쓰며,color.adjust와color.scale은 계산 방식이 다릅니다.
연습 문제
1. 작성 시점과 선택 시점을 비교하기
variant 세 값만 쓰고 외부 CSS가 없다고 가정합니다. warning 버튼의 글자색과 hover 이동을 예상한 뒤, 렌더링할 때 variant를 primary로 바꾸면 Sass를 다시 컴파일해야 하는지 판단해 보세요. 이어서 SCSS 소스의 $warning-color 자체를 바꾸는 경우와 비교해 보세요.
해설 보기
warning은 btn과 warning을 함께 적용하므로 글자색 #333과 공통 translateY(-2px)를 쓰고, 배경의 hover 값은 warning 규칙이 정합니다. primary로 바뀌면 React가 btn과 primary를 골라 글자색이 white가 됩니다. 두 variant의 CSS가 이미 만들어져 로드되어 있으므로, props만 바뀐 경우에는 Sass를 다시 계산할 필요가 없습니다.
$warning-color는 Sass 소스의 입력이므로, 그 값을 바꾼 결과를 보려면 CSS를 다시 만들어 화면에 반영해야 합니다. 개발 도구가 이 과정을 자동으로 처리하더라도, 브라우저가 Sass 변수를 직접 실행한다고 설명하지 않습니다. 이 예제에는 CSS 사용자 정의 변수로 런타임 테마를 바꾸는 별도 구현이 없습니다.
2. 출력 선택자가 고르는 요소
실제 DOM이 <button class="btn primary"><span class="icon">저장</span></button>입니다. 출력 선택자 .btn.primary, .btn .primary, .btn .icon이 각각 선택하는 요소를 적고, 같은 버튼의 primary 상태를 중첩으로 작성할 때 &가 필요한 이유를 설명해 보세요. 지역 이름으로 바뀌기 전의 표기로 풉니다.
해설 보기
.btn.primary는 두 클래스를 모두 가진 button을 선택합니다. .btn .primary는 btn의 후손인 primary 요소를 찾으므로 이 DOM에서는 대상이 없고, .btn .icon은 span을 선택합니다. .btn 안의 &.primary는 바깥 .btn을 그대로 붙여 .btn.primary를 만들지만, 그냥 .primary를 중첩하면 사이에 후손을 뜻하는 공백이 들어갑니다.
3. @extend와 상속
.base { padding: 8px; }와 .accent { @extend .base; }에서 두 선택자가 이 padding 규칙을 함께 쓰게 됩니다. accent 버튼 안의 클래스 없는 span도 그 이유만으로 padding을 물려받을까요? 믹스인을 쓰는 방식과도 구별해 보세요.
해설 보기
물려받지 않습니다. button은 넓어진 선택자 목록의 padding 규칙에 매칭되지만, span은 .base나 .accent 선택자에 매칭되지 않고 padding은 기본적으로 상속되는 속성도 아니므로 8px가 되지 않습니다. 믹스인은 선언을 @include한 곳에 복사하고, @extend는 기존 규칙의 선택자 목록을 넓힌다는 점이 다릅니다.
4. adjust와 scale 계산하기
HSL 명도 50%에서 color.adjust의 $lightness: -8%와 color.scale의 $lightness: -8%를 각각 계산해 보세요. 같은 방식으로 명도 20%에서도 계산해 보세요.
해설 보기
50%에서 adjust는 50 - 8 = 42%, scale은 50 × 0.92 = 46%입니다. 20%에서 adjust는 20 - 8 = 12%, scale은 20 × 0.92 = 18.4%입니다. 같은 -8% 표기라도 두 함수의 계산 의미가 다릅니다.
5. @use의 이름공간
변수 모듈을 A.scss가 @use하고, B.scss는 A.scss만 @use합니다. B가 변수 모듈의 $primary-color를 직접 읽으려면 무엇이 필요할까요? 그 값을 바꾼 소스가 브라우저에 반영되는 시점도 적어 보세요. 별도의 CSS 사용자 정의 속성 구현은 없다고 가정합니다.
해설 보기
B에서도 변수 모듈을 @use하고, 그 이름공간으로 참조해야 합니다. A가 가져온 모듈의 멤버가 B의 이름공간에 자동으로 풀리지는 않습니다. 값을 바꾼 결과는 Sass 소스를 다시 컴파일해 생성된 CSS를 갱신한 뒤 화면에 반영되며, 브라우저가 $primary-color를 직접 실행하거나 고치지 않습니다.
다음 글 CSS-in-JS 솔루션에서는 JavaScript 안에서 스타일을 작성하는 CSS-in-JS 라이브러리를 App Router에서 쓰는 방법을 알아봅니다.