뒤로가기
CSS Grid로 실무 레이아웃 5개 만들기

December 1, 2020

cssfrontend

CSS Grid를 "알고는 있는데 실무에서 잘 안 쓰게 되는" 기술이라고 말하는 사람이 많다. Flexbox로 대부분 해결되니까. 나도 그랬다. 그런데 어느 순간부터 Flexbox로 하면 코드가 지저분해지는 레이아웃이 자꾸 생겼다. 특히 2차원 배치가 필요한 경우.

Grid를 실무에서 쓰기 시작한 건 대시보드 리뉴얼 작업 때였다. 그때 만든 레이아웃 패턴 중에서 지금까지 반복적으로 재사용하는 5개를 정리한다.

1. 대시보드 그리드#

대시보드는 Grid의 가장 자연스러운 사용처다. 카드 크기가 다르고, 특정 카드는 2칸을 차지하고, 화면 크기에 따라 배치가 달라져야 한다.

css
.dashboard {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 200px;
  gap: 16px;
  padding: 24px;
}

.card-wide {
  grid-column: span 2;
}

.card-tall {
  grid-row: span 2;
}

.card-large {
  grid-column: span 2;
  grid-row: span 2;
}

@media (max-width: 1024px) {
  .dashboard {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 640px) {
  .dashboard {
    grid-template-columns: 1fr;
  }

  .card-wide,
  .card-tall,
  .card-large {
    grid-column: span 1;
    grid-row: span 1;
  }
}

이걸 Flexbox로 하면 어떻게 될까. flex-wrap을 쓰고, 각 카드에 flex-basis를 퍼센트로 주고, gap 대신 margin을 계산해서 넣고... 가능은 하지만 코드가 두 배는 길어진다. 특히 card-tall처럼 세로로 2칸을 차지하는 건 Flexbox로는 자연스럽게 구현이 안 된다.

2. 사이드바 + 메인 콘텐츠#

전형적인 어드민 레이아웃이다. 왼쪽 사이드바는 고정 너비, 오른쪽 메인은 남은 공간을 차지한다.

css
.layout {
  display: grid;
  grid-template-columns: 260px 1fr;
  grid-template-rows: 64px 1fr;
  height: 100vh;
}

.header {
  grid-column: 1 / -1;
  border-bottom: 1px solid #e5e7eb;
}

.sidebar {
  grid-row: 2;
  border-right: 1px solid #e5e7eb;
  overflow-y: auto;
}

.main {
  grid-row: 2;
  overflow-y: auto;
  padding: 24px;
}

"이건 Flexbox로도 쉽게 되잖아"라고 할 수 있다. 맞다. 이 정도는 Flexbox로도 충분하다. 그런데 여기서 요구사항이 추가된다.

사이드바를 접었다 펼 수 있어야 하고, 접었을 때 아이콘만 보여야 한다. Grid에서는 grid-template-columns만 바꾸면 된다.

css
.layout {
  display: grid;
  grid-template-columns: 260px 1fr;
  transition: grid-template-columns 0.2s ease;
}

.layout.collapsed {
  grid-template-columns: 64px 1fr;
}

한 줄로 끝난다. 사이드바 안의 내용물은 CSS나 컴포넌트 레벨에서 조건부 렌더링하면 된다. transition도 부드럽게 걸린다.

3. 반응형 카드 그리드#

이건 진짜 많이 쓴다. 상품 목록, 블로그 포스트 목록, 갤러리 등. 카드가 화면 크기에 따라 자동으로 열 수가 바뀌는 패턴.

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

이 한 줄이 미디어 쿼리 없이 반응형 그리드를 만들어준다. auto-fill은 가능한 많은 열을 채우고, minmax(280px, 1fr)은 각 카드가 최소 280px, 최대로는 남은 공간을 균등 분할한다.

auto-fillauto-fit의 차이가 종종 헷갈리는데, 실무적으로 중요한 차이는 아이템 수가 적을 때 나타난다.

css
/* auto-fill: 빈 열도 공간을 차지함 */
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));

/* auto-fit: 빈 열은 공간을 0으로 줄여서 기존 아이템이 늘어남 */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));

카드가 2개인데 화면이 넓을 때, auto-fill은 카드 오른쪽에 빈 공간이 남고, auto-fit은 카드 2개가 전체 너비를 나눠 가진다. 대부분의 목록 UI에서는 auto-fill이 더 자연스럽다. 카드가 3개인데 한 줄에 2개씩 들어가는 화면에서 카드가 비정상적으로 커 보이면 안 되니까.

4. Holy Grail 레이아웃#

헤더, 푸터, 사이드바 2개, 메인 콘텐츠. 웹의 고전적인 레이아웃인데, Grid의 grid-template-areas를 쓰면 코드가 시각적으로 읽힌다.

css
.page {
  display: grid;
  grid-template-areas:
    "header  header  header"
    "nav     main    aside"
    "footer  footer  footer";
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
}

.header { grid-area: header; }
.nav    { grid-area: nav; }
.main   { grid-area: main; }
.aside  { grid-area: aside; }
.footer { grid-area: footer; }

@media (max-width: 768px) {
  .page {
    grid-template-areas:
      "header"
      "main"
      "nav"
      "aside"
      "footer";
    grid-template-columns: 1fr;
    grid-template-rows: auto;
  }
}

grid-template-areas의 장점은 미디어 쿼리에서 레이아웃 순서를 바꾸기가 쉽다는 거다. 모바일에서 navmain 아래로 내리고 싶으면 areas 문자열만 수정하면 된다. HTML 순서를 건드릴 필요가 없다.

단점도 있다. 이름을 붙이는 게 귀찮고, 복잡한 레이아웃에서는 area 이름이 많아져서 오히려 가독성이 떨어질 수 있다. 팀 내에서는 "3영역 이하일 때만 areas를 쓰자"는 암묵적 합의가 있다.

5. 폼 레이아웃#

폼을 Grid로 만들면 라벨과 인풋의 정렬이 깔끔해진다. 특히 라벨이 왼쪽, 인풋이 오른쪽인 가로 배치 폼에서.

css
.form {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: 12px 16px;
  align-items: start;
  max-width: 600px;
}

.form label {
  text-align: right;
  padding-top: 8px;
  font-weight: 500;
  color: #374151;
}

.form .full-width {
  grid-column: 1 / -1;
}

.form .actions {
  grid-column: 2;
  display: flex;
  gap: 8px;
}

@media (max-width: 640px) {
  .form {
    grid-template-columns: 1fr;
  }

  .form label {
    text-align: left;
    padding-top: 0;
  }

  .form .actions {
    grid-column: 1;
  }
}
tsx
<form className="form">
  <label htmlFor="name">이름</label>
  <input id="name" type="text" />

  <label htmlFor="email">이메일</label>
  <input id="email" type="email" />

  <label htmlFor="message">메시지</label>
  <textarea id="message" rows={4} />

  <div className="full-width">
    <p className="help-text">* 개인정보 처리 방침에 동의합니다</p>
  </div>

  <div className="actions">
    <button type="submit">제출</button>
    <button type="reset">초기화</button>
  </div>
</form>

gap: 12px 16px에서 앞이 row gap, 뒤가 column gap이다. 이걸 Flexbox로 하면 각 행을 div로 감싸야 한다. Grid에서는 label과 input을 그냥 나열하면 된다. 2열로 정의했으니까 자동으로 줄바꿈된다.

subgrid는 아직 이르다#

CSS subgrid를 쓰면 자식 요소가 부모의 그리드 트랙을 물려받을 수 있다. 카드 내부의 제목, 설명, 버튼 높이를 다른 카드와 맞출 때 유용한데, 브라우저 지원이 아직 완전하지 않다. 2024년 기준 Chrome, Firefox, Safari 최신 버전에서는 되지만, 사내 시스템처럼 IE나 구형 브라우저를 지원해야 하는 프로젝트에서는 못 쓴다.

나도 아직 프로덕션에서 subgrid를 쓴 적은 없다. 써보고 싶은 마음은 있는데, "아직 너무 이르지 않나"라는 생각과 "지원 안 되는 브라우저에서 레이아웃이 깨지면 어쩌지"라는 걱정이 앞선다. 이런 새로운 CSS 기능들은 항상 그렇다. 알고는 있되, 프로덕션에 투입하는 건 1~2년 기다려야 마음이 편하다.