새로운 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 Typed OM

Houdini 노력의 일환인 새로운 CSS 유형 객체 모델 (유형 OM)은 CSS 값에 유형, 메서드, 적절한 객체 모델을 추가하여 이러한 세계관을 확장합니다. 문자열 대신 값이 JavaScript 객체로 노출되어 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는 Map과 유사한 객체이므로 일반적인 용의자 (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 vs el.style['background-color']) 더 이상 추측할 필요가 없습니다. 유형화된 OM의 CSS 속성 이름은 항상 문자열이며, CSS에 실제로 작성하는 내용과 일치합니다.

브라우저 지원 및 기능 감지

입력된 OM은 Chrome 66에 적용되었으며 Firefox에서 구현되고 있습니다. Edge는 지원 신호를 보였지만 아직 플랫폼 대시보드에 추가하지는 않았습니다.

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

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

API 기본사항

스타일 액세스

값은 CSS Typed 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

값 고정 / 반올림

새 객체 모델의 유용한 기능 중 하나는 계산된 스타일 값의 자동 클램핑 또는 반올림입니다. 예를 들어 허용되는 범위 [0, 1]을 벗어난 값으로 opacity를 설정하려고 한다고 가정해 보겠습니다. 스타일을 계산할 때 유형이 지정된 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 - 수학 표현식 (예: "calc(56em + 10%)")과 같이 값이 2개 이상인 값/단위가 포함됩니다.

단위 값

단순 숫자 값 ("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 객체는 수학적 표현식을 나타내며 일반적으로 두 개 이상의 값/단위를 포함합니다. 일반적인 예는 CSS calc() 표현식을 만드는 것이지만 모든 CSS 함수(calc(), min(), max())에 대한 메서드가 있습니다.

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

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

위치 값

공백으로 구분된 x/y 위치를 사용하는 CSS 속성(예: object-position)은 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를 포착하는 데 유용할 수 있습니다.

전체 스타일을 파싱합니다.

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는 약간 장황하지만, 앞으로 버그가 줄고 성능이 더 우수한 코드가 나올 것으로 기대됩니다.