Flexbox와 Grid로 반응형 레이아웃 설계하기: auto-fit, minmax, 그리고 미디어 쿼리 줄이기

Frontend

이 글은 React로 반응형 웹 구성하기 시리즈 3편입니다.

1편에서 카드 그리드를 만들 때 우리는 min-width: 768px, min-width: 1024px처럼 브레이크포인트마다 열 수를 직접 지정했습니다. 화면이 세 종류라면 괜찮지만, 카드 폭이 얼마 이하로 좁아지면 자동으로 줄을 바꾸는 식으로 만들고 싶을 때가 많습니다. 미디어 쿼리로는 "특정 폭에서 몇 열"만 지정할 수 있을 뿐, "카드가 200px보다 좁아지지 않는 선에서 알아서 채워라" 같은 규칙은 표현하기 어렵습니다.

이걸 가능하게 하는 게 Flexbox와 Grid입니다. 이 둘을 잘 쓰면 미디어 쿼리를 거의 쓰지 않고도 콘텐츠가 스스로 반응하는 레이아웃(intrinsic responsive)을 만들 수 있습니다.

한눈에 보면

  • Flexbox는 1차원(한 방향 흐름), Grid는 2차원(행과 열)을 다룹니다. 이 차이가 선택 기준입니다.
  • flex-wrap: wrap + flex-basis만으로도 카드가 폭에 따라 알아서 줄바꿈되는 반응형이 됩니다.
  • Grid의 repeat(auto-fit, minmax(200px, 1fr))은 "최소 200px, 남으면 늘려서, 들어가는 만큼 자동 배치"를 한 줄로 표현합니다. 반응형 그리드의 핵심 관용구입니다.
  • auto-fitauto-fill남는 공간을 어떻게 처리하느냐에서 갈립니다. 대부분 auto-fit이 직관적입니다.
  • 2편의 clamp()gapminmax에 섞으면 미디어 쿼리 없이도 여백까지 유동적으로 반응합니다.
  • 그래도 레이아웃 구조 자체를 바꿔야 할 때(사이드바를 위로 올린다든지)는 여전히 미디어 쿼리가 필요합니다.

Flexbox와 Grid, 언제 무엇을 쓸까

가장 먼저 잡아야 할 기준은 "몇 차원을 다루는가"입니다.

flowchart TD
    start["반응형 레이아웃이 필요하다"]
    start --> dim{"행과 열을<br/>동시에 정렬해야 하나?"}
 
    dim -->|"아니오 · 한 방향 흐름"| flex["Flexbox<br/>내비게이션, 툴바,<br/>줄바꿈되는 태그 목록"]
    dim -->|"예 · 격자 정렬"| grid["CSS Grid<br/>카드 그리드, 대시보드,<br/>페이지 전체 레이아웃"]
 
    style flex fill:#dbeafe,stroke:#3b82f6,color:#111
    style grid fill:#dcfce7,stroke:#22c55e,color:#111
  • Flexbox: 한 줄(또는 줄바꿈되는 흐름) 안에서 아이템을 배치할 때. 내비게이션 바, 버튼 그룹, 태그 목록처럼 "한 방향으로 흐르다 넘치면 다음 줄로" 같은 경우.
  • Grid: 행과 열을 함께 통제할 때. 카드 그리드, 대시보드, 페이지 전체 골격처럼 "칸을 짜 놓고 채우는" 경우.

실무에서는 페이지 골격은 Grid로, 그 안의 작은 컴포넌트는 Flexbox로 섞어 쓰는 경우가 많습니다.

Flexbox로 만드는 반응형: wrap과 basis

Flexbox의 반응형 핵심은 flex-wrap: wrapflex-basis입니다. 아이템에 "이상적인 기준 폭"을 주고, 공간이 부족하면 다음 줄로 넘기게 하는 방식입니다.

.list {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1rem, 3vw, 2rem); /* 2편의 유동 간격 */
}
 
.item {
  /* 최소 240px를 기준으로, 남는 공간은 나눠 갖고, 좁아지면 줄바꿈 */
  flex: 1 1 240px;
}

flex: 1 1 240pxflex-grow: 1, flex-shrink: 1, flex-basis: 240px의 축약입니다.

  • flex-basis: 240px: 아이템의 기준 폭
  • flex-grow: 1: 남는 공간이 있으면 함께 늘어남
  • flex-shrink: 1: 공간이 부족하면 줄어들다가, 한계를 넘으면 다음 줄로

이 한 줄이면 미디어 쿼리 없이 "화면이 넓으면 여러 개, 좁으면 한 줄에 하나"가 됩니다. 다만 Flexbox의 줄바꿈은 마지막 줄의 아이템 개수가 들쭉날쭉할 수 있습니다. 예를 들어 5개 아이템이 위 3개, 아래 2개로 나뉘면 아래 2개가 남은 폭을 나눠 가져 위보다 넓어집니다. "모든 칸의 폭이 정확히 같아야" 한다면 Grid가 낫습니다.

Grid로 만드는 반응형: auto-fit과 minmax

여기가 이 글의 핵심입니다. Grid의 repeat(auto-fit, minmax(...))은 반응형 그리드를 한 줄로 표현하는 강력한 관용구입니다.

.grid {
  display: grid;
  gap: clamp(1rem, 3vw, 2rem);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

이 한 줄을 풀어 읽으면 이렇습니다.

  • minmax(200px, 1fr): 각 열은 최소 200px, 남는 공간이 있으면 1fr로 늘어남.
  • repeat(auto-fit, ...): 200px짜리 열을 들어가는 만큼 자동으로 만들고, 남는 공간은 각 열을 늘려 채움.

결과적으로 화면 폭에 따라 열 수가 알아서 1 → 2 → 3 → 4로 바뀝니다. 미디어 쿼리가 한 줄도 없습니다.

● 넓은 화면 (약 900px) — 열이 4개 들어감
+-----+ +-----+ +-----+ +-----+
|  1  | |  2  | |  3  | |  4  |
+-----+ +-----+ +-----+ +-----+
 
● 중간 화면 (약 640px) — 열이 3개
+-----+ +-----+ +-----+
|  1  | |  2  | |  3  |
+-----+ +-----+ +-----+
 
● 좁은 화면 (약 420px) — 열이 2개
+-----+ +-----+
|  1  | |  2  |
+-----+ +-----+
 
  ↑ minmax(200px, 1fr): 200px 밑으로는 안 좁아지고,
    남는 폭은 1fr로 나눠 각 열이 함께 늘어남

auto-fit vs auto-fill: 남는 공간의 처리

repeat()의 첫 인자로 auto-fit 대신 auto-fill을 쓸 수도 있는데, 이 둘의 차이가 자주 헷갈립니다. 차이는 아이템이 열 개수보다 적어 빈 열이 생길 때 드러납니다.

  • auto-fit: 빈 열을 접어서(collapse) 없애고, 실제 아이템들이 남은 공간을 나눠 넓게 채웁니다.
  • auto-fill: 빈 열을 그대로 유지해서, 아이템은 원래 폭을 지키고 오른쪽에 빈 공간이 남습니다.
아이템 2개 · 넓은 화면 (열은 5개까지 들어갈 폭)
 
● auto-fit — 빈 열을 접고 2개가 폭을 나눠 가짐
+------------------+ +------------------+
|        1         | |        2         |
+------------------+ +------------------+
 
● auto-fill — 빈 열 3개를 유지, 아이템은 원래 폭
+------+ +------+ (빈)   (빈)   (빈)
|  1   | |  2   |
+------+ +------+

대부분의 카드 그리드에서는 "아이템이 적어도 꽉 차 보이는" auto-fit이 직관적입니다. 반대로 "칸의 폭을 항상 일정하게 유지하고 싶다"(예: 정해진 크기의 상품 카드)면 auto-fill이 맞습니다.

구조 전환은 여전히 미디어 쿼리로

auto-fit/minmax가 강력하지만, 만능은 아닙니다. 레이아웃의 구조 자체가 바뀌어야 할 때는 여전히 미디어 쿼리가 필요합니다. 대표적인 예가 "모바일에선 세로로 쌓고, 데스크톱에선 사이드바 + 본문"입니다.

.layout {
  display: grid;
  gap: 24px;
  grid-template-columns: 1fr; /* 모바일: 한 줄로 쌓기 */
}
 
@media (min-width: 1024px) {
  .layout {
    /* 데스크톱: 사이드바 260px + 본문 나머지 */
    grid-template-columns: 260px 1fr;
  }
}

이건 "열 개수 조정"이 아니라 "배치 구조 변경"이라, 유동 규칙으로는 표현하기 어렵습니다. 정리하면 이렇습니다.

  • 아이템이 균일하게 채워지는 반복 그리드auto-fit/minmax (미디어 쿼리 불필요)
  • 영역의 역할·배치가 바뀌는 골격 → 미디어 쿼리로 grid-template 자체를 교체

React 예시: 미디어 쿼리 없는 카드 그리드

2편의 유동 단위와 이번 글의 Grid를 합쳐, 미디어 쿼리 없이 완전히 반응하는 카드 그리드를 만들어 보겠습니다.

/* AutoGrid.module.css */
.grid {
  display: grid;
  gap: clamp(1rem, 2.5vw, 2rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}

minmax(min(100%, 240px), 1fr)에서 min(100%, 240px)를 쓴 이유가 있습니다. 만약 컨테이너가 240px보다도 좁으면 minmax(240px, ...)는 가로 스크롤을 만들 수 있는데, min(100%, 240px)로 감싸면 "컨테이너보다 넓어지지 않도록" 안전장치를 겁니다. 아주 좁은 화면까지 안정적으로 대응하는 관용구입니다.

import styles from './AutoGrid.module.css';
 
type Item = { id: string; title: string };
 
export function AutoGrid({ items }: { items: Item[] }) {
  return (
    <ul className={styles.grid}>
      {items.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

Tailwind CSS라면 임의 값으로 같은 표현이 가능합니다.

export function AutoGrid({ items }: { items: Item[] }) {
  return (
    <ul className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,240px),1fr))] gap-[clamp(1rem,2.5vw,2rem)]">
      {items.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

트레이드오프 정리

관점 Flexbox (wrap + basis) Grid (auto-fit + minmax)
차원 1차원 (한 방향 흐름) 2차원 (행·열 동시 제어)
반응 방식 넘치면 다음 줄로 wrap 들어가는 만큼 열 자동 생성
칸 폭 균일성 마지막 줄이 들쭉날쭉할 수 있음 모든 칸 폭이 정확히 균일
미디어 쿼리 대부분 불필요 반복 그리드는 불필요, 구조 전환은 필요
잘 맞는 곳 내비게이션, 태그, 버튼 그룹 카드 그리드, 대시보드, 페이지 골격
정렬 세밀함 주축/교차축 정렬이 간결 행·열 트랙과 영역 배치가 강력

정리하면

Flexbox와 Grid를 제대로 쓰면 반응형에서 미디어 쿼리의 비중이 크게 줄어듭니다.

  • 한 방향 흐름은 Flexboxflex-wrap: wrap + flex: 1 1 기준폭이면 알아서 줄바꿈됩니다.
  • 격자 반복은 Gridrepeat(auto-fit, minmax(240px, 1fr))이 반응형 그리드의 기본 관용구입니다.
  • auto-fit은 빈 열을 접고, auto-fill은 유지 — 대부분 auto-fit이 직관적입니다.
  • 아주 좁은 화면 안전장치로 minmax(min(100%, 240px), 1fr) 를 기억해 두세요.
  • 구조 전환(사이드바↔세로 쌓기)만 미디어 쿼리로 처리하면 됩니다.

여기까지는 모두 "뷰포트 폭"을 기준으로 반응했습니다. 그런데 같은 컴포넌트라도 사이드바에 있을 때와 본문에 있을 때 이상적인 레이아웃이 다릅니다. 다음 글에서는 뷰포트가 아니라 컴포넌트가 놓인 컨테이너의 폭을 기준으로 반응하는 컨테이너 쿼리를 다룹니다.

같이 보면 좋은 글