몇 년 전, 저는 대시보드를 만들기 위해 자리에 앉았습니다. 거창한 건 아니었습니다. 고정된 사이드바, 메인 콘텐츠 영역, 그리고 푸터가 전부였죠. Flexbox가 당연한 선택처럼 느껴졌고, 처음에는 잘 작동했습니다. 그러다 메인 섹션에 카드 한 줄을 넣었을 때, 전체 레이아웃이 무너져 버렸습니다. 그 후 세 시간 동안 flex-grow 비율 및 중첩된 퍼센트 너비와 씨름하며, 패딩을 아무리 줄여도 컨테이너들이 정렬되지 않는 모습을 지켜봐야 했습니다. 페이지는 마치 곧 무너질 것 같은 탑처럼 느껴졌습니다.

그 쓰라린 경험을 통해 저는 현대 CSS 레이아웃에 관한 가장 중요한 사실 하나를 배웠습니다. Flexbox와 Grid는 경쟁 관계가 아니라는 점입니다. 이들은 서로 다른 작업을 위한 도구이며, 대부분의 레이아웃 실패는 잘못된 도구를 선택한 데서 비롯됩니다.

차이는 차원(Dimension)에 있습니다

Flexbox를 복도라고 생각해보세요. 아이템들은 행(row) 또는 열(column) 중 한 방향으로 흐릅니다. 이는 1차원적 배분을 위해 만들어졌습니다. 브라우저는 콘텐츠를 확인한 다음, 해당 단일 축을 따라 어떻게 늘리거나 줄일지, 혹은 정렬할지를 결정합니다. 덕분에 Flexbox는 콘텐츠가 레이아웃을 결정하는 상황에 완벽합니다. 내비게이션 바, 폼 필드 스택, 또는 동일한 간격이 필요한 태그 세트 등은 모두 하나의 논리적 라인을 공유하므로 flex 컨테이너 안에 자연스럽게 배치됩니다.

반면, Grid는 평면도입니다. 행과 열을 동시에 다루는 엄격한 2차원 방식입니다. 먼저 구조를 정의한 다음, 그 안에 아이템을 배치합니다. 이 덕분에 Grid는 전체 페이지 아키텍처, 즉 수직 및 수평 위치를 함께 제어해야 하는 모든 곳에 이상적입니다. 복잡한 2차원 디자인을 중첩된 Flexbox 컨테이너로 강제하려 하면, 마크업이 깊어지고, 래퍼(wrapper)가 취약해지며, 요소 하나만 추가해도 무너지는 깨지기 쉬운 계산식에 직면하게 됩니다.

Flexbox도 여전히 제 역할을 합니다

모든 flex 컨테이너를 Grid로 바꾸려고 서두르지 마세요. 개별 컴포넌트 내부에서는 Flexbox가 더 깔끔하고 직관적인 경우가 많습니다. 두 개의 버튼이 있는 카드 푸터에서 하나는 왼쪽에, 다른 하나는 오른쪽에 정렬하고 싶다면, justify-content: space-between이 의도한 대로 정확히 작동합니다. 높이를 알 수 없는 버튼 내부의 아이콘을 수직 중앙 정렬해야 한다면, align-items: center가 가장 간단한 방법입니다. 이름 길이가 제각각인 사용자 배지 행처럼 가변적인 콘텐츠는 콘텐츠 우선(content-first) 방식인 Flexbox 안에 편안하게 들어맞습니다. 컨테이너가 담고 있는 내용에 맞춰 적응하기 때문입니다.

Grid는 컨테이너 우선(container-first) 방식입니다. 브라우저에 트랙(track)이 몇 개인지, 크기는 얼마여야 하는지 알려준 다음, 해당 셀에 아이템을 떨어뜨립니다. 만약 레이아웃이 단순히 공간을 공유하는 형제 요소들의 단일 행이라면, Grid는 불필요하게 복잡할 뿐입니다.

페이지 레이아웃은 Grid의 영역입니다

여기서부터 Grid가 주도권을 잡습니다. 제 대시보드 악몽을 기억하시나요? 처음부터 구조가 이렇게 되어 있었어야 했습니다:

.page {
  display: grid;
  min-height: 100vh;
  grid-template-rows: auto 1fr auto;
  grid-template-columns: 200px 1fr;
}

.header { grid-row: 1; grid-column: 1 / -1; }
.sidebar { grid-row: 2; grid-column: 1; }
.main { grid-row: 2; grid-column: 2; }
.footer { grid-row: 3; grid-column: 1 / -1; }

어떻게 작동하는지 살펴보겠습니다. .page 컨테이너는 3개의 행과 2개의 열을 설정합니다. 첫 번째 행은 auto 덕분에 헤더의 내용에 맞춰 크기가 결정됩니다. 중간 행은 1fr을 사용하여 남은 모든 공간을 차지합니다. 푸터는 마지막 행에 배치되며, 자신의 콘텐츠 크기에 맞춰 조절됩니다. 한편, 사이드바는 너비가 200픽셀로 고정되고, 메인 영역은 나머지를 채웁니다.

배치 로직이 정말 편리합니다. 헤더는 열 라인 1부터 -1까지 확장되는데, 이는 나중에 열을 몇 개를 추가하더라도 두 열 모두에 걸쳐 늘어난다는 것을 의미합니다. 사이드바와 메인 콘텐츠는 2행을 공유하지만 별도의 열을 차지합니다. 푸터는 3행에서 다시 전체 너비로 돌아옵니다. 자식 요소에 퍼센트 계산을 할 필요가 없습니다. 높이를 두고 싸우는 중첩된 래퍼도 필요 없습니다. 한 곳에서 사이드바 너비를 변경하면 메인 영역이 자동으로 조정됩니다. 전체 레이아웃이 하나의 시스템으로서 유기적으로 움직입니다.

grid-template-areas를 통한 가독성 향상

더 명확하게 만들고 싶다면 섹션에 이름을 붙일 수 있습니다. 라인 번호를 관리하는 대신 시각적인 지도를 그리는 것입니다:

.page {
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
}

.header { grid-area: header; }
.main { grid-area: main; }

이제 스타일시트가 와이어프레임처럼 읽힙니다. 만약 다른 브레이크포인트에서 사이드바를 오른쪽에 나타나게 하고 싶다면, 템플릿 문자열에서 단어 두 개만 바꾸면 됩니다. 중첩된 flex 컨테이너의 탑 안에서는 결코 달성할 수 없는 구조적 가독성입니다.

두 가지 뼈아픈 교훈

Grid는 마법이 아닙니다. 레이아웃을 순식간에 망가뜨릴 수 있는 두 가지 실수가 있습니다.

첫 번째는 부주의한 단위 사용입니다. 만약 grid-template-columns: 1fr 1fr 1fr라고 정의했지만 디자인에 콘텐츠 열이 두 개만 필요하다면, 세 번째 트랙은 여전히 존재하게 됩니다. 이 트랙은 비어 있는 상태로 남아 예상치 못한 간격을 만들거나, 실제 콘텐츠를 디자인을 해칠 정도로 좁게 압축하게 만듭니다. 의도를 명확히 하세요. 두 개의 열을 의도했다면 repeat(2, 1fr)라고 작성하세요.