Ryu.log

[SASS] SASS(SCSS) 본문

Front-end/SASS(SCSS)

[SASS] SASS(SCSS)

류뚝딱 2020. 12. 24. 15:28

Sass (Syntactically Awesome Style Sheets)

Created: Jun 4, 2020 2:50 PM
Tags: CSS3, SASS, SCSS
요약: 더나은 Sass 사용법을 위한 Sass 파악

Sass (Syntactically Awesome Style Sheets)

Sass(Syntactically Awesome StyleSheets)는 CSS의 한계와 단점을 보완하여,
보다 가독성 높고, 재사용에 유리한 CSS를 생성하기 위한 CSS 확장(extension)이다.

Sass가 제공하는 기능

CSS의 간결한 문법은 배우기 쉽고 명확하다. 프로젝트 초기에는 문제가 보이지않지만,
규모가 커지고 수정이 빈번하게 발생함에 따라 쉽게 지저분해진다.

이러한 CSS의 태생적 한계를 보완하기 위해 Sass는 아래와 같은 추가 기능 및 유용한 도구들을 제공한다.

  • 변수의 사용
  • 연산자
  • 조건문과 반복문
  • Import
  • Nesting
  • Mixin
  • Extend / Inheritance
  • ...

변수

Sass에서는 변수를 사용할 수 있다. 문자열, 숫자, 컬러(#aa443f) 등을 사전에 변수에 저장하고 필요할 때 불러 사용할 수 있다. 변수명은 $로 시작한다.

$site_max_width: 960px;
$font_color: #333;
$link_color: #00c;
$font_family: Arial, sans-serif;
$font_size: 16px;
$line_height: percentage(20px / $font_size);

body {
  color: $font_color;

  // Property Nesting
  font: {
    size: $font_size;
    family: $font_family;
  }

  line-height: $line_height;
}

#main {
  width: 100%;
  max-width: $site_max_width;
}

🤔 변수의 Scope

변수에는 유효범위(scope)가 존재한다.

아래 예에서 변수 $width는 top level에 기술되었으므로 전역변수(global variable)가 된다. 전역변수는 전역은 물론 하위의 어떤 코드 블록 내에서도 유효하다.

코드 블록 내에서 선언된 변수는 지역변수(local variable)가 된다. 지역변수의 유효범위는 자신이 속한 코드 블록과 하위 코드 블록이다.

$width: 960px; // global variable

header {
  width: $width;
  margin: 0 auto;
}

#main {
  $color: #333; // local variable
  width: $width;
  margin: 20px auto;
  section {
    p {
      color: $color;

      a:link {
        color: $color;
      }
    }
  }
}

footer {
  width: $width;
  margin: 0 auto;
  color: $color;
}

위 코드를 컴파일하면 Undefined variable: “$color”라는 에러가 발생한다. 이는 #main에서 선언한 $color는 #main 내에서만 유효한 지역변수이기 때문이다.

코드 블록 내에서 선언한 지역변수를 전역변수화하는 방법은 아래와 같다.

#main {
  $color: #333 !global; // global variable
  width: $width;
  ...
}

연산자(Operation)

숫자 연산자

숫자 연산자

컬러 연산자

모든 산술 연산자는 컬러 값에도 사용할 수 있다.

p {
  color: #010203 + #040506;
  // R: 01 + 04 = 05
  // G: 02 + 05 = 07
  // B: 03 + 06 = 09
  // => #050709
}

p {
  color: #010203 * 2;
  // R: 01 * 2 = 02
  // G: 02 * 2 = 04
  // B: 03 * 2 = 06
  // => #020406
}

p {
  color: rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75);
  // alpha 값은 연산되지 않는다
  // color: rgba(255, 255, 0, 0.75);
}

alpha 값은 연산되지 않는다. alpha 값의 연산을 위해서는 opacify 함수 또는 transparentize 함수를 사용한다.

  • opacify 함수: 첫번째 argument의 alpha값에 두번째 argument를 더해 불투명도를 증가시킨다.(더 불투명해진다)
  • transparentize 함수: 첫번째 argument의 alpha값에 두번째 argument를 빼서 불투명도를 감소시킨다.(더 투명해진다)
$translucent-red: rgba(255, 0, 0, 0.5);

p {
  color: opacify($translucent-red, 0.3);
  // => color: rgba(255, 0, 0, 0.8);

  background-color: transparentize($translucent-red, 0.25);
  // => background-color: rgba(255, 0, 0, 0.25);
}

문자열 연산자

+ 연산자는 자바스크립트와 같이 문자열을 연결할 수 있다.

p {
  cursor: e + -resize;  // e-resize
}

따옴표가 있는 문자열과 없는 문자열을 함께 사용하는 경우, 좌항의 문자열을 기준으로 따옴표를 처리한다.

p:before {
  content: "Foo " + Bar;        // "Foo Bar"
  font-family: sans- + "serif"; // sans-serif
}

불린 연산자

불린 연산자

Interpolation: #{}

인터폴레이션은 변수의 값을 문자열 그대로 삽입한다. 인터폴레이션에 의해 삽입된 문자열은 연산의 대상으로 취급되지 않는다.
변수는 프로퍼티값으로만 사용할 수 있으나 #{}을 사용하면 프로퍼티값은 물론 셀렉터와 프로퍼티명에도 사용할 수 있다.

$name: foo;
$attr: border;

p.#{$name} {            // p.foo
  #{$attr}-color: blue; // border-color: blue;
}

.someclass {
  $font-size: 12px;
  $line-height: 30px;
  // 연산의 대상으로 취급되지 않도록
  font: #{$font-size} / #{$line-height}; // 12px / 30px
}

Ampersand(&)

&는 부모요소를 참조하는 셀렉터이다.

a {
  color: #ccc;

  &.home {
    color: #f0f;
  }

  &:hover {
    text-decoration: none;
  }

  // & > span (X)
  > span {
    color: blue;
  }

  span {
    color: red;
  }
}

!default

!default flag는 할당되지 않은 변수의 초기값을 설정한다.

$content: null;
$content: "Non-null content" !default;

#main {
  content: $content; // "Non-null content"
}

이미 값이 할당되어 있는 변수에 !default flag를 사용하면 적용되지 않는다.

$content: "First content";
$content: "Second content?" !default;
$new_content: "First time reference" !default;

#main {
  content: $content; // "First content"
  new-content: $new_content; // "First time reference"
}

Nesting

Nesting은 Sass의 유용한 확장 기능으로 선언을 중첩(Nesting)하는 것이다.
CSS는 후손 셀렉터(Descendant Combinator)의 경우, 부모요소를 기술하여야 한다.

#navbar {
  width: 80%;
  height: 23px;
}

#navbar ul {
  list-style-type: none;
}

#navbar li {
  float: left;
}

#navbar li a {
  font-weight: bold;
}

Sass의 Nesting은 후손 셀렉터를 간단히 기술이 가능하다. 또한 HTML의 구조를 반영한 CSS를 기술할 수 있다.

#navbar {
  width: 80%;
  height: 23px;

  ul { list-style-type: none; }

  li {
    float: left;
    a { font-weight: bold; }
  }
}

너무 깊은 Nesting은 가독성을 나쁘게 하고 셀렉터를 복잡하게 만든다.

// Bad case
div#main {
  #sidebar {
    #navbar {
      width: 80%;
      height: 23px;

      aside {
        div {
          ul {
            list-style-type: none;

            li {
              float: left;

              a {
                font-weight: bold;
              }
            }
          }
        }
      }
    }
  }
}

부모요소의 참조가 필요한 경우 &를 사용한다. 예를들어 :hover 또는 ::before 등의 가상 클래스 선택자 (Pseudo-Class Selector)를 지정하는 경우 부모요소의 참조가 필요하다.

.myAnchor {
  color: blue;

  // .myAnchor:hover
  &:hover {
    text-decoration: underline;
  }

  // .myAnchor:visited
  &:visited {
    color: purple;
  }
}

Nesting은 프로퍼티에도 사용할 수 있다.

.funky {
  font: {
    family: fantasy;
    size: 30em;
    weight: bold;
  }
}

@-Rules and Directives

@import

1개의 CSS 파일에 모든 스타일을 기술하는 것은 가독성을 나쁘게 한다. 또한 기능에 따라 CSS 파일을 분리하면 재사용 및 유지보수 측면에서 유리하다.
따라서 룰을 정하여 파일을 분리하여 개발하는 것은 효과적인 방법이다.

Sass는 @import directive를 사용하여 분리된 stylesheet 파일을 import할 수 있다. 기존의 CSS @import보다 편리한 기능을 제공한다.

@import "foo.scss";

// 확장자는 생략 가능하다
@import "foo";

// import multiple files
@import "rounded-corners", "text-shadow";

$family: unquote("Droid+Sans");
@import url("http://fonts.googleapis.com/css?family=#{$family}");

여러 개의 파일로 분할하는 것 또는 분할된 파일을 partial 이라 하며 partial된 Sass 파일명의 선두에는 underscore(_)를 붙인다. (_reset.scss, _module.scss, _print.scss)

예를 들어 “_foo.scss”라는 partial된 Sass 파일이 있고 이 파일을 import하는 경우 아래와 같이 기술한다. 파일명 선두의 _와 확장자는 생략할 수 있다.

@import "foo";

partial된 Sass 파일명 선두에 붙인 _의 의미는 import는 수행하되 CSS로의 컴파일은 수행하지 말라는 의미를 갖는다.
따라서 partial은 import시에는 CSS 파일로 컴파일되지 않기 때문에 최종적으로 CSS로 컴파일을 수행할 Sass 파일에서 import한다.

https://poiemaweb.com/img/partial.png

Webpack이나 Parcel, Gulp 같은 일반적인 빌드툴에서는 Partials 기능을 사용할 필요 없이, 설정된 값에 따라 빌드된다.. 하지만 되도록 _를 사용할 것을 권장한다고 한다.

@import는 top-level에서 사용하는 것이 일반적이지만 CSS rule 또는 @media rule 내에 포함시는 것도 가능하다.

#main {
  @import "example";
}

위 코드의 컴파일 결과는 아래와 같다.

#main .example {
  color: red;
}

@extend

기존 스타일을 상속하고자 할 경우 @extend를 사용한다

<div class="error seriousError">
  애러 발생!
</div>

기존에 선언되어 있는 error class를 사용하면서 일부 rule set에 대해서는 다른 선언이 필요한 경우 자주 사용하는 방법이다.

이러한 경우 사용할 수 있는 방법이 상속이다. 상속되는 rule set을 그대로 상속받아 다른 부분만 별도 선언하면 된다.

.error {
  border: 1px #f00;
  background-color: blue;
}

.seriousError {
  @extend .error;

  border-width: 3px;
  border-color: darkblue;
}

위 코드의 컴파일 결과는 아래와 같다. .error.seriousError가 공통으로 사용하는 프로퍼티를 묶어 합리적인 룰셋을 생성한다.

.error, .seriousError {
  border: 1px #f00;
  background-color: blue;
}

.seriousError {
  border-width: 3px;
  border-color: darkblue;
}

@extend를 사용하면 아래와 같이 하나의 클래스만 적용시키면 된다.

<div class="seriousError">
  Oh no! You've been hacked!
</div>

🚨 @extend를 @media 블록과 같이 사용하는 경우, 제대로 작동하지 않는다. 다시말해 @media 안에서 외부의 선택자를 @extend할 수 없다.

.foo {
  color: red;
}

@media print {
  .bar {
    // ERROR
    @extend .foo;
  }
}

@extend를 사용하면 컴파일 후 자신의 셀렉터가 어디에 첨부될 것인지 예상하기 어렵고, 예상치 못했던 부작용이 발생할 수 있다.
따라서 @extend의 사용은 최대한 지양하고 Mixin 을 사용하는게 바람직하다. @extend 부작용

Placeholder Selectors

Placeholder Selector는 Sass 3.2부터 제공되는 기능으로 재이용이 가능한 rule set을 % 키워드로 지정하는 @extend 전용 Selector이다.
Placeholder Selector은 상속만을 위한 rule set으로 자신은 컴파일되지 않는다.

%input-style {
  font-size: 14px;
}

.input-black {
  @extend %input-style;

  color: black;
}

.input-red {
  @extend %input-style;

  color: red;
}

컴파일 결과는 아래와 같다.

.input-black, .input-red {
  font-size: 14px;
}

.input-black {
  color: black;
}

.input-red {
  color: red;
}

조건과 반복

Sass는 Javascript 같은 프로그래밍 언어와 같이 제어문(Control flow statement)을 사용할 수 있는 기능을 제공한다.

if()

built-in if() 함수는 주어진 조건을 판단하여 결과를 리턴한다. Javascript의 삼항연산자와 유사하게 동작한다.

if(condition, if_true, if_false)

condition이 true이면 if_true를, false이면 if_false를 반환한다.

$type: ocean;

p {
  color: if($type == ocean, blue, black); // color: blue;
}

@if

@if를 사용하면 조건분기가 가능하다.

$type: monster;

p {
  @if $type == ocean {
    color: blue;
  } @else if $type == matador {
    color: red;
  } @else if $type == monster {
    color: green;
  } @else {
    color: black;
  }
}

컴파일 결과는 아래와 같다.

p {
  color: green;
}

@for

@for으로 반복문을 사용할 수 있다.

@for $i from 1 through 3 {
  .item-#{$i} { width: 2em * $i; }
}

컴파일 결과는 아래와 같다.

.item-1 {
  width: 2em;
}
.item-2 {
  width: 4em;
}
.item-3 {
  width: 6em;
}

@each

@each와 list 또는 map의 요소에 대해 반복을 실시한다.

// List
@each $animal in puma, sea-slug, egret, salamander {

  .#{$animal}-icon {
    background-image: url('/images/#{$animal}.png');
  }
}

// Map
// $header: h1, $size: 2em
// $header: h2, $size: 1.5em
// $header: h3, $size: 1.2em
@each $header, $size in (h1: 2em, h2: 1.5em, h3: 1.2em) {
  #{$header} {
    font-size: $size;
  }
}

컴파일 결과는 아래와 같다.

.puma-icon {
  background-image: url("/images/puma.png");
}

.sea-slug-icon {
  background-image: url("/images/sea-slug.png");
}

.egret-icon {
  background-image: url("/images/egret.png");
}

.salamander-icon {
  background-image: url("/images/salamander.png");
}

h1 {
  font-size: 2em;
}

h2 {
  font-size: 1.5em;
}

h3 {
  font-size: 1.2em;
}

@while

@while으로 반복문을 사용할 수 있다.

$i: 6;
@while $i > 0 {
  .item-#{$i} { width: 2em * $i; }
  $i: $i - 2;
}

컴파일 결과는 아래와 같다.

.item-6 {
  width: 12em;
}

.item-4 {
  width: 8em;
}

.item-2 {
  width: 4em;
}

Mixin

Mixin은 Sass의 매우 유용한 기능으로 중복 기술을 방지하기 위해 사용 빈도가 높은 마크업을 사전에 정의하여 필요할 때에 불러 사용하는 방법이다.
@extend와 유사하나 프로그래밍 언어의 함수와 같이 argument를 받을 수 있다.
사용법은 매우 간단하다. @mixin 선언하고 @include로 불러들인다.

// 지름이 50px인 원
@mixin circle {
  width: 50px;
  height: 50px;
  border-radius: 50%;
}

// 지름이 50px인 원을 위한 mixin을 include한 후, 배경을 추가 지정
.box {
  @include circle;

  background: #f00;
}

@extend와 차이가 없어 보이나 Mixin은 함수와 같이 argument를 사용할 수 있다.

@mixin circle($size) {
  width: $size;
  height: $size;
  border-radius: 50%;
}

.box {
  @include circle(100px);

  background: #f00;
}

argument의 초기값을 설정할 수도 있다.

@mixin circle($size: 10px) {
  width: $size;
  height: $size;
  border-radius: 50%;
}

.box {
  // 인자가 없으면 초기값을 사용한다.
  @include circle();
  background: #f00;
}

🎁 Mixin을 활용한 유용한 예제

// Vendor Prefix
@mixin vendorPrefix($property, $value) {
  @each $prefix in -webkit-, -moz-, -ms-, -o-, '' {
    #{$prefix}#{$property}: $value;
  }
}

.border_radius {
  @include vendorPrefix(transition, 0.5s);
}
// Opacity
@mixin opacity($opacity) {
  opacity: $opacity; /* All modern browsers */
  $opacityIE: $opacity * 100;
  filter: alpha(opacity=$opacityIE); /* For IE5~IE9 */
}

.box {
  @include opacity(0.5);
}
// Position
@mixin position($position, $top: null, $right: null, $bottom: null, $left: null) {
  position: $position;
  top: $top;
  right: $right;
  bottom: $bottom;
  left: $left;
}

.box {
  @include position(absolute, $top: 10px, $left: 50%);
}

꼭 필요한 mixin기능이 있다면, 위처럼 작성해도 무방하지만 일반적으로는 Sass Framework/Library를 사용하는 것이 매우 바람직한 방법이다.

Function

Function은 Mixin과 유사하나 리턴값에 차이가 있다.

  • Mixin : style markup을 리턴
  • Function: @return 을 이용하여 값을 리턴
$grid-width: 40px;
$gutter-width: 10px;

@function grid-width($n) {
  @return $n * $grid-width + ($n - 1) * $gutter-width;
}

#sidebar { width: grid-width(5); }  // width: 240px;

기본 내장 함수 (Built-in Function)

Sass Built-in function에는 다양한 내장 함수를 제공한다. 그 중에서 활용 빈도가 높은 함수 몇가지를 알아보자.

Number Functions

percentage 숫자값을 %로 전환

percentage(0.2)          => 20%
percentage(100px / 50px) => 200%

round 소숫점 이하 반올림

round(10.4px) => 10px
round(10.6px) => 11px

ceil 소숫점 이하 올림

ceil(10.4px) => 11px
ceil(10.6px) => 11px

floor 소숫점 이하 절사

floor(10.4px) => 10px
floor(10.6px) => 10px

abs 절대값 취득

abs(10px) => 10px
abs(-10px) => 10px

Introspection Functions

type-of Data type 취득

type-of(100px)  => number
type-of(asdf)   => string
type-of("asdf") => string
type-of(true)   => bool
type-of(#fff)   => color
type-of(blue)   => color

unit Data unit 취득

unit(100)   => ""
unit(100px) => "px"
unit(3em)   => "em"
unit(10px * 5em) => "em*px"
unit(10px * 5em / 30cm / 1rem) => "em*px/cm*rem"

unitless 값에 단위가 있는지 확인

unitless(100)   => true
unitless(100px) => false

comparable 2개의 값을 합산, 감산, 비교 가능한지 확인

comparable(2px, 1px)   => true
comparable(100px, 3em) => false
comparable(10cm, 3mm)  => true

String Functions

quote 따옴표 붙이기

quote("foo") => "foo"
quote(foo)   => "foo"

unquote 따옴표 제거

unquote("foo") => foo
unquote(foo)   => foo

List Functions

length 리스트 요소의 갯수 취득

length(10px)                        => 1
length(10px 20px 30px)              => 3
length((width: 10px, height: 20px)) => 2

nth 리스트의 n번째 요소 취득

nth(10px 20px 30px, 1)                 => 10px
nth((Helvetica, Arial, sans-serif), 3) => sans-serif
nth((width: 10px, length: 20px), 2)    => length 20px

$n: nth(width: 10px, length: 20px);
nth(($n, 2), 1)                        => length

index 요소의 index 취득

index(1px solid red, solid)                       => 2
index(1px solid red, dashed)                      => null
index((width: 10px, height: 20px), (height 20px)) => 2

append 리스트의 마지막에 단일 요소 추가

append(10px 20px, 30px)      => 10px 20px 30px
append((blue, red), green)   => blue, red, green
append(10px 20px, 30px 40px) => 10px 20px (30px 40px)
append(10px, 20px, comma)    => 10px, 20px
append((blue, red), green, space) => blue red green

join 리스트와 리스트의 결합

join(10px 20px, 30px 40px)      => 10px 20px 30px 40px
join((blue, red), (#abc, #def)) => blue, red, #abc, #def
join(10px, 20px)                => 10px 20px
join(10px, 20px, comma)         => 10px, 20px
join((blue, red), (#abc, #def), space) => blue red #abc #def

zip 복수의 리스트를 각자의 순서에 맞추어 재결합

zip(1px 1px 3px, solid dashed solid, red green blue)
=> 1px solid red, 1px dashed green, 3px solid blue

Map Functions

map-get key로 value 취득

map-get(("foo": 1, "bar": 2), "foo") => 1
map-get(("foo": 1, "bar": 2), "bar") => 2
map-get(("foo": 1, "bar": 2), "baz") => null

Color Functions

adjust-hue 색상(hue) 변경

$base-color: #ad141e;

.adjust-hue {
  color: adjust-hue($base-color, 20%);
  // => #ad3d14
}

https://poiemaweb.com/img/adjust-hue.png

saturate desaturate채도(saturation) 변경

$base-color: #ad141e;

p {
  .saturate {
    color: saturate($base-color, 20%);
  }

  .desaturate {
    color: desaturate($base-color, 20%);
  }
}

https://poiemaweb.com/img/saturation.png

lighten darken 밝기 (lightness) 변경

$base-color: #ad141e;

p {
  .darken {
    color: darken($base-color, 10%);
  }

  .lighten {
    color: lighten($base-color, 10%);
  }
}

https://poiemaweb.com/img/lightness.png

rgba 투명도(opacity) 변경

$base-color: #ad141e;

.rgba {
  color: rgba($base-color, .7);
}

/*
.rgba {
  color: rgba(173, 20, 30, 0.7); }
*/

alpha transparentize alpha 연산

$base-color: rgba(255, 0, 0, 0.5);

// alpha +
// 불투명도를 증가시킨다.(더 불투명해진다)
.opacify {
  color: opacify($base-color, 0.3);
}

// alpha -
// 불투명도를 감소시킨다.(더 투명해진다)
.transparentize {
  color: transparentize($base-color, 0.25);
}

/*
.opacify {
  color: rgba(255, 0, 0, 0.8); }

.transparentize {
  color: rgba(255, 0, 0, 0.25); }
*/

tint shade Tint & Shade 색상은 흰색(tint)과 검정색(shade)의 값으로 혼합되며 darken, lighten과 유사하다.

$base-color: #ad141e;

.tint {
  color: tint($base-color, 10%);
}

.shade {
  color: shade($base-color, 10%);
}

https://poiemaweb.com/img/Tint&Shade.png

주석

CSS는 멀티 라인 주석 /* */만을 지원하지만,
Sass는 /* */// 모두 사용할 수 있다.
컴파일 후, 한 줄 주석 //은 CSS에서 사라지고, 멀티 라인 주석은 CSS에 나타난다.

유용한 사이트

Sass Play Ground

SassMeister | The Sass Playground!

Sass Guidelin

Sass Guidelines

Comments