[Sass] viewport에 반응하는 스타일 만들기

2024-04-10T20:04:52.000Z

데브시스터즈 기술블로그에 “쪼그라드는 웹페이지”라는 포스트가 있다. 해당 포스트에서는 CSS 전처리기 Sass를 사용하여, px 단위를 사용하여 반응형 UI를 만들 수 있는 방법을 소개하고 있는데, 포스트에서는 구체적인 동작 방식을 다음과 같이 설명하고 있다.

- PC 최소 사이즈보다 작은 화면에서는 모바일(태블릿) 화면을 보여준다
- 태블릿 정도 크기에서는 모바일 화면을 보여주고 좌우에 여백을 준다 (PC 화면처럼)
- 모바일 화면은 하나만 작업해서 기준 사이즈보다 작은 기기에서는 화면을 쪼그라뜨린다.

위와 같이 동작하기 위해 작성한 SassScript는 대부분의 경우 완벽하게 동작하나, 포스트 본문에도 언급되어 있듯이 프로퍼티 값으로 translate(), filter()와 같은 CSS 함수를 받는 경우는 대응할 수 없다는 한계가 있다.

🤔 왜 CSS 함수를 받는 경우를 대응할 수 없을까?

translate(), filter()와 같은 CSS 함수는 Sass에서 number가 아닌 string 타입으로 취급되는데, 데브시스터즈 팀에서 작성한 SassScript는 오직 타입이 number인 경우만 처리하고 있기 때문이다. 해당 내용은 아래에서 좀 더 자세히 다루도록 하겠다.

하지만 위 한계를 극복한, 즉 값으로 CSS 함수를 받는 경우도 정상적으로 처리될 수 있도록 하는 SassScript를 작성하는 법을 동료분께 배울 수 있었는데, 이를 다뤄보도록 하겠다.

분석

우선, 코드의 대략적인 구조를 살펴보자.

@use 'sass:map';
@use 'sass:list';
@use 'sass:math';
@use 'sass:string';

$mobile-contents-width: 720px;

$one-px: math.div(1px, $mobile-contents-width) * 100;

$digit-chars-map: (
  '0': 0,
  '1': 1,
  '2': 2,
  '3': 3,
  '4': 4,
  '5': 5,
  '6': 6,
  '7': 7,
  '8': 8,
  '9': 9,
);

@function to-length($value, $unit) { ... }

@function to-number($value) { ... }

@function remove-unit($value) { ... }

@function calc-viewport-width($value) { ... }

@function is-digit($char) { ... }

@function tokenization($str) { ... }

@mixin scale($property, $values) { ... }

전체 코드가 약 200 줄에 달하므로 @function, @mixin 각각의 전체 코드는 아래에서 다룬다.

오직 scale @mixin 1개만 정의했던 데브시스터즈 코드와 달리 여러개의 유틸 함수를 추가로 정의한데다, 기존 scale @mixin 또한 내부 로직이 상당히 복잡해진 것을 확인할 수 있다…

하지만 하나하나 살펴보면 그렇게 어려운 것은 아니다. 우선 유틸 함수를 차례로 분석하고, 마지막으로 scale @mixin을 분석해보도록 하자.

모듈과 전역변수

@use 'sass:map';
@use 'sass:list';
@use 'sass:math';
@use 'sass:string';

코드 최상단에는 위와같이 Sass의 built-in 모듈 4가지를 불러온다. 이를 통해 각 모듈이 제공하는 함수들을 사용할 수 있다.

각 모듈에서 제공하는 모든 함수는, 자신과 완전 동일한 기능을 제공하는 전역 함수 또한 존재한다.


예를 들어, 문자열의 길이를 반환하는 빌트인 모듈 함수 string.length()전역 함수 str-length()완전히 동일한 기능을 제공한다. 어찌 보면 모듈 대신 전역 함수를 사용하는 것이 @use문을 사용할 필요도 없어서 더 좋아보인다.


하지만, Sass 팀에서 전역 함수의 사용 대신 모듈을 사용할 것을 권장하고 있고, 미래에는 전역 함수의 유지보수를 중단할 것이라고 하였기 때문에, 신규 프로젝트에서 Sass를 사용하게 된다면 모듈을 사용하는 것이 좋을 것이다.

@use 문 다음에는 3개의 전역 변수를 선언하였다.

$mobile-contents-width: 720px;
// PC 뷰 최소 `width`를 의미한다.
// 이보다 viewport의 width가 작으면, 단위로 `px`대신 `vw`를 사용한다.

$one-px: math.div(1px, $mobile-contents-width) * 100;
// 모바일 뷰에서 사용할 기준단위이다.

$digit-chars-map: (
  '0': 0,
  '1': 1,
  '2': 2,
  '3': 3,
  '4': 4,
  '5': 5,
  '6': 6,
  '7': 7,
  '8': 8,
  '9': 9,
);
// 숫자형 문자를 key로, 숫자를 value로 갖는 map이다.
// 특정 값이 숫자형 문자인지 판별하기 위해 사용된다.

유틸함수

1️⃣ to-length

@function to-length($value, $unit) {
  @if $unit != 'px' {
    @error "Expected 'px', but got #{$unit}.";
  }

  @return $value * 1px;
}

to-length 함수는 바로 아래에서 다룰 to-number 함수 내부에서만 사용되며 $value, $unit를 인수로 받아 단위가 포함된 숫자값을 반환한다.

to-length(10, "px"); // 10px

각각의 매개변수는 다음을 의미한다.

  • $value: 단위가 존재하지 않는 순수 숫자값
  • $unit: 단위를 뜻하는 문자열
📝 Sass의 number 타입

Sass에서 number 타입의 값은 2가지 형태로 표현할 수 있다.

  • 1️⃣ 단위가 있는 경우: 10px, 1.5rem
  • 2️⃣ 단위가 없는 경우: 5, 10.31

단위의 유무만 다를 뿐, 두 형태 전부 number 타입임을 명심하자.

2️⃣ to-number

@function to-number($value) {
  @if type-of($value) == 'number' {
    @return $value;
  } @else if type-of($value) != 'string' {
    @error "Expected a string, but got #{$value}.";
  }

  $result: 0;
  $digits: 0;
  $minus: string.slice($value, 1, 1) == '-';

  @for $i from if($minus, 2, 1) through string.length($value) {
    $char: string.slice($value, $i, $i);

    // if문 🅰️
    @if not(list.index(map.keys($digit-chars-map), $char) or $char == '.') {
      @return to-length(
        if($minus, -$result, $result),
        string.slice($value, $i)
      );
    }

    // if문 🅱️
    @if $char == '.' {
      $digits: 1;
    } @else if $digits == 0 {
      $result: $result * 10 + map.get($digit-chars-map, $char);
    } @else {
      $digits: $digits * 10;
      $result: $result + math.div(map.get($digit-chars-map, $char), $digits);
    }
  }

  @return if($minus, -$result, $result);
}

to-number 함수는 숫자형 문자열 $value를 인수로 받아 진짜 숫자로 변환하여 반환하는 함수이다. 만약 값으로 진짜 숫자를 전달받는다면, 인수를 그대로 반환한다.

to-number("1.5");  // 1.5
to-number("10px"); // 10px
to-number(35);     // 35
🤔 굳이 to-number 함수가 필요한 것일까?

JS에서는 다음 방법으로 string 타입의 값을 number 타입의 값으로 변환할 수 있다.

  1. new 연산자 없이 Number 생성자 함수 사용
  2. parseInt, parseFloat 함수 사용
  3. + 단항 산술 연산자 사용

하지만 Sass는 JS처럼 stringnumber로 손쉽게 변환하는 방법을 지원하지 않기에, to-number와 같은 별도의 유틸 함수를 정의하여 사용하는 것이다.

Early Return 로직을 통과한 $valuestring 타입이며, $result, $digits, $minus 3가지 변수를 선언하였다.

  • $result: 숫자형 문자열 $value에서 단위를 제외한 순수 숫자값을 저장하는 변수
  • $digits: $result를 계산할 때 임시로 사용할 자릿수
  • $minus: 양수, 음수 여부를 boolean 값으로 저장하는 변수

JS에서 문자열은 유사 배열 객체array-like object이므로, 문자열의 i번째 인덱스에 해당하는 문자에 접근하기 위해서는 str[i]와 같이 할 수 있지만, Sass에서는 그러한 방식을 지원하지 않기 때문에 string.slice 함수를 사용해야 한다.

string.slice($value, $i, $i);

위와 같이 string.slice 함수를 사용하면, 문자열 $value의 i번째 문자에 접근할 수 있다.

Sass는 인덱스가 1부터 시작한다!

이후 반복문이 시작되며, 내부 코드 블럭은 if문 🅰️와 🅱️로 나뉜다. 설명을 쉽게 하기 위해 먼저 🅱️부터 살펴보자.

if문 🅱

// if문 🅱️
@if $char == '.' {
  // 다음 순회에서는 소수 부분을 다룸.
  // $digits을 사용해야 하므로 1로 초기화
  $digits: 1;
} @else if $digits == 0 {
  // 정수 부분을 다루는 경우 ($digits 사용할 필요 ❌)
  $result: $result * 10 + map.get($digit-chars-map, $char);
} @else {
  // 소수 부분을 다루는 경우 ($digits 사용할 필요 ✅)
  $digits: $digits * 10;
  $result: $result + math.div(map.get($digit-chars-map, $char), $digits);
}

if문 🅱️는 숫자형 문자열 $value에서 순수 숫자를 추출하는 로직을 담당한다.

if문 🅰️에서 $char가 숫자가 아닌 경우 함수를 종료하기 때문에, if문 🅱️에서 $char는 1️⃣숫자형 문자 또는 2️⃣.임이 보장되므로 추가로 예외 처리를 할 필요는 없다.

아래 예시를 통해 위 로직을 이해해보자.

["32.14px"$value로 전달받는 경우]

1) $char: 3     $digits: 0      $result: 3
2) $char: 2     $digits: 0      $result: 32 (3 * 10 + 2)
3) $char: .     $digits: 1      $result: 32
4) $char: 1     $digits: 10     $result: 32.1 (32 + 0.1)
5) $char: 4     $digits: 100    $result: 32.14 (32.1 + 0.04)

$char"p"가 되는 순간 if문 🅰로 코드 흐름이 이동하며, 이때 $result32.14가 된다.

if문 🅰️

// if문 🅰️
@if not(list.index(map.keys($digit-chars-map), $char) or $char == '.') {
  // $char가 숫자형 문자도 아니고, '.'도 아닌 경우
  @return to-length(
    if($minus, -$result, $result),
    string.slice($value, $i)
  );
}

만약 현재 문자 $char가 number도 아니고, .도 아니라면, $char는 단위 문자열의 첫 번째 문자임을 의미한다. 즉, $value40px이라면 $charp, "3rem"이라면 "r"을 의미한다.

$char가 단위 문자열의 첫 번째 문자라는 것은, 단위를 제외한 순수 숫자 부분은 전부 순회하여 그 처리결과를 $result 변수에 저장했음을 의미한다. 따라서 더 이상 반복문을 순회할 이유가 없으므로, 그 값을 to-length 함수에 넘긴다.


3️⃣ remove-unit

@function remove-unit($value) {
  @return math.div($value, ($value * 0 + 1));
}

해당 함수의 동작방식은 제법 특이한데, 인수로 전달받은 $value에서 px 단위를 제거한 순수 숫자값을 반환한다.

remove-unit(20px) -> 20

🤔어떻게 숫자 값에서 단위만 깔끔하게 제거할 수 있는 것일까? 공식 문서에서 math.div 함수를 어떻게 설명하고 있는 지 살펴보자.

"Any units shared both numbers will be canceled out."

즉, 나눗셈의 두 숫자의 단위가 서로 동일하다면 상쇄되어 없어진다!

math.div(1, 2);       // 0.5
math.div(100px, 5px); // 20 (상쇄!)
math.div(100px, 5);   // 20px
math.div(100px, 5s);  // 20px/s

4️⃣ calc-viewport-width

@function calc-viewport-width($value) {
  @if type-of($value) == "number" and unit($value) == "px" {
    @return remove-unit($value * $one-px * 2) + vw
  } @else {
    @return $value;
  }
}

calc-viewport-width 함수는 인수로 전달받은 $valuenumber 타입이고 단위가 px일 때, $valuevw 단위로 변환하여 반환한다.

변수 $one-px은 위에서 살펴보았던 전역 변수로, 1px$mobile-viewport-width로 나눈 뒤 100을 곱한 값인, 단위가 존재하지 않는 순수 숫자값이다.

하지만 인수로 전달받은 $value에는 px 단위가 붙어있기 때문에, 표현식 $value * $one-px * 2단위가 포함number 타입의 값이 된다. 모바일 뷰에서는 px 대신 vw를 사용해야 하므로, 위에서 다뤘던 remove-unit 함수를 사용하여 단위를 제거한 뒤, + 연산자를 사용해 문자열 vw를 뒤에 붙인 값을 반환한다.

📝 Sass의 string 타입

Sass에서 string 타입의 값은 2가지 형태로 표현할 수 있다.

  • 1️⃣ 따옴표가 있는 경우 (quoted string): "Helvetica Neue"
  • 2️⃣ 따옴표가 없는 경우 (unquoted string): bold, solid

calc-viewport-width 함수는 내부에서 🅰️"px", 🅱️vw란 표현식을 사용하는 데, 따옴표 유무만 다를 뿐 전부 string 타입이다.


참고로, unquoted string 중 아래 4가지string으로 취급되지 않는다!

  • 1️⃣ 색상을 의미하는 문자열 (color 타입으로 취급됨)
  • 2️⃣ null
  • 3️⃣ true, false
  • 4️⃣ not, and, or
🚨 vw를 quoted string 형태로 더하면 안되는 이유
@return remove-unit($value * $one-px * 2) + vw

calc-viewport-width 함수는 number 타입인 remove-unit 함수의 반환값에 unquoted string 형태의 문자열 vw를 더함으로써, 최종적으로 string 타입의 값을 반환한다.

@return remove-unit($value * $one-px * 2) + "vw"

하지만, vw"vw" 둘 다 Sass에서 지원하는 문자열 표기형식이다. 그렇다면 위와 같이 작성해도 정상적으로 동직하지 않을까?

@include scale(border, 100px solid black);

위와 같이 scale mixin을 사용했다고 하자. 예상과는 달리, 아래처럼 vw로 변환된 값에 따옴표가 붙게 되어, number가 아닌 string 타입의 값을 CSS 속성으로 사용하는 것을 확인할 수 있다…


이렇게 동작하는 이유는 Sass만의 문자열 연산 특징 때문이다.


400 + vw   // 400vw
400 + "vw" // "400vw"

, number 타입의 값이 string 타입의 값으로 잘못 사용되는 것을 방지하기 위해, unquoted string 형태의 vw를 사용하는 것이다.

5️⃣ is-digit

@function is-digit($char) {
  @if type-of($char) == 'string' {
    @return map.get($digit-chars-map, $char) != null;
  } @else {
    @return false;
  }
}

is-digit 함수는 인수로 전달받은 단일 문자열 $char가 숫자형 문자라면 true를, 아니라면 false를 반환한다.

함수 내부에서 사용된 빌트인 함수 map.get($map, $key)는 첫번째 인수로 전달받은 map 요소들 중에서 두번째 인수로 전달받은 key와 일치하는 키가 존재한다면 해당 요소의 값을, 존재하지 않는다면 null을 반환한다.

6️⃣ tokenization

@function tokenization($str) {
  @if type-of($str) == 'string' {
    $tokens: ();
    $cursor: 0;

    @if string.index($str, '(') == null or string.index($str, ')') == null {
      @return $tokens;
    }
    @if string.index($str, 'rem') != null or string.index($str, 'em') != null {
      @error "Expected 'px', but got something else.";
    }

    @for $i from 1 through string.length($str) {
      $curr: string.slice($str, $i, $i);
      $next: string.slice($str, $i + 1, $i + 1);

      @if $curr == 'p' and $next == 'x' {
        $chars: '';
        $is-paused: false;

        @for $j from $i - 1 through 0 {
          @if $is-paused {
            // sass doesn't support break statement
          } @else {
            $curr: string.slice($str, $j, $j);

            @if is-digit($curr) or $curr == '.' or $curr == '-' {
              $chars: $curr + $chars;
            } @else {
              $is-paused: true;
            }
          }
        }

        $tokens: list.append(
          $tokens,
          ($chars + 'px', $i - string.length($chars))
        );
      }
    }

    @return $tokens;
  } @else {
    @error "The argument must be a string.";
  }
}

tokenization 함수는 translate(), filter()와 같이 CSS 값으로 함수가 사용된 경우를 대응하기 위해 사용되는 함수이다. 따라서 인수로 전달받은 $str이 CSS 함수이며, px 단위를 사용하는 경우에만 로직을 수행한다.

📝 Sass의 for 문 사용방법

Sass에서는 to, through 두가지 키워드를 통해 순회를 몇 회 할 것인지를 결정할 수 있다.

  1. to N: N - 1 까지 반복 (i < N)
  2. through N: N까지 반복 (i <= N)

Sass의 반복문은 JS처럼 반복문 변수를 증가시킬 지 감소시킬 지를 명시적으로 정할 수 없다! 대신 반복문 변수 초기값이 to 혹은 through 키워드 뒤에 오는 값보다 크고 작은지의 여부에 따라 증감이 자동으로 결정된다.

@for $i from 1 to 5 {
  // (증가) 1, 2, 3, 4, 5
}

@for $i from 1 through 5 {
  // (증가) 1, 2, 3, 4
}

@for $i from 5 to 1 {
  // (감소) 5, 4, 3, 2
}

@for $i from 5 through 1 {
  // (감소) 5, 4, 3, 2, 1
}

이러한 동작방식을 인지한 상태에서, tokenization 함수의 for 문을 살펴보자.

@for $i from 1 through string.length($str) {
  $curr: string.slice($str, $i, $i);
  $next: string.slice($str, $i + 1, $i + 1);

  @if $curr == "p" and $next == "x" {
    $chars: "";
    $is-paused: false;

    @for $j from $i - 1 through 0 {
      @if $is-paused {
        // sass doesn't support break statement
      } @else {
        $curr: string.slice($str, $j, $j);

        @if is-digit($curr) or $curr == "." or $curr == "-" {
          $chars: $curr + $chars;
        } @else {
          $is-paused: true;
        }
      }
    }

    $tokens: list.append($tokens, (
      $chars + "px",
      $i - string.length($chars),
    ));
  }
}

인수로 전달받은 문자열 $str을 순회하며, 현재 인덱스에 해당하는 문자를 $curr, 다음 인덱스에 해당하는 문자를 $next에 저장한다. 만약 $curr === "p"이고 $next === "x"라면, 이는 px 단위를 사용하는 숫자형 문자열임을 의미한다. 그렇다면 현재 인덱스인 $i를 기준으로, px 단위를 제외한 숫자형 문자열을 추출해야 한다.

tokenization(translateX(50px);

위와 같이 tokenization의 인수로 전달받은 값이 translateX(50px)라고 해보자. 조건식 @if $curr == "p" and $next == "x"이 true로 평가되는 순간은 $i가 13일 때이다.

그렇다면 아래와 같이 $i - 1를 초기값으로 하는 $j를 통해 $str을 역순으로 순회하며, px 단위에 어떤 숫자값이 사용되었는 지를 찾아낸다.

1) $j: 12    $curr: "0"    $chars: "0"
2) $j: 11    $curr: "5"    $chars: "50"
3) $j: 10    $curr: "("    $chars: "50" (전부 찾았음)

$j가 10이 되었을 때 $curr의 값은 숫자가 아니며, 이는 $str$i번째 인덱스에 위치한 px앞에 사용된 숫자값을 전부 찾았음을 의미한다. 이 경우 더 이상 반복문을 수행할 필요는 없지만, Sass는 break 문을 지원하지 않아 반복문을 탈출할 수 없다! 따라서 $is-paused 변수를 추가로 선언해 해당 값이 true라면 반복문 로직을 수행하지 않도록 한다.

$chars에는 “50”이 저장되어 있고, $i - string.length($chars)는 13 - 2 = 11이므로, 결과적으로 ("50px", 11)이 반환된다.

🤔 값("50px") 뿐만 아니라 인덱스(11)도 리스트에 저장하는 이유는?

scale mixin을 구조를 설명할 때 다시 다루겠지만, CSS 함수에 전달된 px 형태의 인수를 vw 형태로 치환하려면, px 형태의 값이 문자열 어디에 위치하는 지를 알아야 하기 때문이다.

$expression: "translateX(40px)";
$from: 40px;
$to: 11.1111111111vw;

$start-index: string.index($expression, $from); // 11
$result = '';
$cursor = 1;

$part: string.slice($expression, $cursor, $start-index - 1) + $to;
$result = $result + $part;
// $result: translateX(11.1111111111vw

$cursor = $start-index + string.length($from); // 15
$result = $result + string.slice($expression, $cursor, string.length($expression));
// $result: translateX(11.1111111111vw)

scale mixin

@mixin scale($property, $values) {
  $attributes: ();
  $mobile-attributes: ();

  @each $value in $values {
    @if type-of($value) == 'string' {
      // 함수인 경우
      $tokens: tokenization($value);
      $cursor: 1;
      $result: '';
      $expression: $value;

      @each $token in $tokens {
        $raw: list.nth($token, 1);
        $start-index: list.nth($token, 2);
        $viewport-width: calc-viewport-width(to-number($raw));

        $part: string.slice($expression, $cursor, $start-index - 1) +
          $viewport-width;
        $cursor: $start-index + string.length($raw);
        $result: $result + $part;
      }

      $result: $result +
        string.slice($expression, $cursor, string.length($expression));

      $attributes: list.append($attributes, $value);
      $mobile-attributes: list.append(
        $mobile-attributes,
        string.unquote($result)
      );
    } @else if type-of($value) == 'number' {
      // 단위를 사용하는 숫자인 경우
      $viewport-width: calc-viewport-width($value);
      $attributes: list.append($attributes, $value);
      $mobile-attributes: list.append($mobile-attributes, $viewport-width);
    } @else {
      // 단위를 사용하지 않는 값인 경우  (ex. black, rgba(0, 0, 0, 0.12) 등)
      $attributes: list.append($attributes, $value);
      $mobile-attributes: list.append($mobile-attributes, $value);
    }
  }

  @if length($attributes) > 0 {
    #{$property}: $attributes;
  }

  @if length($mobile-attributes) > 0 {
    @media (max-width: $mobile-contents-width) {
      #{$property}: $mobile-attributes;
    }
  }
}

scale mixin은 전체 코드 중 가장 복잡한 로직을 처리한다. translate(), filter()와 같은 CSS 함수를 포함한 모든 값을 처리할 수 있도록 작성되었다.

로직 최상단에는 CSS 속성을 담을 빈 list 2개를 선언하며, 각각 PC, 모바일 뷰포트에 대응하는 속성을 담을 것이다.

이후에는 @each 문을 사용하여 인수로 전달받은 list 타입의 $values를 순회하며, $values의 각 요소인 $value의 타입이 무엇인지에 따라 별도의 처리를 한다.

$value가 string 타입인 경우

다음과 같이 2가지 경우를 예상할 수 있다.

// 1️⃣ 함수 ❌
@include scale(border, 1px solid black); // 1px: number, solid: string, black: color
@include scale(display, none);

// 2️⃣ 함수 ⭕️
@include scale(transform, translateX(50px));
1️⃣ 함수 ❌

$value함수의 형태가 아니므로, tokenization($value)의 반환값은 항상 빈 리스트이다. 따라서 별도의 문자열 치환 로직을 수행하지 않고, $value를 그대로 사용한다.

2️⃣ 함수 ⭕️

$value가 함수의 형태이므로, tokenization($value)의 반환값인 리스트가 비는 경우는 없다. 리스트의 각 요소는 (1️⃣숫자형 문자열, 2️⃣해당 문자열 시작 인덱스) 형태이다.

$value를 순회하며, tokenization 함수를 통해 반환된 리스트의 각 요소를 순회하며, calc-viewport-width 함수를 통해 px 단위를 vw 단위로 변환한다.

$value: "translateX(50px)";

$tokens: tokenization($value); // (("50px", 11))
$cursor: 1;
$result: '';
$expression: $value;

$raw: list.nth($token, 1);         // "50px"
$start-index: list.nth($token, 2); // 11

$viewport-width: calc-viewport-width(to-number($raw)); // 11.1111111111vw

$part: string.slice($expression, $cursor, $start-index - 1) + $viewport-width;
$result: $result + $part;
// $part: translateX(11.1111111111vw

$cursor: $start-index + string.length($raw); // 15
$result: $result + string.slice($expression, $cursor, string.length($expression));
// $result: translateX(11.1111111111vw)

위와 같이 $result 값을 구한 뒤, list.append 함수를 통해 PC, 모바일 뷰포트에 대응하는 속성을 담는 list에 각각 추가한다.

$attributes: list.append($attributes, $value);
$mobile-attributes: list.append(
  $mobile-attributes,
  string.unquote($result)
);
🤔 string.unquote 함수로 따옴표를 제거하는 이유는?
$mobile-attributes: list.append(
  $mobile-attributes,
  $result
);

$mobile-attributes 에 값을 추가할 때 string.unquote 함수를 쓰지 않으면 어떻게 될까?


그렇다면 위와 같이 문자열 값이 전부 quoted string 형태로 저장되는데, 이는 올바른 CSS 문법이 아니다. 따라서 string.unquote 함수를 사용하여 따옴표를 제거한 뒤에 리스트에 값을 추가해야 한다.

$value가 number 타입인 경우

이 경우는 $value가 단위를 포함한 숫자인 경우이다. 이때는 calc-viewport-width 함수를 통해 px 단위를 vw 단위로 변환한다.

$value가 stringnumber 타입도 아닌 경우

이 경우는 $valueblack, rgba(0, 0, 0, 0.12)와 같이 단위를 사용하지 않는 값인 경우로, 대부분 color 타입의 값일 것이다. 이 경우는 $value를 그대로 사용한다.

✅ 최종 결과

최종 결과

내 블로그에 적용한 결과는 위와 같다. CSS 함수를 값으로 전달받는 경우를 처리하는 복잡한 로직을 추가로 수행해야 하다 보니, UI 변경이 빠르게 이루어지지 않는다는 점은 조금 아쉽다.

하지만 불편함이 느껴지는 수준은 아니고 “약간 아쉽다” 정도이고, 그 대신 CSS 함수를 scale mixin에 사용할 수 있다는 편리함을 얻었으니, trade off로 충분히 받아들일 만하다.

이처럼 개선된 scale 로직을 내 블로그에 적용시켜보고 동작 방식도 자세히 살펴보았는데, 덕분에 Sass 이해도가 몇 배는 더 높아져서 매우 기쁘다 😄

추가로, 여기 Sass Playground 링크에서 scale 코드를 테스트할 수 있도록 했으니, 혹시 관심이 있다면 들어가서 이것저것 건드려보길 바란다.

🔗 레퍼런스


Greenhead
Written by@Greenhead
프론트엔드 개발자 윤녹두입니다 :)

GitHub