
【Frontend CSS – 파트 10】 브라우저가 바라보는 Subgrid: 중첩된 레이아웃은 어떻게 동기화되는가?
요약
CSS Grid Level 2의 Subgrid 기능을 통해 중첩된 레이아웃의 트랙을 부모 그리드와 동기화하는 방법을 설명합니다. 각 카드의 높이가 달라 정렬이 어긋나는 문제를 해결하고, 브라우저의 트랙 크기 결정 알고리즘을 공유하는 원리를 다룹니다.
핵심 포인트
- Subgrid는 부모 그리드의 트랙을 빌려와 동일한 알고리즘으로 크기를 결정함
- 중첩된 그리드 간의 행/열 정렬 불일치 문제를 해결할 수 있음
- 코드 중복을 줄이고 반응형 레이아웃 구현을 용이하게 함
- 단순한 값 복사가 아닌 부모 그리드 트랙에 직접 참여하는 메커니즘임
주의사항
이 기사는 AI의 지원을 받아 작성되었습니다.
제9회를 다 읽었는데도, 중첩된 레이아웃(Nested Layout) 때문에 여전히 머리가 아프신가요? Subgrid에 오신 것을 환영합니다 — CSS Grid Level 2의 '스파이스(Spice)'를 통해, 같은 행에 있는 카드들이 '높이 경쟁'을 멈추고 깔끔하게 정렬되도록 만들 수 있습니다.
이 기사는 display: grid를 알고 있는 프론트엔드 엔지니어를 대상으로 합니다. 카드 목록에서 버튼, 제목, 설명문이 제각각 어긋나서 곤란을 겪고 있는 분들을 위해, Nested Grid의 과제 → Subgrid의 구문 → 브라우저의 Track Sizing Algorithm (트랙 크기 결정 알고리즘) → React + TypeScript 실례 (Fallback 포함) → 자주 빠지는 함정 순서로 설명하겠습니다.
이런 경험, 없으신가요?
케이스 1: 3열 상품 카드 목록을 Grid Layout으로 만든다. 각 카드에는 이미지, 제목, 설명, '자세히 보기' 버튼이 있다. 카드 A의 제목은 2줄이고, 카드 B는 1줄이다 — 그러면 순식간에 내용물이 전부 어긋난다. 카드 A의 버튼이 카드 B보다 위에 위치하게 된다. 마치 선반의 가로 칸막이 없이 물건을 채워 넣은 듯한 모습이 된다. -
케이스 2: 각 카드를 독립된 Nested Grid로 만들어 이미지 / 제목 / 설명 / 버튼용 행을 나눈다. 하지만 깨닫고 보면, 각 카드가 자신만의 행 높이를 결정하고 있어 옆 카드와는 무관하다. 결과는? 역시 어긋난다. 어라, 왜?
Grid Level 1은 중첩된 트랙 동기화를 해결할 수 없습니다. 각 자식 Grid는 독립된 '왕국'입니다 — 스스로 트랙을 결정하며, 옆이 얼마나 높은지는 신경 쓰지 않습니다.
Subgrid는 바로 이 부분을 메우기 위해 CSS Grid Level 2에서 탄생했습니다.
Subgrid는 CSS Grid Layout Module Level 2의 일부로, grid-template-columns와 grid-template-rows에 추가된 새로운 값입니다.
트랙을 직접 선언하는 대신, subgrid는 부모 Grid 상에서 span(범위 지정)하고 있는 트랙을 빌려와서, Track Sizing Algorithm (트랙 크기 결정 알고리즘)에 함께 참여합니다. 크기, gap (기본값), 그리드 라인 이름 — 이 모든 것이 이 메커니즘 안에 포함되어 있습니다.
Subgrid는 '상속' 같은 단순 복사-붙여넣기가 아닙니다.
부모 Grid와 subgrid는 동일한 Track Sizing Algorithm을 실행합니다. subgrid의 내용이 레이아웃 전체의 트랙을 늘릴 수도 있습니다 — 이는 부모의 수치를 복사해서 '운에 맡기는' 것과는 완전히 다릅니다.
| 과제 | Subgrid의 역할 |
|---|---|
| 카드의 행이 어긋남 | 동일한 행 밴드의 카드가 부모 Grid의 행 정의를 공유함 |
| 트랙 복사-붙여넣기가 번거로움 | 직접 참조하므로 코드 중복이 줄어듦 |
display: contents가 시맨틱을 해침 | 래퍼(Wrapper)는 Grid 아이템으로 유지하면서, 자식 Grid가 부모의 트랙을 빌려옴 |
| 반응형 구현이 어려움 | 부모 Grid를 수정하면 subgrid도 자동으로 추종함 |
기억할 것은 딱 3단계뿐입니다 — 커피를 내리는 것보다 짧습니다:
코드 예시:
/* 단계 1: 부모 Grid */
.parent {
display: grid;
...
}
| 사용법 | CSS | 의미 |
|---|---|---|
| 행만 | grid-template-rows: subgrid; | 부모의 행을 빌려옴, 열은 직접 관리 |
| 열만 | grid-template-columns: subgrid; | 부모의 열을 빌려옴, 행은 직접 관리 |
| 둘 다 | grid-template: subgrid / subgrid; | 행과 열을 모두 빌려옴 |
요소가 subgrid가 되면:
- ✅ span 하고 있는 트랙 시스템 (
1fr,minmax(),auto,px,%등) - ✅ 부모 Grid의 간격 (gap) — 기본적으로 상속됨, 고유한
gap설정 가능
덮어쓰기도 가능 - ✅
그리드 라인 이름 (Named lines) - ❌
Grid area (영역 이름은 참조할 수 없음)
/* named lines가 포함된 부모 Grid */
.parent {
display: grid;
...
Subgrid의 진가는 정렬(alignment)에만 있는 것이 아닙니다.
대규모 디자인 시스템에서는 부모 Grid에서 정의한 Named Lines ([content-start] / [content-end] 등)를 subgrid를 통해 공유할 수 있다는 점이 가장 큰 장점이 될 수 있습니다. 손자 요소가 grid-column: content-start / content-end와 같이 루트 Grid와 동일한 라인 이름을 그대로 참조할 수 있다는 것 — 트랙 크기(Track sizing)의 동기화를 넘어, 레이아웃의 어휘(vocabulary)를 트리 전체에서 공유할 수 있는 것이 Subgrid의 강점입니다.
| 관점 | 일반적인 중첩 Grid | Subgrid |
|---|---|---|
| 트랙 크기 조정 (Track sizing) | 직접 정의 | 부모로부터 빌려옴 |
| 자식 Grid의 동기화 | ❌ 불가능 | ✅ 가능 (동일한 트랙 밴드 내에서 Track Sizing 공유) |
| Named lines | 빌려올 수 없음 | 빌려올 수 있음 |
gap | 직접 설정 | 기본값은 부모의 것을 따름 (덮어쓰기 가능) |
| 트랙 수 | 직접 결정 | 부모의 span 수와 동일 |
| 반응형 (Responsive) | 각 자식 Grid를 개별적으로 수정 | 부모 Grid만 수정하면 OK |
| 코드 복잡도 | 높고 중복되기 쉬움 | 낮음 (DRY) |
<!-- 동일한 HTML -->
<div class="grid">
<div class="card">
...
일반적인 중첩 Grid: 각 카드가 행(row)의 높이를 스스로 결정함 → '보기' 버튼의 위치가 위아래로 제각각임.
Subgrid: 동일한 **트랙 밴드 (track band)**에 속하는 카드끼리 부모 Grid의 트랙 크기를 공유함 → 버튼이 깔끔하게 정렬되어 훨씬 '정돈된' 모습이 됩니다.
위의 두 장은 플레이스홀더입니다. Qiita에서 읽히는 기사에는 실제 스크린샷이 효과적입니다. 특히 Chrome DevTools → Grid 오버레이를 통해 부모 트랙과 subgrid의 span 영역을 보여주는 사진 한 장은 Mermaid 다이어그램 5장만큼의 설득력이 있습니다. 데모를 구동한 후에는 Before/After 비교 이미지로 교체해 주세요.
subgrid를 만나더라도 브라우저는 display: grid를 무시하지 않습니다 — 요소는 Grid 컨테이너인 상태를 유지하되, 단지 독자적인 트랙을 정의하지 않고 부모 Grid 상에서 span 된 트랙을 빌려올 뿐입니다. CSS Grid Level 2 사양에 따른 처리 흐름은 다음과 같습니다:
기억해야 할 점은, subgrid는 새로운 트랙을 만들어내지 않는다 — 그저 부모에게 있는 트랙을 빌려올 뿐이라는 것입니다.
디버깅 팁: Chrome DevTools → Grid 오버레이를 켜고 부모의 트랙과 subgrid의 span 영역을 확인해 보세요. subgrid 오류의 대부분은 트랙 부족이나 span 수의 불일치에서 발생합니다 — 오버레이를 보면 바로 알 수 있습니다.
많은 사람(저도 처음에는 그랬습니다)이 grid-template-rows: subgrid를 '부모의 행을 그대로 복사하는 것'이라고 생각하기 쉽습니다. 이는 큰 오해입니다.
브라우저는 트랙 값을 복사하지 않습니다. subgrid는 부모 Grid의 크기 계산 루프(Grid Level 2 사양의 track sizing)에 직접 참여합니다. subgrid의 내용물이 부모 트랙을 늘릴(stretch) 수도 있습니다 — 단순히 정해진 수치를 받아오기만 하는 것이 아닙니다.
이해하기 쉬운 예: subgrid의 1행에 거대한 이미지가 있다면 → 부모 Grid의 1행도 이를 수용하기 위해 확장되어야 합니다. 두 요소가 동일한 계산 루프 안에 있기 때문이며, 서로 다른 세계가 아닙니다.
자, 이제 상품 목록 컴포넌트를 만들어 봅시다 — 목표는 동일한 행에 있는 카드들의 이미지, 제목, 설명, 가격, 버튼이 선반에 진열된 것처럼 나란히 정렬되는 것입니다.
이 데모는 Subgrid의 메커니즘을 설명하기 위해 간략화되었습니다.
실제 Grid Layout 환경에서는 부모 Grid에 카드 내부의 행을 grid-template-rows로 하드코딩하는 패턴은 잘 사용하지 않습니다. 일반적으로는 grid-auto-rows나 repeat(auto-fit, minmax(...))를 조합하며, 카드 측에서 grid-template-rows: subgrid + grid-row: span N을 사용합니다. 아래 코드는 실제 환경에 더 가까운 형태입니다.
- 반응형 그리드: 데스크톱 3열, 태블릿 2열, 모바일 1열
- 각 카드: 이미지, 제목, 설명, 가격, "장바구니에 추가" 버튼
- 동일한 트랙 밴드 (Track Band) 내의 카드끼리 정렬됨 (데스크톱 3열이라면 카드 1~3 등) - 구형 브라우저를 위한 폴백(Fallback) 포함 (단, 폴백 시 카드 간의 행 동기화는 기대하지 마세요)
import React from 'react';
import './ProductGrid.css';
interface Product {
...
/* ========================================
부모 Grid — 열을 정의, 행은 implicit으로 생성
======================================== */
...
요약하자면: 부모 Grid에 트랙(grid-auto-rows에 의한 implicit 또는 explicit 모두 포함)이 필요하며, 이를 subgrid가 빌려옵니다. 각 카드는 span 5 행을 차지하며, 자식 요소에 올바른 grid-row를 지정합니다. 동일한 트랙 밴드 (Track Band) 내의 카드(3열 그리드의 1행이라면 카드 1~3)는 **트랙 크기 결정 알고리즘 (Track Sizing Algorithm)**에 의해 각 행의 높이가 맞춰집니다.
subgrid가 모든 카드를 한꺼번에 동기화하는 것은 아니라는 점을 기억해 두세요. 동기화되는 것은 부모 Grid 상에서 동일한 트랙 세트(Track Set)를 span 하고 있는 카드들뿐입니다.
3열 레이아웃에서 각 카드가 grid-row: span 5인 경우:
| 카드 | 부모 Grid 상의 위치 | 동기화되는 대상 |
|---|---|---|
| 카드 1~3 | 1~5행 (1, 2, 3열) | ✅ 서로 간에 |
| ... |
이는 버그가 아니라 사양(Specification)대로 동작하는 것입니다. 각 밴드에는 고유한 트랙이 있습니다. 여러 행으로 구성된 카드 그리드에서도 이 패턴으로 문제가 없는 경우가 많습니다. 사용자는 보통 같은 행에 나란히 있는 카드들끼리 비교하므로, 1행의 버튼과 3행의 버튼이 맞을 필요는 별로 없기 때문입니다.
explicit tracks를 사용하는 경우:
데모를 최소 구성으로 만들고 싶을 때는 부모 Grid에 행을 명시적으로 작성할 수도 있습니다:
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
...
하지만 실제 환경에서는 위의 grid-auto-rows 패턴이 더 일반적입니다. explicit과 implicit은 레이아웃에 맞춰 유연하게 조합하여 사용하세요.
카드에 grid-template-rows: subgrid를 작성했음에도 불구하고, 부모 Grid 측의 트랙 정의가 subgrid가 참조하려는 트랙 수보다 부족하면 기대했던 동기화 레이아웃이 나오지 않을 수 있습니다. explicit 트랙과 implicit 트랙 모두 생성되지만, span 수와 일치하지 않으면 **트랙 크기 결정 알고리즘 (Track Sizing Algorithm)**이 의도한 대로 작동하지 않습니다.
/* ❌ NG: 부모에게 행 정의가 없어 span보다 트랙이 부족할 가능성 있음 */
.product-grid {
display: grid;
...
결과: Subgrid가 참조하는 트랙 수보다 부모 측의 트랙 정의가 부족한 경우, 레이아웃은 "작동"은 하지만 기대한 동기화는 이루어지지 않습니다. implicit 트랙이 생성될 수도 있지만, span 수와의 정합성을 확인해야 합니다.
대처법: 부모 Grid에 충분한 트랙을 준비하세요. explicit이든 implicit이든 상관없습니다.
/* ✅ 방법 1: Explicit tracks */
.product-grid {
display: grid;
...
부모 Grid에 grid-template-areas를 선언하여 subgrid에서 'header'나 'sidebar'를 호출하려고 해도… 안타깝게도 subgrid는 영역 이름(area name)을 참조할 수 없습니다.
/* ❌ grid-template-areas는 참조할 수 없음 */
.parent {
display: grid;
...
}
해결 방법: subgrid에서 참조하고 싶다면 **이름이 지정된 라인(named lines)**을 사용하세요.
자식 요소가 5개인데 grid-row: span 3만 설정되어 있다면 — 공간이 부족하여 요소들이 같은 행에 뭉치거나, 범위를 벗어나게 됩니다.
.product-card {
display: grid;
grid-template-rows: subgrid;
...
}
해결 방법: grid-row: span N의 N을 필요한 행 수와 일치시켜 주세요.
.product-card {
display: grid;
grid-template-rows: subgrid;
...
}
subgrid는 만능약이 아닙니다. 콘텐츠가 너무 길면 카드가 부풀어 오를 수도 있습니다 — 그럴 때는 자식 요소에 min-width: 0이나 overflow: hidden을, 필요하다면 subgrid 자체에도 지정해 보세요.
.product-card {
display: grid;
grid-template-rows: subgrid;
...
}
또는 각 자식 요소에서 개별적으로 처리:
.product-card__name {
overflow: hidden;
text-overflow: ellipsis;
...
}
subgrid는 몇 단계라도 중첩(nesting)할 수 있습니다. 2단계 subgrid는 직계 부모(1단계 subgrid일 수도 있음)로부터 트랙(track)을 빌려옵니다. 1단계가 루트 Grid로부터 트랙을 빌려오고 있으므로, 트리 전체가 동일한 트랙 크기 조정(track sizing) 루프에 참여하게 됩니다.
중첩된 컴포넌트에서도 레이아웃을 관통하여 동기화하고 싶을 때 유용합니다 — display: contents 해킹이 더 이상 필요하지 않습니다.
| 브라우저 | 지원 버전 | 비고 |
|---|---|---|
| Chrome | 117+ (2023년 9월) | 풀 서포트 (Full support) |
| Edge | 117+ (2023년 9월) | 풀 서포트 (Full support) |
| Firefox | 71+ (2019년 12월) | 상당히 일찍부터 지원 |
| Safari | 16.0+ (2022년 9월) | 풀 서포트 (Full support) |
| IE11 | ❌ 미지원 | 폴리필 (polyfill) 불가 |
요약: Chrome, Edge, Firefox, Safari는 모두 지원합니다. 2026년이라면 Subgrid를 실무에서 마음 편히 사용할 수 있습니다 — 만약 아직 IE11을 지원해야 한다면… 행운을 빕니다, 폴리필은 없으니까요.
간단한 방법: 기본 레이아웃을 디폴트로 설정하고, 지원하는 브라우저에서만 Subgrid를 활성화합니다. ProductGrid.css의 6.3과 동일한 패턴입니다.
폴백(fallback)인 grid-template-rows: auto auto auto auto auto는 각 카드 내부의 레이아웃만 유지합니다 — Subgrid처럼 카드 간의 행 동기화는 할 수 없습니다. 오래된 브라우저 대응 시에는 이 트레이드오프(trade-off)를 받아들여 주세요.
/* 기본값: 모든 브라우저를 위한 폴백 */
.product-card {
display: grid;
...
}
열(column) 방향으로도 사용할 수 있습니다:
@supports (grid-template-columns: subgrid) {
/* 열용 Subgrid 코드 */
}
subgrid가 작동하지 않거나 레이아웃이 어긋나나요? 바로 Flexbox로 도망가지 말고 — 이 체크리스트를 차례대로 확인해 보세요:
여러 행의 카드를 가로질러 동기화되기를 기대하고 있지는 않나요?
Subgrid는 **동일한 행 밴드(row band)**에 있는 카드들만 동기화합니다. 2행의 카드와 1행의 카드의 버튼이 맞지 않는 것은 버그가 아니라 사양(specification)대로 동작하는 것입니다.
부모 Grid의 트랙 정의가 subgrid의 span과 일치하나요?
explicit/implicit 트랙이 subgrid의 참조 수보다 부족하면, 기대했던 동기화 레이아웃이 되지 않을 수 있습니다. -
subgrid 요소에 display: grid가
있나요?
이것이 없으면 Grid 컨텍스트(Grid context)에 진입할 수 없습니다. -
span은
충분한가요?
자식 요소가 5개라면 grid-row: span 5가 필요합니다 (열이라면 grid-column: span N). -
grid-template-areas를
사용하려고 하고 있지는 않나요?
Subgrid는 영역 이름(area name)을 빌려올 수 없습니다 — named lines로 전환하세요. -
gap이
의도치 않게 덮어씌워지고 있지는 않나요?
기본값은 부모의 gap이지만, subgrid 측의 gap으로 덮어쓸 수 있습니다. -
브라우저가 지원하나요?
@supports (grid-template-rows: subgrid)로 확인해 보세요. -
요소가 부모의 직접적인 Grid 아이템인가요?
직접적인 Grid 아이템만이 subgrid가 될 수 있습니다. -
Intrinsic sizing으로 인해 여전히 부풀어 오르나요? min-width: 0은
시도해 보셨나요?
Subgrid만으로는 min-width: auto 문제를 전부 해결할 수 없습니다.
Subgrid는 레이아웃을 좋아하는 사람들을 위한 단순한 "있으면 좋은" 정도의 기능이 아니라, CSS Grid Level 2가 Nested Grid의 트랙 동기화에 부족했던 조각을 채워준 것입니다.
레이아웃 기법의 용도별 구분을 정리해 둡시다:
| 용도 | 추천 |
|---|---|
| 1차원 레이아웃 (가로 정렬 · 세로 정렬) | → Flexbox |
| 2차원 레이아웃 (행과 열을 동시에 제어) | → Grid |
| 중첩된 트랙 동기화 (카드 간의 행 맞춤 등) | → Subgrid |
Subgrid는 Grid Layout의 연장선에 있으며, Flexbox와 경쟁하는 것이 아닙니다.
- Subgrid는 부모 Grid 상에서 span한
트랙을 빌려옵니다 — 복사가 아니라, 부모와 동일한 track sizing에 참여합니다. -
행, 열, 또는 둘 다 빌릴 수 있습니다. -
동일한 **트랙 밴드(track band)**의 subgrid는 크기를 공유하므로 → 인접한 카드끼리 정렬됩니다. -
기본적으로 부모의gap을 사용하며(덮어쓰기 가능); named lines는 빌릴 수 있지만,grid-template-areas는 빌릴 수 없습니다. -
모던 브라우저는 지원 완료 — 2026년이라면 실무에서 안심하고 사용할 수 있습니다. -
Intrinsic sizing (min-width: auto등)은 여전히 수동으로 대처해야 할 수도 있습니다.
| 상황 | 추천 |
|---|---|
| 카드 그리드에서 카드 내부의 요소를 맞추고 싶을 때 | ✅ 행(row)의 subgrid |
| ... |
- MDN: Subgrid – CSS Grid Layout
- WebKit Blog: Subgrid – how to line up elements
- CSS Grid Layout Module Level 2 Specification
- Can I Use: CSS Subgrid
- Smashing Magazine: CSS Grid Level 2 – Here Comes Subgrid
【Frontend CSS – 파트 11】브라우저 관점에서의 Intrinsic Sizing ─ 콘텐츠와 컨테이너 중 무엇이 크기를 결정하는가?
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기