안녕하세요! 오늘 함께 살펴볼 내용은 CSS에서 가장 많이 쓰이는 속성 중 하나인 display 속성에 새롭게 도입된 다중 키워드 구문 (Multi-keyword syntax)에 대한 내용입니다.
우리가 흔히 쓰는 display: block, display: flex 같은 속성들은 사실 하나의 요소가 외부(부모나 형제)와 어떻게 상호작용할지, 그리고 내부(자식들)를 어떻게 배치할지를 동시에 결정하는 복합적인 속성입니다. 이 개념을 명확히 이해하면 컴포넌트를 설계할 때 훨씬 더 유연하게 레이아웃을 다룰 수 있습니다. 프론트엔드 면접에서도 "display 속성의 작동 원리에 대해 설명해 보세요"라는 질문에 깊이 있는 답변을 할 수 있는 좋은 무기가 될 테니 함께 자세히 알아봅시다!
CSS display 모듈은 CSS display 속성을 위한 다중 키워드 구문(multi-keyword syntax)을 정의합니다. 이 가이드에서는 이 다중 키워드 구문에 대해 설명합니다.
💡 노트 (Note):
다중 키워드 구문은 "두 값 구문(two-value syntax)" 또는 "다중 값 구문(multi-value syntax)"이라고도 불립니다.
우리가 CSS에 대해 가장 처음 배우는 것 중 하나는 어떤 요소들은 블록 레벨(block-level)이고 어떤 요소들은 인라인 레벨(inline-level)이라는 사실입니다. 이것이 바로 요소들의 외부(outer) 디스플레이 타입입니다. 예를 들어, <h1>이나 <p>는 기본적으로 블록 레벨이고, <span>은 인라인 레벨입니다. 우리는 display 속성을 사용하여 블록과 인라인 사이를 전환할 수 있습니다. 예를 들어 제목(heading)을 인라인으로 만들려면 다음과 같은 CSS를 사용합니다:
h1 {
display: inline;
}
display 속성은 또한 display: grid나 display: flex가 설정되었을 때 우리가 CSS 그리드 레이아웃(Grid Layout)이나 플렉스박스(Flexbox)를 사용할 수 있게 해줍니다.
여기서 이해해야 할 중요한 개념은 요소의 display 값을 변경하면 해당 요소의 직계 자식들의 서식 문맥(formatting context)이 바뀔 수 있다는 점입니다. 여러분이 display: flex나 display: grid를 사용하면, 그 요소의 자식들은 플렉스 아이템이나 그리드 아이템이 되며 그리드와 플렉스박스 명세서에 있는 속성들에 반응하게 됩니다.
하지만 그리드와 플렉스박스가 우리에게 보여주는 것은, 요소가 외부(outer) 디스플레이 타입과 내부(inner) 디스플레이 타입을 모두 가지고 있다는 점입니다.
예를 들어, 우리가 display: flex를 사용하면 플렉스 자식들을 가진 블록 레벨 컨테이너를 생성하는 것입니다. 자식들은 '플렉스 서식 문맥'에 참여한다고 설명됩니다. 이것은 일반적으로 인라인 레벨 요소인 <span>에 display: flex를 적용해 보면 명확히 알 수 있습니다. 이 <span>은 블록 레벨 요소가 됩니다. 레이아웃 내의 다른 상자(box)들과의 관계에서는 블록 레벨 요소처럼 행동하죠. 마치 span에 display: block을 적용한 것과 같습니다. 하지만 동시에 그 자식들의 행동 방식도 (플렉스로) 변하게 됩니다.
아래 라이브 예제는 display: flex가 적용된 <span>을 보여줍니다. 이 요소는 인라인 방향(가로)의 사용 가능한 모든 공간을 차지하는 블록 레벨 박스가 되었습니다. 이제 여러분은 두 플렉스 아이템 사이의 공간을 띄우기 위해 justify-content: space-between을 사용할 수 있습니다.
<span class="flex"> Some text <em>emphasized text</em> </span>
body {
font: 1.2em / 1.5 sans-serif;
}
.flex {
border: 5px solid #cccccc;
display: flex;
justify-content: space-between;
}
인라인 플렉스 컨테이너를 만드는 것도 가능합니다. 단일 값인 inline-flex를 사용하면, 여러분은 플렉스 자식을 가진 인라인 레벨 박스를 얻게 됩니다. 자식들은 (블록 레벨 컨테이너의 플렉스 자식들과) 완전히 동일한 방식으로 행동합니다. 유일하게 달라진 점은 부모가 이제 '인라인 레벨 박스'가 되었다는 것뿐입니다. 따라서 부모는 다른 인라인 레벨 요소들처럼 행동하며, 블록 레벨 상자처럼 전체 너비(또는 인라인 방향의 전체 크기)를 다 차지하지 않습니다. 이는 플렉스 컨테이너 바로 옆에 다른 텍스트가 나란히 올 수 있음을 의미합니다.
💡 강사의 실무 팁!
"버튼 안에 아이콘과 텍스트를 나란히 배치해야 하는데 버튼이 부모 영역을 다 차지해버려요!"
프론트엔드 개발 입문자분들이 자주 겪는 문제죠. 이때display: flex대신display: inline-flex를 사용하면, 버튼은 자신의 콘텐츠만큼만 너비를 가지면서도 내부의 아이콘과 텍스트는 flex의 강력한 정렬 기능(align-items: center등)을 그대로 사용할 수 있습니다.
<div class="flex">
<div>One</div>
<div>Two</div>
</div>
Text following the flex container.
body {
font: 1.2em / 1.5 sans-serif;
}
.flex > div {
border: 2px solid rgb(96 139 168);
border-radius: 5px;
background-color: rgb(96 139 168 / 0.2);
}
.flex {
border: 5px solid #cccccc;
display: inline-flex; /* 박스 자체가 인라인 요소처럼 동작하면서 내부는 flex로 동작합니다. */
}
이 원리는 그리드(grid) 레이아웃을 다룰 때도 똑같이 적용됩니다. display: grid를 사용하면 블록 레벨 상자가 되며 직계 자식들을 위한 그리드 서식 문맥을 생성합니다. display: inline-grid를 사용하면 인라인 레벨 상자가 되며 자식들을 위한 그리드 서식 문맥을 생성합니다.
위의 설명에서 알 수 있듯이, display 속성은 상당한 권한을 가지고 있습니다. 페이지의 다른 상자들과의 관계에서 어떤 요소가 블록 레벨인지 인라인 레벨인지를 나타낼 뿐만 아니라, 그 속성이 적용된 상자 '내부'의 서식 문맥(formatting context)도 함께 나타냅니다. 이러한 동작을 더 잘(명시적으로) 설명하기 위해, display 속성은 외부 값(outer value)과 내부 값(inner value)이라는 두 가지 값을 설정할 수 있도록 허용합니다. (물론 기존의 단일 값 구문도 여전히 유효합니다.)
이것은 우리가 플렉스 자식을 가진 블록 레벨 상자를 만들기 위해 display: flex를 설정하는 대신, display: block flex를 사용할 수 있다는 뜻입니다. 플렉스 자식을 가진 인라인 레벨 상자를 만들기 위해 display: inline-flex 대신 display: inline flex를 사용할 수 있습니다. 아래 예제가 이 값들을 보여줍니다.
<h1>Multiple values for display</h1>
<div class="flex flex1">
<div>Item One</div>
<div>Item Two</div>
<div>Item Three</div>
</div>
<p>The first example is a block element with flex children.</p>
<div class="flex flex2">
<div>Item One</div>
<div>Item Two</div>
<div>Item Three</div>
</div>
The second example is an inline element with flex children.
body {
font: 1.2em / 1.5 sans-serif;
}
.flex {
border: 5px solid #cccccc;
gap: 10px;
}
.flex > * {
border: 2px solid rgb(96 139 168);
border-radius: 5px;
background-color: rgb(96 139 168 / 0.2);
}
.flex1 {
display: block flex; /* 외부로는 block, 내부로는 flex */
}
.flex2 {
display: inline flex; /* 외부로는 inline, 내부로는 flex */
}
기존의 모든 display 값들에 대한 매핑(다중 키워드 버전)이 존재합니다. 가장 일반적인 것들을 아래 표에 나열했습니다. 전체 목록을 보려면 display 속성 명세서의 표를 확인하세요.
| 단일 값 (Single value) | 다중 값 (Multi value) |
|---|---|
block | block flow |
flow-root | block flow-root |
inline | inline flow |
inline-block | inline flow-root |
flex | block flex |
inline-flex | inline flex |
grid | block grid |
inline-grid | inline grid |
이 다중 값 구문이 CSS 레이아웃을 명확하게 이해하는 데 어떻게 도움이 되는지 확인하기 위해, 위 표에서 여러분에게 조금 낯설 수 있는 값들을 살펴보겠습니다. 다중 키워드인 display: block flow-root는 단일 값인 display: flow-root와 매핑됩니다. 이 값의 유일한 목적은 새로운 블록 서식 문맥(Block Formatting Context, BFC)을 생성하는 것입니다. BFC는 상자 안에 있는 모든 것이 상자 안에 머물도록 하고, 상자 밖에 있는 것들이 상자 안으로 침범하지 못하도록 보장합니다.
아래 예제에서, 하나는 <div> 안에 있고 다른 하나는 밖에 있는 두 개의 <p> 요소가 디스플레이 값들이 서식 문맥에 어떻게 영향을 미치는지 보여줍니다.
가장 먼저 데모 컨트롤이 있는 <div> 요소는 숨겨져 있어서 우리는 이어지는 요소들에 집중할 수 있습니다.
우리가 주목해야 할 요소들은 "parent(부모)", "child(자식)", 그리고 "sibling(형제)"이라는 ID로 구분할 수 있는 <div>와 <p> 요소들입니다.
이 레이아웃에서 주목할 점은 부모와 자식 요소 사이에 아무런 콘텐츠가 없으며, 자식 요소에 위쪽 마진(top margin)이 적용되어 있다는 점입니다.
여러분은 자식의 위쪽 마진이 부모 요소 안에서 자식을 아래로 밀어낼 것이라고 기대할 수 있지만, 실제로 일어나는 현상은 마진 상쇄(margin collapse)라고 불리는 현상입니다.
이 경우, 자식 요소의 마진이 부모의 경계 박스 위로 연장되어 부모 요소 전체를 페이지 아래로 밀어냅니다. (이것은 브라우저 개발자 도구에서 자식 요소의 박스 모델을 검사해 보면 더 쉽게 볼 수 있습니다.)
<select> 요소에서 선택된 옵션을 변경하여 다양한 display 값의 효과를 확인해 보세요.
flow-root가 포함된 값을 사용하면 부모를 위한 새로운 서식 문맥(BFC)이 생성되어, 자식 요소의 마진이 부모의 바깥쪽 가장자리에 상대적으로 적용되도록 만들고 마진 상쇄를 피할 수 있습니다.
display: flow-root와 display: block flow-root 사이를 변경해 보면, 단일 값인 flow-root 키워드와 완전히 동일한 효과를 얻을 수 있음을 알 수 있습니다.
const parentDiv = document.getElementById("parent");
const siblingDiv = document.getElementById("sibling");
const displayTypeSelect = document.getElementById("displayType");
function changeDisplayType() {
parentDiv.style.display = displayTypeSelect.value;
siblingDiv.style.display = displayTypeSelect.value;
}
displayTypeSelect.addEventListener("change", changeDisplayType);
#controls {
padding: 1rem;
outline: 2px dashed black;
}
body {
margin: 10px;
font-family: sans-serif;
}
div,
p {
outline: 2px solid black;
background-color: cornflowerblue;
display: block;
margin-bottom: 2rem;
}
#parent {
background-color: oldlace;
min-height: 2rem;
}
#child {
margin-top: 4rem; /* 이 마진이 부모 밖으로 빠져나가는 마진 상쇄 현상 발생 */
outline: 2px dashed red;
}
#sibling {
background-color: lavender;
}
<div id="controls">
<label for="displayType">display:</label>
<select id="displayType">
<option value="block">block</option>
<option value="flow-root">flow-root</option>
<option value="block flow-root">block flow-root</option>
<option value="inline">inline</option>
<option value="inline flow-root">inline flow-root</option>
</select>
</div>
<div id="parent">
<p id="child">The #child paragraph (nested in #parent).</p>
</div>
<p id="sibling">The #sibling paragraph (sibling of #parent).</p>
flow-root라는 값은, 흔히 정상적인 흐름(normal flow)이라고 불리는 '블록 및 인라인 레이아웃'을 생각해 보면 그 의미가 통합니다. 우리의 HTML 페이지는 새로운 서식 문맥(플로트와 마진이 경계를 넘어 확장될 수 없는)을 생성하고, 우리가 다른 서식 문맥을 사용하기 위해 display 값을 변경하지 않는 한 우리의 콘텐츠는 블록 및 인라인 레이아웃을 사용하여 정상적인 흐름으로 배치됩니다. 그리드나 플렉스 컨테이너를 생성하는 것 역시 새로운 서식 문맥(각각 그리드 서식 문맥 또는 플렉스 서식 문맥)을 생성합니다. 이것들 또한 내부에 있는 모든 것을 가두어 둡니다(contain). 하지만, 만약 여러분이 플로트와 마진을 가두고 싶지만 여전히 기존의 블록 및 인라인 레이아웃을 계속 사용하고 싶다면, 새로운 'flow root'를 생성하여 블록 및 인라인 레이아웃을 새로 시작할 수 있습니다. 그 지점부터 아래로는 모든 것이 새로운 flow root 안에 갇히게 됩니다.
이것이 바로 display: flow-root가 다중 키워드 구문인 display: block flow-root로 작성될 수 있는 이유입니다. 여러분은 블록 레벨 박스를 가지고, 자식들이 정상적인 흐름(normal flow)에 참여하는 블록 서식 문맥(BFC)을 만들고 있는 것입니다.
그렇다면 짝을 이루는 display: inline flow-root는 어떨까요? 이것이 바로 현재 display: inline-block을 설명하는 정확한 방식입니다.
display: inline-block 값은 CSS 초창기부터 존재해 왔습니다. 우리가 이 값을 주로 사용하는 이유는, 예를 들어 내비게이션 아이템을 만들 때 인라인 아이템들이 패딩을 통해 서로를 밀어내게 하거나, 아래 예제처럼 인라인 요소에 패딩과 함께 배경을 추가하고 싶을 때입니다.
<p>
This paragraph has a span <span class="inline-block">with padding</span> it is
an inline-block so the padding is contained and pushes the other line boxes
away.
</p>
body {
font: 1.2em / 1.5 sans-serif;
}
p {
border: 2px dashed;
width: 300px;
}
.inline-block {
background-color: rgb(0 0 0 / 0.4);
color: white;
padding: 10px;
display: inline-block;
}
하지만 display: inline-block이 적용된 요소는 플로트(floats)도 감쌉니다(contain). 인라인 레벨 상자 안에 있는 모든 것을 감싸는 것이죠. 따라서 display: inline-block은 본질적으로 display: flow-root가 하는 일과 똑같은 일을 하지만, 블록 레벨 상자가 아닌 '인라인 레벨 상자'의 형태로 수행하는 것입니다. 두 값 구문(two-value syntax)은 이 값을 사용할 때 실제로 무슨 일이 일어나고 있는지를 아주 정확하게 설명해 줍니다. 위 예제에서 display: inline-block을 display: inline flow-root로 변경해도 똑같은 결과를 얻을 수 있습니다.
💡 강사의 기술 면접 꿀팁!
면접관이 "inline-block과 block 요소의 차이점은 무엇인가요?"라고 묻는다면, 단순히 "가로 배치가 되고 안 되고의 차이입니다"라고 끝내지 마세요. "inline-block은 외부적으로는 인라인 요소처럼 텍스트 흐름을 타지만, 내부적으로는 스스로 새로운 BFC(Block Formatting Context, 즉 flow-root)를 생성하여 자식 요소들의 마진이나 플로트가 밖으로 빠져나가는 것을 막아주는 강력한 역할을 합니다. 다중 키워드 구문으로는inline flow-root로 표현될 수 있습니다."라고 답변한다면 완벽합니다!
단일 값(single values)으로 구성된 display는 명세서에서 레거시(legacy, 구식) 값으로 설명되어 있습니다. 현재로서는 앞서 보여드린 표처럼 각 다중 키워드 버전이 레거시 버전에 1:1로 직접 매핑되기 때문에 굳이 다중 키워드 버전을 사용한다고 해서 얻는 특별한 이점은 없습니다 (브라우저 호환성 측면에서는 오히려 기존 단일 값을 쓰는 것이 안전합니다).
단일 값을 다룰 때 명세서는 block이나 inline 같은 '외부 값(outer value)'만 사용된 경우 어떻게 처리해야 하는지 설명하고 있습니다:
"만약
<display-outside>값이 지정되었으나<display-inside>가 생략되었다면, 해당 요소의 내부 디스플레이 타입은 기본값인flow가 된다."
이것은 기존의 단일 값을 쓰던 시절과 정확히 똑같은 동작입니다. 만약 여러분이 display: block이나 display: inline을 지정한다면, 그것은 박스의 '외부' 디스플레이 값을 변경하지만 모든 자식 요소들은 여전히 정상 흐름(normal flow)을 계속 따릅니다.
반대로 flex, grid, flow-root 같은 '내부 값(inner value)'만 지정된 경우, 명세서는 외부 값이 block으로 설정되어야 한다고 설명합니다:
"만약
<display-inside>값이 지정되었으나<display-outside>가 생략되었다면, 해당 요소의 외부 디스플레이 타입은 기본값인block이 된다—단,ruby의 경우는 예외로inline이 기본값이 된다."
마지막으로, 다음과 같이 사전에 조합된(pre-composed) 레거시 인라인 레벨 값들이 존재합니다:
inline-blockinline-tableinline-flexinline-grid만약 이 다중 키워드를 지원하는 브라우저가 위와 같은 단일 값들을 마주치면, 브라우저는 그것들을 다음과 같은 다중 키워드 버전과 동일하게 취급합니다:
inline flow-rootinline tableinline flexinline grid결론적으로, 현재 존재하는 모든 상황들이 아주 깔끔하게 커버되어 있습니다. 이는 명세서가 진화하고 발전하는 동안에도 기존의 단일 값을 사용하는 기존 사이트 및 새로운 사이트들과의 호환성을 완벽하게 유지한다는 것을 의미합니다.
이 내용과 관련하여 더 깊이 알고 싶다면 아래 링크들을 참고해 보세요 (MDN 원문 참고).
지금까지 display 속성의 다중 키워드 구문에 대해 알아보았습니다. 프론트엔드 개발 시 레이아웃을 잡다 보면 "이게 왜 안 먹히지?" 하는 순간들이 오는데, 오늘 배운 내부(자식 제어)와 외부(자신의 배치)의 개념을 명확히 분리해서 생각하시면 문제 해결의 실마리를 훨씬 쉽게 찾으실 수 있을 거예요! 더 궁금한 점이 있으시다면 편하게 질문해 주세요.