안녕하세요, SmartEditor FE에서 UI개발을 담당하고 있는 길선영입니다.
저희 팀에서는 신규 저장소는 Dart Sass로 스타일을 설계하고, 기존 저장소들은 deprecated된 node-sass를 제거하고, Dart Sass로 환경을 전환하는 작업을 진행하고 있습니다. 이 글에서는 node-sass에서 Dart Sass로 환경을 전환하는 과정에서 경험한 이슈와 팁을 공유드리고자 합니다.

1. Dart Sass 전환 배경과 장단점
Dart Sass의 등장
Sass는 CSS 전처리기로, CSS로 컴파일되는 스타일시트 언어입니다. Sass를 사용하면 변수(variable), 믹신(mixin), 함수(function), 중첩 규칙(nesting) 등 CSS에 존재하지 않는 기능들을 사용하여 보다 가독성 높고, 재사용 가능한 스타일을 설계할 수 있습니다.
Sass의 문법과 구현 방식은 계속해서 발전하고 있습니다. 2006년 Ruby로 구현된 Ruby Sass를 시작으로 이후 여러 다양한 언어에 쉽게 통합할 수 있도록 C/C++로 구현된 LibSass가 등장하였습니다. 시간이 지남에 따라 현재의 Sass는 기능과 CSS 호환성 면에서 업데이트되고, Dart로 구현된 Dart Sass로 발전하였습니다.
node-sass의 deprecated
우리가 아는 node-sass는 Node.js 환경에서 사용할 수 있는 LibSass입니다. LibSass는 더 이상 사용되지 않고 Ruby Sass처럼 앞으로 사라질 예정(deprecated)입니다. 이로 인해 우리가 주로 사용하던 node-sass에서는 신규 Sass 기능을 이용할 수 없게 되었고, 우리는 Dart Sass로 전환을 시작하였습니다.
node-sass의 Node.js 버전 의존성
Dart Sass의 장점은 Sass 모듈 시스템과 같은 신규 기능도 있지만, 개발 환경 면에서 또 다른 장점도 있습니다. 아마 한 번쯤 기존 node-sass를 사용하는 저장소를 설치 후, 실행했을 때 아래와 같은 Node Sass와 관련된 오류 메시지를 보신 적이 있다면 좀 더 쉽게 이해할 수 있습니다.

보통 이 오류는 github의 sass/node-sass > Node version support policy에 안내된 가이드에 따라 node-sass와 Node.js 버전을 맞춰주면 해결이 됩니다. 이처럼 node-sass는 Node.js 버전에 의존적입니다. 이로 인해 우리는 A 저장소에서는 Node.js 10, B 저장소에서는 Node.js 12로 버전을 매번 바꾸어 사용해야 하는 불편함이 있었습니다. 하지만, Dart Sass로 전환한 저장소는 기존 노드 버전뿐만 아니라 현재 최신 LTS 버전인 Node.js 16도 사용할 수 있게 되었습니다. Dart Sass는 Node.js 버전에 대한 의존도가 높지 않아 이러한 설치 오류를 줄여줄 수 있는 장점도 있습니다.
Dart Sass의 컴파일 속도 이슈
그렇다고 Dart Sass가 무조건 더 좋은 것은 아닙니다.
Node-Sass or Dart-Sass: The CSS PreProcessorDilemma 라는 글을 참고해 보자면,
node-sass, Dart Sass(JS complied version), Dart Sass(Dart version runs in Dart VM) 3가지 버전의 컴파일 속도를 비교했을 때, Dart VM에서 실행되는 Dart Sass는 node-sass보다 빠르지만, JS 컴파일 버전의 Dart Sass는 몇 배 더 느립니다. 이는 빌드 속도를 떨어뜨리는 성능적 문제가 있습니다.
실제 SmartEditor 저장소에서 node-sass 와 Dart Sass를 비교해 보았을 때도 아래와 같이 JS 컴파일 버전인 Dart Sass보다 node-sass 환경에서 SmartEditor의 산출물을 빌드한 속도가 더 빨랐습니다.
node-sass 도입시 빌드 시간 | Dart Sass 도입시 빌드 시간 |
---|---|
![]() | ![]() |
그럼에도 불구하고 Dart Sass
빌드 속도에 대한 딜레마는 있지만, 앞서 설명했듯이 node-sass는 더 이상 사용을 권장하지 않는 deprecated 버전이고, Dart Sass는 더 다양한 신규 Sass 기능을 담고 있기 때문에 Sass의 정책을 따라 Dart Sass를 선택하였습니다.
LibSass와 차별화된 Dart Sass의 매력적이고 대표적인 신규 기능은 Sass 모듈 시스템입니다. 단순히 하나의 파일에 스타일을 모아주고, 전역적인 @import
규칙 대신 파일을 모듈로 로드하여 지역적으로 스타일을 설계할 수 있는 @use
, @forward
등의 새로운 규칙들이 등장했습니다.
SmartEditor는 다른 서비스에 커스텀을 제공하기 위한 다양한 스타일 설정값을 갖고 있으며, 이로 인해 믹신(mixin), 함수(function) 등 많은 Sass의 고유 기능들을 다각도로 사용하고 있습니다. 나아가 앞으로는 다크모드를 비롯해 다양한 테마를 만들어내야 하기 때문에 스타일을 지역화, 캡슐화할 수 있는 이러한 모듈 시스템의 철학적 설계와 내장 모듈들은 앞으로의 스타일 설계를 위해 너무나 매력적인 솔루션입니다.
또한 Sass 팀의 모듈 시스템에 대한 문서의 타임라인 내용에 따르면,
Sass 팀은 Dart Sass로 생태계가 매끄럽게 전환할 수 있도록 아직은 @use
와 @import
를 함께 지원하지만, 궁극적으로는 @import
규칙과 대부분의 전역 함수들(function)을 완전히 제거하고자 합니다. 원래는 22년 10월 1일까지만 지원하려 했으나, 최근에 Dart Sass의 사용자가 80%(npm 다운로드 기준으로 측정)로 늘어나고 최소 1년 후까지 기다렸다가 @import
사용 중단하고 제거하는 것으로 일정이 연기되었다고 합니다.
우리가 자주 사용하는 이 @import
규칙과 전역 함수들은 Sass 팀에서는 제거를 계획하고 있기 때문에 언제 사라질지 모릅니다. 우리는 더 이상 Dart Sass로의 전환을 미룰 수 없었고, 이 스타일 모듈화 작업에 앞서 우리는 전환으로 인해 발생하는 문제들을 해결해야 했습니다.
참고 자료: Sass의 모듈 시스템
모듈시스템에 대해 궁금하시다면 아래 링크를 참고해주세요.
✔️ https://css-tricks.com/introducing-sass-modules/
✔️ https://sass-lang.com/documentation/at-rules/use
✔️ https://sass-lang.com/documentation/at-rules/forward
2. 전환 시, 발생한 변경사항
Dart Sass 전환을 위한 환경 세팅, 변경된 Sass 문법에 대해 설명드립니다.
설치
node-sass
제거 후, sass
(Dart Sass)를 설치합니다.
$npm uninstall node-sass && npm install sass
Webpack 변경사항
webpack 환경에서 sass-loader
를 사용하는 경우, Dart Sass를 지원하는 버전인지 확인하고 아래와 같은 옵션 변경이 필요합니다.
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
...
{
loader: "sass-loader",
options: {
implementation: require("sass"), // Prefer `dart-sass`
outputStyle: "compressed", // `dart-sass` not support "compact" and "nest"
},
},
],
},
],
},
};
https://github.com/webpack-contrib/sass-loader#options 에 옵션 값에 대한 정보가 있습니다.
implementation
: Dart Sass 사용 시,require(”sass”)
또는resolve.require(”sass”)
옵션 값이 필요합니다.outputStyle
: Dart Sass는 node-sass와outputStyle
옵션 값이 다릅니다. node-sass와 달리compact
와nested
(node-sass 기본값)를 지원하지 않으며, 기본값은expanded
이고compressed
값은 그대로 지원합니다.
참고 자료: node-sass VS Dart Sass outputStyle
✔️ node-sass: https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#outputStyle
✔️ Dart Sass: https://sass-lang.com/documentation/js-api/modules#OutputStyle
※ outputStyle
외에도 레거시가 된 Javascript API가 있습니다. https://github.com/sass/dart-sass#legacy-javascript-api 내용을 참고해 주세요.
Gulp 변경사항
gulp 환경에서 gulp-sass
를 사용하는 경우, 마찬가지로 Dart Sass를 지원하는 버전으로 업데이트하고, 옵션 변경이 필요합니다.
const sass = require('gulp-sass')(require('sass'));
https://github.com/dlmanning/gulp-sass#render-with-options 에 옵션 값에 대한 정보가 있습니다.
- Dart Sass를 사용하는 경우 위와 같이
require('sass')
로 코드를 변경하거나 추가하여야 합니다. outputStyle
의 변경된 옵션 값 기준은 Webpack 변경사항에서의 설명과 동일하여 생략합니다.
플러그인 주의사항
node-sass
를 제거하더라도 Node.js 버전에 의존적인 node-sass
를 참조하는 플러그인이 남아있을 수 있습니다. 예를 들면, SmartEditor에는 karma-scss-preprocessor
, thread-loader
(구 버전) 등의 플러그인들이 node-sass
를 참조하고 있습니다.
이런 경우 Dart Sass를 지원하는 버전으로 업데이트하는 것이 제일 간단하지만, 경우에 따라 플러그인을 교체해야 하거나 node-sass
와 공존해야 하는 이슈가 있을 수 있습니다.
Sass 문법 주요 변경사항
01. Built-In Modules
Dart Sass에서 큰 변화 중 하나는 앞서 이야기한 Sass 모듈 시스템입니다. 이 Sass 모듈 시스템이 도입되기 전에는 모든 Sass 함수들은 전역으로 사용되었습니다. 이러한 함수들 역시 Dart Sass에서는 모듈 단위로 나뉘어 지역적으로 사용할 수 있습니다. Sass 내장 모듈들(Built-In Modules)은 @use
규칙으로 로드할 수 있으며, 모든 내장 모듈 URL은 Sass의 일부임을 나타내기 위해 sass:
로 시작합니다. 예를 들면 아래 코드와 같습니다.
Lib Sass (node-sass)
quote(Helvetica); // "Helvetica"
Dart Sass
@use "sass:string"
string.quote(Helvetica); // "Helvetica"
이 외에도 각 모듈별 다양하고 새로운 함수들이 https://sass-lang.com/documentation/modules에 소개되어 있으며, if()
, rgb()
등 몇몇 기능들은 새로운 모듈 시스템에서도 여전히 전역으로 사용됩니다.
여전히 많은 함수들이 호환성을 위해 전역 이름(ex. quote()
)을 갖고 있기 때문에 전환 작업 시, 변경할 사항이 많지는 않았습니다. 하지만 Sass 팀은 기존 전역 함수에 대해 앞으로 지원하지 않는 방향을 검토하고 있으며, 사용을 권장하지 않습니다. SmartEditor도 사용하고 있는 Sass 전역 함수들이 많기 때문에 점진적으로 내장 모듈 함수를 적용하는 방향으로 개선해가고 있습니다.
다음은 node-sass에서는 발생하지 않았지만, Dart Sass 전환 시 발생한 Error와 Warning 메시지를 통해 세부적인 변화에 대해서도 알아보겠습니다.
02. Math – 나눗셈 부호
가장 흔히 발생하는 변경사항으로 사칙연산 중 나눗셈 부호(/
) 사용 시, 경고 메시지가 노출됩니다. /
슬래시 기호는 나눗셈뿐만 아니라 grid 와 같은 속성값에서 다른 용도로도 사용되기 때문에 혼란을 방지하고자 나눗셈을 의미하는 /
는 Sass 내장 모듈인 math를 @use 'sass:math'
로 불러와 부호를 math.div(분자, 분모)
로 변경해야 합니다.
Lib Sass (node-sass)
$width: 300px;
.element {
margin-top: -$width/2;
}
Dart Sass – Warning
Deprecation Warning: Using / for division outside of calc() is deprecated and will be removed in Dart Sass 2.0.0.
Recommendation: math.div(-$width, 2)
More info and automated migrator: <a href="https://sass-lang.com/d/slash-div">https://sass-lang.com/d/slash-div</a>
╷
13 │ margin-top: -$width/2;
│ ^^^^^^^^^
╵
Dart Sass
@use 'sass:math';
...
.element {
margin-top: math.div(-$width, 2);
}
참고 문서
✔️ https://sass-lang.com/documentation/breaking-changes/slash-div
✔️ https://sass-lang.com/documentation/values/numbers#units
✔️ https://sass-lang.com/documentation/modules/math#div
03. String – Parent Selector
에러가 발생한 부분은 quote
함수의 인자 값인 부모 선택자 &
입니다. 이 값을 보간(Interpolation)인 #{&}
표현식으로 수정해야 합니다. 앞서 Build-In Modules에서 설명했다시피 quote()
함수는 호환성을 위해 전역 함수도 지원되기 때문에 이로 인한 에러는 아니지만, Sass 모듈 시스템 정책에 맞게 우선 내장 모듈 함수(string.quote()
)로 변경했습니다.
Lib Sass (node-sass)
@mixin viewport-pc() {
$current-selectors: quote(&);
@at-root .viewport-pc {
#{$current-selectors} {
@content;
}
}
}
.component {
@include viewport-pc {
padding: 20px;
}
}
Dart Sass – Error
Error: $string: (.component,) is not a string.
╷
19 │ $current-selectors: quote(&);
│ ^^^^^^^^
╵
Dart Sass
@use "sass:string";
@mixin viewport-pc() {
$current-selectors: string.quote(#{&});
...
}
참고 문서
✔️ https://sass-lang.com/documentation/values/strings
✔️ https://sass-lang.com/documentation/interpolation
04. Variable – !global
보통 전역 변수는 전역 스코프에 선언하지만 간혹 아래와 같이 특정 클래스 내의 지역 스코프에서 !global
문법을 사용한 케이스가 종종 있었습니다. Dart sass에서는 !global
문법 사용 방식이 변경되어 오류가 발생합니다. !global
로 지역 스코프에 선언하는 경우, 전역 스코프에 null
값이나 기본값으로 변수를 반드시 먼저 선언해 주어야 합니다.
Lib Sass (node-sass)
.icon-link {
$icon-link-position-right: 10px !global;
$icon-link-position-bottom: 8px !global;
width: 10px;
height: 8px;
}
.module-imageGroup {
.icon-link {
right: $icon-link-position-right;
bottom: $icon-link-position-bottom;
}
}
Dart Sass – Warning
Deprecation Warning: As of Dart Sass 2.0.0, !global assignments won't be able to declare new variables.
Recommendation: add `$icon-link-position-bottom: null` at the stylesheet root.
╷
41 │ $icon-link-position-bottom: 8px !global;
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
╵
Dart Sass
/* 해결방법 1 */
$icon-link-position-right: null;
$icon-link-position-bottom: null;
.icon-link {
$icon-link-position-right: 10px !global;
$icon-link-position-bottom: 8px !global;
}
/* 해결방법 2 */
$icon-link-position-right: 10px;
$icon-link-position-bottom: 8px;
.icon-link {
...
// !global 변수 제거
}
05. Color Name
.theme-#{$theme-name}
보간(Interpolation)에서 yellow
라는 테마가 있습니다. 이는 색상 이름이 아닌 테마 이름을 의미하고 따옴표로 묶이지 않은 문자열처럼 보이지만, 실제로는 색상 이름이라는 다른 타입이 있기 때문에 이러한 혼란을 방지하고자 아래와 같은 경고 메시지가 발생합니다. 색상 이름을 문자열이나 map key로 사용할 때는 "yellow"
같이 따옴표가 붙은 인용을 사용해야 합니다. 보간 표현식 내에 "" +
를 추가하여 .theme-#{"" + $theme-name}
와 같은 코드로도 해결할 수도 있지만, 대부분의 경우 map key에 따옴표가 없는 문자열보다는 따옴표를 사용하는 것이 좋습니다.
Lib Sass (node-sass)
@mixin component-theme($values...) {
$component-config: nth($values, 1);
$theme-name: map-get($component-config, theme);
$background-color: map-get($component-config, background-color);
.theme-#{$theme-name} {
.component {
background-color: $background-color;
}
}
}
@include component-theme((
theme: default,
background-color: transparent,
));
@include component-theme((
theme: yellow,
background-color: #ff0,
));
Dart Sass – Warning
Warning: : You probably don't mean to use the color value yellow in interpolation here.
It may end up represented as yellow, which will likely produce invalid CSS.
Always quote color names when using them as strings or map keys (for example, "yellow").
If you really want to use the color value here, use '"" + $theme-name'.
╷
60 │ .theme-#{$theme-name} {
│ ^^^^^^^^^^^
╵
Dart Sass
/* 해결방법 1 */
@mixin component-theme($values...) {
...
.theme-#{"" + $theme-name} {
...
}
}
@include component-theme((
theme: default,
background-color: transparent,
));
@include component-theme((
theme: yellow,
background-color: #ff0,
));
/* 해결방법 2 */
@mixin component-theme($values...) {
...
.theme-#{$theme-name} {
...
}
}
@include component-theme((
theme: "default",
background-color: transparent,
));
@include component-theme((
theme: "yellow",
background-color: #ff0,
));
참고 자료: Dart Sass의 주요 변경사항
이 외에도 @extend
나 CSS Variable 등과 관련된 여러 변경사항이 있습니다.
아래 링크를 참고해 주세요.
✔️ https://sass-lang.com/documentation/breaking-changes
✔️ https://github.com/sass/dart-sass#behavioral-differences-from-ruby-sass
3. 마무리
Dart Sass에는 더 다양한 기능들이 있지만, 이번 작업의 목적은 node-sass를 Dart Sass로 환경 전환에 있었기에 전환 배경, 환경 관련 이슈와 간단한 문법적 변경사항에 대해 소개하였습니다.
아직 SmartEditor에도 Dart Sass를 잘 활용하기 위한 많은 숙제들이 남아있어 FE 팀에서 다 함께 고민하고 있습니다. 앞으로 @use
, @forward
등 새로운 Sass 문법들을 통해 스타일을 모듈화하고, Dart Sass로의 변화에 맞게 스타일 설계하는 방향으로 나아갈 예정입니다.
Dart Sass 전환 작업에 도움이 되셨길 바랍니다. ☺️🙏🏻