‘URL에서 상호작용으로’ 시리즈 세 번째인 이번 글은 브라우저가 CSS를 이용해 중괄호를 픽셀로 만드는 방법을 살펴본다. 나아가 최종 사용자의 상호작용이 이 처리 과정에 어떻게 영향을 미치는지 간단히 다룰 것이다. 다룰 범위가 넓다. 그러니 <좋아하는 음료 이름을 넣으시오>를 한 잔 준비하고 시작하자.
해석
시리즈 두 번째 글 ‘태그에서 DOM으로’에서 배운 것과 비슷하게 브라우저가 CSS를 다운로드하고 나면, 발견되는 모든 CSS로 그림을 그리기 위해 CSS 해석기를 준비한다. 해석 대상은 개별 문서에 있는 CSS나 <style>
태그 안, DOM 요소의 인라인 style
속성에 있는 CSS가 될 수도 있다. 모든 CSS는 문법 명세에 따라 해석되고 토큰으로 변환된다. 다 처리하고 나면 모든 선택자와 속성, 각 속성 값으로 이뤄진 데이터 구조가 생성된다.
예컨대 다음과 같은 CSS가 있다고 하자.
이 코드는 나중에 활용하기 좋게 다음과 같은 데이터 구조를 만든다.
선택자 | 속성 | 값 |
---|---|---|
.fancy-button |
background-color |
rgb(0,255,0) |
.fancy-button |
border-width |
3px |
.fancy-button |
border-style |
solid |
.fancy-button |
border-color |
rgb(255,0,0) |
.fancy-button |
font-size |
1em |
주목할 만한 것은 브라우저가 background
와 border
축약 문법을 긴 문법의 변형태로 풀었다는 것이다. 축약 문법은 주로 개발자를 위한 것이다. 이때부터 브라우저는 풀어 쓴 긴 형태만 다룬다.
이어서 엔진은 DOM 트리를 구축한다. 트래비스 릿헤드Travis Leithead가 ‘태그에서 DOM으로’에서 다룬 내용이다. 아직 읽지 않았다면 그것부터 읽기 바란다. 기다리겠다.
계산
마주한 콘텐츠에 있는 모든 스타일을 해석했으므로 이제 스타일 계산을 할 시간이다. 모든 값에는 변환할 수 있는 표준화한 계산 값이 있다. 계산 단계를 끝내고 나면 크기에 관한 모든 값이 auto
, 퍼센트, 픽셀 셋 중 하나로 줄어든다. 명확히 하기 위해 웹 개발자가 작성한 값이 다음 계산에서 어떤 값이 되는지 몇 가지 예를 살펴보자.
웹 개발자 | 계산 값 |
---|---|
font-size: 1em |
font-size: 16px |
width: 50% |
width: 50% |
height: auto |
height: auto |
width: 506.4567894321568px |
width: 506.46px |
line-height: calc(10px + 2em) |
line-height: 42px |
border-color: currentColor |
border-color: rgb(0,0,0) |
height: 50vh |
height: 540px |
display: grid |
display: grid |
데이터 저장소에 있는 모든 값을 계산했다. 이제 캐스케이드를 다룰 차례다(캐스케이드는 ‘계단식 폭포’라는 뜻으로 CSSCascading Style Sheet의 C에 해당한다. 정의된 CSS 속성을 각 요소에 적용할 때 우선순위를 매기는 원리를 캐스케이드라고 부른다ㅡ역주).
캐스케이드
다양한 소스에서 CSS가 올 수 있으므로 브라우저에는 주어진 요소에 어떤 스타일을 적용할지 정하는 방법이 필요하다. 이를 위해 브라우저는 특정도specificity라는 공식을 사용한다. 특정도 공식은 선택자가 사용한 태그와 클래스, id, 속성 선택자, !important
선언 개수를 센다. 요소에 인라인 style
속성으로 매긴 스타일은 <style>
블록이나 외부 스타일 시트에 있는 것보다 우선 적용되고, 만약 웹 개발자가 값에 !important
를 이용했다면 그 값은 위치와 상관없이 모든 CSS에 우선 적용된다. 물론 인라인 속성에 있는 !important
는 다른 곳에 있는 !important
에 우선한다.
이를 명확하게 하기 위해 선택자를 몇 개 살펴보고 특정도 점수가 어떻게 되는지 보자.
선택자 | 특정도 점수 |
---|---|
li |
0 0 0 0 1 |
li.foo |
0 0 0 1 1 |
#comment li.foo.bar |
0 0 1 2 1 |
<li style="color: red"> |
0 1 0 0 0 |
color: red !important |
1 0 0 0 0 |
그렇다면 특정도가 같을 때 브라우저 엔진은 어떻게 할까? 주어진 선택자가 두 개 이상이고 특정도가 같다면 승자는 문서의 마지막에 나타나는 선택자다. 다음 예에서 div
의 배경은 파란색이 된다.
.fancy-button
예를 조금 더 확장해보자.
CSS는 다음과 같은 데이터 구조를 만든다. 이 글 내내 이 코드를 계속 작성해나갈 것이다.
선택자 | 속성 | 값 | 특정도 점수 | 문서에 등장하는 순서 |
---|---|---|---|---|
.fancy-button |
background-color |
rgb(0,255,0) |
0 0 0 1 0 |
0 |
.fancy-button |
border-width |
3px |
0 0 0 1 0 |
1 |
.fancy-button |
border-style |
solid |
0 0 0 1 0 |
2 |
.fancy-button |
border-color |
rgb(255,0,0) |
0 0 0 1 0 |
3 |
.fancy-button |
font-size |
16px |
0 0 0 1 0 |
4 |
div .fancy-button |
background-color |
rgb(255,255,0) |
0 0 0 1 1 |
5 |
원천 이해하기
시리즈 첫 번째 글인 ‘서버에서 클라이언트로’에서 알리 알라바스Ali Alabbas는 원천origin을 브라우저 탐색과 연관해 검토했다. CSS 역시 원천이 있으나 목적이 다르다.
- 사용자: 사용자가 유저 에이전트 전체 영역에 설정한 모든 스타일
- 작성자: 웹 개발자의 스타일
- 유저 에이전트: CSS를 이용하고 렌더링할 수 있는 모든 것(예: 웹 브라우저)
각 원천 캐스케이드는 사용자, 작성자, 유저 에이전트의 순서로 강력한 힘을 발휘한다. 데이터 세트를 좀 더 확장하고 사용자가 자기 브라우저의 최소 글자 크기를 2em으로 맞췄을 때 무슨 일이 벌어지는지 보자.
원천 | 선택자 | 속성 | 값 | 특정도 점수 | 문서에서 등장하는 순서 |
---|---|---|---|---|---|
Author |
.fancy-button |
background-color |
rgb(0,255,0) |
0 0 0 1 0 |
0 |
Author |
.fancy-button |
border-width |
3px |
0 0 0 1 0 |
1 |
Author |
.fancy-button |
border-style |
solid |
0 0 0 1 0 |
2 |
Author |
.fancy-button |
border-color |
rgb(255,0,0) |
0 0 0 1 0 |
3 |
Author |
.fancy-button |
font-size |
16px |
0 0 0 1 0 |
4 |
Author |
div .fancy-button |
background-color |
rgb(255,255,0) |
0 0 0 1 1 |
5 |
User |
* |
font-size |
32px |
0 0 0 0 1 |
0 |
캐스케이드 계산하기
브라우저는 모든 원천에서 선언 전체에 걸쳐 데이터 구조를 만든 다음 특정도에 따라 정렬한다. 우선 원천에 따르고 그다음 특정도에 따르며 마지막으로 문서에 등장한 순서에 따라 정렬한다.
원천 ⬆ | 선택자 | 속성 | 값 | 특정도 점수 ⬆ | 문서에 등장하는 순서 ⬇ |
---|---|---|---|---|---|
User |
* |
font-size |
32px |
0 0 0 0 1 |
0 |
Author |
div .fancy-button |
background-color |
rgb(255,255,0) |
0 0 0 1 1 |
5 |
Author |
.fancy-button |
background-color |
rgb(0,255,0) |
0 0 0 1 0 |
0 |
Author |
.fancy-button |
border-width |
3px |
0 0 0 1 0 |
1 |
Author |
.fancy-button |
border-style |
solid |
0 0 0 1 0 |
2 |
Author |
.fancy-button |
border-color |
rgb(255,0,0) |
0 0 0 1 0 |
3 |
Author |
.fancy-button |
font-size |
16px |
0 0 0 1 0 |
4 |
이것은 .fancy-button
에서 ‘승리한’ 속성과 값의 결과다(표 위쪽이 우선한다). 위의 표를 예로 들면 사용자의 브라우저 설정이 웹 개발자의 스타일보다 우선한다는 것을 알 수 있다. 마지막으로 브라우저는 표시된 선택자에 맞는 모든 DOM 요소를 찾아서 일치하는 요소에 계산된 스타일을 적용한다. 이 경우에는 .fancy-button
이 있는 div
다.
속성 | 값 |
---|---|
font-size |
32px |
background-color |
rgb(255,255,0) |
border-width |
3px |
border-color |
rgb(255,0,0) |
border-style |
solid |
캐스케이드 작동 방법을 더 알고 싶다면 공식 명세를 살펴보자.
CSS 객체 모델
많은 것을 했지만 아직 하지 않은 게 있다. 바로 CSS 객체 모델(CSSOM) 갱신이다. CSSOM은 document.stylesheets에 있다. 우리는 CSSOM을 갱신해서 지금까지 해석하고 계산한 모든 것을 CSSOM이 나타내도록 해야 한다.
웹 개발자는 아마 이 정보가 무엇인지도 모른 채 활용하고 있을 것이다. 예컨대 getComputedStyle()을 호출할 때 필요하다면 위에서 말한 처리 과정이 벌어진다.
레이아웃
이제 스타일을 적용한 DOM 트리tree가 생겼다. 시각적인 표시를 위해 트리를 쌓아 올리는 처리 과정을 시작할 차례다. 모든 최신 브라우저 엔진에는 박스 트리로 불리는 이 트리가 있다. 박스 트리를 구성하기 위해 우리는 DOM 트리를 따라 내려가면서 CSS 박스를 생성한다. CSS 박스 각각에는 바깥 여백margin, 외곽선border, 안 여백padding과 내용 박스가 있다.
여기서는 다음과 같은 CSS 레이아웃 개념을 검토할 것이다.
- 양식화 문맥Formatting context, FC: 다양한 양식화 문맥이 있다. 대부분은 웹 개발자가 요소의
display
값을 변경해서 발동한다. 가장 일반적인 양식화 문맥은 블록(블록 양식화 문맥 Block Formatting Context, BFC), 플렉스flex, 그리드grid, 테이블 셀table-cells과 인라인inline이다. 다른 CSS도 새로운 양식화 문맥을 강제로 설정할 수 있다.position: absolute
나float
혹은 다단을 활용해서 그렇게 할 수 있다. - 포함 블록Containing block: 현재 요소의 스타일에 영향을 주는 조상ancestor 블록이다.
- 인라인 방향Inline direction: 요소의 쓰기 방식에 좌우되는 텍스트 배치 방향이다. 라틴 기반 언어에서는 가로축이고 한중일CJK 언어에서는 세로축이다(라틴 기반 언어는 알파벳이라고 생각하면 이해하기 쉽다. 한중일 인라인 방향은 라틴 기반 언어처럼 가로축이다. 여기서 한중일 언어의 인라인 방향이 세로축이라는 것은 예전 방식으로 세로쓰기를 할 때 그렇다는 것이다ㅡ역주).
- 블록 방향Block direction: 인라인 방향과 완전히 똑같이 작동하지만 인라인 방향 축과 수직을 이룬다. 따라서 라틴 기반 언어에서는 세로축이고 한중일 언어에서는 가로축이다(위와 마찬가지로 한중일 언어는 예전 방식으로 세로쓰기를 할 때를 말하는 것이다ㅡ역주).
AUTO
결정
계산 단계에서 크기 값이 auto
, 퍼센트, 픽셀 세 값 중 하나가 될 수 있다고 한 것을 기억해보자. 레이아웃의 목적은 모든 박스의 크기와 위치를 가늠해서 채색을 할 수 있도록 박스 트리에 배치하는 것이다. 나는 시각에 민감한 사람으로서 박스 트리가 어떻게 구축되는지 쉽게 이해할 수 있는 예제를 찾았다. 따라오기 쉽게끔 CSS 박스 전부가 아니라 주요 박스principal box(요소에 지정된 스타일을 구현하고 자식 요소와 내용을 담는 박스. 여기서는 바깥 여백, 외곽선, 안 여백, 내용 박스를 생략하겠다는 뜻으로 주요 박스라는 용어를 사용했다ㅡ역주)만 보여줄 것이다. 다음 코드를 사용한 기본적인 ‘Hello world’ 레이아웃을 보자.
플롯 다루기
이제 좀 더 복잡한 곳으로 가보자. ‘Share It’ 버튼이 있는 일반적인 레이아웃을 다룰 것이다. 그리고 그 버튼을 라틴 문장(로렘 입숨)의 왼쪽으로 플롯float할 것이다. 플롯 자체는 ‘나눠서 맞춤shrink-to-fit’문맥으로 여겨진다. 그 이유는 크기 값이 auto
인 경우 박스가 내용을 나눠서 다음 줄로 내릴 것이기 때문이다. 플롯된 박스는 이 레이아웃 유형에 해당하는 박스 유형 중 하나다. 하지만 이 유형에 해당하는 다른 박스도 많다. 예를 들면 절대적absolute으로 위치를 잡은 박스(position: fixed
요소 포함)나 auto
기반으로 크기를 잡은 표의 칸table cell이 있다. 버튼 시나리오 코드를 보자.
나누기 이해하기
레이아웃 동작 방식에서 마지막으로 다룰 것은 나누기fragmentation다. 웹 페이지를 출력해본 일이 있거나 CSS 다단multi-column을 사용해본 적이 있다면 나누기 개념을 이해하는 데 유리할 것이다. 나누기는 내용을 서로 다른 모양geometry에 맞춰 나누는 논리다(여러 페이지, 구역, 단으로 내용이 나뉠 때 CSS가 이를 처리하는 방법ㅡ역주). CSS 다단을 사용하는 예제를 살펴보자.
채색
자, 이제 어디까지 왔는지 살펴보자. 모든 CSS 코드를 찾아서 해석했고 캐스케이드를 비교해 DOM 트리에 적용했으며 레이아웃을 생성했다. 하지만 색, 외곽선, 그림자 등 레이아웃 디자인은 적용하지 않았다. 이것을 채색painting이라고 한다.
채색은 CSS에 대략적으로만 표준화돼 있는데 순서는 다음과 같이 간단하다(전체 내용을 CSS 2.2 Appendix E에서 볼 수 있다).
- 배경
- 외곽선
- 그리고 내용
그래서 앞의 ‘SHARE IT’ 버튼으로 이 처리 과정을 따른다면 이렇게 보일 것이다.
완료하고 나면 비트맵으로 변환된다. 맞다. 궁극적으로 모든 레이아웃 요소는 (심지어 텍스트도) 내부적으로는 이미지가 된다.
Z-INDEX
오늘날 웹사이트 대부분은 단일 요소로 이뤄져 있지 않다. 더군다나 우리는 때때로 특정 요소를 다른 요소 앞에 나타나게 하고 싶어 한다. 그렇게 하려면 z-index
의 힘을 이용해 한 요소를 또 다른 요소 위에 겹쳐 놓으면 된다. 이것은 마치 디자인 소프트웨어에서 레이어로 작업하는 것과 비슷하다. 하지만 레이어가 있는 곳은 오직 브라우저의 컴포지터Compositor(최신 브라우저에서 레이어를 합성해서 화면을 그리는 역할을 하는 부분ㅡ역주) 안이다. 마치 z-index
로 새 레이어를 만드는 것처럼 보이지만 그렇지 않다. 그렇다면 우리가 하는 건 뭘까?
우리는 새로운 쌓임 맥락stacking context을 만든다. 예시를 보자.
z-index
를 사용하지 않으면 위 문서는 나온 순서대로 ‘Item 1’ 위에 ‘Item 2’를 그릴 것이다. 하지만 z-index
때문에 그리는 순서가 달라진다. 앞서 레이아웃에서 한 것처럼 각 단계를 밟아보자.
z-index
는 색상과 관련이 없고 어떤 요소가 사용자에게 표시될지와 관련 있다. 따라서 어떤 텍스트와 색상이 보일지에 관한 작업에 사용한다.
합성
채색 단계에서 컴포지터로 전달된 가장 작은 단일 비트맵이 있다(컴포지터는 합성기로 번역할 수 있을 테지만 일반적으로 컴포지터라고 하므로 따로 번역하지 않는다ㅡ역주). 컴포지터의 역할은 하나 이상의 레이어를 생성하고 사용자가 볼 수 있도록 화면에 비트맵을 표시하는 것이다.
여기서 나올 법한 질문은 ‘비트맵이나 컴포지터 레이어가 여러 개 필요한 사이트가 있는 이유는 뭔가?’이다. 그동안 본 예제에서는 그렇지 않았지만 조금 더 복잡한 사례를 보자. MS 오피스 개발팀이 클리피Clippy(MS 오피스 2000부터 2003까지 도움말 제공 용도로 화면 구석에 튀어나왔던 캐릭터다. 별 도움은 안 되고 방해만 됐기 때문에 놀림거리가 됐다ㅡ역주)를 온라인에 다시 데려오려 한다고 가정해보자. 그리고 CSS transform
을 이용해 클리피가 통통 튀어 주의를 끌게 하려 한다고 해보자.
클리피를 움직이게 하는 코드는 대략 이렇다.
웹 개발자가 클리피를 무한히 움직이기 원한다는 것을 브라우저가 이해하면 두 가지 선택지가 생긴다.
- 모든 애니메이션 프레임마다 다시 그리는 단계로 돌아가서 새로운 비트맵을 생성해 컴포지터로 돌려보낸다.
- 두 가지 다른 비트맵을 생성하고 컴포지터가 애니메이션이 적용된 레이어만 움직이도록 한다.
대부분의 환경에서 브라우저는 두 번째 옵션을 선택하고 다음을 생성할 것이다(예제에서는 워드 온라인Word Online이 생성하는 레이어의 양을 의도적으로 단순화했다).
그러고 나서 브라우저는 클리피 비트맵을 적절한 위치에 재합성해 주기적으로 움직이는 애니메이션을 만들 것이다. 이것은 성능 면에서 매우 유리하다. 많은 브라우저 엔진에서 컴포지터는 자신만의 스레드를 다룸으로써 메인 스레드가 멈추지 않게 해준다. 만약 브라우저가 첫 번째 선택지를 고른다면 같은 결과를 얻기 위해 모든 프레임마다 멈춰야할 것이다. 그러면 최종 사용자의 성능과 반응성 측면에 부정적인 영향을 미칠 것이다.
상호작용한다는 환상 만들어내기
여기까지 배웠듯이 우리는 모든 스타일과 DOM으로 생성한 이미지를 최종 사용자에게 표시했다. 그러면 브라우저는 상호작용한다는 환상을 어떻게 만들까? 이미 알 거라고 생각하지만 ‘SHARE IT’ 버튼을 상호작용하는 예제로 살펴보자.
추가한 것은 사용자가 버튼 위에 마우스를 올려놨을 때 버튼의 배경과 글자색을 바꾸도록 브라우저에 알려주는 가상 클래스밖에 없다. 그렇다면 브라우저는 이것을 어떻게 다룰지 의문이 생긴다.
브라우저는 다양한 입력을 지속적으로 추적한다. 그리고 그 입력이 움직이는 동안 브라우저는 히트 테스팅hit testing으로 불리는 처리 과정을 수행한다. 이 예제에서 처리 과정은 다음과 같다.
- 사용자가 버튼 위로 마우스를 움직인다.
- 브라우저는 마우스가 움직였다는 이벤트를 발생하고 히트 테스팅 알고리즘으로 진입한다. 이 알고리즘은 근본으로 들어가면 ‘마우스가 건드린 박스(들)는 무엇인가?’라고 묻는다.
- 히트 테스팅 알고리즘은 ‘SHARE IT’ 버튼과 연결된 박스를 돌려주는 것으로 답한다.
- 브라우저는 ‘마우스가 네 위에 떠 있을 때 해야 하는 게 있니?’라고 질문한다.
- 이 박스와 자식들에 대한 스타일과 캐스케이드가 재빨리 실행되고 결론이 나온다. 답은 ’그렇다’이다. 선언된 블록 안에 채색에만 적용되는
:hover
가상 클래스가 있다. - 해당 스타일을 앞 단계에서 배운 것처럼 DOM 요소에 적용한다. 이 경우 DOM 요소는
button
이다. - 옛 레이아웃을 건너뛰고 바로 새 비트맵을 그린다.
- 새 비트맵이 컴포지터에 전달되고 사용자에게 표시된다.
이 과정은 상호작용한다는 느낌을 효과적으로 사용자에게 전한다. 브라우저는 단지 오렌지색 이미지를 초록색 이미지로 교체한 것 뿐인데 말이다.
이제 됐다!
연재한 ‘중괄호를 CSS가 처리하는 방법’부터 ‘브라우저가 픽셀을 표시하는 방법’까지 미스터리의 일부가 해소됐기를 바란다. 이 과정에서 CSS가 어떻게 해석되는지, 값이 어떻게 계산되는지, 캐스케이드가 실제로 어떻게 작동하는지 다뤘다. 그리고 레이아웃, 채색, 컴포지터의 작동까지 깊게 파고들었다.
시리즈의 마지막 연재까지 지켜봐주길 바란다. 마지막 연재는 자바스크립트 언어의 설계자 중 하나가 브라우저의 자바스크립트를 컴파일하고 실행하는 방법을 다룰 것이다.
레이철 앤드루의 신간 『새로운 CSS 레이아웃』
그리드를 사용한 레이아웃을 기존 레이아웃과 비교해서 살펴볼 수 있습니다. 예제를 통해 그리드 레이아웃이 어떻게 활용되는지 확인해보세요. 레이아웃만을 다룬 ‘그리드 레이아웃’ 전문서 『새로운 CSS 레이아웃』입니다. 본질의 웹 디자인 시대 흐름에 뒤처지지 않으려면 이 책을 꼭 읽어야 합니다!!
books@webactually.com