이 글은 https://developers.google.com/web/updates/2018/03/cssom 문서 번역을 기초로, 필요한 설명을 일부 추가한 글입니다.

요약

최근 W3C Houdini WG에서 표준화 하고 있는 CSS Typed Object Model이 크롬 66에 추가되었습니다. CSS Typed Object Model은 아래와 같이 사용할 수 있습니다:

el.attributeStyleMap.set('padding', CSS.px(42));
const padding = el.attributeStyleMap.get('padding');
console.log(padding.value, padding.unit); // 42, 'px'

이제 CSS는 JavaScript에서 사용하기에 적절한 객체 기반 API를 갖게 되었으며, 기존의 CSSOM의 미묘한 버그들와 문자열로 인한 제약을 해결할 수 있습니다!

1. 소개

CSSOM

이전의 CSS에는 수년간 Object Model(CSSOM)이 존재했습니다.

CSSOM은 JavaScript에서 CSS를 조작할 수 있게 해주는 API입니다. CSSOM은 웹 페이지에서 발견되는 CSS 스타일의 기본 ‘맵’으로, DOM과 결합된 CSSOM은 브라우저에서 웹 페이지를 표현하는데 사용됩니다.

JavaScript에서 .style을 read 또는 set할 때 항상 아래와 같이 사용해왔습니다.

// 요소의 스타일
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // opacity가 문자열이다!?
// 스타일시트 규칙
document.styleSheets[0].cssRules[0].style.opacity = 0.3;

CSS Typed OM

새로 나온 CSS Typed Object Model(Typed OM)은 CSS 값에 타입과 메소드, 적절한 객체 모델을 추가함으로써 세계관을 넓혔습니다. 값이 문자열이 아닌 JavaScript 객체로 나타나기 때문에 CSS를 효율적으로(정상적으로) 조작할 수 있습니다.
element.style을 사용하는 대신, .attributeStyleMap 속성을 사용하여 요소의 스타일에 접근할 수 있습니다. 스타일시트 규칙에는 .styleMap 속성을 사용합니다. 두 속성 다 StylePropertyMap 객체를 반환합니다.

// 요소의 스타일
el.attributeStyleMap.set('opacity', 0.3);
typeof el.attributeStyleMap.get('opacity').value === 'number' // opacity가 숫자값이다!
// 스타일시트 규칙
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');

StylePropertyMap은 Map과 유사한 객체이기 때문에, 일반적인 함수(get/set/keys/values/entries)를 전부 지원합니다. 따라서 아래와 같이 유연하게 작업할 수 있습니다.

// 아래 3가지가 모두 동일하다.
el.attributeStyleMap.set('opacity', 0.3);
el.attributeStyleMap.set('opacity', '0.3');
el.attributeStyleMap.set('opacity', CSS.number(0.3)); // 'Unit values' 파트 참고
// el.attributeStyleMap.get('opacity').value === 0.3
// StylePropertyMaps은 반복 가능하다.
for (const [prop, val] of el.attributeStyleMap) {
  console.log(prop, val.value);
} // → opacity, 0.3
el.attributeStyleMap.has('opacity') // true
el.attributeStyleMap.delete('opacity') // opacity 제거
el.attributeStyleMap.clear(); // 모든 스타일 제거

두 번째 예에서 opacity를 문자열 '0.3'으로 set 했지만 속성을 read 할 때는 숫자로 읽힌다는 것을 명심하세요.

주어진 CSS 속성이 숫자를 지원한다면, Typed OM은 문자열 값을 입력하더라도 항상 숫자값을 반환합니다!

2. 이점

그렇다면 CSS Typed OM이 해결하려는 문제가 무엇일까요? 위의 예를 보면, CSS Typed OM이 이전의 Object Model보다 훨씬 장황하다고 주장할 수도 있습니다.
Typed OM을 작성하기 전에 아래의 몇 가지 주요 특징을 고려하세요.

  1. 적은 버그 – 예) 숫자 값은 문자열이 아니라 항상 숫자로 반환됩니다.
el.style.opacity += 0.1;
el.style.opacity === '0.30.1' // CSSOM은 문자열로 붙는다!
  1. 산술 연산 및 단위 변환 – 절대 길이 단위를 변환하고(px → cm), 기본 수학 연산을 수행할 수 있습니다. 자세한 내용은 아래 ‘산술 연산’ 파트를 참고하세요.
  2. 값 클램핑 & 반올림 – Typed OM은 값을 반올림 및 클램핑해서 속성의 허용 범위 내에 있을 수 있습니다. 자세한 내용은 아래 ‘값 클램핑 / 반올림’ 파트를 참고하세요.

    컴퓨터 그래픽에서 ‘클램핑’이란, 어떤 위치를 범위 안으로 한정시키는 방법입니다. 위치를 제일 가까운 사용 가능한 값으로 옮깁니다.

  3. 성능 향상 – 브라우저는 문자열 값을 직렬화, 병렬화하는 작업을 줄여야 합니다. 이제 엔진은 JS, C++과 비슷한 방식으로 CSS 값을 이해합니다. Tab Akins는 초기 CSS 벤치마크에서 Typed OM이 기존의 CSSOM을 사용할 때보다 초당 작동 속도가 30%까지 빠르다는 것을 입증했습니다. 이는 requestionAnimationFrame()를 사용하여 빠른 CSS 애니메이션을 구현할 때 중요합니다. 자세한 내용은 아래 ‘예) 큐브 애니메이션’ 파트를 참고하세요.
  4. 오류 처리 – 새로운 파싱 메소드는 CSS 세계에서 오류 처리를 제공합니다. 자세한 내용은 아래 ‘파싱 값’, ‘오류 처리’ 파트를 참고하세요.
  5. “CSS 이름은 camel-case로 작성해야하나요? 문자열로 작성해야하나요?” CSSOM은 이름이 camel-case인지 문자열인지 가늠할 수 없었습니다(ex. el.style.backgroundColor vs el.style['background-color']). Typed OM의 CSS 속성 이름은 항상 문자열이며, 실제 CSS에서 작성한 것과 일치시키면 됩니다.

3. 브라우저 지원 & 기능 감지

Typed OM은 Chrome 66에 도달했으며, Firefox에서도 구현 중입니다. Edge는 지원하려는 움직임이 보이지만, 아직 플랫폼 대시보드에 추가하지 않았습니다.

현재 Chrome 66+는 CSS 하위 속성만 지원됩니다.

기능 감지는 CSS.* 숫자 팩토리 중 하나가 정의되어 있는지 아닌지로 확인할 수 있습니다.

if (window.CSS && CSS.number) {
  // CSS Typed OM을 지원한다.
}

4. API 기본

(1) 스타일 접근

CSS Typed OM에서 값은 단위와 별개입니다. 스타일을 get하면 value와 unit을 포함하는 CSSUnitValue를 반환합니다.

el.attributeStyleMap.set('margin-top', CSS.px(10));
// el.attributeStyleMap.set('margin-top', '10px'); // 문자열도 OK
el.attributeStyleMap.get('margin-top').value  // 10
el.attributeStyleMap.get('margin-top').unit // 'px'
// 일반 텍스트 값에는 'CSSKeywordValue'를 사용한다.
el.attributeStyleMap.set('display', new CSSKeywordValue('initial'));
el.attributeStyleMap.get('display').value // 'initial'
el.attributeStyleMap.get('display').unit // undefined<code>
</code>

(2) Computed style

computed style은 window의 API에서 HTMLElement의 새로운 메소드인 computedStyleMap()로 바뀌었습니다.

기존 CSSOM

el.style.opacity = 0.5;
window.getComputedStyle(el).opacity === "0.5" // 또 문자열이다!

새로운 Typed OM

el.attributeStyleMap.set('opacity', 0.5);
el.computedStyleMap().get('opacity').value // 0.5

window.getComputedStyle()과 element.computedStyleMap()의 차이점 중 하나는 전자는 resolved 값을 반환하는 반면, 후자는 computed 값을 반환한다는 것입니다. 예를 들어, width: 50%와 같이 백분율 값이 있을 때, CSSOM은 절대 길이 값으로 변환(ex. width: 200px)하는 반면, Typed OM은 백분율 값을 그대로 유지합니다.

참고: resolved 값이란, getComputedStyle()에 의해 반환된 값입니다. 대부분의 속성은 이 값이 computed 값이지만, 상속 받는 일부 속성(width, height 포함)은 used 값입니다. computed 값은 상속하면서 부모로부터 자식으로 전달되는 값이고, used 값은 computed 값의 모든 계산이 수행된 후의 값입니다.

값 클램핑 / 반올림

새로운 Object Model의 장점 중 하나는 computed style 값의 자동 클램핑 및 반올림입니다. 예를 들어, 당신이 opacity를 허용 범위 [0,1]을 벗어나는 값으로 적용한다고 가정해봅시다. Typed OM은 스타일을 계산할 때 그 값을 1로 옮깁니다(클램핑).

el.attributeStyleMap.set('opacity', 3);
el.attributeStyleMap.get('opacity').value === 3  // 값이 클램핑되지 않음
el.computedStyleMap().get('opacity').value === 1 // computed style이 값을 클램핑

마찬가지로 z-index: 15.4는 15로 반올림 돼서 정수가 됩니다.

el.attributeStyleMap.set('z-index', CSS.number(15.4));
el.attributeStyleMap.get('z-index').value  === 15.4 // 반올림 되지 않음
el.computedStyleMap().get('z-index').value === 15   // computed style이 반올림 됨

5. CSS 숫자 값

Typed OM에서 숫자는 두 가지 유형의 CSSNumericValue로 나타납니다.

  1. CSSUnitValue – 단일 단위를 포함하는 값 (ex. "42px")
  2. CSSMathValue – 수학 표현식 같이 하나 이상의 값/단위를 포함하는 값 (ex. "calc(56em + 10%)")

(1) Unit values

단순한 숫자값("50%")은 CSSUnitValue 객체로 표시됩니다. 이 객체를 직접 생성(new CSSUnitValue(10,'px'))할 수도 있지만, 대부분의 경우는 CSS.*와 같이 팩토리 메소드를 사용하게 됩니다.

const {value, unit} = CSS.number('10');
// value === 10, unit === 'number'
const {value, unit} = CSS.px(42);
// value === 42, unit === 'px'
const {value, unit} = CSS.vw('100');
// value === 100, unit === 'vw'
const {value, unit} = CSS.percent('10');
// value === 10, unit === 'percent'
const {value, unit} = CSS.deg(45);
// value === 45, unit === 'deg'
const {value, unit} = CSS.ms(300);
// value === 300, unit === 'ms'

예에서 볼 수 있듯이, 이러한 메소드들은 숫자를 나타내는 Number 또는 String으로 전달될 수 있습니다.

CSS.* 메소드의 전체 목록은 스펙 문서를 참고하세요.

(2) Math values

CSSMathValue 객체는 수학적 표현을 나타내며 일반적으로 둘 이상의 값/단위를 포함합니다. 일반적인 예로는 CSS calc() 함수가 있습니다. 하지만 그 외에도 min(), max()와 같이 CSS의 모든 함수에 대한 메소드가 있습니다.

new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString(); // "calc(100vw + -10px)"
new CSSMathNegate(CSS.px(42)).toString() // "calc(-42px)"
new CSSMathInvert(CSS.s(10)).toString() // "calc(1 / 10s)"
new CSSMathProduct(CSS.deg(90), CSS.number(Math.PI/180)).toString();
// "calc(90deg * 0.0174533)"
new CSSMathMin(CSS.percent(80), CSS.px(12)).toString(); // "min(80%, 12px)"
new CSSMathMax(CSS.percent(80), CSS.px(12)).toString(); // "max(80%, 12px)"

– 중첩된 표현식
수학 함수를 사용하여 더 복잡한 값을 생성하려고 하면, 다소 혼란스러울 수 있습니다. 시작을 위해 아래 몇 가지 예를 준비했습니다. 이해를 돕기 위해 들여쓰기도 추가해보았습니다.
calc(1px - 2 * 3em)은 다음과 같이 구성됩니다.

new CSSMathSum(
  CSS.px(1),
  new CSSMathNegate(
    new CSSMathProduct(2, CSS.em(3))
  )
);

calc(1px + 2px + 3px)은 다음과 같이 구성됩니다.

new CSSMathSum(CSS.px(1), CSS.px(2), CSS.px(3));

calc(calc(1px + 2px) + 3px)은 다음과 같이 구성됩니다.

new CSSMathSum(
  new CSSMathSum(CSS.px(1), CSS.px(2)),
  CSS.px(3)
);

(3) 산술 연산

CSS Typed OM의 가장 유용한 기능 중 하나는 CSSUnitValue 객체에 대한 수학 연산을 수행할 수 있다는 것입니다.
– 기본 연산
기본 연산(add / sub / mul / div / min / max)이 지원됩니다.

CSS.deg(45).mul(2) // {value: 90, unit: "deg"}
CSS.percent(50).max(CSS.vw(50)).toString() // "max(50%, 50vw)"
// CSSUnitValue 전달 가능:
CSS.px(1).add(CSS.px(2)) // {value: 3, unit: "px"}
// 여러 값 전달 가능:
CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString() // "calc(1s + -200ms + -300ms)"
// `CSSMathSum` 전달 가능:
const sum = new CSSMathSum(CSS.percent(100), CSS.px(20)));
CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))"

– 변환
절대 길이 단위는 다른 길이 단위로 변환 가능합니다.

// px을 다른 절대적/물리적 길이로 변환
el.attributeStyleMap.set('width', '500px');
const width = el.attributeStyleMap.get('width');
width.to('mm'); // CSSUnitValue {value: 132.29166666666669, unit: "mm"}
width.to('cm'); // CSSUnitValue {value: 13.229166666666668, unit: "cm"}
width.to('in'); // CSSUnitValue {value: 5.208333333333333, unit: "in"}
CSS.deg(200).to('rad').value // 3.49066...
CSS.s(2).to('ms').value // 2000

– 등식

const width = CSS.px(200);
CSS.px(200).equals(width) // true
const rads = CSS.deg(180).to('rad');
CSS.deg(180).equals(rads.to('deg')) // true

6. CSS Transform 값

CSS transform은 CSSTransformValue로 생성되며, transform 값(ex. CSSRotateCSScaleCSSSkewCSSSkewXCSSSkewY)의 배열을 전달합니다. 예를 들어 아래의 CSS를 재작성한다고 해봅시다.

transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px);

Typed OM으로 변환해봅시다.

const transform =  new CSSTransformValue([
  new CSSRotate(CSS.deg(45)),
  new CSSScale(CSS.number(0.5), CSS.number(0.5)),
  new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10))
]);

이러한 많은 기능 외에도, CSSTransformValue는 몇 가지 멋진 기능이 있습니다. 2D와 3D transform을 boolean 값으로 구별하는 속성이 있고, transform을 표현하는 DOMMatrix를 반환하는 .toMatrix()메소드가 있습니다.

new CSSTranslate(CSS.px(10), CSS.px(10)).is2D // true
new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)).is2D // false
new CSSTranslate(CSS.px(10), CSS.px(10)).toMatrix() // DOMMatrix

예) 큐브 애니메이션
transform 예를 살펴봅시다. 큐브 애니메이션을 위해 JavaScript와 CSS transform을 사용해보겠습니다.

const rotate = new CSSRotate(0, 0, 1, CSS.deg(0));
const transform = new CSSTransformValue([rotate]);
const box = document.querySelector('#box');
box.attributeStyleMap.set('transform', transform);
(function draw() {
  requestAnimationFrame(draw);
  transform[0].angle.value += 5; // transform의 각도를 업데이트 한다.
  // rotate.angle.value += 5; // 또는 CSSRotate 객체를 바로 업데이트 할 수도 있다.
  box.attributeStyleMap.set('transform', transform); // 적용
})();

여기서 주목할 점:

  1. 숫자 값은 우리가 직접 수학을 사용해서 각도를 증가시킬 수 있음을 뜻합니다!
  2. DOM을 건드리거나, 모든 프레임에서 값을 다시 읽는(ex. box.style.transform=`rotate(0,0,1,${newAngle}deg)`) 대신, 위의 애니메이션은 기본 CSSTransformValue 데이터 객체를 업데이트하여 구현되기 때문에 성능을 향상시킵니다.

7. CSS 커스텀 속성 값

CSS var()은 Typed OM에서 CSSVariableReferenceValue 객체가 됩니다. 그 값들은 모든 타입(px, %, em, rgba() 등)을 취할 수 있기 때문에 CSSUnparsedValue로 파싱됩니다.

const foo = new CSSVariableReferenceValue('--foo');
// foo.variable === '--foo'
// Fallback 값:
const padding = new CSSVariableReferenceValue(
    '--default-padding', new CSSUnparsedValue(['8px']));
// padding.variable === '--default-padding'
// padding.fallback instanceof CSSUnparsedValue === true
// padding.fallback[0] === '8px'

커스텀 속성의 값을 가져오고 싶다면, 다음 작업을 수행해야 합니다:

<style>
  body {
    --foo: 10px;
  }
</style>
<script>
  const styles = document.querySelector('style');
  const foo = styles.sheet.cssRules[0].styleMap.get('--foo').trim();
  console.log(CSSNumericValue.parse(foo).value); // 10
</script>

8. Position 값

object-position과 같이, 공백으로 구분된 x/y 위치 값을 취하는 CSS 속성은 CSSPositionValue 객체로 표현합니다.

const position = new CSSPositionValue(CSS.px(5), CSS.px(10));
el.attributeStyleMap.set('object-position', position);
console.log(position.x.value, position.y.value);
// → 5, 10

9. 파싱 값

Typed OM은 웹 플랫폼에 파싱 메소드를 도입했습니다! 이는 마침내 당신이 CSS 값을 사용하기 전에 프로그래밍 방식으로 파싱할 수 있음을 뜻합니다. 이 새로운 기능은 초기 버그와 형식이 잘못된 CSS를 잡아낼 수 있는 생명의 은인입니다.
Full 스타일 파싱하기:

const css = CSSStyleValue.parse(
    'transform', 'translate3d(10px,10px,0) scale(0.5)');
// → css instanceof CSSTransformValue === true
// → css.toString() === 'translate3d(10px, 10px, 0) scale(0.5)'

CSSUnitValue로 값 파싱하기:

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}
// 사실 이 경우는 팩토리 함수를 사용하는게 더 간단하다:
CSS.px(42.0) // '42px'

오류 처리

예) CSS parser가 아래의 transform 값을 사용할 수 있는지 확인합니다.

try {
  const css = CSSStyleValue.parse('transform', 'translate4d(bogus value)');
  // css 사용
} catch (err) {
  console.err(err);
}

10. 마치며

그동안 JavaScript에서 문자열로 작업해왔는데, 드디어 CSS Object Model이 업데이트 되다니 멋집니다! CSS Typed OM API는 약간 장황하긴 하지만, 버그가 적고 성능이 우수한 코드를 작성할 수 있길 기대합니다.

참고 자료


0개의 댓글

답글 남기기

아바타 플레이스홀더

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다