CSS Grid 레이아웃: grid-template-areas, fr·minmax·auto-fit, Flexbox와 함께 쓰기

이 글의 핵심

CSS Grid로 2차원 레이아웃을 잡는 방법을 컨테이너 설정, 영역 이름 붙이기, fr과 minmax를 이용한 반응형 열, 아이템 배치 순으로 정리하고 Flexbox와 나눠 쓰는 기준을 다룹니다.

HTML/CSS 시리즈 #06 · CSS Grid

실제로는, 페이지 뼈대·대시보드·갤러리는 Grid가 답이야. Flexbox? 한 줄(가로 또는 세로) 흐름엔 잘 맞는데, 1차원이라 “몇 열·몇 행”을 동시에 잡는 건 늘 애매해집니다. Grid는 2차원—행(row) + 열(column)을 한 번에 설계하는 도구다. (너희 팀이 “플렉스로만” 하다 퍼센트·마이너스 마진 지옥 본 경험 있지?)


막힐 때 먼저 볼 것: DevTools Grid 오버레이

Grid가 예상과 다르게 배치될 때 코드를 다시 읽기보다 브라우저 개발자 도구의 Layout → Grid 오버레이를 켜는 것이 가장 빠릅니다. 라인 번호와 트랙 크기가 화면 위에 그려지므로, grid-column: 1 / -1이 실제로 어느 라인을 가리키는지, span한 아이템이 자동 배치와 부딪혀 다음 행으로 밀렸는지, 긴 내용을 가진 카드가 min-width: auto 때문에 fr 열을 밀어내고 있는지가 바로 보입니다. 특히 뷰포트를 줄였을 때 grid-template-areas로 배치한 영역이 엉뚱한 곳으로 가면, 미디어 쿼리 안의 areas 문자열이 바깥 선언과 칸 수가 맞지 않거나 한 이름이 직사각형이 아닌 경우가 대부분입니다.


구식 float + clearfix 대신, 의도는 이렇게 쓰는 쪽이 낫습니다.

/* 레거시 */
.col-4 { float: left; width: 33.33%; }
/* 의도가 바로 읽힘 */
.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

2차원 잡는다 = 명시적 트랙·gap·(필요하면) areas. Flex로만 역할 바꿔 쓰다 보면 “한 줄” 사고에 갇힌다. Grid가 답, Flexbox는 1차원—이 조합(바깥 Grid, 안 Flex) 머리에 박아 두면 편합니다.


언제 뭘 쓸지 (표 말고 그림)

Flex에 잘 맞는 것:  [툴바 ············ 버튼들 →→→  ]
                    (한 축, 한 줄 흐름)

Grid에 잘 맞는 것:  ┌────┬────┬────┐
                    │ H  │ H  │ H  │  ← 행+열 동시
                    ├────┼────┼────┤
                    │ S  │ M  │ M  │
                    └────┴────┴────┘
  • 네비·툴바·버튼 한 줄 → Flex
  • 헤더/사이드/메인 뼈대, 대시보드 → Grid
  • 한 칸이 여러 열·행 먹어야 하면 → Grid (span, grid-area, 라인)

조합: 바깥은 Grid, 카드 안쪽 정렬은 Flex. 레이어 나누면 CSS 읽기 쉬워집니다.


컨테이너: display: grid, grid-template-*

display: grid면 직접 자식이 그리드 아이템. 용어는 ASCII로 보면 빠름.

      1     2     3     4   ← 열 라인
   1  ┌─────┬─────┬─────┐
      │  1  │  2  │  3  │
   2  ├─────┼─────┼─────┤
      │  4  │  5  │  6  │
   3  └─────┴─────┴─────┘
   ↑ 행 라인
  • 3열 = 열 라인 4개 — “전체 너비”는 grid-column: 1 / -1 같은 식
  • min-width: auto 때문에 안 줄어드는 자식이 있으면, 실무에선 min-width: 0 같이 같이 쓰는 경우 많다
.container {
  display: grid;
  grid-template-columns: 200px 200px 200px;
  grid-template-rows: 100px 100px;
  gap: 20px;
}

grid-template-areas — 말로는 “ASCII 덩어리”

.app {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;
  grid-template-rows: 80px 1fr 1fr 60px;
  grid-template-areas:
    "header header header"
    "sidebar main main"
    "sidebar main main"
    "footer footer footer";
  min-height: 100vh;
  gap: 10px;
}
.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

같은 이름 셀은 직사각형이어야 한다—꼬이면 areas가 통째로 invalid.


라인·fr·gap·repeat·minmax·auto-fit

라인 번호 grid-column: 1 / 4 = 1번~4번 열 라인 사이 = 3칸. -1은 마지막 라인.

gap은 트랙 사이만. fr은 고정+gap 뺀 남는 공간 비율—퍼센트+갭이랑 싸울 때 fr이 덜 꼬이는 경우가 많습니다.

.page {
  display: grid;
  grid-template-columns: 200px 1fr 2fr;
}

반응형 한 방은 흔히 이거:

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}

auto-fill은 빈 열 슬롯이 남을 수 있으며, auto-fit은 빈 열을 접어서 남는 칸이 늘어납니다. “카드 꽉 채우기”는 보통 auto-fit부터 써 본다.

auto-fill:  [1][2][3][ ][ ]   … 빈 트랙 남을 수 있음
auto-fit:   [  1  ][  2  ][  3  ]  … 접힘, 셀 확장

아이템: span, place-self, 정렬

.card-wide {
  grid-column: 1 / 3;
  grid-row: 1 / 2;
}
.feature {
  grid-column: span 2;
}
  • place-items: 셀 안 정렬(컨테이너)
  • place-content: 그리드 덩어리 전체가 작을 때 남는 여백(컨테이너에 높이/고정 트랙 있을 때)

grid-auto-flow: dense는 빈칸 메울 수 있지만 시각 순서 vs 포커스 순서 괴리 날 수 있어서, 폼·리스트는 신중히.


실전: 대시보드 뼈대만

<div class="dashboard">
  <header class="dashboard__header">헤더</header>
  <aside class="dashboard__side">사이드</aside>
  <div class="dashboard__stats">…KPI 3칸…</div>
  <section class="dashboard__chart">차트</section>
</div>
.dashboard {
  min-height: 100vh;
  display: grid;
  gap: 20px;
  padding: 20px;
  grid-template-columns: 240px 1fr 1fr;
  grid-template-rows: 72px 140px 1fr;
  grid-template-areas:
    "head head  head"
    "side stat  stat"
    "side chart chart";
}
.dashboard__header { grid-area: head; }
.dashboard__side   { grid-area: side; }
.dashboard__stats  { grid-area: stat; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.dashboard__chart  { grid-area: chart; }

좁아지면 @media에서 grid-template-areas만 세로로 갈아끼우면 됩니다. Holy Grail·갤러리 span 트릭·카드 auto-fit은 같은 원리라 여기서 굳이 길게 안 늘렸다. 더 필요하면 시리즈 다른 글·심화 글 봐.


Grid + Flex (이건 정말 자주 씀)

.page { display: grid; grid-template-columns: 220px 1fr; }
.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

뼈대 2D = Grid, 한 줄·한 축 = Flex — 고집할 주제가 아닙니다. 다만 “전체를 Flex만으로 2D 흉내” 내는 쪽이 더 빨리 지저분해지는 경향은 있습니다. Grid가 답인 자리는 Grid 쓰자.


브라우저·트러블 (한 덩어리로)

모던 브라우저는 Grid Level 1 잘 씁니다. IE11 레거시면 -ms-·폴리필 이야기가 따로—2026 신규면 보통 대상 밖. subgrid는 Can I use 보고.

자주 터지는 것들을 표 대신:

  • 넘침 → min-width: 0 / overflow / word-break
  • fr 이상 → 콘텐츠가 트랙 최소를 밀어 올림 — minmax(0, 1fr) 등 상황마다
  • areas 깨짐 → 같은 이름이 직사각형 아님 — ASCII 다시 그려
  • align-content 안 먹음 → 행 합이 이미 뷰포트 꽉 참 — min-height/height·행 정의 점검

DevTools → Layout → Grid 오버레이는 필수다. Grid 디버깅으로 하루 쓴 사람은 다 동의함.


display: grid 기본 3×2 느낌 예시

<div class="grid">
  <div class="item i1">1</div>
  <div class="item i2">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
</div>
.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(2, 100px);
  gap: 10px;
}
.i1 { grid-column: 1 / 3; }
.i2 { grid-column: 3 / 5; grid-row: 1 / 3; }

심화 1: 트랙 크기는 어떤 순서로 정해지나

CSS Grid Layout에서 열(column) 트랙과 행(row) 트랙은 각각 독립적인 1차원 문제로 풀린 뒤, 아이템이 두 축에 걸친 제약을 만족하도록 조정됩니다. 개발자가 체감하는 “이 열이 왜 이렇게 좁지?” 같은 현상은 대부분 트랙 크기 함수(track sizing function)와 그리드 아이템의 최소·최대 기여(min/max contribution)가 충돌할 때 발생합니다.

트랙에 쓰이는 크기의 종류

트랙 한 칸은 대략 다음 범주로 이해할 수 있습니다.

  • 고정 길이: 200px, 10rem 등 — 컨테이너와 무관하게 먼저 자리를 차지합니다.
  • 내재(intrinsic) 키워드: min-content, max-content, auto 등 — 아이템 내용·최소 크기 규칙과 연동됩니다.
  • 가변 fr: 남은 공간을 비율로 나눕니다. gap과 고정 트랙을 제외한 free space가 0이면 fr 트랙도 0에 가까워질 수 있습니다.
  • minmax(최소, 최대): 먼저 최소 조건을 만족하도록 트랙 하한을 잡으며, 그다음 최대 쪽(1fr, auto 등)으로 늘어날 수 있는지 판단합니다.

알고리즘을 한 줄로 요약하면

스펙 관점에서 열(또는 행) 트랙 크기 결정은 단순화하면 다음 순서를 따릅니다.

  1. 고정·명시된 트랙과 gap으로 사용 가능한 공간의 윤곽을 잡습니다.
  2. min-content / max-content / auto 등과 연관된 아이템의 최소·최대 기여를 반영해 트랙의 최소·최대를 갱신합니다. 이때 그리드 아이템의 기본 min-width: auto(최소 크기가 내용보다 작아지지 않음)가 걸리면, 의도보다 트랙이 부풀어 오르는 경우가 많습니다. 실무에서는 종종 min-width: 0(또는 minmax(0, 1fr))으로 하한을 재정의합니다.
  3. fr 트랙은 “이 시점까지 확정된 최소 요구”를 만족한 뒤 남은 공간에 대해 가중치 합(1fr+2fr 등)으로 분배합니다.

즉 fr은 “전체 너비를 n등분”이 아니라 “남은 공간을 비율로 나눔”입니다. 사이드바가 200px이고 갭이 있다면, 1fr 1fr은 그 이후의 남은 폭을 반으로 나눕니다.

왜 minmax(200px, 1fr)이 카드 그리드에 강한가

repeat(auto-fit, minmax(240px, 1fr)) 같은 패턴에서 각 열의 최소는 240px입니다. 뷰포트가 줄면 들어갈 수 있는 열 개수가 줄고, 남은 공간은 1fr이 균등하게 흡수합니다. 여기서 1fr은 “남은 폭을 균등 분할”이지만, 그 전제는 min이 이미 만족되었다는 뜻입니다. 최소가 만족되지 않으면 브라우저는 줄바꿈(열 개수 감소)이나 가로 스크롤 같은 다른 전략을 취합니다(오버플로 설정·컨테이너 너비에 따라 다름).


심화 2: 자동 배치(auto-placement)

grid-column / grid-row / grid-area로 좌표가 고정되지 않은 아이템은 자동 배치 알고리즘에 따라 빈 셀에 채워집니다. 이 동작을 알아야 “왜 이 요소가 여기로 갔지?”를 디버깅할 수 있습니다.

흐름 방향: grid-auto-flow

  • row(기본): 행을 채우며 왼쪽→오른쪽, 행이 끝나면 다음 행.
  • column: 열을 채우며 위→아래, 열이 끝나면 다음 열.
  • dense: 앞선 배치로 생긴 구멍을, 뒤에 오는 작은 아이템이 메울 수 있으면 끌어올립니다. 레이아웃은 촘촘해지지만 시각적 순서가 DOM 순서와 달라질 수 있어, 키보드 포커스 순서·스크린 리더 사용자에게 혼란을 줄 수 있습니다.

알고리즘의 직관적 단계

완전한 스펙 단계를 모두 열거하기보다, 실무에서 필요한 직관은 다음과 같습니다.

  1. 명시적으로 라인을 지정한 아이템이 먼저 “자리를 선점”합니다. span이 큰 블록이 있으면 그 주변의 빈 칸 구조가 결정됩니다.
  2. 자동 배치 아이템은 grid-auto-flow 방향으로, 가능한 가장 앞선(일반적으로 가장 왼쪽 위) 빈 셀부터 배치됩니다.
  3. dense가 켜지면, 배치 중 남는 구멍을 뒤쪽 아이템이 채울 수 있는지 추가로 검사합니다.

order와의 관계

Flex와 마찬가지로 order는 페인팅·배치 순서에 영향을 줄 수 있습니다. 자동 배치는 정렬된 아이템 순서를 입력으로 사용하므로, order만 바꿔도 그리드 상 위치가 달라질 수 있습니다. 디자인을 order로 맞추는 것은 접근성 리스크가 있으므로, DOM 순서 자체를 의미 있게 유지하는 편이 안전합니다.


심화 3: minmax()와 fr의 실제 계산

fr이 나누는 것: free space

열 방향 예시로, 컨테이너 콘텐츠 박스 너비가 W이고, 열이 200px, 1fr, 1fr이며 column-gap이 g라고 합시다.

  1. 고정 트랙: 200px.
  2. 갭: 열이 3개이면 갭은 2개 → 2 * g.
  3. 남은 공간 F = W - 200px - 2g (음수면 오버플로·스크롤 등 다음 단계 규칙).
  4. 1fr 두 개가 남은 공간을 반반 → 각각 약 F / 2.

퍼센트(%)와 달리 fr은 갭을 “이미 뺀 뒤”의 공간을 나누는 쪽에 가깝게 동작해, 갭과 동시에 쓰기 쉽다는 말이 나옵니다.

minmax(최소, 1fr)에서 일어나는 일

  • 최소 쪽이 240px이면, 각 열은 그 너비 이하로 줄지 않으려 합니다.
  • 뷰가 넓어져 남는 폭이 생기면 1fr이 그 여유를 열마다 동일하게 흡수해 카드가 “늘어난다”는 느낌을 줍니다.
  • 최소가 auto인 경우, 아이템 내용·기본 최소 크기 때문에 열이 의도보다 커질 수 있습니다. 이때 minmax(0, 1fr) 또는 아이템에 min-width: 0을 주는 패턴이 반복됩니다.

fit-content(), min(), max()와의 조합

트랙 한 줄을 더 정교하게 제한하려면 minmax()의 인자에 min(), max(), clamp() 를 넣어 상한·하한을 함께 겁니다. 이때 주의할 점은 fr은 길이가 아니라서 min()·max()·calc() 안에 넣을 수 없다는 것입니다. minmax(200px, min(1fr, 400px))처럼 쓰면 선언 전체가 무효가 되어 브라우저가 조용히 무시하고, 격자가 아예 적용되지 않은 것처럼 보입니다. fr은 minmax()의 최대값 자리에 단독으로만 씁니다.

.grid {
  display: grid;
  /* 열은 200~400px 사이, 넓어져도 400px에서 멈춤 */
  grid-template-columns: repeat(3, minmax(200px, 400px));
  justify-content: center;   /* 남는 공간은 트랙 바깥에 */
}
.cards {
  display: grid;
  /* 좁은 화면에서 280px보다 좁아지면 100%로: min()은 길이끼리만 */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}

프로젝트 브라우저 지원 범위에 따라 min()/max() 사용 여부를 정하며, 필요하면 @supports로 폴백을 두는 것이 안전합니다.


실무에서 반복되는 Grid 패턴

앱 셸(App shell): 사이드바 + 스크롤 영역

.app {
  display: grid;
  min-height: 100dvh;
  grid-template-columns: 260px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    'header header'
    'sidebar main'
    'footer footer';
}
.app__header { grid-area: header; }
.app__sidebar { grid-area: sidebar; }
.app__main {
  grid-area: main;
  min-height: 0; /* 자식 스크롤 영역이 그리드 행을 밀어내는 것 방지 */
  overflow: auto;
}
.app__footer { grid-area: footer; }

main에 min-height: 0(또는 overflow와 함께)을 주는 이유는, 그리드 아이템의 기본 min-height: auto가 자식 콘텐츠 높이만큼 행을 늘리려 해서 뷰포트 스크롤이 이중으로 생기는 문제를 막기 위함입니다.

12컬럼 그리드 + 브레이크아웃(full-bleed)

콘텐츠는 12칸 중 8칸, 히어로만 가로 전체를 쓰는 식의 패턴입니다.

.page {
  display: grid;
  grid-template-columns: [full-start] 1fr [content-start] repeat(12, minmax(0, 1fr)) [content-end] 1fr [full-end];
  column-gap: 1.5rem;
}
.page__hero {
  grid-column: full;
}
.page__article {
  grid-column: content;
}

명명된 라인(full, content)은 디자인 시스템과 맞추기 좋습니다.

반응형 카드: auto-fit + minmax

.cards {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}

min(100%, 280px)로 좁은 뷰에서 한 열이 삐져나가는 현상을 줄입니다.

서브그리드(Subgrid)로 폼·카드 정렬

행 방향 서브그리드를 쓰면 카드끼리 제목 줄의 기준선을 맞추기 쉽습니다. 지원 브라우저 범위를 확인하고 @supports (grid-template-rows: subgrid)로 점진적 향상을 고려합니다.

@supports (grid-template-rows: subgrid) {
  .list {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  }
  .card {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
  }
}

grid-auto-rows와 암묵적 행

명시한 행보다 아이템이 많으면 암묵적 행이 생깁니다. 이때 grid-auto-rows: minmax(0, auto)처럼 기본값을 걸어두면 예상치 못한 행 높이 증가를 줄이는 데 도움이 됩니다. masonry 수준의 배치는 Grid만으로는 한계가 있어, 컬럼 수를 나눈 Flex/JS/전용 라이브러리를 검토하는 경우가 많습니다.


자주 묻는 질문 (보충)

Q. justify-items와 justify-content의 차이는?
A. …-items는 각 셀 안의 아이템, …-content는 트랙 전체가 작을 때 남는 여백.

Q. grid와 inline-grid는?
A. inline-grid는 인라인 흐름에 격자 끼워 넣을 때. 기본은 grid 쪽이 많습니다.

Q. Grid만으로 모든 걸?
A. 아닙니다. 1차원 정렬·툴바는 Flex가 단순한 경우 많습니다. 대신 “2D를 Flex로만” 억지로 만들지 말자.

같이 보면 좋은 글