볼링 핀 배치의 비밀 — CSS Grid auto-placement로 피라미드 레이아웃 구현하기

9 min read
CSS Grid레이아웃nth-child반응형auto-placement
볼링 핀 배치의 비밀 — CSS Grid auto-placement로 피라미드 레이아웃 구현하기

카드 UI를 그리드로 배치할 때 보통은 균일한 격자를 사용한다. 3열, 4열, 반응형으로 줄이거나 늘리거나. 하지만 때로는 1개, 2개, 3개, 4개… 식으로 행마다 아이템이 늘어나는 피라미드 배치가 필요하다. 포트폴리오, 팀 소개, 가격 비교 페이지에서 시각적 위계를 표현하기에 효과적인 레이아웃이다. 볼링 핀을 떠올려보면 된다 — 맨 앞에 1개, 그 뒤로 2개, 3개, 4개. 바로 이 배열이다.

Grid를 써야 하는 이유

과거에는 Flexbox와 마진 조정으로 피라미드를 만들었다. 각 행의 첫 아이템에 margin-left를 줘서 중앙 정렬 효과를 흉내내는 방식이었다. 하지만 이 접근법은 줄자 없이 가구를 배치하는 것과 같다. 아이템 수가 바뀌거나 화면 크기가 달라지면 마진 값을 전부 다시 계산해야 한다.

CSS Grid는 열과 행을 명시적으로 정의할 수 있으므로, 아이템의 위치를 정밀하게 제어한다. 핵심 무기는 Grid의 auto-placement 알고리즘이다. 특정 아이템의 열만 지정하면, 나머지 아이템은 자동으로 그 뒤에 배치된다.

[💡 잠깐! 이 용어는?] auto-placement 알고리즘: CSS Grid에서 명시적으로 위치를 지정하지 않은 아이템들을 자동으로 빈 셀에 배치하는 브라우저의 내장 로직. grid-auto-flow 속성으로 행 우선(row) 또는 열 우선(column) 배치를 제어할 수 있다.


피라미드의 수학 — 삼각수 패턴

피라미드 그리드에서 각 행의 첫 번째 아이템 번호는 규칙적인 패턴을 따른다.

pyramid-pattern.txt
행 1: 아이템 1          → 1개
행 2: 아이템 2, 3       → 2개
행 3: 아이템 4, 5, 6    → 3개
행 4: 아이템 7, 8, 9, 10 → 4개

각 행의 첫 번째 아이템 번호는 1, 2, 4, 7, 11, 16… 이다. 이것은 삼각수(triangular number)에 1을 더한 값과 같다. 이 패턴을 CSS :nth-child() 셀렉터로 타겟팅한다.

최대 열 수를 정하고(예: 6열), 각 행의 첫 번째 아이템을 해당 열에 배치하면 나머지는 auto-placement가 알아서 채운다. 볼링 핀 배치에서 첫 번째 핀의 위치만 정해주면 나머지 핀은 자연스럽게 옆에 놓이는 것과 같은 원리다.

pyramid-grid-base.css
.pyramid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 8px;
  justify-items: center;
}
 
.pyramid > :nth-child(1) {
  grid-column: 4;
}
 
.pyramid > :nth-child(2) {
  grid-column: 3;
}
 
.pyramid > :nth-child(4) {
  grid-column: 2;
}
 
.pyramid > :nth-child(7) {
  grid-column: 1;
}

[💡 잠깐! 이 용어는?] 삼각수(Triangular Number): 1, 3, 6, 10, 15… 처럼 n번째 삼각수는 1부터 n까지의 합이다. 피라미드 그리드에서 각 행의 첫 아이템 인덱스를 구할 때 사용한다. n행의 첫 아이템 번호 = (n-1)번째 삼각수 + 1이다.


반응형 전환 — 피라미드에서 일반 그리드로

피라미드 레이아웃은 화면이 넓을 때 아름답지만, 모바일에서는 실용적이지 않다. 핵심 전략은 피라미드가 더 이상 성립하지 않을 때 자연스럽게 일반 그리드로 전환하는 것이다.

2021년에는 미디어 쿼리로 특정 브레이크포인트에서 레이아웃을 전환했다. 2026년 방식은 다르다. 컨테이너 쿼리를 사용해, 부모 컨테이너의 너비가 충분하면 피라미드, 부족하면 자동 그리드로 전환한다.

responsive-pyramid.css
.pyramid-container {
  container-type: inline-size;
  max-width: 900px;
  margin: 0 auto;
  padding: 16px;
}
 
.pyramid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 8px;
}
 
@container (min-width: 600px) {
  .pyramid {
    grid-template-columns: repeat(8, 1fr);
  }
 
  .pyramid > :nth-child(1) {
    grid-column: 4;
  }
 
  .pyramid > :nth-child(2) {
    grid-column: 4;
  }
 
  .pyramid > :nth-child(4) {
    grid-column: 3;
  }
 
  .pyramid > :nth-child(7) {
    grid-column: 2;
  }
}

좁은 화면에서는 auto-fillminmax()가 아이템을 가용 공간에 맞춰 자동 배치하고, 넓은 화면에서는 고정 열 수로 피라미드를 구성한다. 비유하면, 비가 오면 실내에서 하고, 맑으면 야외에서 하는 행사 기획과 같다 — 조건에 따라 배치가 자동으로 바뀐다.

[💡 잠깐! 이 용어는?] auto-fill vs auto-fit: 둘 다 repeat() 함수와 함께 사용해 자동으로 열 수를 결정한다. auto-fill은 빈 트랙도 유지하고, auto-fit은 빈 트랙을 축소해 아이템이 남은 공간을 채운다. 피라미드에서는 빈 공간이 필요하므로 auto-fill이 더 적합하다.


완성 코드

pyramid-complete.html
<div class="pyramid-container">
  <div class="pyramid">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
    <div class="item">9</div>
    <div class="item">10</div>
  </div>
</div>
pyramid-complete.css
.pyramid-container {
  container-type: inline-size;
  max-width: 900px;
  margin: 0 auto;
  padding: 16px;
}
 
.pyramid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 8px;
}
 
@container (min-width: 600px) {
  .pyramid {
    grid-template-columns: repeat(8, 1fr);
  }
 
  .pyramid > :nth-child(1) {
    grid-column: 4;
  }
 
  .pyramid > :nth-child(2) {
    grid-column: 4;
  }
 
  .pyramid > :nth-child(4) {
    grid-column: 3;
  }
 
  .pyramid > :nth-child(7) {
    grid-column: 2;
  }
}
 
.item {
  background: #4a90d9;
  color: white;
  padding: 24px;
  text-align: center;
  border-radius: 8px;
  font-weight: 600;
}

2021년 방식 vs 2026년 방식

기준2021년 (Flexbox + margin)2026년 (Grid + auto-placement)
레이아웃 제어margin 수동 계산grid-column 명시적 배치
반응형 전환미디어 쿼리 브레이크포인트컨테이너 쿼리 + auto-fill
유지보수아이템 추가 시 마진 재계산nth-child 패턴만 확장
정렬justify-content 의존그리드 셀 기반 정밀 정렬
브라우저 지원모든 브라우저98% 이상 (Grid 지원)

Grid 기반 접근이 모든 면에서 우위다. 특히 아이템을 추가하거나 제거할 때 다른 아이템의 스타일을 건드릴 필요가 없다는 점이 가장 크다.


마무리

피라미드 그리드의 원리는 단순하다. 각 행의 첫 번째 아이템의 열 위치만 지정하면 나머지는 auto-placement가 처리한다. :nth-child()로 삼각수 패턴의 아이템을 타겟팅하고, 컨테이너 쿼리로 좁은 화면에서 일반 그리드로 자연스럽게 전환하면 된다. 볼링 핀 배치를 생각하면 된다 — 첫 번째 핀의 위치만 잡아주면, 나머지는 자연스럽게 따라온다.


참고:

관심 있을 만한 포스트

AI 코딩의 맹점 — Artifacts 없이 에이전트는 기억을 잃는다

PRD, ADR, TDD가 AI 코딩 워크플로우에서 왜 선택이 아닌 필수인지, 실전 구조와 함께 살펴본다.

AI 코딩Artifacts

Next-Translate 3.0 — Turbopack과 App Router를 위한 i18n 재건

1년간 공백 후 돌아온 Next-Translate 3.0이 Turbopack 지원, 비동기 params, App Router 안정화를 한 번에 처리하는 방법.

Next.jsi18n

V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법

V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.

V8WebAssembly

Vinext — Vite 위에서 Next.js를 1주일 만에 다시 만든 이야기

Cloudflare가 AI와 함께 단 일주일, $1,100의 API 비용으로 Next.js 호환 프레임워크를 Vite 위에 구축한 과정.

VinextNext.js

Tsonic — TypeScript를 네이티브 바이너리로 컴파일하는 실험

TypeScript → C# → NativeAOT 파이프라인으로 네이티브 실행 파일을 만드는 Tsonic. 어떻게 동작하고, 어떤 한계가 있는지 살펴봤다.

TypeScriptNativeAOT

VS Code 팀의 AI 에이전트 병렬화 — 월간 릴리스를 주간으로 만든 워크플로우

VS Code 팀이 월간 릴리스에서 주간 릴리스로 전환한 비결. 에이전트 세션 병렬화, 자동화 파이프라인, 품질 게이트 설계 전반을 공개했다.

VS CodeAI

React Compiler의 한계 — 뭘 최적화하고 뭘 못 하는가

React Compiler가 자동 메모이제이션으로 해결하는 것과 해결하지 못하는 것. 컴파일러 기반 UI 프레임워크의 능력 경계를 정리했다.

ReactReact Compiler

Native JSON Modules — 번들러 없이 JSON을 import하는 시대

Import Attributes와 함께 표준이 된 native JSON module. 어떻게 동작하고, 기존 번들러 방식과 뭐가 다른지 정리했다.

JavaScriptESM