CSS Position Schemes- Absolute Positioning
이번 주제는 CSS Position Schemes의 세 번째 방식인 float이고 이번 주제의 마지막 편이다. 우선 offset에 대해 살펴본 후, parent offset에 따라서 absolute positioning이 어떻게 계산되는지 알아보도록 하겠다. 이번에 다루는 내용들은 코드스피츠 76 CSS Rendering의 강의를 정리한 것이다.
Offset과 Frame
첫 번째 글에서 CSS에서는 화면 요소의 계산이 직접적으로 수학과 물리학 공식을 적용한 함수를 사용하는 대신 속성에 추상적인 값을 주는 방식으로 구성한다고 했다. normal flow의 경우에는 position: static, position: relative로 값을 주었고, float의 경우에는 float: left, float: right로 값을 주었다. 그런데 첫 번째 글의 도입에서 했던 이야기를 상기해보자. 결국 그림을 그리기 위해서는 화면 요소가 구체적인 값으로 계산되어서 각각의 pixel에 주어져야 한다. 바꾸어 말하면, 결국 우리가 추상적인 값으로 지정한 화면 요소들을 결국 고정된 숫자 값으로 계산된다. 이렇게 계산된 숫자 값을 offset이라고 한다. 즉, offset은 reflow 계산이 끝난 결과 값이다. 때문에 offset 값은 변경이 불가능하며 읽기만 가능하다.
이런 점을 생각해보면, 결국 html, css로 짠 코드는 우리의 희망사항같은 것이다. 우리가 원하는 대로 그림을 그려줄지 말지는 브라우저에 달린 문제이다. 실제로 구형 ie6에서는 float값을 주면 의도하지 않게 padding: 3px이 추가되는 버그가 존재했다. 따라서 html, css rendering으로 그림을 그리는 한, 브라우저가 우리 의도대로 그림을 그리는지 그리지 않는지를 확인하는 방법은 실제 계산된 값은 offset 값을 확인하는 방법 이외에 없다.
그러면 구제적인 값을 알기 위해서 offset값을 읽고 offset 값을 바탕으로 우리가 원하는 대로 화면이 그려지도록 하면 되는 것일까? 안타깝게도 그렇지 않다. 첫 번째 글에서 이야기 했다시피 그림을 그리는 일은 복잡하고 계산량이 많은 작업이다. 따라서 브라우저는 최대한 계산을 효율적으로 하기 위한 최적화 방법을 사용한다. 보통 element들이 그려질 때, 이후에 그려진 요소가 이전에 그려진 요소에 영향을 주거나 반대의 경우가 많이 존재한다. 만약에 복잡한 layout에서 요소 하나를 그릴 때마다 영역 계산을 한번씩 한다면 계산량이 많아진다. 그래서 브라우저는 각각의 요소들 중에 다른 element에 영향을 주거나 영향을 받는 요소들만 묶어서 한번에 계산하려고 한다. 이렇게 한번에 묶어서 계산하는 단위를 frame이라고 한다.
브라우저는 그림을 그릴 때, 변경이 일어날 요소들을 queue에 쌓아두고 frame 단위로 묶어서 frame 단위로 한 번씩만 계산을 하려고 한다. 이렇게 frame 단위로 변경이 일어날 요소들을 한번에 그릴 때 일어나는 일을 flush라고 한다. 브라우저는 쌓여있는 queue를 flush시키는 방식으로 전체 재계산하는 횟수를 줄이려고 한다. 그런데 만약 우리가 rendering 과정 중간에 dom에 접근해서 elementd의 offset을 조사하면 어떻게 될까?
만약 rendering 중간에 offset 값을 요청하게 되면, offset값을 알려주기 위해서 queue의 있는 값들을 지우고 바로 해당 요소의 offset값을 계산해서 보여준다.(뒤의 요소에 따라서 재계산되서 변할 수도 있다.) 그래서 rendering 중간에 offset을 요청하면 브라우저가 해야하는 재계산의 횟수가 늘어나고 브라우저의 최적와 로직이 깨진다. 따라서 offset값은 함부로 사용하면 안되고, 계산이 다 끝난 요소에 한해서 더 이상 다른 요소에 영향을 주지도 영향을 받지도 않는 경우에 읽는 것이 바람직하다. 이런 요소들은 queue에 쌓이지 않기 때문에 offset값이 재계산 되지 않는다. 이런 브라우저의 최적화 방식을 모를 경우, layout을 구성할 때 이전 요소의 offset을 사용해서 다시 layout을 그리려는 실수를 한다. 이런 방식으로 layout을 구성할 경우 재계산 횟수가 늘어나기 때문에 rendering이 느려지는 현상이 발생한다.
정리하자면
- offset 값은 조회 전용이다
- offset 값을 읽는 행위는 offset 재계산이 일어나지 않는 경우에만 권장된다.
- offset 값을 바탕으로 layout을 다시 그리려고 해서는 안된다.
Offset Parent
컴퓨터 입장에서는 offset을 계산하려면, 실제 pixel 값을 계산해야 한다. offset을 계산하는 가장 기본적인 방식은 기준점으로부터의 상대적인 위치이기 떄문에 어디가 기준점인지를 찾는 것이 우선이다.(offset의 원래 의미가 ~부터 ~만큼 떨어져있다는 의미라는 것을 생각해보면 이해하기 쉽다.) 만약 어떤 요소에 left: 100px, top: 100px의 값을 주었다면, 이 말은 기준점으로부터 왼쪽으로 100px 아래로 100px이라는 말이다. 따라서 offset parent는 바로 offset을 계산하기 위한 기준점을 지칭하는 용어이다. 그런데 offset parent도 normal flow의 경우와 동일하게 dom의 구조와 일치하지 않는다. 따라서 dom에서는 부모 요소라도 꼭 offset parent가 아닐 수도 있다. 앞서 다루었던 normal flow를 예로 들면, inline 요소 내부에 block 요소가 있는 경우 bfc의 offset parent를 계산할 때, 부모요소인 inline 요소를 고려하지 않는다. 해당 block요소는 부모 요소들을 거슬러 올라가다가 block요소가 있는 경우 그 block 요소를 offset parent로 삼는다.
offset parent: null
html 요소 중에는 offset parent가 null인 경우, 즉 offset parent가 없는 경우가 있다. 다음의 3가지 요소들은 offset parent가 null이다.
root, html, body: 해당 html요소가 root, html, body인 경우 offset parent가 null이다.(이 요소들은 기준 요소이기 때문에 offset parent가 존재하지 않는 시작점이다.)
position: fixed: position: fixed의 경우, offset parent가 존재하지 않는 기준 요소가 되버리기 때문에 offset parent가 없다.
out of dom tree: javascript를 통해 createElement로 생성된 요소는 해당 요소가 appendChild로 dom tree안에 추가되지 않는 한 offset parent가 없다.
position: absolute
position: absolute의 경우, offset parent를 정하는 방식이 표준에 정해져 있다.(따라서 dom의 부모는 무시된다.) position: absolute의 경우에는 recursive search를 통해서 offset parent를 찾는다. 재귀적인 방식으로 offset parent를 찾을 때까지 부모요소를 거슬러 올라간다. recursive search가 offset parent를 찾는 방식은 다음과 같다.
부모요소가 position: fixed인 경우: 앞에서 이야기 한 것과 같이 position: fixed는 offset parent가 null이기 때문에 position: absolute의 offset parent도 null이 된다.
부모요소가 position: static이 아닌경우: 즉 position absolute 혹은 position relative인 경우, 해당 부모 요소는 offset parent가 된다.
부모 요소가 body인 경우: body의 경우에도 offset parent가 될 수 있다.
부모 요소가 td, th, tabled인 경우: 표준에 따르면 td, th, table인 경우에도 offset parent가 될 수 있어야 하지만, 대부분의 브라우저에서 이렇게 작동하지 않는다. 일반적으로는 td요소 안에 position: relative인 div를 주고 absolute를 적용한다.
position: absolute 요소는 부모 요소를 거슬러 올라가다가 위의 경우에 해당하는 요소를 offset parent로 삼는다.
정리하면
position: absolute라면 offset의 기준점이 내 dom 상에서의 부모가 아니라 조상들 중에 위의 경우에 해당하는 요소이다. 대부분의 요소들이 기본 값인 position: static이기 때문에 일반적으로는 position: relative인 부모를 offset parent로 삼는다. 따라서 많은 경우에 position: relative는 static을 기준으로 요소를 상대적인 위치로 이동할 때 사용하기 보다는 position: absolute의 offset parent로 설정할 때 사용한다.
참조 가능한 offset 값들
이제 offset parent를 알았다면, 브라우저가 계산한 다양한 offset값들을 참조할 수 있다.(읽기 전용)
offsetLeft
offsetTop
-> 위의 2개의 값들은 offset parent로 부터 얼만큼 떨어져 있는 지를 알 수 있다.
offsetWidth
offsetHeight
-> 위의 2개의 값들은 해당 요소가 실제로 차지하는 크기를 알 수 있다.
offsetScrollLeft
offsetScrollTop
-> 위의 2개의 값들은 content가 offset parent보다 큰 경우 cotent의 실제 위치를 알 수 있다.
offsetScrollWidth
offsetScrollHeight
-> 위의 2개의 값들은 content가 offset parent보다 큰 경우 cotent의 실제 크기를 알 수 있다.
html은 요소에 overflow: hidden을 주면 scroll을 기본적으로 제공한다. 부모 block 보다 큰 content를 넣었기 때문에 scroll이 발생한다는 것을 생각해 본다면, 진짜 컨텐츠의 크기를 알기 위해서는 offsetScroll 값을 확인해야 한다.