Flexible box layout/Wrapping flex items

김동현·2026년 3월 21일

mdn 학습 번역 - CSS

목록 보기
96/190

안녕하세요! 프론트엔드 개발 강사입니다. 이번에 가져오신 내용은 CSS Flexbox(플렉스박스)를 다룰 때 필수적으로 알아야 하는 플렉스 아이템 줄바꿈(Wrapping)에 대한 문서군요!

Flexbox는 기본적으로 한 줄로만 쭉 나열하려는 성질이 있어서, 자식 요소들이 넘칠 때 어떻게 줄바꿈을 처리할지가 레이아웃의 관건이 됩니다. 여기서 flex-wrap 속성과 Grid 레이아웃과의 차이점을 명확히 아는 것이 중요해요. 영문 문서라 이해하기 조금 까다로우셨을 텐데, 실무에서 제가 직접 쓰는 꿀팁과 함께 알기 쉽게 구어체로 싹 번역해 드릴게요! 자, 시작해 볼까요? 😊


플렉스 아이템 줄바꿈 마스터하기 (Mastering wrapping of flex items)

Flexbox는 기본적으로 '1차원(single-dimensional)' 레이아웃 도구로 설계되었습니다. 즉, 아이템들을 가로(행, row)로 배치할지 세로(열, column)로 배치할지 단 하나의 방향만을 다루죠. 두 방향을 동시에 통제하지는 않습니다.

하지만, 공간이 부족할 때 플렉스 아이템들을 새로운 줄로 넘겨버리는(wrap) 것은 가능합니다. flex-directionrow일 때는 새로운 행(줄)을 만들고, column일 때는 새로운 열(칸)을 만들어 냅니다. 이 가이드에서는 Flexbox의 줄바꿈(wrapping)이 어떻게 작동하는지, 어떤 용도로 설계되었는지, 그리고 어떤 상황에서 Flexbox 대신 CSS 그리드 레이아웃(CSS grid layout)을 써야 하는지 설명해 드릴게요.


이 문서의 내용


항목들을 줄바꿈하게 만들기 (Making things wrap)

flex-wrap 속성의 초기 기본값은 nowrap입니다. 즉, 기본적으로 플렉스 아이템들은 공간이 부족해져도 절대 줄바꿈을 하지 않습니다. 아이템들이 컨테이너보다 넓어지면, 아이템 크기가 강제로 줄어들거나 밖으로 삐져나가게(overflow) 되죠.

공간이 좁을 때 아이템들을 다음 줄로 넘어가게 만들려면, flex-wrap: wrap을 추가하거나, 단축 속성인 flex-flowrow wrap 또는 column wrap이라고 적어주면 됩니다. 이렇게 하면 아이템들이 컨테이너를 넘어갈 때 자연스럽게 새로운 줄을 만들며 배치됩니다.

아래 예제에는 flex-basis160px로 설정된 10개의 플렉스 아이템이 있습니다. (이 아이템들은 늘어나거나 줄어들 수 있습니다.) 첫 번째 줄에 160px짜리 아이템을 하나 더 넣을 공간이 부족해지는 순간, 브라우저는 즉시 새로운 플렉스 라인(줄)을 만들어 냅니다. 이렇게 모든 아이템이 배치될 때까지 필요한 만큼 계속 새로운 줄이 생성되죠.
여기서 아이템들은 늘어날 수 있는(flex-grow) 속성을 가지고 있기 때문에, 남는 공간을 채우기 위해 쫙쫙 늘어납니다. 만약 마지막 줄에 아이템이 딱 하나만 남았다면? 그 아이템 혼자서 마지막 줄의 남은 공간을 전부 다 차지하게 됩니다!

💡 강사의 실무 팁! "마지막 줄 아이템이 너무 커져요!"
실무에서 쇼핑몰 상품 리스트를 flex-wrap: wrap으로 만들다 보면 윗줄은 상품 3개가 예쁘게 나오는데, 마지막 줄에 상품이 1개만 남으면 그 상품 혼자 화면 전체 너비로 쫙 늘어나버리는 흉측한(?) 현상을 자주 겪게 됩니다. 이럴 땐 Flexbox 대신 display: grid를 쓰는 게 훨씬 정신건강에 좋답니다! (자세한 내용은 아래에서 다시 다룰게요.)

(MDN Playground에서 실행해보기 (Play))

<div class="box">
  <div>One</div>
  <div>Two</div>
  <div>Three</div>
  <div>Four</div>
  <div>Five</div>
  <div>Six</div>
  <div>Seven</div>
  <div>Eight</div>
  <div>Nine</div>
  <div>Ten</div>
</div>
.box {
  width: 500px;
  border: 2px dotted rgb(96 139 168);
  display: flex;
  flex-wrap: wrap; /* 자리가 모자라면 밑으로 떨어뜨리세요! */
}

.box > * {
  border: 2px solid rgb(96 139 168);
  border-radius: 5px;
  background-color: rgb(96 139 168 / 0.2);
  flex: 1 1 160px; /* 늘어나고(1), 줄어들며(1), 기본 너비는 160px입니다. */
}

이 현상은 열(column) 방향의 Flexbox에서도 똑같이 일어납니다. 세로 방향으로 아이템들이 배치되다가 공간이 없어서 줄바꿈(새로운 열 생성)을 하려면, 당연히 컨테이너에 고정된 높이(height)가 있어야 합니다. 열(column) 모드일 때, 아이템들은 세로로 늘어나서(stretch) 각 열의 남는 높이를 가득 채우게 됩니다.

(MDN Playground에서 실행해보기 (Play))

.box {
  border: 2px dotted rgb(96 139 168);
  height: 300px; /* 세로로 줄바꿈을 하려면 높이가 필요합니다 */
  display: flex;
  flex-direction: column; /* 방향을 세로로 바꿉니다 */
  flex-wrap: wrap;
}
.box > * {
  border: 2px solid rgb(96 139 168);
  border-radius: 5px;
  background-color: rgb(96 139 168 / 0.2);
  flex: 1 1 80px;
}

줄바꿈과 flex-direction (Wrapping and flex-direction)

줄바꿈(flex-wrap) 속성은 flex-direction 속성과 짝을 이루어 여러분이 예상하는 대로 찰떡같이 작동합니다. 만약 flex-directionrow-reverse로 설정한다면? 아이템들은 컨테이너의 끝(오른쪽)에서부터 시작해서 채워지고, 줄바꿈이 일어날 때마다 역방향으로 줄이 생성됩니다.

(MDN Playground에서 실행해보기 (Play))

<div class="box">
  <div>One</div>
  <div>Two</div>
  <div>Three</div>
  <div>Four</div>
  <div>Five</div>
  <div>Six</div>
  <div>Seven</div>
  <div>Eight</div>
  <div>Nine</div>
  <div>Ten</div>
</div>
.box {
  border: 2px dotted rgb(96 139 168);
  display: flex;
  flex-wrap: wrap;
  flex-direction: row-reverse; /* 오른쪽에서 왼쪽으로 채워나갑니다 */
  width: 500px;
}
.box > * {
  border: 2px solid rgb(96 139 168);
  border-radius: 5px;
  background-color: rgb(96 139 168 / 0.2);
  flex: 1 1 160px;
}

여기서 주의할 점은, 뒤집기(reversing)는 오직 인라인 축, 즉 가로 방향(row direction)으로만 일어난다는 것입니다. 오른쪽에서 시작해서 왼쪽으로 오다가 줄이 다 차면, 밑에 있는 두 번째 줄로 내려와서 다시 오른쪽에서 왼쪽으로 시작합니다. 아래쪽에서 시작해서 위쪽으로 올라가는 등 양방향이 모두 뒤집히는 것은 아닙니다!


1차원 레이아웃에 대한 설명 (Single-dimensional layout explained)

위의 예제들에서 보셨다시피, 아이템들이 마음껏 늘어나고 줄어들 수 있도록 허용(flex-grow, flex-shrink)해 둔 상태에서 마지막 줄이나 마지막 열에 아이템 개수가 모자라게 남는다면, 그 남은 아이템들은 빈 공간을 꽉 채우기 위해 쫙쫙 늘어납니다.

Flexbox에는 "아랫줄에 있는 아이템들을 윗줄에 있는 아이템들과 수직 선을 딱딱 맞춰라"라고 지시할 수 있는 기능이 없습니다.
왜냐하면 Flexbox에서 각 줄(line)은 완전히 새롭고 독립적인 플렉스 컨테이너인 것처럼 행동하기 때문입니다. Flexbox는 오직 자기가 속한 그 한 줄(main axis) 안에서만 공간을 어떻게 분배할지 고민합니다. 만약 마지막 줄에 아이템이 딱 하나 남았고 그 녀석이 늘어날 수 있는 권한(flex-grow)이 있다면, 그 줄을 자기 혼자 독차지하며 쫙 늘어나버리는 거죠.
만약 여러분이 아이템들을 가로와 세로, 2차원으로 반듯하게 정렬하고 싶다면 십중팔구 Grid(그리드) 레이아웃이 여러분이 찾는 정답입니다.

아래 예제에서 CSS Grid 레이아웃을 사용해 만든 동일한 레이아웃을 보며 두 방식의 차이점을 비교해 보세요. 이 Grid는 최소 160px 너비의 칸(column)을 화면에 들어갈 수 있는 한 최대한 많이 만들고(auto-fill), 남는 공간을 모든 칸이 공평하게 나눠 가지도록(1fr) 설정했습니다.
마크업(HTML)은 앞서 보았던 Flexbox 줄바꿈 예제와 완벽하게 똑같지만, 부모 요소에 display: flex 대신 display: grid를 주었습니다. Flexbox에서만 작동하는 flex 속성을 각각의 아이템에 직접 주는 대신, 부모 컨테이너 자체에 grid-template-columns 속성을 주어 뼈대(그리드 트랙)를 설정했죠.

Grid를 사용하면 마지막 남은 아이템은 윗줄의 칸 크기와 똑같이 맞춰진 자기 자리(grid cell)에 얌전히 머뭅니다. 마지막 줄에 아이템 개수가 적다고 해서 공간을 독차지하려고 마구잡이로 늘어나지 않아요!

(MDN Playground에서 실행해보기 (Play))

<div class="box">
  <div>One</div>
  <div>Two</div>
  <div>Three</div>
  <div>Four</div>
  <div>Five</div>
  <div>Six</div>
  <div>Seven</div>
  <div>Eight</div>
  <div>Nine</div>
  <div>Ten</div>
</div>
.box {
  border: 2px dotted rgb(96 139 168);
  display: grid;
  /* 핵심! 160px 이상으로 채울 수 있는 만큼 열을 만듭니다 */
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  width: 500px;
}

.box > * {
  border: 2px solid rgb(96 139 168);
  border-radius: 5px;
  background-color: rgb(96 139 168 / 0.2);
}

이것이 바로 1차원 레이아웃(Flexbox)과 2차원 레이아웃(Grid)의 결정적인 차이점입니다. Flexbox 같은 1차원 레이아웃 방식에서는 우리는 가로면 가로, 세로면 세로 딱 한 방향의 흐름만 통제합니다. 반면 2차원인 Grid 레이아웃에서는 가로와 세로를 동시에 완벽하게 통제하죠.
따라서 공간 분배를 각 줄(row)마다 개별적으로 유동적이게 하고 싶다면 Flexbox를 쓰시고, 전체 아이템들이 위아래 줄 맞춰 십자열(+)로 딱딱 맞아떨어지길 원한다면 CSS Grid를 쓰시면 됩니다!


Flexbox 기반의 그리드 시스템은 어떻게 작동하나요? (How do flexbox-based grid systems work?)

물론 억지로 쥐어짜 내면 Flexbox를 가지고도 Grid 시스템처럼 줄을 딱딱 맞춰서 배열할 수는 있습니다. 하지만 그건 Flexbox가 본래 의도한 목적은 아니죠.
만약 플렉스 아이템들에 flex-basis를 퍼센트(%)로 지정하거나, 아이템에 직접 퍼센트 width를 주고 flex-basis: auto로 둔다면, 억지로 2차원 Grid 레이아웃과 비슷한 느낌을 만들어 낼 수는 있습니다.

다음 예제에서는, 아이템들이 자기 맘대로 크기를 바꾸지 못하도록 flex-growflex-shrink0으로 묶어버렸습니다. 그리고 오직 퍼센트(%)를 통해서만 유연성(flexibility)을 통제하고 있죠.

(MDN Playground에서 실행해보기 (Play))

<div class="box">
  <div>One</div>
  <div>Two</div>
  <div>Three</div>
  <div>Four</div>
  <div>Five</div>
  <div>Six</div>
  <div>Seven</div>
  <div>Eight</div>
  <div>Nine</div>
  <div>Ten</div>
</div>
* {
  box-sizing: border-box;
}

.box {
  width: 500px;
  border: 2px dotted rgb(96 139 168);
  display: flex;
  flex-wrap: wrap;
}

.box > * {
  border: 2px solid rgb(96 139 168);
  border-radius: 5px;
  background-color: rgb(96 139 168 / 0.2);
  flex: 0 0 33.3333%; /* 늘어나지도(0) 줄어들지도(0) 않게 묶고, 무조건 너비의 1/3을 차지하게 만듭니다. */
}

이 꼼수를 쓰면 교차 축(cross-axis, 윗줄과 아랫줄)에서도 플렉스 아이템들의 줄을 맞출 수는 있습니다. 하지만, 이렇게 억지로 플렉스 아이템들에 너비(width)를 계산해서 집어넣고 있거나, 줄을 맞추기 위해 투명한 빈 플렉스 아이템(empty flex items)을 빈자리에 욱여넣고 있는 자신을 발견했다면? 그건 바로 해당 컴포넌트를 CSS Grid 레이아웃으로 당장 바꿔야 한다는 아주 강력한 신호입니다!


아이템 사이에 간격(여백) 만들기 (Creating gutters between items)

플렉스 아이템들 사이에 간격(gap)이나 여백(gutter)을 띄우려면, 플렉스 컨테이너에 직접 gap 속성을 사용해서 인접한 아이템들 사이에 고정된 빈 공간을 만들어 줄 수 있습니다. gap 속성은 사실 row-gapcolumn-gap을 한 번에 써주는 단축어(shorthand)예요. 이 속성들은 Grid, Flexbox, Multi-column 레이아웃 환경에서 요소들의 가로/세로 간격의 크기를 아주 손쉽게 지정해 줍니다.

물론 gap 속성만이 아이템 사이에 공간을 추가하는 유일한 방법은 아닙니다. 마진(margins), 패딩(padding), 그리고 justify-contentalign-content 같은 정렬 속성들 또한 실제 아이템 사이의 거리에 영향을 미쳐 공간을 띄워줄 수 있습니다.

gap 속성과 요소의 margin 속성이 가로/세로 축에서 어떻게 다르게 작동하는지 확인해 보려면, 아래 라이브 예제의 CSS 코드에서 .box 컨테이너의 gap 값을 변경해 보거나, .box > *margin 값을 직접 추가해 보세요.

👨‍🏫 강사의 실무 팁! "gap의 위대함"
예전에는 아이템 사이를 띄울 때 margin을 많이 썼습니다. 하지만 margin을 쓰면 리스트의 맨 첫 번째나 맨 마지막 요소의 바깥쪽까지 마진이 튀어나가서 정렬이 틀어지는 아주 골치 아픈 문제가 있었죠(그래서 last-child { margin-bottom: 0 } 같은 꼼수를 매번 썼어야 했어요). 하지만 gap은 오직 요소와 요소 '사이'에만 공간을 만들어 줍니다! Flexbox를 쓸 때 요소 간격 띄우기는 무조건 gap을 쓰시는 걸 강력히 추천합니다.

(MDN Playground에서 실행해보기 (Play))

<div class="wrapper">
  <div class="box">
    <div>One</div>
    <div>Two</div>
    <div>Three</div>
    <div>Four</div>
    <div>Five</div>
    <div>Six</div>
    <div>Seven</div>
    <div>Eight</div>
    <div>Nine</div>
    <div>Ten</div>
  </div>
</div>
.wrapper {
  border: 2px dotted rgb(96 139 168);
  width: 500px;
}
.box {
  display: flex;
  flex-wrap: wrap;
  gap: 10px; /* 아이템 '사이'에만 10px의 간격을 띄웁니다! */
}
.box > * {
  flex: 1 1 160px;
  border: 2px solid rgb(96 139 168);
  border-radius: 5px;
  background-color: rgb(96 139 168 / 0.2);
}

접힌(Collapsed) 아이템 (Collapsed items)

Flexbox 사양서에는 플렉스 아이템에 visibility: collapse 속성을 부여해서 요소를 '접었을(collapsed)' 때 어떤 일이 일어나야 하는지가 아주 구체적으로 적혀 있습니다. visibility 속성에 대한 MDN 문서도 참고해 보세요. 사양서에서는 이 동작을 다음과 같이 설명합니다:

"플렉스 아이템에 visibility: collapse를 지정하면 해당 요소는 접힌 플렉스 아이템(collapsed flex item)이 됩니다. 이는 테이블 행(table-row)이나 테이블 열(table-column)에 visibility: collapse를 주었을 때와 비슷한 효과를 냅니다: 접힌 플렉스 아이템은 렌더링 화면에서 완전히 사라지지만, 해당 아이템이 속했던 플렉스 줄(flex line)의 교차 축(cross-size) 크기를 유지시켜 주는 보이지 않는 "버팀목(strut)"을 남겨둡니다. 따라서 플렉스 컨테이너가 단 한 줄뿐이라면, 자바스크립트로 아이템을 동적으로 접었다 폈다 할 때 가로(main size) 너비는 계속 변하겠지만, 세로(cross size) 높이는 전혀 영향을 받지 않도록 보장되어 페이지 나머지 레이아웃이 덜컥거리며 "흔들리는(wobble)" 현상을 막아줍니다. 하지만 아이템이 접힌 후에는 다시 줄바꿈(wrapping) 계산이 새로 진행되기 때문에, 여러 줄(multiple lines)을 가진 플렉스 컨테이너의 경우 높이가 변할 수도 있고 안 변할 수도 있습니다." - Collapsed items

이 기능은 자바스크립트를 사용해서 동적으로 플렉스 아이템을 보여줬다 숨겼다(토글) 할 때 아주 유용합니다. 사양서의 예제에서도 이런 패턴을 보여주고 있습니다.

다음 라이브 예제는 줄바꿈을 허용하는(wrap) 다중 줄 플렉스 컨테이너입니다. 세 번째 아이템에는 내용이 많아서 여러 줄의 텍스트가 들어가 있고, 그 때문에 이 아이템이 있는 첫 번째 줄의 높이가 가장 높게 잡혀 있습니다. align-items의 기본값은 normal이며, 플렉스 아이템에서 normalstretch(늘리기)로 작동하기 때문에, 같은 줄에 있는 모든 아이템이 가장 키가 큰 세 번째 아이템의 높이에 맞춰 자동으로 쫙쫙 늘어난 상태입니다.

이제 체크박스를 눌러 첫 번째 줄의 키를 결정하던 세 번째 아이템에 visibility: collapse를 줘서 숨겨보세요. 화면에서 아이템은 뿅 하고 사라지지만(브라우저에 따라 숨김 처리됨), 플렉스 컨테이너는 눈에 보이지 않는 버팀목(strut)을 세워두기 때문에, 그 줄(첫 번째 줄)의 원래 세로 높이는 쪼그라들지 않고 그대로 텅 빈 채 유지됩니다.

하지만! 다중 줄(multi-line) 컨테이너에서는 아이템이 숨겨진 직후에 빈 공간을 채우기 위해 텍스트(아이템)들을 다시 끌어올려서 줄바꿈 계산(rewrap)을 새로 해버립니다. 원래는 두 번째 줄에 있던 'Four'나 'Five' 아이템이 텅 빈 첫 번째 줄의 공간을 메꾸기 위해 윗줄로 슉 이동하게 되죠. 자리가 바뀌면서 새로운 줄의 구성원들이 달라지니, 자연스럽게 각 줄의 높이(교차 축 크기)도 예기치 않게 변동될 수 있습니다.

📝 참고:
다른 주요 브라우저들은 collapse를 일반적인 hidden처럼 동일하게 취급하는 버그가 있기 때문에, 아래 예제의 정확한 의도를 눈으로 확인하시려면 Firefox(파이어폭스) 브라우저를 사용해 주시기 바랍니다.

(MDN Playground에서 실행해보기 (Play))

<p>
  <label><input type="checkbox" /> Toggle <code>visibility</code> value</label>
</p>
<div class="box">
  <div>One</div>
  <div>Two is the width of this sentence.</div>
  <div class="collapse">Three <br />is <br />five <br />lines <br />tall.</div>
  <div>Four</div>
  <div>Five<br />Five</div>
  <div>Six</div>
  <div>Seven</div>
  <div>Eight</div>
  <div>Nine</div>
  <div>Ten</div>
  <div>Eleven is longer</div>
</div>
.box {
  border: 2px dotted rgb(96 139 168);
  width: 500px;
  display: flex;
  flex-wrap: wrap;
}
.box > * {
  padding: 10px;
  border: 2px solid rgb(96 139 168);
  border-radius: 5px;
  background-color: rgb(96 139 168 / 0.2);
  flex: 1 1 auto;
  min-width: 50px;
}
.collapse {
  visibility: collapse; /* 이 요소가 사라져도 윗줄의 빈 높이는 그대로 유지됩니다 (Firefox 기준) */
}
p:has(:checked) + div .collapse {
  visibility: visible;
}

만약 자바스크립트로 요소를 숨겼을 때 이런 줄바꿈 변동이나 높이 흔들림 현상이 레이아웃에 골칫거리가 된다면, HTML 구조 자체를 다시 생각해 보아야 합니다. 예를 들어, 윗줄과 아랫줄 아이템이 서로 뒤섞이지 않도록 각 줄을 별도의 플렉스 컨테이너(div)로 따로따로 감싸서 완벽하게 분리해 버리는 방법이 있습니다.

visibility: hiddendisplay: none 사용하기

위의 라이브 예제에서 visibility: collapse 대신에 visibility: hidden 이나 display: none을 넣고 체크박스를 눌러보세요.

  • visibility: hidden을 사용하면, 아이템의 알맹이는 투명인간처럼 안 보이게 되지만, 그 박스 자체가 차지하던 물리적인 자리(공간)는 그대로 텅 빈 채 남아있습니다. 따라서 레이아웃의 구조나 줄바꿈은 전혀 변하지 않죠.
  • 반면 display: none을 사용하면, 해당 아이템은 화면뿐만 아니라 브라우저의 레이아웃 계산 구조에서조차 흔적도 없이 완벽하게 삭제되어 버립니다. 아예 존재하지 않았던 것처럼 뒤에 있는 아이템들이 그 빈자리를 휙 채워버리죠. 구조가 완전히 날아갔기 때문에, 요소의 순번을 세는 카운터(counters)도 이 녀석을 건너뛰고, CSS 애니메이션(transitions) 같은 것도 당연히 작동하지 않게 됩니다.
profile
프론트에_가까운_풀스택_개발자

0개의 댓글