포스트

[CSS] 노말 플로우로 사고하기

노말플로우를 통해 브라우저에서 레이아웃이 배치되는 원리를 이해하고 BFC, IFC에 대해서 알아보자

💡 시작에 앞서

예전에 CSS를 공부하다가 마진병합 현상에 대해서 알게되었는데 그 과정에서 normal flow, BFC 등 생소한 내용이 등장하면서 혼란스러웠던 적이 있었습니다. 다소 추상적일 수 있는 내용이라 쉽지 않지만 HTML문서에서 요소들의 레이아웃 배치와 정렬, 상호작용 등을 설명하는 이러한 노말 플로우의 동작을 이해하고 정리하려고 합니다. 😓


💡 노말 플로우 (Normal Flow)

노말 플로우는 브라우저가 레이아웃을 배치하는 가장 기본적인 배치 방식을 의미합니다. 즉, 요소들을 화면에 어떻게 배치할지를 결정하는 기본 방식을 설명하는 개념입니다.

  • div 태그는 블록 요소이기 때문에 부모의 너비를 상속받고 화면을 꽉채운다
  • span 태그는 인라인 요소이기 때문에 자신의 크기만큼의 너비를 갖는다

뭔가 노말 플로우라는 단어 자체가 낯설게 느껴지지만 위의 div, span 태그의 기본 동작처럼 우리가 평상시에 자연스럽게 받아들이는 레이아웃의 기본 규칙을 의미하는데 Normal Flow 라는 말 그대로 자연스러운 흐름 이라고 생각하면 됩니다.


💡 Formatting Context (*FC)

Formatting Context란 요소들이 어떻게 배치되고 정렬되는지를 결정하는 규칙과 메커니즘이 적용되는 영역을 의미하는데 블록 레벨의 요소라면 BFC에 의해서, 인라인 레벨의 요소라면 IFC에 의해서 배치됩니다. 실제 스펙은 W3C CSS2.1에서 정의 되어있는데 float, overflow, margin collapse, line box등 어려운 얘기가 많이 나오고 복잡하지만 결국은 우리가 알고 있는 것 처럼 블록 요소는 수직으로 배치되고 텍스트나 인라인 요소들은 수평으로 배치된다는 얘기를 하고있습니다.

당연하다고 생각했던 요소들의 기본 동작들은 이러한 노말 플로우의 BFC, IFC에 의해서 레이아웃이 배치되고 있었던 것 입니다. 하지만 앞서 말했듯이 노말 플로우는 실제 스펙상으로 훨씬 어렵고 복잡한 규칙인데 이러한 BFC, IFC에 대해서 조금 더 자세히 알아보도록 합시다.


💡 BFC (Block Formatting Context)

문서의 최상위 요소인 HTML 요소는 특수하게도 BFC 영역을 생성하는데 따라서 body요소를 비롯해서 내부의 자식 요소들은 기본적으로는 상단으로부터 수직으로 배치되어 웹페이지의 기본 레이아웃을 유지하고 하나의 독립된 컨텍스트에서 예측 가능하고 일관되게 배치됩니다. 하지만 BFCCSS 속성을 통해서 의도적으로 생성하는게 가능한데 MDN 문서에 따르면 새로운 BFC가 생성되는 경우는 아래와 같습니다.

  • 문서의 최상위 요소인 HTML
  • float 속성을 기본값인 none이 아닌 속성으로 플로팅 했을 때
  • position 속성을 absolute, fixed로 해서 노말 플로우에서 벗어났을 때
  • overflow 속성이 hidden 또는 scroll일 때
  • display 속성이 inline-block 또는 flow-root 일때
  • ... 이외에도 더 많은 경우가 있음
1
2
3
.selector  {
    display: flow-root /* 새로운 BFC 생성 */
 }

BFC를 만들때 주로 overflow: hidden 속성이 많이 사용되던데 overflow 속성의 원래의 목적과 다른 사용일 뿐더러 사이드 이펙트가 발생할 수 있어 주의해야 할 필요가 있습니다. 그래서 상황에 맞게 적절한 방법을 선택하면 될 것 같고 flow-root는 다른 방법들과 다르게 사이드 이펙트 없이 단순히 BFC를 만들기 때문에 이 글에서는 flow-root를 선택했습니다.

BFC와 관련해서 레퍼런스를 찾아보고 공부 하면서도 뭔가 크게 와닿지 않는 느낌이 들어서 의문점을 정리해 보고 나름대로의 해답을 찾아보려고 했습니다.

🚩 내가 div 태그를 만들면 새로운 BFC를 생성 하는걸까?

그건 아니다 div 태그는 블록 요소는 맞지만 단순히 BFC 안에서 배치될 뿐 위에서 언급한 것처럼 특정 상황에서만 새로운 BFC를 생성한다.

🚩 새로운 BFC를 만든다는 것은 무슨 의미일까?

모든 요소들은 최상위 요소인 HTML 요소에 의해서 만들어진 BFC의 영향을 받는다. 하지만 새로운 BFC를 만든다는 것은 해당 요소와 그 자식 요소들이 배치되는 독립적인 영역을 생성하는 것을 의미하는데 같은 BFC일지라도 서로 다른 BFC 영역으로 배치되어 CSS의 레이아웃 모델을 더욱 정교하게 제어할 수 있다.

대표적으로 플로팅 문제와 마진 병합으로 발생하는 문제를 해결 할 수 있다

🚩 BFC는 중첩될 수 있을까?

중첩될 수 있다. 하지만 독립적인 영역을 만들기 때문에 BFC 라는 같은 범주에 있지만 결국 서로 다른 레이아웃 영역이 된다.

🚩 그래서 BFC의 중요성은 무엇인데?

BFC는 브라우저에서 블록 레벨의 요소들이 배치되는 방식을 규정하는 영역을 의미한다 따라서 BFC를 이해하고 적절히 활용하는 것은 레이아웃이 배치되는 원리를 이해하고 더욱 정교하며 예측 가능한 설계를 만드는 데 도움이 될 수 있다.


💡 플로팅 (floating)

CSSfloat 속성은 MDN float 스펙에 의하면 노말 플로우에서 벗어나 텍스트나 인라인 요소들이 자신을 감싼다고 표현하고 있습니다. float의 특징을 정리하자면 아래와 같습니다.

  • 새로운 BFC를 만든다 (BFC의 생성 조건에서 확인 가능)
  • 노말 플로우를 벗어나 위에 떠오른다 (floating)
  • 텍스트와 인라인 요소에 대한 경계면을 만든다

float 속성은 요즘에는 잘 사용하지 않지만 텍스트 래핑 등을 구현할 때 사용될 수 있는데 중요한 건 노말 플로우에서 벗어나 위에 떠버리기 때문에 일반적인 문서의 흐름에서 벗어나 주변 요소들과 상호작용이 되지 않는 문제가 발생합니다.

1
2
3
4
5
6
7
8
.container  {
    background-color: green;
 }
.container  div {
    float: left;
    margin: 10px;
    background-color: lightgreen;
 }
1
2
3
4
<div class="container">
  <div>Sibling</div>
  <div>Sibling</div>
</div>

위의 코드는 오른쪽 그림과 같은 결과를 기대했지만 float된 요소가 노말 플로우에서 벗어났고 부모는 명시적으로 높이를 지정해 주지 않았기 때문에 높이를 잃어버려서 왼쪽 그림과 같이 렌더링됩니다. 이런 문제를 해결하기 위해 BFC를 사용할 수 있는데 BFC는 자신이 포함하고 있는 float된 요소들을 포함하여 다른 컨텐츠의 높이와 동일하게 만든다는 특징이 있습니다.

1
2
3
4
.container  {
    display: flow-root; /* 새로운 BFC 생성 */
    background-color: green;
 }

따라서 새로운 BFC가 생성되도록 컨테이너에 속성을 추가하면 높이를 잃어버리지 않고 의도한 대로 렌더링됩니다.

플로팅으로 인한 높이를 갖지않는 문제는 clear속성을 사용해서 플로팅된 요소가 다른 요소와 겹치지 않도록 해서 해결하는 방법도 있습니다.


💡 마진병합 (margin collapse)

마진병합 이란 인접한 블록요소 사이의 마진이 병합되어 더 큰 마진으로 합쳐지는 현상을 의미합니다. 상식적으로라면 둘의 마진의 합 만큼 적용되어야 할 것 같지만 CSS는 의도적으로 이렇게 설계되어 문서의 안정성을 높이도록 동작합니다.

이러한 마진병합 이라는 CSS의 기본동작 때문에 때로는 의도치 않은 레이아웃이 구현될 수 있는데 마진병합 현상을 막을 수 있는 방법을 알아봅시다

MDN 문서에 따르면 마진병합의 발생 조건을 아래와 같이 정의 하고있습니다.

  • 인접한 형제 요소간 발생
  • 부모 자식 사이에 인라인 요소가 없고 부모에 border, padding 등과 같은 스타일로 여백이 생기지 않으며, BFC가 생성되지 않은경우 부모 자식간 발생

발생 조건에서 볼 수 있듯이 새로운 BFC생성이 없어야한다는 것은 결국 부모와 자식이 동일한 BFC내에 존재할 때 마진병합이 발생한다는 의미이고 따라서 마진병합이 일어나지 않도록 의도한다면 테두리, 패딩 등의 속성으로 인접한 요소 사이에 공간을 만들던지 아니면 새로운 BFC를 생성해서 인접한 블록을 서로 다른 BFC에 속하도록 하면 마진 병합을 막을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
 .parent {
   background-color: tomato;
   display: flow-root
 }

 .child {
   margin: 1rem;
   background-color: cornsilk;
 }
 
 .bfc {
   display: flow-root
 }
1
2
3
4
5
6
 <div class="parent">
   <div class="child">child1</div>
   <div class="bfc"> 
     <div class="child">child2</div>
   </div>
 </div>

위의 코드에서 parent는 새로운 BFC의 생성으로 부모 자식 간의 마진병합이 발생하지 않으며, .bfc 클래스로 인해 새로운 BFC를 생성해서 child는 각각 서로 다른 BFC에 속해있기 때문에 child간의 마진 병합 역시 발생하지 않습니다.


💡 IFC (Inline Formatting Context)

IFC는 인라인 레벨의 요소들이 배치되는 영역으로 텍스트나 인라인 요소들이 수평으로 배치되며 BFC와는 다르게 a태그나 span 태그와 같은 인라인 요소를 만들면 자동으로 새로운 IFC가 생성됩니다.

⭐ 라인박스 (Line Box)

라인박스는 여러 인라인 요소들이 수평으로 배치될 때, 이들이 결합된 한 줄의 박스 영역을 의미합니다. 그래서 라인박스의 높이는 라인박스에 포함된 가장 큰 인라인 요소의 높이에 맞춰지며 라인박스의 폭을 초과하면 새로운 라인박스가 만들어져서 수직으로 쌓입니다. 아래는 라인박스와 관련된 속성들의 기본동작 입니다.

  • line-height 속성은 라인박스의 최소 높이를 설정한다
  • vertical-align 속성은 라인박스의 범위를 기준으로 수직 정렬한다

IFC랑 라인박스에 대한 관계를 제가 이해한 것을 바탕으로 직접 그림을 그려서 표현 해봤습니다. IFC는 인라인 요소들의 배치 영역을 정의하며, 인라인 요소들은 라인 박스 안에서 정렬됩니다.

⭐ IFC와 BFC의 중첩

BFC 내부에는 블록 요소뿐만 아니라 인라인 요소도 포함될 수 있는데 이런 인라인 요소들은 자기들끼리 IFC 영역을 형성합니다. 하지만 반대로 IFC 내부에서 블록 요소를 추가하게 되면 해당 IFC는 그대로 종료되어 닫히고 상위 BFC영역에 이어서 블록 요소가 배치됩니다. 따라서 BFC내부에는 IFC가 존재할 수 있지만 반대의 경우는 불가능합니다.

IFC에 대해서 관련된 내용을 더 찾다 보면 font metrics, content-area 등 굉장히 어려운 내용들이 나오던데 기회가 된다면 공부해서 나중에 관련된 글을 써보고 싶다는 생각을 했습니다.


💡 CSS: Position

CSS에 요소를 배치하는 방식과 그 위치를 결정하는 Position 속성은 노말 플로우와 깊은 연관이있는데 노말 플로우는 Position 속성이 오직 staticrelative, sticky 일때만 적용됩니다. (MDN - position 에서 확인 가능)

따라서 absolute 또는 fixed로 포지션을 설정하면 노말 플로우에서 벗어나기 때문에 기존의 노말 플로우에서 벗어나 자유로운 배치를 할 수 있게되는 특징이 있습니다.

  • static : 노말 플로우에 의해 배치되는 기본값
  • relative : 노말 플로우에 의해 배치된 이후 상대적으로 위치 조정
  • absolute : 노말 플로우에서 벗어나 offset parent로 부터 위치 조정
  • fixed : 항상 브라우저의 뷰포트를 기준으로 위치 고정
  • sticky: 노말 플로우에 의해 배치되며 특정 위치에 도착 후 고정

span 태그와 같은 인라인 요소에 absolute로 포지셔닝 하면 BFC 생성 조건에 따라서 새로운 BFC를 만들게 되는데, 재밌는 건 span태그는 원래 인라인 요소지만 display 속성을 확인 해보면 block으로 속성이 바뀌었으며, 블록 요소임에도 불구하고 명시적으로 너비와 높이를 지정하지 않으면 마치 인라인 요소처럼 자신의 컨텐츠 만큼의 크기를 갖습니다.

🚩 같지만 다른 BFC

  1. position: absolute는 기존의 노말 플로우에서 벗어남
  2. BFC 생성조건에 의해 absolute는 새로운 BFC를 생성함
  3. 새로 생긴 BFC영역은 기존의 BFC의 규칙과는 다르게 동작함

즉, absolute로 인해 만들어진 BFC 영역은 기존의 BFC의 규칙과는 다르게 동작하는데 그럼에도 불구하고 왜 똑같이 BFC라고 불리는 건지 궁금증이 생겼습니다. 비단 absolute만의 문제는 아니고 float, overflow, display 속성 등으로 각각 BFC를 생성하면 마진병합이 사라지는 BFC만의 특성이 나타나면서도 각 속성이 적용되는 것을 보면, 같은 BFC 일지라도 BFC의 특성은 유지하면서 유연하게 레이아웃 규칙이 바뀔 수 있고 여전히 블록 레벨 요소의 레이아웃 컨텍스트라는 관점에서는 부합하니까 그대로 BFC라고 하는게 아닐까 싶습니다.


💡 마무리

주제 자체가 어려운 주제라서 많은 문서를 참조 하면서 정확하게 내용을 전달하려고 노력했는데 내용전달이 잘 됐는지 모르겠습니다. (아직도 어렵다 😅) 또한 브라우저가 레이아웃을 계산할 때 BFC, IFC의 규칙에 따라서 요소들이 배치될 뿐 단순히 문서의 구조를 나타내는 DOM트리의 포함관계와 레이아웃은 무관하다는 것을 기억하면 좋을 것 같습니다.

📕 참고 문서

W3C - Visual formatting model
MDN - Block and inline layout in normal flow
MDN - Block formatting context
MDN - Mastering margin collapsing
Understanding Block Formatting Contexts in CSS
카카오 기술 블로그 - Line Box

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.