[번역] CSS @function의 기본 원리와 개발자 경험

mocossi·2026년 6월 18일

해외 Articles

목록 보기
1/6

원문 : https://master.dev/blog/the-fundamentals-and-dev-experience-of-css-function/


CSS는 이제 함수(Function) 기능을 도입하면서, 개발자가 스타일시트 전체에서 속성의 동작을 캡슐화하고 재사용할 수 있게 되었습니다. 덕분에 같은 코드를 반복해서 작성할 필요가 없어졌고, 단 한 번만 사용되는 중간 1) --_variables 를 만들기 위해 DOM을 불필요하게 복잡하게 만들 필요도 줄어들었습니다.

함수를 활용하면 정말 다양하고 유용한 작업들을 할 수 있습니다. 이번 기초 아티클에서는, 다음 글에서 다룰 핵심 내용을 제대로 이해하기 전에 먼저 알아야 할 몇 가지 CSS의 특성과 주의할 점을 살펴보려고 합니다.

쉽게 말해, 다음 단계에서 제대로 활용하기 위한 기본 규칙과 한계가 무엇인지, 그리고 CSS 함수가 할 수 있는 것과 할 수 없는 것이 무엇인지 먼저 이해해보겠습니다.

이것이 커스텀 CSS 함수입니다.

@function --hello-world() {
  result: "Hello World";
}

그리고 아래와 같은 방식으로 해당 함수를 호출할 수 있습니다.

body::after {
  content: --hello-world() "!";
}

곧 지원될 예정이지만 아직은 불가능한 기능으로, 앞으로는 하나의 함수 호출만으로 서로 다른 여러 속성에 각각 다른 값을 설정할 수 있게 될 예정입니다.
이를 위해 함수가 쉼표(,)로 구분된 여러 결과값을 반환하고, 이를 각각의 속성에 나누어 적용하는 방식이 도입될 예정입니다.

주의할 점

현재 시점에서는 함수가 오직 하나의 속성 값만 반환할 수 있습니다.
정확히 말하면, 하나의 속성 전체 값 또는 그 일부만 반환할 수 있습니다.

만약 커스텀 함수를 호출해서 --variable 값을 설정했다면, 해당 --variable은 이후 원하는 만큼 여러 번 참조할 수 있습니다.
즉, 함수가 반환한 하나의 결과값을 필요한 곳 어디에서든 반복해서 재사용할 수 있다는 의미입니다.


함수 캡슐화(Function Encapsulation)

함수 내부에서 사용하는 값들을 외부와 분리해서 관리하는 개념입니다.

함수 내부에서는 최종 결과를 계산하기 위해 중간 변수를 선언해서 사용할 수 있습니다.

@function --the-answer() {
  --a: 4px;
  --b: 10;
  --c: 2px;
  result: calc(var(--a) * var(--b) + var(--c));
}

이렇게 선언한 중간 변수들은 요소 외부로 노출되지 않습니다.
즉, 이 변수들은 함수 내부에서만 사용되는 내부 전용 변수입니다.

body {
  padding: --the-answer();
  /* --a, --b, and --c are NOT defined here! */
}

주의할 점

함수 안의 변수는 너무 독립적이라서, 밖에서 @property 같은 전역 설정으로 타입을 지정하는 것도 불가능합니다.

@property --a {
  syntax: "<color>";
  initial-value: hotpink;
  inherits: true;
}

@function --the-answer() {
  --a: 4px; /* uses the value 4px and doesn't break */
  --b: 10;
  --c: 2px;
  result: calc(var(--a) * var(--b) + var(--c));
}

body {
  padding: --the-answer();
  --a: 4px;
  background: var(--a);
}

body 요소의 padding 값은 42px 이고, 배경색(background)은 hotpink 로 적용됩니다.


함수 인자 (Function Arguments)

함수를 호출할 때 인자를 전달할 수 있습니다.

body {
  padding: --the-answer(99);
}

주의할 점

위의 함수는 오류가 발생하더라도 아무런 에러 메시지 없이 조용히 실패(silent failure) 합니다.
즉, 개발자가 문제를 디버깅할 수 있도록 친절한 개발 경험(DX)을 제공하지 않습니다.

또한, @function 에서 정의한 개수보다 더 많은 매개변수(parameter)를 전달해서 함수를 호출할 수 없습니다.

아마 앞으로는 이 부분의 개발자 경험(DX) 이 개선되어,
추가로 전달된 매개변수를 단순히 무시하도록 바뀔 수도 있을 것입니다. 🤞👽

다행히도, 원하는 개수만큼 인자를 정의하는 것 자체는 매우 간단합니다.

@function --the-answer(--arg1, --arg2, --arg3) {
  --a: 4px;
  --b: calc(var(--arg1) - var(--arg1) + 10);
  --c: 2px;
  result: calc(var(--a) * var(--b) + var(--c));
}

주의할 점

반대로 함수를 호출할 때 필요한 인자보다 적은 개수의 값을 전달해도,역시 별도의 오류 메시지 없이 조용히 실패합니다. 즉, 디버깅할 수 있는 힌트조차 제공되지 않습니다.

하지만 함수 인자에 기본값을 설정할 수 있으며, 이렇게 하면 해당 인자는 선택적으로 사용할 수 있습니다.

@function --the-answer(--required, --arg2: 0px, --arg3: initial) {
  --a: 4px;
  --b: calc(var(--required) - var(--required) + 10);
  --c: calc(clamp(1px, round(var(--arg2)), 1px) * 2);

  result: calc(var(--a) * var(--b) + var(--c));
}

body {
  padding: --the-answer(99);
}

결과적으로 body 요소의 padding 값은 안정적으로 42px 이 적용됩니다.

특히 initial 값은 기본값으로 매우 유용합니다.

왜냐하면 구현 내부에서 var(--arg3, fallback) 형태로 사용할 수 있어서, 상황에 따라 동작을 분기(branch) 할 수 있기 때문입니다.

현재처럼 조용히 실패하는 방식 대신, initial 값이 기본 인자로 자동 설정된다면 개발자 경험(DX)이 훨씬 좋아질 것입니다.

또한 함수 인자에 대해 if(style()) 를 사용해서 조건 분기를 만들 수도 있습니다.

다만 이 방식에도 추가적으로 알아두어야 할 여러 가지 제한 사항이 존재합니다.


타입이 지정된 함수 인자와, 타입 캡슐화를 위한 가짜(Fake) 인자 활용

함수 인자에는 타입을 지정할 수 있습니다.

또한 공식적인 대안이 나오기 전까지의 임시 방법으로, 의도적으로 불필요하고, 문서화되지 않았으며, 실제로 사용하지 않는 타입 지정 인자를 기본값과 함께 추가할 수 있습니다.

이렇게 하면 함수 내부에서 가짜로 등록된 변수 처럼 동작하게 만들 수 있습니다.

왜냐하면 전역 변수 등록은 함수 내부까지 영향을 주지 않기 때문입니다.

@function --divide-by-3(--a <number>, --_pi-ish <integer>: -1) {
  --_pi-ish: calc(3.14);
  /* ^ becomes 3 because it's an integer type */

  result: calc(var(--a) / var(--_pi-ish));
}

함수 내부에서 타입이 지정된 변수는 일반적인 등록 변수보다 매우 중요한 장점을 가집니다.
만약 전달된 값이 지정된 타입으로 계산될 수 없다면, 해당 변수는 자동으로 initial 상태가 됩니다.

그리고 이것은 곧 계산된 fallback 값 을 사용할 수 있다는 의미입니다.

주의할 점

기본적인 전역 동작 방식에서는, 등록된 변수를 var() 함수로 참조할 경우
var() 에 지정한 fallback 값이 절대 실행되지 않게 됩니다. 😵‍💫🪦

@property --a {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

body {
  --a: pizza;
  --divide-by: 3;

  opacity: var(--a,
    calc(
      1 / var(--divide-by)
    )
  );
}

결과적으로 opacity 값은 0으로 계산됩니다.
왜냐하면 fallback 안에 있는 calc() 코드가 실행될 수 없는 상태이기 때문입니다.

이제 이것을 커스텀 함수 구현 방식과 비교해보겠습니다

@function --opacity(--a <number>, --divide-by <integer>: -1) {
  --divide-by: calc(3.14);
  result: var(--a, calc(1 / var(--divide-by)));
}
body {
  opacity: --opacity(pizza);
}

opacity 값은 0.3333 으로 계산됩니다.

왜냐하면 "pizza" 는 숫자가 아니기 때문에 --a 변수는 initial 상태가 되었고,
그 결과 fallback 에 있는 calc() 가 대신 실행되었기 때문입니다.

주의할 점

만약 3.14 값을 calc() 로 감싸지 않았다면, 정수 타입으로 지정된 인자는 initial 상태로 변경되지 않습니다.
그 이유는 소수 문법 자체가 계산 단계에 도달하기 전에, 정수가 아니라는 이유로 먼저 거부되기 때문입니다.

@function --opacity(--a <number>, --divide-by <integer>: -1) {
  --divide-by: 3.14;

  result: var(--a, calc(1 / var(--divide-by, 2)));
}
body {
  opacity: --opacity(pizza);
}

pizza는 숫자 타입이 아니므로 --ainitial 상태가 되었고, 이에 따라 fallback calc()가 실행되었습니다. 또한 3.14 값 할당이 실패하면서 --divide-by 역시 기본값인 2를 사용하게 되어, 최종적으로 opacity 값은 0.5로 계산됩니다.


쉼표로 구분된 인자 (Comma-Separated Arguments)

주의할 점 (Gotcha)

CSS 전체에서 변수가 원래 위치에서 그대로 확장되지 않는 거의 유일한 경우는, 커스텀 함수를 호출할 때 전달하는 매개변수 내부입니다.

  • 요약 : 일반 CSS에서는 변수 값이 그대로 펼쳐지지만, 커스텀 함수의 인자에서는 다르게 동작합니다.
body {
  --rgb: 0, 255, 0;
  background: rgb(var(--rgb));
}

배경색은 밝은 초록색(bright green) 으로 적용됩니다.

@function --rgbFn(--r, --g, --b) {
  result: rgb(var(--r), var(--g), var(--b));
}

body {
  --rgb: 0, 255, 0;
  background: --rgbFn(var(--rgb));
}

함수 호출은 실패했습니다.
그 이유는 전달한 3개의 인자가 모두 --r 하나의 인자 안으로 한꺼번에 들어가 버렸기 때문입니다.
이 부분은 앞으로 수정되기를 기대하고 있습니다.

주의할 점

현재는 의도적으로 값이 펼쳐지지 않도록 만들기 위해, 값을 중괄호 {} 로 감싸는 문법이 구현되어 있습니다.

@function --rgbFn(--rgbArg) {
  result: rgb(var(--rgbArg));
}

body {
  --r: 0;
  --g: 255;
  --b: 0;
  background: --rgbFn({ var(--r), var(--g), var(--b) });
}

배경색은 밝은 초록색으로 적용됩니다. 일관성을 유지하기 위해 원래는 Google Chrome 팀의 Anders(그동안 제가 사용해온 멋진 기능 대부분을 구현한 개발자입니다!)가 쉼표로 구분된 var() 값들이 일반적인 CSS 동작처럼 자동으로 펼쳐지도록 설계했고, 실제로 잠시 구현되기도 했습니다.

그 경우 동일한 펼쳐짐 방지 효과를 얻기 위해서는 의도적으로 var() 를 중괄호로 감싸야 했습니다.

@function --rgbFn(--rgbArg) {
  result: rgb(var(--rgbArg));
}
body {
  --rgb: 0, 255, 0;
  background: --rgbFn({ var(--rgb) });
}

배경색은 밝은 초록색으로 적용됩니다.

현재 변수에 대한 anti-spread 동작은 여전히 구현되어 있습니다.
따라서 앞으로 이 기능이 복구되거나 수정될 가능성을 고려한다면,
쉼표로 구분된 var() 인자들을 미리 중괄호 {} 로 감싸두는 것이 좋은 방법입니다.
다만 현재는 커스텀 repeat 함수커스텀 loop 함수를 제외하면,
실질적으로 이 기능을 활용할 방법이 거의 없습니다.
왜냐하면 아직은 내부적으로 값을 처리할 수 있는 기능이 없기 때문입니다.
그래서 현재로서는 전달된 값을 그대로 사용하는 것 외에는 할 수 있는 것이 없습니다.
즉 현재 시점에서는 일반적인 --var 변수만으로도
csvargument(쉼표로 구분된 값)에 할 수 있는 대부분의 작업이 가능합니다.
그렇기 때문에 굳이 이것을 커스텀 함수에 전달하는 것은 별 의미가 없습니다.
아마 아직 실제로 이렇게 사용해본 사람도 거의 없을 것입니다.

하지만 앞으로 csvarguments가 일반 함수 호출처럼, 그리고 다른 CSS 기능들처럼 자동으로 펼쳐지는 방식으로 동작하게 된다면, 수백 가지 새로운 가능성이 열리게 됩니다.

예를 들면 하나의 함수 호출만으로 여러 개의 값을 반환 하는 것도 가능해집니다.

왜냐하면 리스트에서 각각의 값을 꺼낼 수 있는 --nth-item() 같은 함수를 쉽게 만들 수 있기 때문입니다.

요약 : CSS 함수가 JavaScript 함수처럼 여러 값을 반환하는 방향으로 발전할 수 있다는 뜻입니다.

@function --nth-item(--nth, --p0, --p1) {
  result: if(
    style(--nth: 0): var(--p0);
    style(--nth: 1): var(--p1);
    else: black;
  );
}

body {
  --x: 1;
  --arrayOfArgs: skyblue, lime;
  --bg: --nth-item(var(--x), var(--arrayOfArgs));
  background-color: var(--bg);
}

여기까지가 우리가 알아야 할 핵심 내용의 대부분입니다.

중간중간 꽤 복잡하고 헷갈리는 제한 사항들이 많았지만,
만약 지금까지 내용을 이해했고 이러한 동작 방식을 다룰 수 있다면,
여러분은 이미 CSS 변수를 깊이 이해하고 있으며,
커스텀 CSS 함수의 잠재력을 제대로 활용할 수 있는 단계에 꽤 가까워진 것입니다.

개발자 관점 한 줄 요약:

현재 CSS @function 은 쉼표로 구분된 여러 인자를 아직 완벽하게 처리하지 못하지만,
이 부분이 개선되면 하나의 함수에서 여러 값을 반환하는 등 CSS의 표현력이 크게 확장될 가능성이 있다.


함수 반환값 (Function Results)

커스텀 CSS 함수의 기본 개념과 관련해 몇 가지 더 알아둘 내용이 있습니다.
함수에서는 인자를 정의한 뒤, returns 지시어를 사용해 반환값의 타입도 지정할 수 있습니다.

쉽게 말하면 함수가 어떤 타입의 값을 반환해야 하는지 미리 선언할 수 있습니다.

@function --opacity(--a <number>, --divide-by <integer>: -1) returns <number> {
  --divide-by: calc(3.14);
  result: var(--a, calc(1 / var(--divide-by, 2)));
}
body {
  opacity: --opacity(pizza); /* 0.3333 */
}

주의할 점

함수 인자와 마찬가지로,만약 함수가 반환한 값이 지정된 반환 타입과 일치하지 않으면 해당 함수는 initial 값을 반환하게 됩니다.

함수 내부에서는 다른 함수를 호출할 수 있습니다.

주의할 점

현재 함수는 자기 자신을 다시 호출할 수 없습니다. 즉, 재귀 호출은 지원되지 않습니다.
그 이유는 CSS가 이런 구조를 순환 참조로 판단하기 때문이며, 결과적으로 함수는 initial 상태로 실패하게 됩니다.

주의할 점

함수는 값을 반환할 수 있지만, 현재는 그 반환된 값을 다시 같은 함수에 전달해서 재사용할 수 없습니다.
이 동작은 사실상 버그처럼 느껴질 정도로 상당히 문제가 있어 보입니다.
이 문제가 해결되기 전까지는, 단순한 calc() 수준이 아닌 대부분의 수학 기반 커스텀 함수 는 사실상 사용할 수 없는 상태(DOA, Dead On Arrival)입니다.
그리고 동적으로 조합하는 고급 기능 역시 거의 불가능합니다.

게시 전 수정: 제가 Google Chrome 측에 보고했던 이슈에 대해 Tab이 의견을 남겼는데,
처음에는 “기술적으로 버그는 아니다” 라고 판단되었지만,
결국 이것이 명세 수준의 버그 라는 것이 확인되었고,
곧 수정될 예정이라고 합니다.

@function --add-a-quarter(--a <number>) returns <number> {
  result: calc(var(--a) + 0.25);
}
body {
  --quarter: --add-a-quarter(0);
  --half: --add-a-quarter(var(--quarter));
  opacity: var(--half);
}

현재 --half 변수는 initial 상태가 됩니다. 😵‍💫🪦💔


주의해야 할 함정들

CSS Custom Function의 개발자 경험(DX)을 정리해보면, 기본적인 기능만 살펴보는 과정에서도 아래와 같은 여러 가지 제한 사항들을 마주하게 됩니다.

  • 아직은 지원되지 않지만, 앞으로는 하나의 함수 호출만으로 여러 개의 서로 다른 값을 설정할 수 있게 될 예정입니다.
  • 함수 내부에서 사용하는 변수들은 매우 강하게 캡슐화되어 있어, 전역 등록조차 해당 변수의 타입을 지정할 수 없습니다.
  • 함수를 호출할 때 정의된 개수보다 많은 인자를 전달하면, 디버깅에 도움이 되는 오류를 반환하지 않고 아무런 메시지 없이 조용히 실패합니다.
  • 반대로 필요한 인자보다 적은 값을 전달했고 기본값도 설정되어 있지 않다면, 이 역시 디버깅할 수 있는 정보 없이 조용히 실패합니다.
  • 기본적인 전역 동작 방식에서는, 등록된 변수를 var() 함수로 참조하면 var() 에 설정한 fallback 값에 도달할 수 없게 됩니다.
  • 정수 타입으로 지정된 인자에 3.14 값을 직접 할당할 경우, 이를 calc() 로 감싸지 않으면 계산 단계 이전에 소수 문법이 정수가 아니라는 이유로 거부되어 initial 상태로 실패하게 됩니다.
  • 현재 기준으로 CSS 전체에서 변수가 원래 위치에서 그대로 확장되지 않는 유일한 경우는, 커스텀 함수를 호출할 때 전달되는 매개변수 내부입니다.
  • 쉼표로 구분된 인자를 의도적으로 펼쳐지지 않게 만들기 위해, 중괄호 {} 로 감싸는 anti-spreading 문법이 현재 구현되어 있습니다.
  • 함수 인자와 마찬가지로, 함수의 반환값이 지정된 반환 타입과 일치하지 않으면 함수는 initial 값을 반환합니다.
  • 현재 함수는 자기 자신을 호출할 수 없습니다. 재귀 호출은 지원되지 않으며, CSS는 이를 순환 참조로 판단해 initial 상태로 실패 처리합니다.
  • 함수는 값을 반환할 수 있지만, 현재는 반환된 값을 다시 동일한 함수에 전달해 사용할 수 없습니다. 이는 명세 단계의 버그이며, 현재 Tab이 수정 작업을 진행하고 있습니다. 🙏

이제 시작일 뿐입니다

전체적으로 현재 시점의 CSS Custom Function 개발자 경험(DX)은… 솔직히 좋지 않습니다.
하지만 잠재력은 상당히 크고, 아직 기능이 제한적이더라도 지금 당장 활용할 수 있는 것들도 꽤 많습니다.

여기까지가 기본 개념입니다.

다음 글에서는 제가 가장 기대하고 있는 주제인 CSS @function 의 Scope 에 대해 더 깊게 이야기해보겠습니다.

그때까지 자유롭게 의견이나 이야기를 나눠주세요. 💚👽


1) 그동안 CSS 라이브러리와 컴포넌트를 만드는 개발자들은, CSS 변수 이름 앞이나 뒤에 (underscore)를 붙이는 관례를 사용해 내부 전용변수와 외부 개발자에게 공개되는 API용 변수를 구분해왔습니다. 이제는 마침내 CSS에서 공식적으로 private 변수를 다룰 수 있는 방법이 생긴 것입니다. 🎉

profile
Design + FE = 🔥

0개의 댓글