티스토리 뷰

HTML & CSS

CodeSpitz - CSSOM & Vendor Prefix

_Bibidi 2021. 12. 20. 16:50

 

[ CSSOM ]

 CSS는 스펙이 난잡하다보니 브라우저마다 지원 성향이 극적으로 변한다. 그래서 새로운 기능은 vendor prefix가 붙었다가 안정화되고 표준화되면 떼는 식으로 하는 게 업계 관행이다. 어떤 식에는 vendor prefix를 붙여야 작동하고 어떤 식에는 또 vendor prefix를 떼야 작동해서 까다롭다.

 

DOM(Document Object Model)

 이때 document는 HTML을 말한다. HJTML을 객체화 시켜서 프로그래밍 가능하게 바꿔놓은 게 DOM이다. 

javascript를 통해서 DOM API를 이용하면 직접 HTML을 수정하지 않고 요소나 속성을 고칠 수 있다.

 

CSSOM

 CSS를 object화 시켜서 모델링한 것으로 javascript를 통해 CSS 조작할 수 있게 해주는 API. CSS 스타일의 기본 맵으로 DOM과 결합되어 브라우저에서 웹 페이지를 표현하는 데 사용된다.

 

Style DOM 구조

Tag 구조

1. tag(=element, DOM element)는 일종의 컨테이너 박스 같은 표준화 되어 있는 래핑 객체이다. 실체는 컨테이너 안에 들어있지만 DOM이라는 형태의 컨테이너로 감싸놓으면 컨테이너 선에 싣을 수 있다.

 ex) style 태그의 실체는 sheet이다. style sheet를 조작하려면 이 sheet를 조작해야 한다.

 canvas 태그의 실체도 getContext('2d')를 이용해 얻는다. style, canvas 모두 다른 종류의 객체지만 DOM으로 포장했기 때문에 같은 선에 실을 수 있다.

2. tag 내의 실체를 꺼내는 방법이 tag마다 다르다. 옛날부터 천천히 태그가 추가되다 보니 그렇다. 구형 element들은 속성으로 꺼내는 경우가 많고 신형 element들은 getContext 메소드를 이용하는 경우가 많다.

 

<style id="s">
  .test {background:#ff0;}
</style>

const el = document.querySelector("#s");
const sheet = el.sheet;
const rules = sheet.cssRules;
const rule = rules[0];

console.log(rule.selectorText); // .test
console.log(rule.style.background); // #ff0

sheet.insertRule('.red{background:red}', rules.length);
sheet.insertRule('.blue{background:blue}', rules.length);

console.log(Array.from(rules).map(v=>v.cssText).join('\n'));

 하나의 rule은 하나의 CSS 정의가 된다. rule은 type, selectorText, style 속성으로 구성된다. 위 코드에 대입해보면 selectorText는 .test가 되고 style은 .test안에 들어있는 CSS 속성이다.

 

 우리가 HTML에서 text로 정의했던 CSS가 실제로 브라우저가 해석 과정을 거치고 나면 내부 메모리에 객체 형태로 저장이 되있어서 조작할 수 있게 된다. HTML에 텍스트로 적어놓은 걸 메모리 상의 구조로 바꾸는 것이 CSSOM이다.

 

 Style 규칙

1. 모든 CSS 구문은 대응되는 rule 객체로 바뀌어 rules에 저장된다.

2. sheet 객체에게 rule 추가를 의뢰하면 style type을 알 필요가 없다.

3. rule의 순서는 중요하다. 나중에 추가된 rule이 먼저 추가된 rule을 이긴다.

4. javascript를 통해 나중에 rule을 insert해도 재적용된다.

5. document에 등록된 sheet 객체에 변화가 생기면 repaint를 실시한다. 상황에 따라서는 reflow까지 한다.

6. documet.styleSheet에는 모든 style sheet가 등록된다. 그리고 마지막 style sheet가 이긴다.

 

 CSS Object의 장점

1. style sheet 하나만 건들면 style이 적용돼 있는 요소들이 일괄로 적용된다.

2. javascript를 통해 style을 건드렸는데도 불구하고 성능 상의 저하가 전혀 없다.

3. CSS object 한 번만 건드리면 되기 때문에 비용이 굉장히 싸다.

4. 태그에 미리 클래스를 적용해 놓아도 아무런 문제가 없다.

 

 세계적으로 유명한 사이트에서는 CSSOM을 적극적으로 사용해서 DOM은 고정적인 형태로 건드리지 않고 class나 DOM 구조에 맞게 CSS Object만 조정해서 스타일을 계속 바꿔주는 경우가 많다. 이게 하나 하나  DOM의 style을 조정하는 것보다 훨씬 빠르기 때문이다.

 

 

[ Compatibility Library ]

 

1. Vendor Prefix -> Runtime Fetch

 vendor prefix의 문제는 실행 중에 그 속성을 확인해 보는 수밖에 없다는 점이다. 예를 들어 브라우저가 크롬이라면 border-radius에 webkit을 붙이자 같은 방식의 코딩이 안 된다. 같은 크롬이라도 버전에 따라 vendor prefix가 붙거나 또는 떨어져야 되기 때문이다.

 

2. Unsupported Property -> Graceful Fail

 브라우저마다 지원하지 않는 속성이나 값이 있다는 것이 문제이다. 예를 들어 IE7에는 rbga라는 color 값을 지정하면 브라우저가 죽는다. 자바스크립트가 뻗는 것처럼 브라우저가 뻗어 버린다. 이런 점을 악용할 수 있다. 보안을 검증하는 자바스크립트가 도는 중간에 CSS 공격으로 브라우저를 멈추게 시키고 보안을 패스시키는 등의 일이 가능하다. 따라서 이러한 문제를 부드럽게 잘 처리해야 한다.

 

3. Hierarchy Optimaze -> sheet.disabled = true;

 계층 구조에서 최적화해야 하는 경우가 많이 생긴다. 예를 들어 class 하나를 계산하려면 스타일 시트 객체를 돌면서 그 안에 있는 rule list를 다 돌면서 그 안에 있는 속성을 다 합쳐서 계산해야 한다. 스타일 시트를 여러 개 등록하면 브라우저가 죽어난다. 이론상 객체 모델을 활용하면 여러 개의 스타일 시트를 하나의 객체로 통합한 다음에 나머지 시트를 꺼버리면 속도를 최적화할 수 있다. FireFox에서는 사이트 렌더링 속도의 저하 원인이 CSS의 중첩된 적용이라는 걸 인지하고 터보 팬 같은 걸 만들어서 속도를 높이는 작업을 했다.

 

UML

 이제 만들어 볼 framework은 이런 구조를 지닌다. 가장 의존성이 없는 것이 CSS Style Declare로 DOM, CSS 모두에 있다. vendor prefix를 알아서 처리하도록 만든다. 이 경우 style은 알아야 하는 것이 없으므로 style부터 만든다.

 

 여기서 style은 위에 CSSOM 구조를 보여주는 그림에서 style을 추상화해서 객체로 만든 것이다. 추상화하는 이유는 style에다가 날로 속성을 넣으면 vendor prefix가 해결되지 않기 때문이다. 우리가 만들 구조를 통해 넣어야지만 vendor prefix가 자동으로 처리되게 만드는 것이다.

 

const Style = (_ => {
  const prop = new Map, prefix = 'webkit,moz,ms,chrome,o,khtml'.split(',');
  const NONE = Symbol();
  const BASE = document.body.style;
  const getKey = key => {
    if (prop.has(key)) return prop.get(key);
    if (key in BASE) prop.set(key, key);
    else if (!prefix.some(v => {
      const newKey = v + key[0].toUpperCase() + key.substr(1);
      if (newKey in BASE) {
        props.set(key, newKey);
        key = newKey;
        return true;
      }
    })) {
      props.set(key, NONE);
      key = NONE;
    }
    return key;
  };
  return class {
    constructor(style) { this._style = style; }
    get(key) {
      key = getKey(key);
      if (key === NONE) return null;
      return this._style[key];
    }
    set(key, val) {
      key = getKey(key);
      if (key !== NONE) this._style[key] = val;
      return this;
    }
  };
})();

prop : key는 일반적으로 쓰고 있는 속성, value는 브라우저가 지원하는 진짜 이름. 캐시 역할

NONE : 브라우저가 지원하지 않는 속성이라고 알릴 때 사용한다.

BASE : document.body.style는 모든 브라우저가 가지고 있다. 이 style이 속성을 가지고 있다면 이 속성은 존재하는 것이다.

getKey :  표준 이름을 브라우저가 지원하는 이름으로 바꾸는 함수.

some() : 배열의 원소를 하나씩 돌다가 true를 반환하면 멈춘다.

 

const Rule = class {
  constructor(rule) {
    this._rule = rule;
    this._style = new Style(rule.style);
  }
  get(key) {
    return this._style.get(key);
  }
  set(key, val) {
    return this._style.set(key, val);
  }
}

 rule도 직접 속성을 다 구현해서 감싸는 것이 아니라 원래 있는 rule 객체를 감싸는 클래스를 만든다. style은 원래 날로 쓰려고 만든 것이 아니라 rule이 쓰려고 만든 것이다.

 

const Sheet = class {
  constructor(sheet) {
    this._sheet = sheet;
    this._rules = new Map;
  }
  add(selector) {
    const index = this._sheet.cssRules.length;
    this._sheet.insertRule('${selector}{}', index);
    const cssRule = this._sheet.cssRules[index];
    const rule = new Rule(cssRule);
    this._rules.set(selector, rule);
    return rule;
  }
  remove(selector) {
    if (!this._rules.contains(selector)) return;
    const rule = this._rules.get(selector);
    Array.from(this._sheet.cssRules).some((cssRule, index) => {
      if (cssRule === rule._rule) {
        this._sheet.deleteRule(index);
        return true;
      }
    });
  }
  get(selector) { return this._rules.get(selector); }
};

 sheet는 rule을 감싸고 있으니 rule을 add, remove 하는 것이 주요 기능이 된다. selector 기반으로 작동하도록 구현한다. add는 가장 끝에 새로 추가하면 되니 rule.lenght를 인덱스로 삼아 넣으면 된다. remove 같은 경우는 기존 deleteRule 메소드를 그대로 사용하지 않고 개선한다. selector를 기반으로 검색해서 지울 수 있으면 사용하기 더 편해진다.

 cssText는 CSS style sheet에서 지원하는 문자들을 한 번에 넣으면 브라우저가 알아서 파싱해서 넣어준다. 자바스크립트 속성 이름이 아니라 CSS 속성 이름을 그대로 넣을 수 있는 것이 장점이다. 그러나 cssText를 이용해 넣으면 지금 만든 클래스의 key 매칭 기능이 무력화된다.

 

@keyframes size {
  from { width: 0}
  to { width: 500px }
}
const Sheet = class {
  constructor(sheet) {
    this._sheet = sheet;
    this._rules = new Map;
  }
  add(selector) {
    const index = this._sheet.cssRules.length;
    this._sheet.insertRule(`${selector}{}`, index);
    const cssRule = this._sheet.cssRules[index];
    let rule;
    if (selector.startsWith('@keyframes')) {
      rule = new KeyFramesRule(cssRule);
    } else {
      rule = new Rule(cssRule);
    }
    this._rules.set(selector, rule);
    return rule;
  }
  remove(selector) {
    if (!this._rules.contains(selector)) return;
    const rule = this._rules.get(selector);
    Array.from(this._sheet.cssRules).some((cssRule, index) => {
      if (cssRule === rule._rule) {
        this._sheet.deleteRule(index);
        return true;
      }
    });
  }
  get(selector) { return this._rules.get(selector); }
};

 animation key frames 같은 경우 중괄호 안이 selector와 속성처럼 생겨 일반적인 style selector와 비슷하게 보인다. 그러나 DOM selector가 아니라 keyframe animation selector가 들어간다. 올 수 있는 것이 %, from, to 같은 것이 나온다. 그래서 기존 style selector와는 파싱하는 방법이 다르다.

 '@keyframes 이름'까지는 sheet의 insertRule이 알아서 type을 잘 파악해서 넣어줄 테니 신경 쓸 필요가 없는데 그 안에 들어가는 내용을 처리하는 부분이 문제다. 기존 코드의 rule 같은 경우 rule을 style rule로 가정하고 있는데, 이건 style rule이 아니다. 따라서 분기해서 따로 처리해야한다. 그리고 이 타입의 rule을 처리하는 방법은 잘 모르겠으니 다른 객체를 만들어 분리해 위임하면 된다. sheet가 할 일은 selector에 뭐가 들어왔는지에 따라 각각의 역할을 잘 분리해서 rule만 얻으면 되는 것이지 rule 자체에 대해서는 관심을 가질 필요가 없다.

 

const KeyFramesRule = class {
  constructor(rule) {
    this._keyframe = rule;
    this._rules = new Map;
  }
  add(selector) {
    const index = this._keyframe.cssRules.length;
    this._keyframe.appendRule(`${selector}{}`);
    const cssRule = this._keyframe.cssRules[index];
    const rule = new Rule(cssRule);
    this._rules.set(selector, rule);
    return rule;
  }
  remove(selector) {
    if (!this._rules.has(selector)) return;
    const rule = this._rules.get(selector);
    Array.from(this._keyframe.cssRules).some((cssRule, index) => {
      if (cssRule === rule._rule) {
        this._keyframe.deleteRule(index);
        this._rules.delete(selector);
        return true;
      }
    })
  }
}

 sheet 객체와 거의 비슷하게 생긴 것을 알 수 있다. keyframes 내에 구조를 보면 왜 비슷한지 짐작할 수 있다.

 애니메이션의 형태가 다변화하는 경우에는 모든 경우의 수를 대응하는 키 프레임 애니메이션을 사전에 정의할 수 없으니까 자바스크립트 애니메이션을 썼다. 그러나 이걸 이용하면 그것에 맞는 키프레임 selector를 정의해서 박아놓고 바로 클래스 때리면 된다. 애니메이션의 동적 정의가 가능하고 재활용도 가능하다. remove rule을 이용해 rule을 날리고 넣고 날리고 넣는 것을 반복하면 똑같은 클래스를 갖고 있는 클래스에 적용되는 그 태그조차도 애니메이션이 계쏙 변할 수 있다.

 

 

[ CSS Typed Object Model ]

  houdini : 미국 대기업들이 만든 협회. CSS에서 요구하는 바를 만들어 W3C에 draft로 제출하는 단체. CSS 엔진과 관련되어 있는 여러 가지 기능들을 빨리 표준화 시키는 것이 목적. 그런데 구글 같은 경우는 표준화 되기도 전에 크롬에 적용시키는 중이다. 사실 구글이 주도하며 W3C라는 협의체가 귀찮아서 무시하기 위해 만든 단체.

 

 CSS Typed Object Model : CSS 값에 타입과 메소드, 적절한 객체 모델을 추가한 것이다.

 

 styleMap의 get, set 메소드를 이용하여 스트링이 아니라 순수한 값을 이용할 수 있다. 실제 구현체의 이름이 버전마다 자주 바뀐다. 밑에서 보다시피 정적인 값을 가지고 있는 것이 특징이다.

 ex) el.styleMap.set('opacity', CSS.number(0.5)); el.styleMap.set('height', CSS.px(500));

 

 다음은 단위 예시이다.

CSS {

  number, percent

 

  em, ex, ch, ic, rem, lg, rlh, vw, vh, vi, vb, vmin, vmax, cm, mm, Q, in, pt, pc, px

 

  deg, grad, rad, turn

 

  s, ms, Hz, kHz, dpi, dpcm, dppx, fr

}

 

 묶음 값 예시

CSSTransformValue

  CSSTransformComponent

    CSSTranslate, CSSRotate, CSSScale, CSSSkew, ...

 

margin: 10px 0 0 10px - CSSPositionValue

background:url('a.png') - CSSImageValue

inset, left... - CSSStyleValue

 

 

 * 내용 추가 : https://wit.nts-corp.com/2018/05/21/5256 // 이 페이지를 이용해 내용 추가할 것.

 

 

'HTML & CSS' 카테고리의 다른 글

CodeSpitz - Semantic Web & CSS Query  (0) 2021.12.20
CodeSpitz - Transform 3D & SCSS & Compass  (0) 2021.12.20
CodeSpitz - CSS Box Model  (0) 2021.12.20
CodeSpitz - Graphics System & Normal Flow  (0) 2021.11.09
HTML & CSS  (0) 2021.10.21
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함