새로운 CSS 유형이 지정된 객체 모델로 작업하기

에릭 비델만

요약

이제 CSS에는 JavaScript에서 값을 사용하기 위한 적절한 객체 기반 API가 있습니다.

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

문자열을 연결하는 시대와 미묘한 버그는 이제 끝났습니다.

소개

이전 CSSOM

CSS는 수년 동안 객체 모델 (CSSOM)을 사용해 왔습니다. 실제로 JavaScript에서 .style를 읽거나 설정할 때마다 이를 사용합니다.

// Element styles.
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // Ugh. A string!?

// Stylesheet rules.
document.styleSheets[0].cssRules[0].style.opacity = 0.3;

새 CSS 유형 OM

Houdini 노력의 일환으로 새로운 CSS 유형이 지정된 객체 모델 (Typed OM)으로 CSS 값에 유형, 메서드, 적절한 객체 모델을 추가하여 이 세계관을 확장합니다. 문자열 대신 값이 자바스크립트 객체로 노출되어 성능이 우수하고 합리적인 CSS 조작을 용이하게 합니다.

element.style를 사용하는 대신 요소의 새로운 .attributeStyleMap 속성 및 스타일시트 규칙의 경우 .styleMap 속성을 통해 스타일에 액세스합니다. 둘 다 StylePropertyMap 객체를 반환합니다.

// Element styles.
el.attributeStyleMap.set('opacity', 0.3);
typeof el.attributeStyleMap.get('opacity').value === 'number' // Yay, a number!

// Stylesheet rules.
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');

StylePropertyMap는 맵과 같은 객체이므로 일반적인 모든 문제 (get/set/keys/values/entries)를 지원하므로 다음과 같이 유연하게 작업할 수 있습니다.

// All 3 of these are equivalent:
el.attributeStyleMap.set('opacity', 0.3);
el.attributeStyleMap.set('opacity', '0.3');
el.attributeStyleMap.set('opacity', CSS.number(0.3)); // see next section
// el.attributeStyleMap.get('opacity').value === 0.3

// StylePropertyMaps are iterable.
for (const [prop, val] of el.attributeStyleMap) {
  console.log(prop, val.value);
}
// → opacity, 0.3

el.attributeStyleMap.has('opacity') // true

el.attributeStyleMap.delete('opacity') // remove opacity.

el.attributeStyleMap.clear(); // remove all styles.

두 번째 예에서 opacity는 문자열 ('0.3')로 설정되었지만 나중에 속성을 다시 읽을 때 숫자가 다시 나옵니다.

이점

그렇다면 CSS Typed OM은 어떤 문제를 해결하려고 할까요? 위의 예와 이 문서의 나머지 부분을 살펴보면 CSS Typed OM이 이전 객체 모델보다 훨씬 더 상세하다고 주장할 수 있습니다. 동의하겠습니다.

Typed OM을 삭제하기 전에 테이블에 제공하는 몇 가지 주요 기능을 고려하세요.

  • 버그가 적습니다. 예를 들어 숫자 값은 항상 문자열이 아닌 숫자로 반환됩니다.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • 산술 연산 및 단위 변환. 절대 길이 단위 (예: px -> cm) 간을 변환하고 기본적인 계산을 실행합니다.

  • 값 고정 및 반올림. 입력한 OM 값이 속성에 허용되는 범위 내에 있도록 반올림 또는 고정합니다.

  • 성능 개선. 브라우저에서 문자열 값을 직렬화 및 역직렬화하는 작업을 줄일 수 있습니다. 이제 엔진은 JS 및 C++에서 CSS 값에 관한 유사한 이해를 사용합니다. Tab Akins는 이전 CSSOM 및 문자열을 사용할 때보다 Typed OM이 초당 약 30% 더 빠른 작업 속도를 제공하는 몇 가지 초기 성능 벤치마크를 보여주었습니다. 이는 requestionAnimationFrame()를 사용하는 빠른 CSS 애니메이션에 중요할 수 있습니다. crbug.com/808933은 Blink에서 추가 성능 작업을 추적합니다.

  • 오류 처리. 새로운 파싱 메서드를 사용하면 CSS에서 오류 처리를 수행할 수 있습니다.

  • "카멜 표기법의 CSS 이름이나 문자열을 사용해야 하나요?" 이름이 카멜 표기법 또는 문자열인지 더 이상 추측할 필요가 없습니다 (예: el.style.backgroundColor 또는 el.style['background-color']). 유형이 지정된 OM의 CSS 속성 이름은 항상 문자열로, 실제로 CSS에 작성하는 이름과 일치합니다.

브라우저 지원 및 기능 감지

입력한 OM은 Chrome 66에 출시되어 Firefox에서 구현 중입니다. Edge는 지원 신호를 표시했지만 아직 플랫폼 대시보드에 추가하지 않았습니다.

특성 감지의 경우 CSS.* 숫자 팩토리 중 하나가 정의되어 있는지 확인할 수 있습니다.

if (window.CSS && CSS.number) {
  // Supports CSS Typed OM.
}

API 기본사항

스타일 액세스

값은 CSS 유형 OM의 단위와는 별개입니다. 스타일을 가져오면 valueunit가 포함된 CSSUnitValue이 반환됩니다.

el.attributeStyleMap.set('margin-top', CSS.px(10));
// el.attributeStyleMap.set('margin-top', '10px'); // string arg also works.
el.attributeStyleMap.get('margin-top').value  // 10
el.attributeStyleMap.get('margin-top').unit // 'px'

// Use CSSKeyWorldValue for plain text values:
el.attributeStyleMap.set('display', new CSSKeywordValue('initial'));
el.attributeStyleMap.get('display').value // 'initial'
el.attributeStyleMap.get('display').unit // undefined

계산된 스타일

계산된 스타일window의 API에서 HTMLElement, computedStyleMap()의 새 메서드로 이동했습니다.

이전 CSSOM

el.style.opacity = 0.5;
window.getComputedStyle(el).opacity === "0.5" // Ugh, more strings!

새 유형의 OM

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

값 고정 / 반올림

새 객체 모델의 좋은 기능 중 하나는 계산된 스타일 값의 자동 클램핑 또는 반올림입니다. 예를 들어 opacity를 허용되는 범위([0, 1])를 벗어난 값으로 설정하려 한다고 가정해 보겠습니다. 입력된 OM은 스타일을 계산할 때 값을 1로 고정합니다.

el.attributeStyleMap.set('opacity', 3);
el.attributeStyleMap.get('opacity').value === 3  // val not clamped.
el.computedStyleMap().get('opacity').value === 1 // computed style clamps value.

마찬가지로 z-index:15.4를 설정하면 15로 반올림되므로 값이 정수로 유지됩니다.

el.attributeStyleMap.set('z-index', CSS.number(15.4));
el.attributeStyleMap.get('z-index').value  === 15.4 // val not rounded.
el.computedStyleMap().get('z-index').value === 15   // computed style is rounded.

CSS 숫자 값

숫자는 유형이 있는 OM의 두 가지 유형의 CSSNumericValue 객체로 표현됩니다.

  1. CSSUnitValue - 단일 단위 유형이 포함된 값 (예: "42px").
  2. CSSMathValue - 수학 표현식과 같이 값/단위를 2개 이상 포함하는 값입니다 (예: "calc(56em + 10%)").

단위 값

단순한 숫자 값 ("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'

CSS.* 메서드의 전체 목록은 사양을 참고하세요.

수학 값

CSSMathValue 객체는 수학 표현식을 나타내며 일반적으로 값/단위를 2개 이상 포함합니다. 일반적인 예는 CSS calc() 표현식을 만드는 것이지만 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)
);

산술 연산

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)"

// Can Pass CSSUnitValue:
CSS.px(1).add(CSS.px(2)) // {value: 3, unit: "px"}

// multiple values:
CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString() // "calc(1s + -200ms + -300ms)"

// or pass a `CSSMathSum`:
const sum = new CSSMathSum(CSS.percent(100), CSS.px(20)));
CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))"

전환

절대 길이 단위는 다른 단위 길이로 변환할 수 있습니다.

// Convert px to other absolute/physical lengths.
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

CSS 변환 값

CSS 변환은 CSSTransformValue를 사용하여 생성되고 변환 값의 배열 (예: CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY)을 전달합니다. 예를 들어 다음 CSS를 다시 만들고 싶다고 가정해 보겠습니다.

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

유형 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 변환을 구분하는 불리언 속성과 변환의 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

예: 큐브에 애니메이션 적용

변환 사용의 실제 예를 살펴보겠습니다. JavaScript 및 CSS 변환을 사용하여 큐브에 애니메이션을 적용할 것입니다.

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; // Update the transform's angle.
  // rotate.angle.value += 5; // Or, update the CSSRotate object directly.
  box.attributeStyleMap.set('transform', transform); // commit it.
})();

짚고 넘어갈 사항:

  1. 숫자 값은 수학을 사용하여 각도를 직접 증가시킬 수 있음을 의미합니다.
  2. 애니메이션은 DOM을 터치하거나 모든 프레임에서 값을 다시 읽는 대신 (예: box.style.transform=`rotate(0,0,1,${newAngle}deg)` 없음) 기본 CSSTransformValue 데이터 객체를 업데이트하여 성능을 개선함으로써 구동됩니다.

데모

브라우저가 Typed OM을 지원하는 경우 아래와 같이 빨간색 큐브가 표시됩니다. 정육면체 위로 마우스를 가져가면 정육면체가 회전하기 시작합니다. 애니메이션은 CSS Typed OM을 사용합니다. 🤘

CSS 맞춤 속성 값

CSS var()는 유형이 있는 OM에서 CSSVariableReferenceValue 객체가 됩니다. 이러한 값은 모든 유형 (px, %, em, rgba() 등)을 사용할 수 있으므로 CSSUnparsedValue로 파싱됩니다.

const foo = new CSSVariableReferenceValue('--foo');
// foo.variable === '--foo'

// Fallback values:
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>

게재순위 값

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

값 파싱

유형이 있는 OM은 웹 플랫폼에 파싱 메서드를 도입합니다. 즉, CSS 값을 사용하기 전에 프로그래매틱 방식으로 CSS 값을 파싱할 수 있습니다. 이 새로운 기능은 조기 버그와 잘못된 형식의 CSS를 포착하는 데 도움을 줍니다.

전체 스타일 파싱:

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'}

// But it's easier to use the factory functions:
CSS.px(42.0) // '42px'

오류 처리

- CSS 파서가 이 transform 값에 만족하는지 확인:

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

결론

마침내 CSS를 위해 객체 모델을 업데이트한 것은 좋습니다. 현을 다루는 일은 저에게는 절대로 적합하지 않았어요. CSS Typed OM API는 약간 장황하지만 앞으로 버그가 줄고 코드의 성능이 향상되기를 바랍니다.