CSSOM
고친 사람 github-actions[bot]
CSSOM 은 브라우저가 읽어 들인 스타일 규칙을 프로그램이 다룰 수 있는 객체로 옮겨 둔 구조입니다. 스타일시트와 그 안의 규칙이 하나하나 객체가 됩니다. 렌더링 엔진은 이 객체를 보고 화면의 생김새를 정합니다. 자바스크립트 코드는 같은 객체를 읽고 고쳐 스타일을 바꿉니다.
쉽고 빠른 이해
CSSOM 은 브라우저가 스타일 규칙을 읽어 객체로 바꿔 둔 것입니다. p { color: gray; } 한 줄을
읽으면 규칙 객체 하나가 생깁니다. 그 안에 「<p> 태그에 적용한다」와 「글자색은 회색」이 들어 있습니다.
이게 없으면 브라우저는 화면을 칠할 때마다 규칙 글자를 다시 훑어야 합니다. 자바스크립트도 스타일 하나를 바꾸려면 글자를 고쳐 써야 합니다.
- 브라우저가 스타일시트 파일을 받아 읽고 규칙마다 객체를 만듭니다
- 웹 문서의 태그 하나하나와 이 객체들을 맞대어 태그마다 생김새를 정합니다
- 자바스크립트가 객체를 고치면 브라우저가 바뀐 만큼 다시 그립니다
대가는 기다림입니다. 브라우저는 스타일시트를 다 읽기 전까지 화면을 그리지 않습니다. 스타일시트 파일이 늦게 오면 첫 화면도 그만큼 늦습니다.
상세
이 절은 CSSOM(CSS Object Model, CSS 객체 모델)이 무엇을 담는지, 브라우저 안에서 언제 쓰이는지, 자바스크립트가 어떻게 다루는지를 차례로 봅니다. 짧은 CSS 한 벌을 예로 삼아 끝까지 따라갑니다.
CSS(Cascading Style Sheets)는 웹 문서의 생김새를 적는 언어입니다. 웹 문서는 <p> 같은
태그로 조각이 나뉩니다. 태그 하나로 적힌 조각을 요소라고 합니다.
CSS 는 규칙 단위로 적습니다. p { color: gray; } 한 줄이 규칙 하나입니다. 중괄호 앞의 p 는
「어느 요소에」를 적는 선택자입니다. 중괄호 안은 「어떻게 보이게」를 적는 선언입니다.
선언은 속성과 값의 짝입니다. color: gray 에서 color 가 속성이고 gray 가
값입니다. 이 문서에서 「속성」은 늘 이 CSS 속성을 가리킵니다.
규칙을 모아 둔 파일이나 <style> 태그 하나를 스타일시트라고 합니다. 한 페이지에 스타일시트가
여럿 걸릴 수 있습니다.
CSSOM 은 이 스타일시트를 객체로 옮긴 모습입니다. 이 이름은 두 가지를 함께 가리킵니다. 하나는 브라우저가 스타일시트를 읽어 만들어 둔 객체 구조입니다. 다른 하나는 그 객체의 이름과 다루는 방법을 정한 규격입니다.
규격은 W3C(World Wide Web Consortium, 월드 와이드 웹 컨소시엄)의 CSS 작업 그룹이 정합니다. 그래서 같은 코드가 어느 브라우저에서나 같은 이름의 객체를 만납니다. 이 문서에서 「CSSOM 이 만들어진다」고 하면 앞의 뜻, 곧 객체 구조를 말합니다.
객체로 옮기는 까닭은 CSS 가 글자이기 때문입니다. 글자로는 「이 요소에 어떤 규칙이 걸리나」를 빨리 물을 수 없습니다. 객체로 옮겨 두면 브라우저는 규칙을 몇 번이든 다시 꺼내 씁니다. 자바스크립트도 글자를 고쳐 쓰는 대신 객체의 값 하나만 바꿉니다.
짝이 되는 규격이 DOM(Document Object Model, 문서 객체 모델)입니다. DOM 은 HTML(HyperText Markup Language) 문서를 요소 객체의 트리로 옮깁니다. CSSOM 은 같은 일을 CSS 에 대해 합니다.
스타일시트가 객체로 바뀐 모습
CSS 세 줄이 어떤 객체가 되는지 봅니다. 아래가 예로 삼을 스타일시트입니다.
body { font-size: 16px; }
p { color: gray; }
.note { color: red; }
첫 규칙은 문서 전체의 글자 크기를 정합니다. 둘째는 모든 <p> 의 글자색을 회색으로 합니다.
셋째의 .note 는 태그에 class="note" 가 적힌 요소를 고르는 선택자입니다.
브라우저는 이 글자를 읽어 객체 세 계층으로 옮깁니다. 맨 위는 스타일시트 객체(CSSStyleSheet)
하나입니다. 그 아래에 규칙 객체(CSSRule) 셋이 적힌 순서대로 매달립니다. 규칙마다 선택자
글자와 선언 묶음(CSSStyleDeclaration)을 하나씩 가집니다.
flowchart TD
S["스타일시트 · CSSStyleSheet"] --> R1["규칙 · body"]
S --> R2["규칙 · p"]
S --> R3["규칙 · .note"]
R1 --> D1["선언 묶음 · font-size: 16px"]
R2 --> D2["선언 묶음 · color: gray"]
R3 --> D3["선언 묶음 · color: red"]
모양이 DOM 과 닮았습니다. 위에서 아래로 매달린 트리입니다. 계층마다 정해진 이름의 객체가 놓입니다. 매달린 것이 요소가 아니라 규칙이라는 점이 다릅니다.
DOM 과 만나는 스타일 계산
브라우저는 HTML 과 CSS 를 따로 읽습니다. 두 갈래가 어디서 만나는지, 만난 뒤 요소의 생김새가 어떻게 하나로 정해지는지 봅니다.
HTML 을 읽어 DOM 을 만듭니다. CSS 를 읽어 CSSOM 을 만듭니다. 둘은 서로 모른 채 따로 만들어집니다. 그다음 스타일 계산 단계가 DOM 의 요소 하나하나에 CSSOM 의 어느 규칙이 걸리는지 가립니다.
한 요소에 규칙 여럿이 걸릴 수 있습니다. <p class="note"> 는 p 규칙에도 걸리고 .note
규칙에도 걸립니다. 둘 다 글자색을 정하니 부딪힙니다.
이렇게 부딪히면 더 좁게 고른 규칙이 이깁니다. 좁은지는 걸리는 요소 수가 아니라 선택자의
종류로 가립니다. 클래스로 고른 .note 가 태그 이름으로 고른 p 보다 좁습니다. 그래서 이
요소의 글자색은 빨강이 됩니다.
이 다툼을 푸는 규칙을 캐스케이드라고 합니다. CSS 라는 이름의 첫 글자가 이 낱말에서 왔습니다.
선택자가 요소를 얼마나 좁게 고르는지 재는 값을 명시도라고 합니다. 태그 이름으로 고른 것보다
클래스로 고른 것이, 그보다 id(문서에서 하나뿐인 요소 이름)로 고른 것이 명시도가 높습니다.
예제의 <p> 에는 글자 크기를 정한 규칙이 없습니다. 글자 크기처럼 물려받는 속성은 이때 부모
요소의 값을 씁니다. 그래서 <p> 의 글자 크기는 body 에 적힌 16px 이 됩니다. 이 물려받기를
상속이라고 합니다.
이 단계가 끝나면 요소마다 모든 속성의 값이 하나로 정해집니다. 이 값을 계산된 스타일이라고 합니다. 뒤따르는 단계는 이 값만 보고 일합니다.
뒤따르는 단계는 둘입니다. 레이아웃은 요소마다 폭과 위치를 셈하는 단계입니다. 페인트는 그 결과대로 화면에 칠하는 단계입니다.
지금까지의 흐름을 그림으로 모으면 이렇습니다. 두 갈래는 스타일 계산에서 처음 만납니다.
flowchart TD
H["HTML → DOM"] --> S["스타일 계산"]
C["CSS → CSSOM"] --> S
S --> K["요소마다 계산된 스타일"]
K --> L["레이아웃 · 페인트"]
L --> V["화면"]
화면을 붙잡는 CSS
CSSOM 이 다 만들어지기 전에는 브라우저가 못 하는 일이 둘 있습니다. 화면 그리기와 스크립트 실행입니다. 스크립트는 페이지에 걸린 자바스크립트 코드를 말합니다. 둘이 왜 기다리는지 차례로 봅니다.
브라우저는 CSSOM 이 완성되기 전에는 화면을 그리지 않습니다. 반쯤 읽은 CSS 로 먼저 그리면 뒤에 온 규칙이 생김새를 뒤집습니다. 사용자는 꾸밈 없는 화면이 번쩍 보였다가 바뀌는 것을 보게 됩니다. 이 번쩍임을 FOUC(Flash Of Unstyled Content, 스타일 없는 콘텐츠의 깜빡임)라고 합니다.
그래서 CSS 는 렌더링 차단 자원입니다. 페이지가 CSS 파일을 불러오는 동안 화면은 비어 있습니다. CSS 파일이 크거나 늦게 오면 첫 화면이 그만큼 늦습니다.
첫 화면이 나오기까지 반드시 거치는 단계를 묶어 중요 렌더링 경로라고 합니다. CSSOM 만들기가 그 안에 듭니다. CSS 파일을 작게 나누거나 첫 화면에 쓰는 규칙만 먼저 보내는 것이 이 경로를 줄이는 흔한 방법입니다.
스크립트도 CSSOM 을 기다릴 수 있습니다. 스크립트는 요소의 스타일을 읽을 수 있습니다. 그 값은 CSSOM 이 다 만들어져야 맞게 나옵니다. 그래서 브라우저는 앞서 불러오던 CSS 가 다 읽힐 때까지 스크립트 실행을 대개 미룹니다.
HTML 안의 스크립트는 대개 실행이 끝날 때까지 뒤의 HTML 읽기를 세웁니다. 스크립트가 문서를 고칠 수 있어서입니다. 그래서 스크립트가 CSS 를 기다리면 HTML 읽기도 함께 멈춥니다. 이렇게 CSS 파일 하나가 페이지 전체를 붙잡을 수 있습니다.
자바스크립트의 세 API
자바스크립트에서 CSSOM 을 다루는 API(Application Programming Interface, 응용 프로그래밍 인터페이스)는 셋입니다. 셋을 표로 먼저 봅니다. 그다음 앞의 예제 스타일시트로 값을 확인합니다.
| API | 다루는 대상 | 쓰는 때 |
|---|---|---|
document.styleSheets |
페이지에 걸린 스타일시트 전부와 그 규칙 | 규칙을 읽거나 새로 넣을 때 |
요소.style |
그 요소 하나에 직접 붙은 선언 | 요소 하나에만 값을 줄 때 |
getComputedStyle(요소) |
모든 규칙을 따진 뒤 정해진 값 | 지금 화면에 쓰이는 값을 읽을 때 |
앞의 둘로는 값을 고칠 수 있습니다. 마지막은 읽기만 됩니다. 계산된 스타일은 스타일 계산 단계의 결과라서 직접 고칠 수 없습니다. 바꾸고 싶으면 규칙이나 요소의 선언을 고쳐 다시 계산되게 합니다.
먼저 스타일시트 쪽 API 로 앞의 예제를 읽어 봅니다. 페이지에 걸린 스타일시트는 그 하나뿐이라고 둡니다.
const sheet = document.styleSheets[0];
sheet.cssRules.length // 3
sheet.cssRules[1].selectorText // 'p'
규칙은 목록이라 번호로 꺼냅니다. 번호는 0 부터 세므로 두 번째 규칙의 선택자 글자 p 가
나옵니다. insertRule 로 이 목록에 규칙을 새로 끼워 넣을 수도 있습니다.
다음은 요소 쪽 API 입니다. 문서에 <p>안녕</p> 한 줄이 있다고 둡니다. 그 요소의 계산된 값을
읽은 뒤 글자색을 바꿔 봅니다.
const p = document.querySelector('p');
const s = getComputedStyle(p);
s.color // 'rgb(128, 128, 128)'
s.fontSize // '16px'
p.style.color = 'blue';
s.color // 'rgb(0, 0, 255)'
처음 읽은 색은 p 규칙이 준 회색입니다. 글자 크기는 규칙이 없으니 body 에서 물려받은
16px 입니다. 색이 이름 대신 rgb(...) 꼴로 나오는 것은 계산된 스타일이 색을 한 가지 꼴로
맞춰 돌려주기 때문입니다.
style 로 파랑을 준 뒤 다시 읽으면 파랑이 나옵니다. 요소에 직접 적은 선언이 스타일시트의
규칙보다 앞서기 때문입니다. s 는 한 번 찍어 둔 값이 아니라 요소를 따라가는 객체입니다. 다시
읽을 때마다 새 값을 돌려줍니다.
읽기가 부르는 다시 계산
스타일을 고친 직후에 값을 읽으면 브라우저 안에서 무슨 일이 생기는지 봅니다. 성능이 갑자기 나빠지는 흔한 원인이 이 대목에 있습니다.
브라우저는 스타일을 고쳤다고 곧바로 다시 계산하지 않습니다. 고친 것을 모아 두었다가 다음 화면을 그리기 직전에 한 번에 계산합니다. 여러 번 고쳐도 계산은 한 번이면 됩니다.
고친 뒤 계산된 값을 읽으면 브라우저는 더 미룰 수 없습니다. 맞는 값을 돌려주려면 바로 스타일
계산을 돌려야 합니다. offsetWidth(요소가 화면에서 차지하는 폭)처럼 폭이나 위치를 읽으면
레이아웃까지 다시 돕니다. 이것을 강제 동기 레이아웃이라고 합니다.
반복문 안에서 el.style.width 를 쓰고 바로 el.offsetWidth 를 읽으면 계산이 매 바퀴 돕니다.
이렇게 쓰기와 읽기를 번갈아 하는 것을 레이아웃 스래싱이라고 합니다. 읽기를 먼저 몰아서
하고 쓰기를 뒤에 몰면 계산은 한 번으로 줄어듭니다.
직접 다룰 때와 클래스로 넘길 때
CSSOM 을 언제 직접 다루고 언제 비켜 가는지 가립니다.
대부분의 화면 변화는 클래스 하나로 끝납니다. 규칙은 CSS 파일에 적어 둡니다. 자바스크립트는 요소의 클래스 이름만 붙였다 뗍니다. 생김새는 CSS 에, 동작은 자바스크립트에 남으니 둘을 따로 고칠 수 있습니다.
클래스로 안 되는 경우에만 CSSOM 을 직접 다룹니다. 흔한 경우는 셋입니다.
| 경우 | 예 | 쓰는 API |
|---|---|---|
| 값이 실행 중에 정해진다 | 끌고 가는 요소의 좌표 | 요소.style |
| 지금 화면에 쓰이는 값이 필요하다 | 요소의 실제 폭 | getComputedStyle |
| 규칙 자체를 실행 중에 만든다 | 자바스크립트 안에 CSS 를 적는 라이브러리 | insertRule |
셋째 줄의 라이브러리 묶음을 CSS-in-JS(CSS in JavaScript)라고 합니다.
세 경우 모두 앞 소절의 비용을 안고 갑니다. 계산된 값을 읽으면 다시 계산을 부를 수 있습니다. 규칙을 새로 넣으면 그 규칙에 걸리는 요소 전부의 스타일이 다시 계산됩니다.
관련 항목
이 규격과 짝을 이루는 웹 규격
DOM · HTML · CSS · 웹 API · CSSOM View
이 규격을 정하는 표준 단체와 문서
W3C · CSS 작업 그룹 · WHATWG · CSS 모듈
CSSOM 을 이루는 구성 요소
스타일시트 · CSS 규칙 · 셀렉터 · 선언 · CSS 속성 · 미디어 쿼리 · 인라인 스타일
CSSOM 을 받아 화면을 만드는 처리 단계
파서 · 스타일 계산 · 렌더 트리 · 레이아웃 · 페인트 · 합성 · 중요 렌더링 경로
스타일이 요소에 붙을 때 적용되는 규칙
캐스케이드 · 명시도 · 캐스케이드와 상속 · 계산된 스타일 · 사용자 에이전트 스타일시트
CSSOM 을 다룰 때 나는 성능 문제
렌더링 차단 리소스 · FOUC · 강제 동기 레이아웃 · 레이아웃 스래싱 · 리플로우
CSSOM 을 만들고 쓰는 주체
브라우저 · 렌더링 엔진 · 자바스크립트 엔진 · JavaScript · CSS-in-JS
다른 이름: CSS Object Model · CSS 객체 모델