CSS Position Schemes- Normal Flow
이번 주제는 CSS Position Schemes이다. 가장 기본적인 주제이지만 그렇다고 다루는 내용이 짧은 것은 아니기 때문에 3편에 나누어서 다루도록 하겠다. 이번 글에서는 normal flow에 해당하는 static, relative를 다루고 다음 글에서는 float를 마지막으로는 absolute와 fixed를 다루도록 하겠다. 이번에 다루는 내용들은 코드스피츠 76 CSS Rendering의 강의를 정리한 것이다.
Graphics System과 CSS
게임, 동영상, 웹 브라우저 등등 우리가 일상적으로 컴퓨터를 사용할 때, 시각적인 요소들이 빠지지 않는다. 우리는 이런 시각적인 요소들을 당연하고 자연스럽게 받아들인다. 그런데 조금 더 생각을 해보면 이런 시각적인 요소들은 당연하지 않다. 우선 화면을 그리는 일은 그 자체로 복잡하고 계산량이 많은 작업이다. 우리가 graphics system을 사용하여 화면을 그리기 위해서는 복잡한 수학과 물리학 공식들을 알고 있어야 한다. 화면에 나타난 시각적인 요소들은 화면이 가지고 있는 무수히 많은 pixel들에게 어떤 값을 주면서 그려지는데, 심지어 화면의 내용은 실시간으로 변한다. 따라서 실시간으로 화면을 그리기 위해서는 화면 각각의 pixel에 복잡한 수학과 물리학 공식을 빠르게 계산하여 pixel에 실시간으로 값을 주어야 한다. 그런데 또 다른 문제가 있다. 우리가 사용하는 컴퓨터의 화면은 각각 다르다. 어떤 화면은 크기가 더 크고, 어떤 화면은 화소가 더 높다. 즉 각 화면마다의 pixel이 다 다르기 때문에, 계산해야 할 결과가 다 다르다. 그럼에도 불구하고 각기 다른 화면에 그려진 시각적인 요소들이 동일하게 표현되어야 한다. 여기서 우리는 graphics system의 도움을 받는데, graphics system은 복잡하고 계산량이 많은 작업인 시각적인 요소를 그리는 일을 각 화면의 크기와 화소가 다르더라도 동일하게 그릴 수 있도록 도와준다.
즉 간략하게 말하자면 화면을 그리는 일은 당연하지도 간단하지도 않은 작업이고 이를 위해 graphics system의 도움을 받는다. 그런데 웹의 graphics 기술은 보통의 graphics system과 다르게 수학과 물리학 공식들을 이해하지 않고도 화면을 그릴 수 있도록 설계되었다. 웹의 graphics 기술은 복잡한 계산 함수들을 나열하지 않고도 추상적인 언어들을 사용하여 화면을 그릴 수 있다. 이렇게 웹에서 간단하고 추상적인 언어를 통해 화면을 그릴 수 있도록 제공하는 언어가 바로 CSS(Cascading Style Sheet)이다.
일반적으로 graphics system에서 시각적인 요소를 그리는 작업은 크게 2가지 단계를 통해 이루어 진다. 먼저 시각적인 요소가 그려질 영역을 계산하는 reflow 단계와, 정해진 영역에 색깔, 질감등을 결정하여 실제 그림을 그리는 단계인 repaint 단계로 나뉜다. 브라우저가 CSS를 해석하여 그림을 그릴 때에도 동일한 단계를 거친다. 먼저 각 element들의 위치와 크기를 계산하는 reflow단계를 거친 후에 실제로 그림을 그리는 repaint 작업을 실행한다.(CSS에서는 repaint이후에도 post-process라는 단계가 더 있지만 이번 주제에서는 다루지 않는다.) 따라서 CSS의 어떤 속성들은 reflow 단계에 영향을 주지만 어떤 속성들은 repaint 단계에 영향을 준다. 이번에 다룰 주제인 CSS position은 화면을 그릴 때에 reflow 단계에 영향을 주는 CSS 속성이다.
Position Schemes
CSS 2.2 Visual Formmating Model 챕터의 Position Schemes 부분을 보면, Box 모델(margin, border, padding, content값으로 계산하는 기본적인 display model)의 위치를 계산할 때, 계산하는 방식이 3가지로 나뉜다고 설명한다. 첫 번째는 오늘 다룰 normal flow 방식이고, 두 번째는 다음에 다룰 float 방식이고 마지막으로는 세 번째로 다룰 absolute positioning 방식이다. 각각의 방식은 추상적인 값으로 표현된다. normal flow를 예로 들면, position 값이 static혹은 relative인 경우 해당 box는 normal flow를 통해 위치가 계산된다. 하지만 우리가 normal flow에 해당되는 position 값인 static과 relative를 읽는 것 자체로는 해당 box가 어떻게 그려질 지 가늠하기가 어렵다. 이 값들을 굳이 우리 말로 위치가 정적이고 상대적이라고 해석하는 것으로도 해당 box가 어떻게 그려질 지 알 수 없다. 따라서 우리가 이 추상적인 값 static, relative를 읽을 때, 추상적인 의미로 받아들여서는 box가 어디에 위치할 지 알 수 없기 때문에 이 값을 통해 해당 box의 위치가 어떤 방식으로 계산되는 지를 알아야 한다. 즉, static, relative라는 값을 문자 그대로 읽는 것이 아니라 화면을 그리기 위한 수학 공식이나 함수로 이해해야 한다.(다른 CSS 속성들도 마찬가지이다.) 그렇다면 normal flow에 해당하는 static, relative라는 값은 box의 위치를 결정하기 위해 어떤 값을 어떤 방식으로 계산하는 것일까? 웹은 요소를 그릴 때, 위에서 아래의 방향으로, 왼쪽에서 오른쪽 방향으로 그림을 그린다. 따라서 normal flow는 화면에 그려질 box요소의 left top의 값을 계산한다.
Normal Flow
Normal Flow는 우리 말로 일반 흐름이라고 해석할 수 있지만, 이렇게 해석하면 의미를 명확하게 알 수 없다. 이 단어는 CSS 공식문서에 정의된 고유 명사이기 때문에, 이 단어가 의미하는 바를 이해하기 위해서는 normal flow를 해당 box요소의 left top을 계산하는 방식으로 이해해야 한다. 이제 normal flow가 어떤 값을 계산하는 지는 알게 되었다. 그렇다면 normal flow는 어떤 방식으로 left top 값을 계산하는 것일까? CSS 2.2 Visual Formmating Model 챕터의 Normal Flow 부분을 보면 normal flow가 left top을 계산하는 방식을 3가지로 분류한다. 첫 번째는 block formatting context(이하 bfc), 두 번째는 inline formatting context(이하 ifc), 마지막으로 relative positioning로 분류한다. relative positioning의 경우 normal flow의 일부이지만, bfc, ifc 계산 이후의 추가적인 조작이기 때문에, 실제 normal flow에 해당하는 방식은 bfc와 ifc 이 두가지라고 할 수 있다.
BFC와 IFC
bfc와 ifc를 설명하기에 앞서서, block과 inline이 의미하는 바를 먼저 이해해야 한다. CSS의 block과 inline또한 CSS에서 사용하는 고유의 의미로 이해해야 한다. CSS에서의 block과 inline은 해당 box 요소가 공간을 자치하는 방식을 의미한다. block은 해당 요소가 부모 요소의 가로 길이를 꽉 채운 한 줄을 차지한다는 의미이다. 반대로 inline은 해당 요소의 content 영역만큼 공간을 차지한다는 의미이다. 이 때문에 inline 요소는 높이와 너비 속성 값을 줘도 적용되지 않는다. 다시 말해, bfc는 해당 box가 부모 요소의 가로 길이 전체를 꽉 채운 한 줄을 차지할 때, left top을 계산하는 방식이고, ifc는 해당 box가 content 영역만큼 공간을 차지할 때, left top을 계산하는 방식이다. normal flow로 box를 그릴 때, block 요소가 그려지는 동안은 bfc가 유지된다.(bfc 방식으로 그림을 그린다.) 그러다가 inline요소가 등장하면 ifc로 바뀐다.
위의 그림은 bfc와 ifc를 간략하게 그린 그림이다. bfc부터 먼저 살펴보면, block은 한 줄 전체를 차지한다고 했기 때문에 모든 block의 left 값은 부모의 left값과 동일하다. 그렇다면, bfc에서 관심을 가져야할 값은 top 값이다. 만약 위에가 block 요소들이었다면 계산은 간편하다. 위에 있는 block요소들의 height값을 모두 더한 값이 새로 그릴 block의 top 값이다. 동시에 bfc의 세로 길이는 bfc에 속한 block 요소들의 높이의 합이 된다.
반면에 ifc의 경우에는 inline 요소는 자신의 content만큼 공간을 차지하기 때문에, 한 줄에 여러 inline 요소가 배치될 수 있다. 따라서 ifc에서는 left 값이 어떤 값이 될지도 고려해야 한다. ifc에서 inline요소의 left 값은 같은 줄 앞에 그려진 inline 요소들의 길이의 합이 된다. 그런데 ifc에서는 inline 요소들의 가로 길이의 합이 부모의 가로 길이를 넘어버리면 다음 줄로 내려가는 기능이 있다. 그렇다면 inline 요소가 자동으로 다음 줄로 넘어 갈 때, 얼만큼 내려가야 할까? 한 줄에 그려진 inline 요소들 중 가장 height 값이 큰 요소의 height 값이 그 줄의 line height가 된다. 따라서 자동으로 다음 줄로 넘어가는 inline 요소의 top 값은 이전 줄 line height값이다.
BFC, IFC 이해하기
아래의 예제를 보자
See the Pen block-ex1 by psy082 (@psy082) on CodePen.
예제를 보면 red block, blue block은 block 요소이기 때문에 red block의 오른쪽에 공간이 남아 있음에도 불구하고 blue block이 다음 줄에 그려진다. 여기서 생각해볼 점은 red block이 300px만큼만 그려졌지만 차지하는 영역이 300px이라는 말이 아니다. 차지하는 영역 중에서 그림이 그려진 fragment영역이 300px인 것이다. red block은 여전히 부모의 width 만큼을 다 차지한다.
다음 예제를 보자
See the Pen inline-ex1 by psy082 (@psy082) on CodePen.
block요소가 부모인 inline 요소 aaa…는 부모가 100px의 너비를 가졌다면 100px에서 끝나야 하는데, block을 뜷고 그려진다. text는 inline 요소인데, 왜 아래 줄로 내려가지 않고 부모의 너비를 넘어서 그려지는 것일까? 만약에 text를 너비 100px에 맞춰서 입력하면 줄에 맞춰서 출력된다. 다음 예제를 보자
See the Pen inline-ex2 by psy082 (@psy082) on CodePen.
이번에는 a문자열을 100px에 맞추어 출력했더니 원하는 대로 a들이 red block 안에 출력되었다. 이는 암묵적으로 브라우저가 그림을 그릴 떄, 공백 문자가 없는 문자열을 하나의 ifc영역으로 본다는 것을 의미한다. enter나 space bar 등의 공백을 주게 되면 문자열 각각은 하나의 inline요소가 된다. 다시 말해, 만약 문자열이 공백문자 없이 하나로 이어졌다면 문자열 전체가 하나의 inline width를 가지게 된다. 만약에 div 내부에 공백 문자를 사용하지 않고 하나로 이어진 문자열을 div 너비에 맞춰서 그려지도록 만들고 싶다면 wordbreak 속성 값을 줘야 한다. wordbreak 속성이 설정되면, 해당 block 안에 있는 문자열의 문자 하나하나가 inline 요소로 처리된다. 이는 브라우저의 reflow 계산양이 늘어난다는 뜻이기 때문에 wordbreak를 많이 설정하게 되면 브라우저의 렌더링이 매우 느려진다.(일반적으로 div요소의 너비를 넘을만큼의 긴 문자열을 작성하는 경우는 거의 없다. 다만 이런 예시들을 통해 block내의 inline요소가 브라우저에게 어떻게 인식되는지를 알 수 있다.)
다음 예제를 보자
See the Pen inline-ex3 by psy082 (@psy082) on CodePen.
코드 결과물을 보기 전에 위의 예제코드를 읽고 화면이 어떻게 그려질 지 예상해 보자. 만약 화면이 어떻게 그려질지 혼동이 온다면 아직 rendering system에 대한 이해가 부족한 것이다. 혼동이 온 이유를 생각해보자. 아마도 span이라는 inline 요소 내부에 red block이 왔기 때문일 것이다. red block의 부모요소는 span이고, red block이 그려지기 전에 WORLD라는 문자열이 왔다. 그러면 red block은 WORLD 옆에 그려지는지 다음 줄에 그려지는 지가 혼동이 온다. 그런데 rendering system을 이해하기 위해서는 rendering system이 DOM의 구조와 일치하지 않는다는 것을 알아야 한다. 그림이 어떻게 그려질 지 예상할 때 혼동이 오는 근본적인 이유는 rendering이 DOM의 구조와 동일하다고 생각하기 때문이다. 그러나 rendering은 element들을 그릴 때, DOM의 맥락에서 그리는 것이 아니라 bfc, ifc의 맥락에서 그린다. DOM의 포함관게에서는 inline 안에 block이 있는 구조이지만 그림을 그리는 브라우저 입장에서는 위의 코드를 그릴 때, bfc시작 -> ifc시작 -> ifc -> 다시 bfc 시작 -> 다시 ifc의 관점에서 그린다.(이는 웹에서 DOM은 의미적인 요소만을 표현하도록 하고 style은 DOM이 아닌 CSS의 조작으로 표현하도록 하는 HTML5 이후의 semantic web의 흐름과도 맞는 관점이다.)
앞의 내용을 정리해보면, position 값이 static, relative인 box 요소의 경우 box 요소의 위치를 계산할 때, normal flow 방식으로 계산한다. 해당 box가 block 요소인 경우 bfc로 그리고, inline인 경우 ifc로 그린다. bfc는 top 값에 관심이 있고, top을 계산하기 위해서는 앞에 그려진 block 요소들의 height 값을 모두 더함으로써 계산할 수 있다. ifc는 left 값에도 관심이 있고, left 값을 계산하기 위해서 앞에 그려진 inline 요소들의 width 값을 모두 더한다. 만약 inline width 값들의 합이 부모 요소의 width 값을 넘어간 경우 마지막 inline 요소는 자동으로 다음 줄에 그려진다. inline요소가 다음 줄에 넘어가서 그려질 때는 이전 줄의 inline 요소들 중 가장 height값이 큰 요소의 height값이 다음 줄에 그려지는 inline 요소의 top값이 된다. 마지막으로 이런 bfc, ifc는 rendering system이기 때문에 dom의 구조와 일치하지 않는다. 계층상 inline요소 안에 block 요소가 있다고 하더라도 rendering system은 각각을 계층적으로 보지 않고 독립적인 bfc, ifc로 그린다.
이제 마지막으로 relative positioning이 남았다.
Relative Positioning
앞에서 relating positioning이 bfc, ifc 계산에 대한 추가적인 조작이라고 말했다. 다르게 말하면, position: relative는 position: static으로 그림을 그린 이후에 무언가 추가적인 조작이 일어난 결과이다. 실제로 브라우저에서 그림을 그릴 때, position: relative의 경우 먼저 static으로 그림을 그린 뒤에 추가적인 위치 조정이 일어난다는 것을 확인할 수 있다. 우선 아래의 예제를 보자
See the Pen relative positioning by psy082 (@psy082) on CodePen.
이전에 그려졌던 예제와 비슷한 예제인데 다른 점은 span에 position: relative를 주었다. 그림이 어떻게 그려졌는지 살펴보자. 우선 ifc가 유지되면서 ** HELLO WORLD
가 그려진다. 그러다가 red block이 등장하면서 bfc가 시작된다. !!로 인해 다시 ifc로 바뀌고 blue block이 등장했기 때문에 다시 bfc가 시작되고 그림 그리는 것이 끝난다. 여기까지가 position: static의 과정이다. position: relative는 단지 이미 position: static으로 그러진 그림에 지정한 위치 값 만큼 요소를 이동시켜서 다시 그리는 것일 뿐이다.
위의 예제를 보면 position: relative가 설정된 span요소 안의 WORLD와 red block이 위에서 50px만큼 내려와 있다. WORLD와 red block이 50px 만큼 다시 위로 올라가면 static으로 그렸을 때와 동일한 그림이다. 앞의 설명에서 브라우저에서 그림을 그릴 때, position: relative의 경우 먼저 static으로 그린 뒤에 추가적인 위치 조정이 일어나는 것을 확인할 수 있다고 했는데, 이번 예제의 경우에도 확인이 가능하다. 브라우저에서 그림을 그릴 때에 나중에 그려진 요소는 먼저 그려진 요소보다 z-index가 높다. 그래서 요소가 겹칠 때 나중에 그려진 요소가 전에 그려진 요소 위에 그려지는 것이다. 그런데 50px 만큼 내려온 red block은 blue block 위에 그려졌다. red block이 먼저 그려졌음에도 불구하고 어떻게 blue block 위에 그려질 수 있을까? 그 이유는 red block이 static으로 그려진 이후에 relative로 위치 조정이 되서 다시 그려졌기 때문에 blue block 보다 z-index가 높아졌기 때문이다. 정리하자면 relative 값은 box의 크기나 width, height 값에 변화를 일으키지 않고 단지 상대적인 위치만 이동시킬 뿐이다. 해당 요소의 reflow계산은 모두 static에서 이루어진다.(static에서 width와 height가 모두 결정된다.)
html element들은 position 값을 따로 주지 않는다면 기본적으로 그 값이 static이다. 따라서 대부분의 element들은 normal flow를 통해 계산된다. 그런데 만약 position: absolute나 position: fixed를 사용하는 경우 더 이상 normal flow로 계산되지 않기 때문에 width와 height값을 자동으로 설정해주지 않고, 위치도 자동으로 잡아주지 않는다. 따라서 position: absolute나 position: fixed인 요소들은 반드시 width와 height값을 직접 지정해 주어야 하고 위치 값도 직접 지정해 주어야 한다.(다시 말해, position: absolute, position: fixed일 때 위치 값과 크기 값을 지정해 주는 이유는 normal flow가 아니기 때문이다.)
※ 추가: inline-block
많은 글에서 block, inline, inline-block을 같이 다루지만 실제 CSS spec에서는 block, inline과 inline-block이 다른 분류에 속한다. CSS에서 element를 그리는 방식은 display model이라는 큰 범주에서 갈라진다. display model은 6개의 분류로 나뉘는데, 오늘 다루는 normal flow의 block과 inline은 display-outside에 해당하고, inline-block은 display-legacy에 해당한다. inline-block이 legacy에 해당하는 이유는 block과 inline의 관심사가 다름에도 불구하고 두 값이 동일하게 display 속성의 값이기 때문에 동시에 사용할 수 없어서 아예 별도로 inline-block값이 생겨났기 때문이다. 좀 더 설명을 하자면 앞서 이야기 했던 것처럼 block요소는 fragment만큼만 그려지지만 실제로는 부모 길이 한 줄 전체를 차지한다. 이 때문에 옆에 공간이 남아 있음에도 불구하고 공간을 사용할 수 없다. 만약에 block 요소이지만 fragment만큼만 공간을 차지하고 그림이 그려지지 않은 남은 공간도 inline요소처럼 사용하고 싶은 경우에는 display: inline-block을 설정하면 되고 이 경우 block 요소지만 ifc처럼 그려진다.