CSS3 Custom Properties

CSS Custom Properties (커스텀 속성)

(https://www.smashingmagazine.com/2017/04/start-using-css-custom-properties/ 문서를 토대로 번역하고, 다른 문서들을 조합해 정리하였다.)

1. 전처리기 변수와 CSS 커스텀 속성

CSS 전처리기는 웹 개발에서 중요한 역할을 수행하고 있다. (Sass, Stylus, Less, PostCSS 등)
전처리기의 주요 장점 중 하나는 변수를 사용할 수 있다는 것인데, 변수를 사용하면 반복적인 코드를 피할 수 있고, 개발과 리팩토링이 간단해질 수 있다.

그러나, 전처리기 변수에는 다음과 같은 몇가지 한계가 있다.

  • 동적으로 변경할 수 없다.
  • DOM 구조를 알지 못한다.
  • JavaScript에서 읽거나 변경할 수 없다.

이러한 문제를 해결하기 위해 CSS 커스텀 속성이 개발되었다. 전처리기와 CSS 커스텀 속성 두 가지가 어떤 점이 다른지는 아래에서 더 자세히 알아보도록 하겠다.

2. CSS 커스텀 속성

CSS 커스텀 속성은 작성자가 정의한 속성의 집합이다. 작성자는 임의로 정한 이름의 속성에 임의의 값을 할당할 수 있다. “CSS 변수”라고 부르기도 하지만 올바른 이름은 “CSS 커스텀 속성” 이다. CSS 커스텀 속성은 아래와 같이 정의할 수 있다.

1
2
3
:root {
--var-name: value;
}
  • root{}:
    커스텀 속성을 전역으로 사용하고자 할 때, :root {} 에 커스텀 속성을 정의한다.
    커스텀 속성은 CSS 변수처럼 [속성]: [값]; 의 구문을 따르기 때문에 반드시 범위(중괄호) 내에 정의되어야 한다.
  • --var-name:
    커스텀 속성의 이름입니다. 이름은 –(이중 하이픈)로 시작하며, 속성 이름은 작성자가 정의한다.
    CSS 커스텀 속성은 CSS 변수처럼 보이고 작동하며, 보통은 속성 이름에 작동 방식을 반영한다.
    그리고 다른 CSS 속성들과는 달리 CSS 커스텀 속성은 대소문자를 구분한다.
  • value:
    커스텀 속성의 값이다. 값에는 모든 CSS 값이 들어갈 수 있다.(색상, 문자열, 레이아웃값, 표현식 등)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
:root {
--main-color: #f00;
--main-bg: #0f0;
--border-color: #ff0;

--header-height: 70px;
--content-padding: 10px 20px;

--base-line-height: 1.5;
--transition-duration: 0.5s;
--external-link: "external link";
--margin-top: calc(2vh + 20px);

/* 유효 커스텀 속성은 후에 JavaScript에서 재사용될 수 있다. */
--foo: if(x > 5) this.width = 10;
}

3. CSS 커스텀 속성 사용하기

커스텀 속성을 사용하려면 var() 함수 안에 속성 이름을 적어 사용한다.

1
2
3
.foo {
color: var(--var-name);
}

var() 함수는 fallback을 제공한다.
이는 쉼표로 구분되는데, var() 함수 안에서 첫 번째 쉼표 뒤의 값이 fallback으로 간주된다.

var() 함수 문법: var( <custom-property-name> [, <declaration-value> ]? )
첫번째 인수는 대체할 커스텀 속성 이름을 적는다. 두번째 인수는 대체 값으로, 커스텀 속성이 유효하지 않을 경우 사용된다.

1
2
3
<div class="box">
CSS Custom Property
</div>
1
2
3
4
5
6
7
8
9
10
11
12
.box {
/* --box-margin은 정의되지 않았기 때문에 20px이 사용돤다. */
margin: var(--box-margin, 20px);

/* --box-color가 정의되지 않았다면 --main-color 변수가 사용된다. */
--main-color: gold;
background: var(--box-color, var(--main-color));

/* common style */
width: 300px;
font-size: 15px;
}

(1) 연산자

커스텀 속성 사용시 기본 연산자를 사용 하고자 할때는 CSS가 제공하는 calc() 함수를 사용한다. 이는 커스텀 속성의 값을 변경한 후 브라우저가 표현식을 재계산하게 한다.

아래와 같이 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="box1">
indent-size
</div>
<div class="box2">
indent-xl
</div>
<div class="box3">
indent-l
</div>
<div class="box4">
indent-s
</div>
<div class="box5">
indent-xs
</div>
1
2
3
4
5
6
7
8
9
10
11
12
:root {
--indent-size: 20px;
--indent-xl: calc(2 * var(--indent-size));
--indent-l: calc(var(--indent-size) + 2px);
--indent-s: calc(var(--indent-size) - 2px);
--indent-xs: calc(var(--indent-size) / 2);
}
.box1 {font-size:var(--indent-size);}
.box2 {font-size:var(--indent-xl);}
.box3 {font-size:var(--indent-l);}
.box4 {font-size:var(--indent-s);}
.box5 {font-size:var(--indent-xs);}

단위가 없는 경우, calc() 함수가 없다면 작동하지 않을 것이다.

1
2
3
4
5
6
7
8
:root {
--spacer: 10;
}

.box {
padding: var(--spacer)px; /* 작동하지 않음 */
padding: calc(var(--spacer)*1px) 0; /* 작동함 */
}
1
2
3
4
5
6
<div class="box1">
padding: var(--spacer)px;
</div>
<div class="box2">
padding: calc(var(--spacer) * 1px) 0;
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
:root {
--spacer: 20;
}
.box1 {
padding: var(--spacer)px; /* 작동하지 않음 */
}
.box2 {
padding: calc(var(--spacer) * 1px) 0; /* 작동함 */
}

/* common style */
div {
width: 300px;
font-size: 15px;
background: gold;
margin: 10px;
}

(2) 키워드

CSS 커스텀 속성은 일반적인 CSS 속성과 같은 규칙을 가지고 있기때문에, inherit, iniial, unset, revert와 같은 일반적인 CSS 키워드를 지정할 수 있다.

  • inherit
    • 부모 요소의 값을 적용한다.
  • initial
    • CSS 스펙에 정의된 초기값을 적용한다. (CSS 커스텀 속성의 일부 경우, 초기값이 빈 값이거나 없다.)
  • unset
    • 속성이 상속되었으면 커스텀 속성의 경우처럼 상속된 값을 적용하고, 속성이 상속되지 않았으면 초기값을 적용한다.
  • revert
    • 속성을 user agent’s style sheet에 의해 초기값으로 리셋한다.(CSS 커스텀 속성의 경우, 초기값은 빈 값이다.)
1
2
3
4
5
6
.common-values {
--border: inherit;
--bgcolor: initial;
--padding: unset;
--animation: revert;
}

요소에 모든 스타일을 리셋하기 위해서는 all 키워드를 사용할 수 있다.

1
2
3
.my-component{
all: initial;
}

안타깝게도, all 키워드는 커스텀 속성은 리셋하지 못하며 모든 CSS 커스텀 속성을 리셋하는 – 접두사를 추가할지에 대한 논의가 진행되고 있다. 추후에는 다음과 같이 전체 리셋이 가능할지도 모른다.

1
2
3
4
.my-component{
--: initial; /* 모든 CSS 커스텀 속성 리셋 */
all: initial; /* 모든 CSS 스타일 리셋 */
}

4. 스코프(scope)와 상속

CSS 커스텀 속성의 스코프를 설명하기에 앞서, 자바스크립트와 전처리기의 스코프에 대해 알아보자.

(1) 자바스크립트

자바스크립트는 지역변수와 전역변수가 있으며, 변수(var)는 함수에 제한된다. 하지만 자바스크립트에는 클로저(closure)라는 개념이 있어서, 자신을 감싸는 외부 함수의 변수에 접근 가능하다. (스코프 체인)
클로저에는 세 개의 스코프 체인이 있으며, 다음과 같은 항목에 접근할 수 있다.

  • 자신의 스코프 (중괄호 사이에 정의된 변수)
  • 외부 함수의 변수
  • 전역 변수

자바스크립트 스코프

1
2
3
4
5
6
7
8
9
10
11
12
window.globalVar = 10;

function enclosing() {
var enclosingVar = 20;

function closure() {
var closureVar = 30;
return globalVar + enclosingVar + closingVar;
}
return closure();
}
console.log(enclosing()); // 60

(2) 전처리기

대표적인 전처리기인 Sass는 지역변수와 전역변수가 있다. (mixin과 같이 선택자 밖에 선언될 경우 전역변수 / 그렇지 않으면 지역변수) Sass는 중첩된 코드 블록에서 자신을 감싸는 블록의 변수에 접근 가능하다. Sass에서 변수의 스코프는 코드 구조에 완전히 의존한다.

SASS 스코프

1
2
3
4
5
6
7
global
<div class="enclosing">
enclosing
<div class="closure">
closure
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
$globalVar: 10px;

.enclosing {
$enclosingVar: 20px;

.closure {
$closureVar: 30px;

font-size: $closureVar + $enclosingVar + $globalVar; // 60px
}
}

(3) CSS 커스텀 속성

커스텀 속성은 선택자 외부에서 선언할 수 없으며, :root 스코프 내에 정의된 경우만 전역으로 사용할 수 있다. CSS 커스텀 속성은 기본적으로 상속되며, 다른 CSS 속성과 같이 캐스케이딩 한다.

1
2
3
4
5
6
7
global
<div class="enclosing">
enclosing
<div class="closure">
closure
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
:root {
--globalVar: 10px;
}
.enclosing {
--enclosingVar: 20px;
}
.enclosing .closure {
--closureVar: 30px;

font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar)); /* 60px */
}

5. 전처리기 vs CSS 커스텀 속성

(1) 커스텀 속성을 바꾸면 모든 인스턴스에 즉시 적용된다.

위의 예시로 볼때, CSS 커스텀 속성은 Sass 변수와 다른 점이 없어보인다. 이번에는 변수를 사용한 후에 다시 할당해보자.

먼저 Sass를 사용해보겠다.

1
2
3
4
5
6
7
global
<div class="enclosing">
enclosing
<div class="closure">
closure
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
$globalVar: 10px;

.enclosing {
$enclosingVar: 20px;

.closure {
$closureVar: 30px;
font-size: $closureVar + $enclosingVar + $globalVar; // 60px

$closureVar: 50px; // 60px
}
}

아래는 CSS 커스텀 속성을 사용한 경우이다.

1
2
3
4
5
6
7
global
<div class="enclosing">
enclosing
<div class="closure">
closure
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
:root {
--globalVar: 10px;
}
.enclosing {
--enclosingVar: 20px;
}
.enclosing .closure {
--closureVar: 30px;
font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar)); /* 60px */

--closureVar: 50px; /* 80px */
}

여기서 전처리기와 CSS 커스텀 속성의 첫번째 차이를 알 수 있다.

Sass는 변수를 다시 할당해도 결과에 아무 영향이 없지만, CSS 커스텀 속성 사용시 변수 값을 다시 할당하면 브라우저는 모든 변수와 calc() 함수를 재계산한다.

(2) 전처리기는 DOM 구조를 알지 못한다.

아래와 같이 highlighted 클래스를 제외하고는 기본 폰트사이즈를 사용하는 예를 생각해보자. 먼저 CSS 커스텀 속성을 사용해보겠다.

1
2
3
4
5
6
<div class="default">
default
</div>
<div class="default highlighted">
default highlighted
</div>
1
2
3
4
5
6
7
8
9
.highlighted {
--highlighted-size: 30px;
}
.default {
--default-size: 10px;

/* highlighted-size가 주어진 경우를 제외하고는 default-size를 사용한다. */
font-size: var(--highlighted-size, var(--default-size));
}

위의 예에서 두번째 div는 default 외에도 highlighted 클래스를 가지고 있기 때문에 –highlighted-size와 –default-size 커스텀 속성 모두 선언된 상태이다. 따라서 font-size는 –highlighted-size인 30px이 적용된다.

CSS 커스텀 속성은 DOM 구조를 이해하며, 다른 CSS 속성과 같은 규칙을 따라 캐스케이딩할 수 있다.

브라우저 동작 원리: (참고: http://d2.naver.com/helloworld/59361)
렌더링 엔진은 HTML 문서를 파싱하고 ‘콘텐츠 트리’ 내부에서 태그를 DOM 노드로 변환한다. 그 다음 외부 CSS 파일과 함께 포함된 스타일 요소도 파싱한다. 스타일 정보와 HTML 표시 규칙은 ‘렌더트리’라고 부르는 또 다른 트리를 생성한다.

이번에는 Sass를 사용해봅시다.

아래에서 사용된 ‘variable-exists’ 함수는 주어진 이름의 변수가 현재 범위 또는 지역 범위에 있는지 확인한다.

1
2
3
4
5
6
<div class="default">
default
</div>
<div class="default highlighted">
default highlighted
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.highlighted {
$highlighted-size: 30px;
}

.default {
$default-size: 10px;

/* highlighted-size가 주어진 경우를 제외하고는 default-size를 사용한다. */
@if variable-exists(highlighted-size) {
font-size: $highlighted-size;
}
@else {
font-size: $default-size;
}
}

지역 변수는 범위 내에서만 사용 가능하기 때문에, default 클래스의 범위 내에서 highlighted-size 변수는 접근 불가하며, default-size 변수만 사용 가능하다. 따라서 default highlighted 클래스를 가진 요소더라도 font-size는 default-size인 10px이 적용된다.

여기서 중요한 것은 전처리기는 컴파일 과정을 거치기 때문에 모든 계산과 처리가 동시에 일어난다는 것이다. 그렇기 때문에 전처리기는 컴파일하기 이전의 코드 구조에 완전히 의존하고, DOM의 구조는 전혀 알지 못한다.

컴파일 과정:
CSS 전처리기는 코드를 작성한 뒤, CSS 문법으로 컴파일 해주는 방식을 취한다.
SASS 컴파일 과정

이것이 전처리기와 CSS 커스텀 속성의 두번째 차이 이다. CSS 커스텀 속성은 DOM 구조를 인식하고, 동적이기 때문에 변수 스코프 지정시 이점을 가진다.

6. 자바스크립트와 함께 사용하기

이전에 CSS에서 자바스크립트로 데이터를 보내기위해서는 CSS 출력에서 JSON을 통해 CSS 값을 작성한 후, 자바스크립트에서 읽어야 했다.

이제는 .getPropertyValue().setProperty() 메소드를 사용하여 자바스크립트에서 CSS 커스텀 속성을 읽고 쓸 수 있다.

아래는 CSS 커스텀 속성을 읽어오고, 내보내는 메서드 이다.

1
<div class="box">with JavaScript</div>
1
2
3
4
5
6
7
8
9
.box {
--my-color: red;
background: var(--my-color);

/* common style */
width: 300px;
font-size: 15px;
margin: 10px;
}
1
2
3
4
5
6
7
8
9
10
11
function writeCssVar(element, varName, value) {
return element.style.setProperty(`--${varName}`, value);
}

function readCssVar(element, varName) {
const elementStyles = getConputedStyle(element);
return elementStyle.getPropertyValue(`--${varName}`).trim();
}

writeCssVar(document.querySelector('.box'), 'my-color', 'gold');
readCssVar(document.querySelector('.box'), 'my-color');

7. CSS 커스텀 속성 활용하기

CSS 커스텀 속성을 사용한 사례가 많으나, 그 중 가장 많이 사용하고, 흥미로운 사례들을 소개한다.

(1) 존재하지 않는 CSS 규칙 생성

CSS “커스텀” 속성이므로 존재하지 않는 속성을 만들 수 있다. 예를 들면, translateX/Y/Z, background-repeat-x/y, box-shadow-color등이 있다. 그 중 box-shadow-color 커스텀 속성을 만들어 보자.

1
<div class="test">--box-shadow-color</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.test {
--box-shadow-color: gold;
box-shadow: 0 0 30px var(--box-shadow-color);
}
.test:hover {
--box-shadow-color: blue;
/* box-shadow: 0 0 30px blue; 사용 */
}

/* common style */
.test {
width: 300px;
margin: 20px;
border-radius: 10px;
font-size: 15px;
}

이처럼 box-shadow의 전체 값을 다시 입력하는 대신, 원하는 부분만 대체할 수 있다.

(2) 컬러 테마

컬러 테마는 CSS 커스텀 속성을 사용하는 가장 일반적인 사례 중 하나이다. 간단한 컬러 테마를 적용해보자.

1
2
3
4
5
6
7
8
.btn {
--shadow-color: #777;
--color: #fff;

text-shadow: 1px 1px 3px var(--shadow-color);
box-shadow: 0px 1px 3px var(--shadow-color);
color: var(--color);
}

body 요소에 클래스명을 추가한 경우에 다른 스타일을 주고싶다면 아래와 같이 override할 수 있다.

1
2
3
4
body.inverted .btn{
--shadow-color: #888;
--color: #000;
}

CSS 전처리기는 이 작업을 수행하기 위해서는 CSS 코드를 중복하는 오버헤드가 필요하다. 하지만 CSS 커스텀 속성을 사용하면 한가지 값만 재정의하면 되기 때문에 명확하고, 복사/붙여넣기를 피할 수 있다.

CSS 커스텀 속성을 사용하면, 아래와 같이 사용자가 직접 테마 색상을 선택하는 코드도 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<header>
<h1>Title</h1>
<div class="subtitle">
Subtitle
</div>
</header>
<main>
테마 색상을 선택하세요 :
<input type="color" id="input-color">
</main>
<footer>
Footer
</footer>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
:root {
--mainColor: orange;
--paddingValue: 10px;
--textColor: white;
}
h1 {
background: var(--mainColor);
padding: var(--paddingValue);
color: var(--textColor);
}
.subtitle {
padding: var(--paddingValue);
background: linear-gradient(
to top,
rgba(0,0,0,0.3),
rgba(0,0,0,0.3)
) var(--mainColor);
}
main {
flex: 1;
padding: var(--paddingValue);
}
footer {
background: var(--mainColor);
padding: var(--paddingValue);
color: var(--textColor);
}

/* common style */
* {
margin: 0;
}
body {
display: flex;
flex-direction: column;
}
html, body {
height: 100%;
}
1
2
3
4
5
6
var colorInput = document.querySelector("#input-color");

colorInput.addEventListener("change", function() {
// 사이트의 테마 색상을 바꿉니다.
document.documentElement.style.setProperty("--mainColor", this.value);
});

(3) 미디어쿼리

커스텀 속성을 사용하여 미디어쿼리를 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
:root {
--gutter: 1rem;

@media (min-width: 40em) {
--gutter: 1.5rem;
}

@media (min-width: 70em) {
--gutter: 2rem;
}
}

div {
padding: var(--gutter);
}
h2 {
margin-bottom: var(--gutter);
}

CSS 출력은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
div {
padding: 1rem;
}
h2 {
margin-bottom: 1rem;
}

@media (min-width: 40em) {
div {
padding: 1.5rem;
}
h2 {
margin-bottom: 1.5rem;
}
}

@media (min-width: 70em) {
div {
padding: 2rem;
}
h2 {
margin-bottom: 2rem;
}
}

일반적으로 작성하는 것과 CSS 결과는 동일하지만, CSS 커스텀 속성을 사용하면 훨씬 간단하며 이해하기 쉽다. 커스텀 속성을 사용하면 모든 요소의 전환점마다 새로운 속성을 지정할 필요가 없다.

8. 브라우저 지원

CSS 커스텀 속성을 지원하는 브라우저는 아래와 같다. (참고: http://caniuse.com/#search=Custom%20Properties)

(1) @supports

만약 하위 브라우저를 지원해야 하는 경우, @supports를 사용한다.

@supports:
@supports는 지원하는 경우에만 적용되고, 지원하지 않으면 넘어갑니다.
즉, 이해하지 못하는 소스는 무시하기 때문에 대체 기능을 제공할 수 있습니다.

괄호 안에는 커스텀 속성과 값을 넣는다. (ex. --color: red)

1
2
3
4
5
6
7
@supports ((--a: 0)) {
/* 지원하는 경우 */
}

@supports (not (--a: 0)) {
/* 지원하지 않는 경우 */
}

자바스트립트에서도 CSS.supports() 메소드를 사용할 수 있다.

1
2
3
4
5
6
7
const isSupported = window.CSS && window.CSS.supports && window.CSS.supports('--a', 0);

if (isSupported) {
/* 지원하는 경우 */
} else {
/* 지원하지 않는 경우 */
}

위와 같이 @supports를 사용할 경우, 제대로 작동하고 바로 수행할 수 있다는 장점이 있지만, 코드가 복잡해질 수 있다.

그렇기 때문에 자동으로 CSS 결과를 처리하는 플러그인을 사용하는 방법도 있다. (PostCSS에서 제공하는 postcss-custom-properties)

(2) 중복 사용하기

하위 브라우저를 지원하기 위해, CSS의 특징을 활용할 수 있다.

1
2
3
4
5
div {
--color: red;
color: red;
color: var(--color);
}

위와 같이 사용하면, 먼저 color: red 가 적용된 다음, CSS 커스텀 속성이 적용된다. 비록 속성을 중복 사용하지만, 기존에 사용하는 코드 내에서 CSS 커스텀 속성을 쉽게 사용할 수 있다.

즉, 브라우저가 CSS 커스텀 속성을 지원하지 않더라도 쉽게 리팩토링할 수 있다.

9. 결론

지금까지 CSS 커스텀 속성에 대해 알아보았으며, 전처리기와 어떤 점이 다른지 살펴보았다. 이 둘의 장점을 비교해보면 아래와 같다.

CSS 커스텀 속성 전처리기
전처리기 없이 사용 가능하다 브라우저 지원을 고려할 필요가 없다
캐스케이딩 한다 단위를 제거할 수 있다
값이 바뀌면 브라우저가 재계산 한다 -
자바스크립트에서 다룰 수 있다 -

그동안 CSS 전처리기를 사용해왔지만, CSS 커스텀 속성의 장점도 많기 때문에 사용해보아도 좋을 것 같다.

참조

공유하기