본문 바로가기
  • Seizure But Okay Developer
FrontEnd/CSS 관련자료

코드스피츠 76-3 (CSSOM & Vendor Prefix)

by Sky_Developer 2019. 10. 20.

** Vendor prefix 참고자료 (출처 : TCP School, 지구별 안내서)

 

벤더 프리픽스(vendor prefix)란?

크롬이나 익스플로러 같이 주요 웹 브라우저 공급자가 새로운 실험적인 기능을 제공할 때 이전 버전의 웹 브라우저에 그 사실을 알려주기 위해 사용하는 접두사(prefix)를 의미합니다.

즉 아직 CSS 권고안에 포함되지 못한 기능이나, CSS 권고안에는 포함되어 있지만 아직 완벽하게 제정된 상태가 아닌 기능을 사용하고자 할 때 벤더 프리픽스를 사용하게 됩니다.

그렇게 하면 해당 기능이 포함되어 있지 않은 이전 버전의 웹 브라우저에서도 그 기능을 사용할 수 있게 됩니다. 사용방법을 예를 통해서 조금 더 살펴보겠습니다 ( 나중에 Vendor-prefix 를 갖고 framework를 만들 것이기 때문에 처음에 개념을 잘 잡고 가는 것이 중요합니다 )

 

사용방법 : 브라우저 접두어를 앞쪽에 쓰고, 맨 마지막에 표준 속성(비 접두어 버전)을 씀. 접두어 버전을 사용하는 브라우저는 그것을 이용할 것이고, 이해하지 못하는 브라우저는 그 속성을 무시합니다. 표준을 지원하는 브라우저는 맨 마지막에 있는 속성을 실행합니다.

 

사용 예 :

div{

-moz-border-radius: 10px;

-webkit-border-radius: 10px;

-o-border-radius: 10px;

border-radius: 10px;

}

 

이러한 브라우저별 접두어를 반복해서 쓰는 것이 번거롭고 성가시지만, 브라우저가 개선되면 필요 없는 접두어 버전을 지우면 됩니다. 하지만 브라우저를 업데이트하지 않는 사용자를 위해 기존의 접두어 코드를 남겨두는 것 또한 괜찮습니다. 예를 들어, 둥근 모서리를 만들기 위해선 위에서 작성한 것 처럼 브라우저별로 접두어를 작성해야 했지만, 현재는 표준 버전 하나만 필요하므로 아래와 같이 작성할 수 있습니다.

 

div {

border-radius: 10px;

}

 

 

CSS 스펙을 살펴보니 난잡하죠? 난잡하다보니까 브라우저마다 지원 사양이 극적으로 변합니다. 매번.

그래서 그것에 대한 vendor prefix가 붙는게 대부분이에요. 새로운 기능들은 vendor prefix가 붙었다가, 안정화되고 표준이 되면 떼는 식으로 하는게 업계 관행이라서 vendor prefix 처리가 굉장히 까다롭죠. 게다가 그냥 까다로운 것도 아니에요. Chrome을 예로 들어보면, 지금 border-radius 를 써봤을 때 vendor-prefix를 붙이면 작동안해요. 어떤 식에는 vendor-prefix를 붙여야지 작동하고 어떤 식에는 떼야지 작동합니다. 이런 복잡한 문제들이 있죠.

CSSOM은 뭐냐, 우리가 DOM이라는 말은 익숙하잖아요? DOM. Document Object Model인데 이 때 말하는 Document는 Html 이거든요? Html을 객체화 시켜서 프로그래밍 가능하게 바꿔놓은 것이 DOM 입니다. 근데 우리가 javascript를 통해서 DOM API를 이용하면 직접 html을 수정하지 않고 해당 DIV를 바꿔놓는 다던지 속성을 고친다던지 이런일을 할 수 있잖아요? CSS도 마찬가지로 Javascript를 통해서 CSS 를 수정할 수 있어요. DOM에 inline 스타일을 넣는게 아니라 CSS 그 자체. 그 자체를 수정할 수 있습니다. 그래서 CSS를 Object화 시켜서 모델링 한게 CSSOM 입니다. 

오늘은 이 두가지를 나가고 시간이 남으면 뒤 편에 준비한 것을 나가겠습니다.

 

inline style : HTML 태그 내 스타일을 지정하는 것 (ex : <p style="width:100px;">abc</p>

 

CSS Object Model(CSSOM)을 이해하기 위해서 간단한 스타일 태그 하나를 살펴보겠습니다.

 

첫번 째로는 STYLE DOM ELEMENT가 있죠. 스타일 태그를 말합니다. 이 스타일 태그는 순수한 html 태그입니다. 그런데 이 style 태그안에는 무언가가 있는데요, 사진에서 보면 style 태그를 el 변수로 받습니다. 이 때 style 태그 안에는 sheet 라는 속성이 있는데(el.sheet) 이 sheet 가 바로 실체입니다.

 

HTML이라는 큰 컨테이너 선에 타기 위해서 각각이 컨테이너 박스처럼 포장되어 있다고 생각하시면 되요. 어떤 컨테이너 안에는 TV가 들어있고 어떤 컨테이너 안에는 장난감이 들어있을 수도 있습니다. 진짜 실체는 이 컨테이너 안에 들어있지만 우리가 DOM 이라는 형태의 컨테이너로 감싸놓으면 컨테이너 선에 실을 수 있습니다. 따라서 style sheet가 진짜 실체이지만 실체는 DOM안에 감춰져 있고 이는 DOM으로 포장해서 HTML 문서에 집어넣은 것입니다.

예를 들어 캔버스라는 태그를 생각해봅시다. 캔버스 태그의 실체는 어디있나요? getContext로 얻어온 canvas2dcontext 안에 있습니다. 우리는 이것을 조작하여 그림을 그립니다. 그러나 이것을 DOM에 삽입하려면 반드시 canvas라는 태그로 감싸야지만 html 안에 끼울 수 있습니다.

그래서 태그는 그 자체가 실체가 아니라, Html에 끼워넣기 위한 일종의 컨테이너 박스 같이 표준화 되어 있는 wrapping 객체인 것이고 그 실체는 원래 태그 안에 들어있습니다. 우리가 querySelector를 통해 얻어온 앨리먼트 자체는 Document 앨리먼트(DOM 앨리먼트)이긴 하지만 이 앨리먼트가 실체가 아니라, 앨리먼트를 끄집어 내어 그 안의 내용물이 바로 실체입니다.

HTML은 한번에 스펙이 짠! 하고 나타난 것이 아니라 역사적으로 태그가 천천히 추가되었습니다. 최초의 HTML은 태그가 거의 몇개 없었고 하나둘 씩 추가되다 보니 HTML 태그 안에 있는 내용물을 끄집어내는 방법이 매번 달랐습니다. canvas는 getContext 라는 메소드를 내용물을 호출해서 끄집어 냅니다. 근데 구형의 element들은 속성(class, id 등)으로 끄집어 내는 경우가 많습니다. 우린 여기서 StyleSheet 라는 객체를 끄집어 낼 것입니다.

console.log(sheet)

element에 sheet 속성을 참고하면 CSSStyleSheet 라는 객체를 얻게 됩니다. 이게 바로 이 style 태그의 실제 실체인 것이며 style(CSS)을 관리할 수 있게 해주는 실체는 element가 아니라 element안에 있는 sheet 객체입니다. 우리가 canvas를 조작하기 위해서 context 한테 명령을 내려야 하는 것처럼 stylesheet를 조작하려면 element의 sheet 속성에 들어있는 CSSStyleSheet 라는 객체를 조작해야 합니다.

Sheet(* CSSStyleSheet를 줄여서 표현)는 내부 속성에 CSSRules라는 리스트(일종의 배열)를 가지고 있습니다. CSSRule는 내부에 수많은 rule들을 소유하게 됩니다.

** 이 때 리스트는 유사배열이라고 부르며 배열처럼 생겨서 length 라는 속성은 있으나, 그 외 배열(Array)에서 쓰는 메소드는 list에서 작동하지 않습니다.

CSSRulelist 안에는 여러 개 rule들이 들어있으며 각 rule들을 하나의 item 이라고 부릅니다. 그래서 rule을 하나만 얻고 싶을 땐 CSSRulelist 에서 각 rule에 해당하는 idx를 부여해 얻을 수 있습니다.

이 때 위 사진에서 볼 수 있듯이 0번 idx에 있는 게 바로 style 태그에 s 라는 id로 선언한 test 속성입니다. 만약 여러 개를 선언하였다면 정의한 순서대로 item이 들어가 있습니다. 지금은 하나만 정의했기 때문에 sheet(style 태그) 안의 하나의 rule(item) 은 0번 idx에 대응하는 rule 객체로 번역이 되어있습니다. 

Rule : 하나의 CSS 정의 ( ex) .test{background:#ff0} )

Html 태그 하나가 DOM element 하나로 번역되는 것처럼,

CSS에선 스타일 태그 내 정의한 속성 하나가 하나의 rule 로 번역이 되고 그 rule들이 모인 게 rules가 됩니다.

rule에는 여러가지 속성이 있습니다.

  • TYPE
  • SELECTORTEXT
  • STYLE 객체 

Style 객체는 보신 적이 있을 겁니다. 바로 DOM 앨리먼트에 보면 똑같은 Style 객체가 들어있습니다. 이는 inline 스타일을 주관하고 있는 것으로, 어떤 Document로부터 element를 얻고 나서 element의 style 속성을 이용해서 조작할 수 있습니다. 이 때의 style 객체와 위에 STYLE 객체는 똑같은 객체입니다. 구체적으로 말하자면, CSSStyleDeclaration 이라는 클래스의 객체가 DOM에도 들어있고 rule안에도 들어있습니다. selectortext는 위 사진에서 봤을 때 '.test' 가 됩니다. 

 

DOM처럼 rule 안에도 style 객체가 있으므로, DOM에서 style 객체를 쓰듯 rule에 있는 style 객체의 background를 위 사진에서처럼 콘솔로 찍어서 조회하면 '#ff0' 즉 노란색이 찍힙니다.

console 결과
CSS Object Model

Html에서 text를 통해 정의했던 CSS가 실제로 브라우저가 해석 과정을 거치고 나면 내부에는 사진에서 보는 형태처럼 메모리에 객체형태로 저장이 되므로, 하나하나 다 설정할 수 있는 형태로 바뀌어져 있습니다.

우리가 Html에 text로 적어놓은 것이 메모리 상의 구조로 바뀐 게 바로 CSS Object Model 인 것입니다.

이 외에도 type 이라는 속성이 있습니다.

사진에서 rule의 타입은 1이라고 나와있습니다. 이는 1번 타입의 rule이라는 것을 의미하며 1번 타입의 rule은 일반적으로 CSS 정의에 대한 rule 입니다. sheet가 소유할 수 있는 type의 종류는 굉장히 많습니다.

맨 처음이 STYLE_RULE 로 내부에 들어가는 것은 CSSOM 객체가 들어가게 됩니다. 이외에도 Charset_Rule(CSS안에서 @charset 로 사용하는 거), Import_Rule(Import 시키는 것), Media_Rule(Media Call이 사용하는 것), Font_Face_Rule(외부의 웹 폰트를 include 하기위해 font_face를 선언하여 사용하는데, 이 때의 font_face 또한 rule로 포함됨) 등을 다 Sheet 가 소유한 rule 들이며,

우리가 CSS에서 사용할 수 있는 모든 구문은 전부 다 거기에 대응되는 특별한 rule 객체로 바뀌어서 rules 안에 원소로 들어가게 됩니다. Full spec을 봤을 때 CSS에 정의할 수 있는 내용이 이렇게나 많습니다. 그 중 Keyframes 같은 경우 'css3-animations' 라는 특별한 객체로 바뀝니다. 이 부분은 나중에 더 살펴보겠습니다. 이렇듯 각 자 해당되는 객체로 바뀌고 해석이 될 때 메모리 상에 저장이 됩니다.

그리고 js를 이용하여 동적으로 CSS를 추가할 수 있습니다. 이는 앞서 설명한 rules에 추가해서 되는 것이 아니라 sheet에 직접 추가를 해야됩니다. 왜냐면 CSSStyleSheet 객체 그 자체가 sheet 이기 때문입니다(?). 그래서 sheet에게 직접 추가를 해달라고 부탁하는 방법이 바로 insert rule 입니다.

 

insert rule은 다음과 같이 사용할 수 있습니다.

 

첫번 째 인자는 Html에서 CSS 구문을 쓸 때의 코드와 완전히 비슷하게 문자열로 주고 있습니다. 이는 '내가 이렇게 값을 줬을 때 너가 알아서 parsing해서 값을 집어넣어줘' 라는 것이며 type도 알아서 판단하게끔 합니다. CSS 속성 그 자체를 문자열로 줍니다.

두번 째 인자로 인덱스 번호로 어느 위치에 넣을 지에 대한 값을 줍니다. 순서를 지정할 수 있는 것인데 CSS는 CSSRulesList 안의 순서가 중요합니다. 왜냐면 만약 같은 속성을 연달아 선언했을 때 나중에 선언된 것이 앞서 선언한 것을 덮어버리기 때문입니다. 이 때 rules.length를 주면 항상 list의 마지막에 추가를 할 수 있습니다.

rule이 잘 추가됬는지 확인할 수 있는 방법으로 현재 자기의 sheet 상태를 표현하고 있는 'cssText' 라는 것을 사용하여 값을 출력해보면 됩니다. 이는 innerHTML 비스무리 합니다.

이제 Sheet에 추가된 것은 확인했지만 정말 추가가 되어 사용할 수 있는지도 확인해보겠습니다. 

잘 추가가 되었습니다. 만약 insertRule을 나중에 하게 됬을 때도 제대로 적용이 되는 것을 볼 수 있습니다.

 

아무 색상이 적용이 안되있음
onclick 속성을 통해 red를 클릭하면 색칠이 됨

이를 통해 알 수 있는 것은 Document는 자기에 소속된 sheet 객체에 변화가 생기면 repaint를 하고 상황에 따라선 reflow까지 처리를 합니다. 그래서 화면을 다시 re-rendering 하는 효과가 있습니다.

document에 등록되어있는 Style Sheet에 변화가 생기게 되면 다시 re-rendering 합니다

그러면, 위에서 살펴본 예제가 Document에 등록된 Style Sheet 라는 사실은 어떻게 아는 걸까요? 그것은 우리가 처음부터 Html에 style 태그를 설정해놓은 것이 있는데, 이렇게 한 것이 '원래부터 Document에 등록되어 있는 태그' 라고 말해주는 것입니다. 이렇게 처음부터 등록되어 있는 스타일 태그이자 Style Sheet는 따로 관리를 합니다.

-> 현재 예제를 작성 후 개발자 console에 document.styleSheets 를 찍어보면 CSSStyleSheet는 script로 작성한 red, blue만 뜨고 있고 그전 StyleSheet에 대한 data가 뜨지 않습니다. 스펙에 변화가 생긴 것인지 확인이 필요합니다.

document.styleSheets : 현재 document에 등록된 StyleSheets를 확인할 수 있습니다

위 사진에서 볼 수 있듯이 script에서 만들었던 CSSRules이 Sheet의 1번 idx에 위치하고 있습니다. 0번을 펼쳐보았을 때 CSSRules에는 아무것도 없습니다. 이를 보았을 때 stylesheet를 link로 불렀던 style 태그를 썼던 우리가 등록한 것들은 StyleSheet에 차곡차곡 쌓이며 등록이 됩니다. 0번째 stylesheet는 Document가 원래 가지고 있는 sheet 입니다. 기본 스타일 객체라고 생각하면 됩니다. 반면 1번째 stylesheet는 우리가 style tag로 집어넣은 것입니다. StyleSheet의 순서에서 맨 마지막에 있는 것이 앞선 것을 덮어버립니다. 그래서 만약 여러 개의 style 태그를 넣는다면 마지막에 있는 style 태그가 적용됩니다.

그리고 stylesheet 속성 중 disabled 라는 것이 있습니다. 이 속성에 true를 주면 해당 stylesheet를 꺼버리게 됩니다. 이 경우 CSSRules를 추가했어도 Sheet에 적용이 안됩니다.

이제 deleteRule 라는 기능을 배우겠습니다. 인자로는 간단히 인덱스 값만 주면 되며 해당 인덱스의 rule를 삭제합니다. 그래서 rules.length-1 으로 인자를 주면 맨 마지막 rule을 날립니다.

red를 클릭 후, blue를 클릭했을 때 blue 속성이 사라짐

CSS Object Model을 다룬다는 것은 DOM의 style 객체를 조작하는 것과 완전히 다른 것입니다. stylesheet를 동적으로 조작할 수 있다는 점이 완전히 다른 이유 중 하나입니다. stylesheet를 건드리면 inline style을 건드리는 것보다 훨씬 더 효율적으로 style를 제어할 수 있습니다. stylesheet 하나만 건드려도 해당 style이 적용되있는 것들에 모두 일괄적으로 효과를 줄 수 있기 때문입니다.

 

javascript로 style을 변경하여 inline style을 건드리지 않고 CSS를 조작하는 경우에는 성능 상에 저하가 전혀 없으며, CSS Object 하나만 건드리면 일괄 적용되기 때문에 비용도 굉장히 쌉니다. 게다가 태그에 미리 스타일을 적용해놓아도 동작하는데는 아무 문제가 없습니다. 세계적으로 유명한 사이트들은 CSS Object Model을 적극적으로 사용하여 DOM을 건드리지 않고(DOM은 고정적인 형태로 정해놓음) 클래스나 DOM 구조에 맞게 CSS Object만 조종해서 지속적으로 style을 바꿔주는 사이트들이 많습니다. 이 방법이 하나 하나 DOM의 스타일을 조정하는 것보다 훨씬 빠르기 때문입니다.

수많은 태그들에 CSS 속성을 CSS Object를 통해 일괄적으로 적용한 예

stylesheet를 직접 건드리는 방법은 inline style를 직접 적용하는 것과는 비교가 안되게 빠릅니다.

CSS Object Model을 왜 변경해야 하고, 어떠한 구조로 되어 있는지 등을 이해하면서, 우리는 이제 CSS Object Model에 입문을 하였습니다.

 

 

CSS Object Model에 대한 기본은 잡았으니, 이를 기반으로 CSS 전체를 안정적이게 script로 통제할 수 있는 framework를 만들어보겠습니다.

 

첫번 째로, Vendor Prefix를 최우선적으로 해결할 수 있다는 이점이 있습니다.

두번 째로, 앞서 예제에서 살펴보았듯이 동적으로 style를 관리할 수 있는 이점이 있습니다.

 

그럼 다음 특징들을 한번 정리해보겠습니다.

  • Vendor Prefix
    • Runtime Fetch : Vendor Prefix는 실행 중에 그 속성을 확인해 볼 수 밖에 없습니다.

      예를 들어, '브라우저가 chrome이라면 border-radius에는 web-kit을 붙이자' 는 통용되지 않습니다.

      왜냐면 똑같은 chrome이지만 버전이 다른 54에선 web-kit을 붙여야하는데 버전 66에선 붙이면 안되는 식으로 작동하기 때문입니다. 그러면 border-radius를 적용하려고 할 때 그런 애가 진짜로 있는지 그 때 그 때 확인을 하는 수 밖에 없습니다.

      이 속성이 정말 있는지 없는지는 미리 특정 공식을 만들어 확인할 수 없고, 실행 도중에 확인할 수 밖에 없습니다.
  • Unsupported Property
    • Graceful fault : 두번 째는 브라우저마다 지원하지 않는 값이나 속성이라는 문제가 존재합니다.

      대표적으로, ie7는 rgba-color값을 지정하면 마치 javascript가 뻗는 것처럼 브라우저가 뻗는 것을 확인할 수 있습니다. opera 또한 이런 방식으로 브라우저가 뻗을 수 있고, 많은 브라우저 들이 stylesheet 코드를 통하여 뻗을 수 있습니다.

      하여 보안을 검증하는 javascript가 돌고 있다가, 중간에 css 공격으로 브라우저를 멈추게 시킨 뒤 보안을 패스시키는 핵들이 많이 유래되었습니다.

      그러므로 지원하지 않는 값이나 속성에 대해서 우아하게(부드럽고 조용하게) 잘 처리를 하게 합니다.
  • Hierachy Optimaze
    • sheet.disabled = true;

      계층 구조에서 최적화라는 개념이 많이 생깁니다.

      계층 구조 최적화는 아까 예제에서 처럼 stylesheet가 여러 개 있을 때 0번 stylesheet를 갖고 우리가 DOM에 적용할 것을 다 계산을 했는데 그런데 또 1번 stylesheet가 있어 이것 또한 합쳐서 또 계산을 하게 됩니다

      만약 style 태그를 20개 쯤 붙여놓는다면, 계산하기란 엄청 힘들 것입니다.

      .red 에 대해서 계산을 하려면 stylesheet 객체를 돌며 그 안의 rulelist를 돌면서 또 rule을 돌고, rule 안에 속성을 또 다 돌아봐야하고 최종적으로 이들을 다 합쳐서 계산을 해야합니다.

      그래서 stylesheet를 style태그나 link 태그를 여러 개 등록하게 되면 브라우저는 과부하가 심하게 옵니다. 하나의 div에 적용될 것을 계산하기 위해 엄청 나게 많은 탐색 비용이 듭니다.

      그래서 객체 모델을 이용하면, 그것들을 모두 하나의 객체로 통합한 다음에 (0번 stylesheet에 있는 rulelist를 다 가져온 다음, 17번에 다 옮기고, 1번에 있는 것 또한 가져다 다 붙이는 등 다 옮겨와서 하나로 만듦) 나머지는 다 sheet.disabled = true로 꺼버림.

      이것으로 인해 속도 차이가 굉장히 많이 납니다.

      큰 사이트들은 stylesheet를 굉장히 많이 가져다 쓰기 때문에 계산이 어렵습니다. firefox는 사이트에 렌더링 속도가 굉장히 느려지는 이유가 css에 중첩된 계산 때문이라는 것을 인지해서 turbo mode 같은 것을 만들어서 속도를 굉장히 높이는 작업을 진행했습니다 (2018/05/18 기준)

framework에 STYLE, RULE, CSS 에 대응하는 각각의 클래스를 하나씩 만들 것입니다. 각각에 대해 설명하겠습니다.

 

  • STYLE : 다른 클래스에 가장 의존성이 없는 객체는 DOM 에도 있고 CSS Rule에도 존재하는 CSSStyleDeclare 입니다. 이를 STYLE 클래스로 wrapping 하며 Vendor prefix를 처리할 것입니다.
  • RULE : 위의 CSSStyleDeclaration을 소유하고 있는 CSSRule 객체를 RULE 클래스로 wrapping 할 것입니다. RULE은 STYLE을 소유할 수 있습니다.
  • CSS : 위 CSSRule을 여러개 소유하고 있는 StyleSheet 객체를 CSS 클래스로 wrapping 합니다. CSS는 RULE을 여러개 소유할 수 있습니다.

화살표가 가리키고 있는 방향은 의존하고 있는 방향입니다. 다시 말해 Sheet 객체는 CSSRule을 알고 있어야 하고 Rule은 CSSStyleDeclaration를 알고 있어야 합니다. 맨 마지막에 위치한 STYLE은 알고 있어야 하는 클래스가 없으므로, STYLE을 먼저 작성합니다.

 

그림에서 보이는 CSSStyleDeclaration을 추상화해서 객체를 만들어야 합니다. 추상화해야 되는 이유는 STYLE에 날 것으로 주면(값 자체를 그대로 주게 되면) Vendor prefix 문제가 해결되지 않기 때문입니다. 추상화를 통해 Vendor prefix가 자동으로 처리되게 하는 구조를 만들것입니다.

 

위 그림과 같이 style 태그 내에 '.test' 가 있다고 했을 때, 이를 얻는 방법은 el > sheet > rules > rule 순으로 참고하는 것입니다. rule 안에는 selectorText와 style 객체가 있습니다. style 객체안에 background는 style 태그의 .test의 backgronud 속성과 대칭이 됩니다. 우리는 rule.style 에 관심을 가지고 더 살펴보겠습니다.

 

이제 style 클래스를 만들것인데, 필요한 utility에 대하여 정리하겠습니다.

 

prefix 변수는 Vendor prefix 문자열입니다. 브라우저마다 다양한 Vendor prefix 문자열이 있습니다. 각각에 대해서 해당 Vendor prefix가 어느 브라우저에 사용되는지를 정리해보겠습니다.

  • 'webkit' : 모든 브라우저용. 흔히 많이 쓰임.
  • 'moz' : mozila 에서 쓰는 firefox 용
  • 'ms' : MS 브라우저 인 Edge나 IE 용
  • 'chrome' : Chrome 브라우저용 (Chrome은 'webkit' 외에도 Chrome 전용속성인 'Chrome'이라는 Vendor prefix가 붙어있음)
  • 'o' : opera 용
  • 'khtml' : 리눅스의 Konqueror 용

major 하지 않은 것들을 포함하면 20종이 넘습니다. 그리고 prop 변수에 진짜 속성과 가짜 속성을 묶어주는 Map을 선언하였습니다. 이 Map의 key는 일반적으로 쓰고 있는 'background' 와 같은 속성이 되고 value는 해당 브라우저에서 Vendor prefix를 포함하여 지원하는 진짜 이름으로 바뀌게 됩니다. NONE은 '이러한 속성은 이 브라우저가 지원하지 않아' 를 표시하기 위한 장치입니다. 그런데, 우리는 공식이 없기 때문에 실행도중에 이 속성이 있는지 물어볼 수 밖에 없다고 앞서 얘기한 바 있습니다. " 'border-radius' 라는 속성이 있니? " 를 누구에게 물어봐서 확인을 해야하는데, 그 누군가는 바로 어떤 브라우저도 반드시 가지고 있는 document.body.style 입니다.

그래서 document.body.style이 속성을 가지고 있다면 해당 속성을 가지고 있다고 확정지을 수 있습니다. 또 반대로 body에게 없는 속성이면 이 브라우저는 그 속성을 가지고 있지 않는 것으로 판단할 수 있습니다. BASE 변수에 선언하고 이제 해당 속성의 존재 유무는 BASE한테 물어볼 것입니다.

 

저희는 두 가지를 해결할 것입니다. 첫번 째는 지원하지 않는 속성에 대하여 부드럽게 실패하기 위해 NONE 을 사용할 것입니다. 두번 째는 Vendor prefix를 Runtime 때 fetch 하기 위해서 BASE(body에 있는 style 객체)를 이용합니다. 이제 차근차근 만들어보겠습니다.

 

이제 표준 이름을 인자로 보냈을 때 진짜 이름을 반환하는 함수를 만들겠습니다. 예를 들어, 표준 이름을 'border-radius' 로 인자를 보냈을 때 실제로 브라우저가 'border-radius' 를 지원한다면 인자를 그대로 받을 것입니다. 반대로 해당 속성이 없지만 '-webkit-border-radius' 가 있는 브라우저라면 '-webkit-border-radius' 를 받아야 할 것입니다. 그러기 위해선 브라우저가 지원하는 진짜 key를 받는 구조로 작성을 해야합니다. 그 외에 IE 브라우저와 같이 해당 속성이 접두어 버전으로도 없는 경우에 NONE으로 반환을 하게 될 것입니다.

정리하자면 표준 이름으로 인자를 주면 해당 브라우저가 지원하는 진짜 이름으로 반환해주는 함수를 만들 것입니다.

 

그러나 이러한 연산을 매번 할 수 없으므로, 한번 연산 시 진짜 이름을 알아냈을 때 캐시로 잡아놓을 것입니다. 이 캐시가 바로 prop 변수입니다(Map 객체). 그래서 동작시 제일 먼저 캐시(prop 변수)에 해당 값이 있는지를 조사하여, 값이 있다면 캐시 안의 값을 주게 됩니다.

정리하자면 한 번이라도 진짜 이름을 확인한 적이 있다면 그 이름에 해당되는 진짜 이름을 캐시에서 꺼내주면 됩니다.

 

다음은 인자로 보낸 key가 BASE에 존재한다면, 해당 key를 가지고 있는 것입니다. 예를 들어 'border-radius' 를 인자로 보냈더니 body.style에 border-radius가 있는 것으로 확인된다면( if(key in BASE) ) 그 시점에 바로 캐시를 잡는 것입니다. 그리고 나중에 border-radius를 찾을 때 이 캐시로 잡은 값을 반환해줍니다.

그러나 반대로, BASE에 해당 속성이 존재하지 않는다면 프리픽스를 붙인 속성은 존재하는지 확인을 할 것입니다. (border-radius가 없다면 -webkit-border-radius, -moz-border-radius 등은 있는지? ) 그래서 전부 순회를 했는데도 프리픽스를 가진 속성이 없다면 해당 키는 NONE으로 표시할 것입니다.

 

v는 prefix 문자열 배열안에 있는 요소로, 원래 키 값의 0번째 idx를 대분자로 바꾼뒤 나머지를 붙이는 식으로 만들었습니다. 그래서 'webkitBackground' 와 같은 형태로 되게 됩니다. 그렇게 만든 key가 BASE에 있는지 확인을 한 후 기본 속성에 대해 접두어를 붙인 형태로 캐시에 잡아놓습니다. 예를 들어, background 속성에 대해 위 사진처럼 'webkitBackground' 로 캐시에 저장이 됩니다. 이렇게 해당 속성을 key를 캐시에 set 하는 것은 처음 한번만 하면 되고, 그 다음부턴 캐시에 저장된 값을 가져다 쓰면 됩니다. 이게 바로 getKey 라는 함수의 작동입니다.

getKey에게 key 인자를 보내면 '그냥 key가 나오던지, newKey가 나오던지, NONE이 나오던지' 셋 중 하나를 반환합니다.

 

이게 바로 Vendor-prefix를 Runtime에 조사해서 진짜 이름을 얻게 되는 우리의 전략입니다. 공식이 통하지 않기 때문에 사실상 유일한 방법입니다. 유일한 방법인 이유는 사례를 들어 설명하겠습니다. 2005년 까지는 firefox에 모듈을 붙어야 border-radius가 적용이 되었습니다. 그러나 2007년 후반기 부터는 모듈을 붙이면 border-radius 가 적용되지 않았습니다. 왜냐면 해당 속성이 표준이 되었기 때문에 Vendor-prefix를 붙인 속성을 없애버렸기 때문입니다. 브라우저들은 예전에 모듈을 붙여 사용한 속성과 표준 두가지가 있을 때, 두가지를 동시에 유지하지 않습니다. 오히려 모듈을 붙인 버전을 빼버립니다. 우리가 작성한 코드는 모든 사람들의 브라우저에서 동작해야 하고, 각 사용자의 크롬 버전이 54, 66과 같이 다르더라도 속성을 쓸 수 있어야 하기때문에 매 순간마다 확인하는 방법을 씁니다. 이외에 전략은 전부 통하지 않습니다.

 

핵심엔진인 진짜 StyleSheet 속성이름을 Vendor prefix를 포함하여 얻는 방법을 찾아냈습니다.

 

이제 이를 바탕으로 class를 만들겠습니다. 우리가 만들 STYLE Class에선 rule의 style을 갖고 태어납니다. (실제로는 rule.style 을 조작하는 것을 wrapping 하고서 좀 더 쉬운 조작을 해주는 class 이기 때문에 rule.style 을 안고 태어납니다(?) -> 무슨 뜻인지 잘 모르겠네요..) STYLE Class의 생성자에선 style 객체를 안고 태어나고 인자로 rule의 style을 줘야 합니다. 그리고 이를 this의 _style에 잡아 놓을 것입니다. 이는 진짜 rule에 있는 style 객체를 잡아두고 있는것입니다.

 

이제 key를 얻기위해 get 함수를 살펴보겠습니다. 만일 특정 StyleSheet에 있는 background라는 값을 얻고 싶다면, 그냥 얻으면 안되고 반드시 getKey 한테 보내서 이름을 얻어야 합니다. 그러면서 webkitBackground, mozBackground 등 처럼 진짜 이름을 얻은 그 key값을 NONE 인지 비교합니다. 만일 key가 NONE 이 나오면 해당 브라우저가 속성을 지원하지 않는 것이므로 브라우저가 죽지 않도록 아예 조회하지 않도록 합니다(return null). 없으면 부드럽게 null 을 return 해주고 반대로 이름이 있으면 style 객체에서 값을 얻어옵니다. 이렇게 하면서 Unspported property - Graceful Fail 을 달성했습니다.

 

기존의 wild한 style 객체

  • 없는 key 문제
  • Vendor prefix 문제
  • 그냥 건드리면 브라우저 죽음

우리 getKey 함수를 내장하는 STYLE 객체 class에게 rule의 style을 넘겨주면

  • 우아하게 null를 받거나
  • 진짜 값을 받음

표준 이름이라는 값만 넘겨주면 우리가 신경쓰지 않아도 브라우저가 알아서 대응할 수 있게 됩니다.

 

다음은 set 함수입니다. set은 인자로 받은 key를 getKey를 통해 진짜 key인지 알아본다음 NONE이 아니면 set을 해줍니다. 그리고 한번 set을 사용하면 여러번 set을 사용하는 경우가 많기 때문에 confluence 로 보내기 위해 this를 return 하여 계속 set을 호출할 수 있게 해줍니다. set 또한 NONE 이면 건드리지 않으므로 Graceful Fail, 우아하게 실패할 수 있습니다. 이렇게 해서 STYLE 객체 Class를 감싸는데 성공했습니다. 핵심은 getKey 함수가 어떻게 우아하게 속성을 얻어오는가 입니다.

 

이제 STYLE class를 직접 이용해보겠습니다. STYLE class에는 앞서 얘기한 것처럼 rule의 style을 넣어줍니다. 그리고 set 함수를 이용해 'borderRadius', 'boxShadow' 등의 속성을 설정합니다. 우리가 만든 구조를 통해 Vendor prefix 걱정없이, 그냥 STYLE class에 속성을 지정하는 것보다 훨씬 안전하게 표준이름을 rule에 지정할 수 있습니다. 그리고 set함수는 this가 return 되는 형태의 confluence 로 만들었기에(=함수 체이닝으로 만듬) set 함수를 반복적으로 사용이 가능합니다.

최종적으로 브라우저의 내부에 반영될 때 box-shadow로 반영될수도 있으나, 브라우저의 특성에 따라서 webkit-box-shadow 로도 반영될 수 있게 됩니다.

 

이제 다음은 이 STYLE 객체 class를 소유하고 있는 Rule 객체 class를 만들 차례입니다. rule은 type, SelectorText, Style 객체를 감싸고 있습니다. 전략적으로 rule은 이들을 전부 직접 감싸지 않고 원래 있는 rule 객체를 감싸는 클래스를 만들기만 합니다. 하나하나 분해해서 클래스의 속성을 잡지않고, 통으로 Style class를 가지는 class를 만들 것입니다.

 

앞서 얘기했듯이 rule을 그대로 사용하면 되므로 본인의 rule에 인자로 받은 rule을 그대로 지정하고 본인의 style에 아까 만든 STYLE class를 만들고 여기에 인자로 받은 rule의 style을 넣어줍니다. Style class에선 속성 검증 과정을 거치므로 안전하게 사용할 수 있습니다. 이제 rule에게 get, set은 직접적인 get, set 이 아니라 본인의 Style 객체에게 get, set 을 쓰는 것이고 마찬가지로 안전하게 사용할 수 있습니다. 결과적으로 rule이 제공하는 get, set 또한 안전해졌습니다.

 

클래스 다이어그램. rule은 style을 소유합니다.

이렇게 Style class를 직접 만날 필요 없이 rule 객체만 알면 Style이 자동으로 mapping 이 되기 때문에 rule에만 get, set를 하면 됩니다. 그래서 우리가 만든 Rule class를 rule 변수에 선언하고 rule에 set을 하면 rule 안에 있는 style class에 알아서 반영이 되게 됩니다. 원래 style은 raw 하게 사용하려고 만든 것이 아니라 rule 이 쓸 수 있도록 하기 위해 만든 것이고 이는 다이어그램에서 보는 대로 rule이 style을 소유하기 때문입니다.

 

rule class는 get, set 을 안전하게 하기 위해 안에 있는 style class에 의존하게 되고, style class는 get, set을 할 때 getKey 라는 함수에게 의존해서 매번 그 이름이 진짜 이름인지 확인하는 절차를 대신해줍니다.

 

내가 할 일을 내가 다하려고 하지 말고 잘하는 놈에게 조금씩 조금씩 나눠주는 것을 보고 있습니다. getKet는 '그 이름이 진짜 이름인가?' 에만 관심이 있고 Style class는 '그 이름이 진짜 이름이라면 getKey에게 맡기되 그 style에 쓸 것이냐 말것이냐' 만 관심이 있습니다. 또 rule은 '그 style class와 관련된 것은 style class에게 맡길 것이고, 난 하나의 rule을 소유해서 rule에 있는 style class를 감싼 style 객체를 만드는 것' 에만 관심 있습니다. 나머지 get, set은 전부다 style에게 위임시킵니다.

'자기 잘하는 일만 하자', '자기의 역할에 맞는 일만 한다' 는 역할 모델이라고 부르며 프로그래밍의 기본입니다. 계속 자기 역할에 맞는 일만 하는 것으로 역할을 나눠줘야지만 수정하거나 기능을 추가할 때 편합니다.

예를 들어, '특정 방식으로 런타임에 인식시켰더니 이름 조사하는 것에 허점이 있었어' 라는 문제가 있을 때 수정해야 될 부분은 'getKey' 뿐입니다. 또 스타일에 속성을 적용할 때 문자로 적용하는 방법 말고도 특수한 방법으로 적용할 수도 있습니다. 이 때는 style class만 바꿔주면 됩니다. rule에 새로운 기능을 추가해야 할 땐 rule만 바꿔주면 됩니다. 이런식으로 자기 역할에 하는 일만 하는 것으로 바꿔줬을 때 필요한 기능을 수정 및 보완할 때 한 가지 부분에 집중하여 고칠 수 있어 편합니다.

 

다음으로 Sheet class를 만들겠습니다. Sheet class는 sheet를 전부 감싸면 됩니다. rule 은 style의 대행자일 뿐, 주인공은 style 였습니다. sheet 는 rule을 감싸고 있으므로 rule을 add, remove하는 것이 주요 기능이 됩니다.

 

먼저 CSSStyleSheet 객체를 받아와서 sheet를 잡습니다. rule은 sheet 안에 있는 rules를 쓰지 않고 새로운 Map을 씁니다. 왜냐하면 아까 만들었던 클래스의 인스턴스를 잡을 것이므로 새로운 객체 구조가 필요하기 때문입니다. 이전에 insertRule, deleteRule가 sheet 객체에 직접 rule을 추가하거나 삭제한 것처럼 add와 remove를 똑같이 만들겠습니다.

insertRule 에 '.red{...}' 를 인자로 준 것처럼 add에는 selector에 해당하는 '.red'만 주고 본문은 주지 않습니다. Sheet class가 rule class를 return 하게 만들 것이고, 이 rule class 가 알아서 본문의 내용을 채워줄 것입니다. 이어서 insertRule에 idx를 rules.length로 주어 마지막에 추가한 것처럼 sheet class에선 진짜 sheet 객체(_sheet)의 cssRules의 length를 얻어 idx를 구합니다. 그리고 실제 StyleSheet에 selector와 idx를 insertRule로 대입하면서 selector에 해당하는 rule을 만들어냅니다.

 

그리고 아까 구했던 idx로 진짜 cssRule을 구하여 이를 Rule 객체에 넣어줍니다. 이렇게 cssRule 를 넣어서 만든 rule은 우리가 만든 Rule class의 인스턴스가 됩니다. 그렇게 해서 만든 rule을(rule 객체) selecto와 같이 아까 Map 객체로 선언한 rules에 잡아둡니다. selector를 key로 잡았으므로 나중에 selector를 조회하여 찾을 수 있습니다. 그리고 나서 rule을 return 합니다.

 

이제 remove를 만들겠습니다. 기존의 deleteRules는 idx만을 넘겨주어 지울 수 밖에 없어 불편한 점이 있었습니다. 우리는 selector(.red, .blue)를 넘겨주면 이를 알아서 찾아서 지우는 구조를 만들 것입니다. 그래서 인자로 selector를 보내줬을 때 Map에 해당 selector가 있는지 확인하고 없으면 그냥 return을 해버립니다. 반대로 있다면 아까 add 할 때 selector로 set을 해줬으므로 selector를 기반으로 조사를 하여 _rule( 좀전에 rule class에서 생성자로 받아왔던 rule을 _rule에 잡아놓았음 ) 을 얻을 수 있습니다. 이렇게 해서 add 할 때 줬던 cssRule을 얻을 수 있습니다. 이 후 sheet에 있는 cssRules 안에서 some으로 순환을 하며 element가 우리가 찾는 rule와 일치하면 그 위치의 rule을 없앱니다.

 

이렇게 해서 Sheet class의 주요 임무는 Sheet 객체를 감싸서 add, remove를 부드럽게 해주는 것입니다.

 

마지막으로 get은 selector를 인자로 받았을 때 해당되는 rule을 반환해줍니다.

 

실제로 우리가 만든 것을 사용해보겠습니다. 오른쪽에 html 코드가 적힌 것처럼 style 태그가 비어있고, body 태그에는 'test' 라는 이름의 class를 가진 태그가 있습니다. 먼저 .test 라는 속성을 rule에 추가하겠습니다. 저희가 만든 Sheet class가 sheet를 받아야 하므로 document.styleSheet[0] 에서 얻어옵니다. ( 0인지 1인지 확인필요). 그리고 아까 add의 인자로 selector를 준 것이 기억나실 겁니다. set에 body를 인자로 주며 빨간색으로 칠하고, '.test' 속성에는 cssText를 사용하여 그림을 그립니다. css에는 일괄로 한꺼번에 속성 값을 밀어넣는 능력이 있습니다. DOM에 있는 style 또한 마찬가지입니다. 특수한 key인 cssText는 한번에 원래 CSSStyleSheet를 지정하는 문자를 밀어넣으면 브라우저가 알아서 파싱을 하므로 한꺼번에 적용이 됩니다. 이는 자바스크립트 속성이름이 아니라 css 이름을 그대로 사용할 수 있는 장점또한 있습니다. 하지만 이렇게 하면 저희의 StyleSheet가 하나하나를 matching해주는 기능이 무력화됩니다(귀찮아서 이렇게 했습니다..) 실제로 동작을 시키면 오른쪽 하단처럼 그림이 그려집니다.

 

이제 우리는 굉장히 복잡한 CSS Object Model을 좀 더 쉽게 다룰 수 있는 여러 가지 방법들을 생각해 볼 수 있는 시각도 키우고, 이렇게 직접 실습을 해야지만 '그냥 오브젝트가 있나보다, 그래서 뭐.' 정도에서 그치지 않고 실제로 와닿기 때문입니다. DOM을 다루기 위해 createElement, appendChild 등의 메소드를 사용했던 것처럼 CSS Object 또한 능숙해지면 insertRule, deleteRule 등의 메소드들을 그처럼 친숙하게 사용할 수 있습니다. 그리고 DOM과 달리 StyleSheet를 직접 조종할 수 있어 유리한 점들이 많았습니다. DOM을 직접 건드려 좋을 일은 없으니, StyleSheet를 건드릴 수 있는 CSSOM과 친숙해지는게 중요합니다. CSS Object Model 도 DOM 처럼 우리가 가깝게 다뤄야할 Model 인 것 입니다.

 

지금까지 설명한 내용은 수많은 CSSRULE type 중 STYLE_RULE만을 다룬 것입니다. Import 하기위한 Import_Rule, Font_Face_Rule 등 다양한 Rule들이 존재합니다. STYLE_RULE은 DOM와 유사한 면이 있어 친숙하게 사용할 수 있습니다StyleSheet에 rule을 정의하기 위해 insertRule, deleteRule 등을 사용한 것이 DOM에 inline_style을 조작하거나 body에 appendChild 하는 것등 비슷하게 생긴점이 그 예이죠. 그러나 이와 완전 동 떨어진 Keyframe(s)_Rule 또한 살펴볼 필요가 있습니다. DOM은 사람들이 쉽게 접하면서 익숙해질 수 있고 객체로 다룰 수 있는 반면 CSS는 중요한 개념이에도 불구, draft나 공식 문서를 잘 찾아보지 않으며 객체로 잘 다루질 못합니다.

 

Keyframes는 애니메이션을 걸 때 많이 사용합니다. 이게 어떻게 객체화 되는지 살펴보겠습니다. (1:08:12)

댓글