CSS 레이아웃 및 반응형 디자인

  1. 1 기본사항
    1. 어떤 레이아웃 모드를 사용해야 하나요?
    2. 작업 원칙
  2. 2 Flexbox
  3. 3 그리드 및 하위 그리드
    1. 코드 예시: 그리드 및 하위 그리드
  4. 4 컨테이너 쿼리
    1. 코드 예: 컨테이너 쿼리 단위를 사용하는 유동적 서체
  5. 5 네이티브 오버레이, 앵커 위치 지정, 쌓임 맥락
  6. 6 오버플로 추적 및 레이아웃 안정성
  7. 7 표시 영역 메커니즘 및 트랙 배포
  8. 8 그리드 레인 (일명 벽돌)

1 기초

성능을 개선하려면 가능한 경우 브라우저의 레이아웃 엔진을 사용하세요. 하드코딩된 크기나 복잡한 미디어 쿼리를 사용하기 전에 고유 크기, 논리적 속성, aspect-ratio를 사용하세요.

1.1 어떤 레이아웃 모드를 사용해야 하나요?

결정 트리를 위에서 아래로 탐색하고 첫 번째 일치 항목에서 중지합니다. 레이아웃은 서로 중첩될 수 있으며 각 결정은 해당 컨테이너의 사용 사례를 기반으로 합니다.

  1. 단순한 행 또는 열로 구성된 항목인가요? Flexbox 사용 — 1D, 콘텐츠 우선, 콘텐츠가 단일 축을 따라 분산됩니다.
  2. 중첩된 요소가 상위 요소의 상위 요소 그리드 트랙과 정렬되어야 하나요? 하위 그리드 사용 — 2D, 관계 우선, 상위 트랙을 상속하므로 하위 요소가 동위 요소 간에 정렬될 수 있습니다.
  3. 행과 열이 있는 복잡한 페이지 또는 구성요소 구조인가요? 그리드 사용 — 2D, 레이아웃 우선, 스켈레톤을 정의하고 콘텐츠가 이를 채웁니다.
  4. 콘텐츠가 균형 잡힌 열로 분할되어야 하는 긴 산문 흐름인가요? 다중 열(1D 흐름, 신문 스타일)을 사용합니다.
  5. 높이가 다양한 상품을 촘촘하게 포장해야 하나요? 지금은 grid-auto-flow: dense와 함께 grid를 사용하세요. 기준 타겟에 기본 masonry (일명 '그리드 레인')가 제공되는 경우에만 이를 사용하세요(§8 참고).
  6. DOM 경계나 스택 컨텍스트를 가로지르더라도 요소가 페이지 위에 떠 있고 트리거에 공간적으로 연결되어 있어야 하나요? 앵커 위치 지정을 사용합니다. 트리거에는 anchor-name, 오버레이에는 position-anchor를 사용합니다(§5 참고).

1.2 작동 원리

  • 레이아웃 크기와 간격에 논리적 속성 (inline-size, block-size, margin-inline, padding-block, inset-inline-start)을 사용합니다. 자세한 내용은 CSS: 현대적인 아키텍처를 참고하세요.
  • 콘텐츠 우선과 레이아웃 우선의 멘탈 모델을 적용합니다. 항목이 흐름을 결정할 때는 flexbox를, 스켈레톤을 먼저 정의할 때는 그리드를 사용합니다.
  • place-* 약식 (place-content, place-items, place-self)을 사용하여 하나의 선언에서 두 축을 정렬합니다.
  • 고정된 width/height 대신 고유 크기 조정 (min-content, max-content, fit-content())과 유연한 트랙 (fr, minmax())을 사용하세요. 미디어 쿼리가 줄어들고 레이아웃이 더 탄력적으로 조정됩니다.
  • overflow: hidden를 사용하여 레이아웃 문제를 가리지 마세요. 소스에서 문제를 해결하세요. 예를 들어 <canvas>, <img>, <svg>, <video>, <iframe>와 같은 특정 대체 요소는 기본적으로 display: inline를 사용하므로 특정 레이아웃에 간격이 추가될 수 있습니다. overflow로 간격을 자르는 대신 블록 display-outside 값 (예: display: block, grid, flex 등)을 적용하여 간격을 삭제하세요.
  • aspect-ratio를 사용하여 미디어 공간을 예약하고 애셋이 로드되기 전에 레이아웃 변경을 방지합니다.
.sidebar       { inline-size: max-content; }    /* Size to longest unbreakable token. */
.main-content  { inline-size: fit-content; }    /* Grow to available space, no further. */
.media         { aspect-ratio: 16 / 9; inline-size: 100%; block-size: auto; }
body.centered  { display: grid; place-content: center; min-block-size: 100dvb; }

calc-size() 및 제약 조건 인식 고유 크기 조절은 고유 크기로 계산을 참고하세요.

2 Flexbox

1차원 레이아웃: 항목이 단일 기본 축을 따라 흐르며 교차 축에 정렬됩니다. 탐색 메뉴, 툴바, 항목 행, 단일 행 또는 열 분포에 사용하세요.

권장사항:

  • display: flex로 컨텍스트를 설정하고 flex-direction (row 기본값)로 기본 축을 설정합니다.
  • 오버플로가 발생할 수 있는 경우 항상 flex-wrap: wrap를 사용하세요. overflow: auto/hidden가 없는 nowrap는 좁은 표시 영역에서 오버플로됩니다.
  • 압축된 항목 (예: 태그 클라우드, 필터 필, 버튼 그룹, 탐색 링크)의 래핑된 행에서 flex-wrap: wrap 뒤에 flex-wrap: balance를 레이어링하여 항목을 여러 줄에 균등하게 분산하고 마지막 행에 단일 분리 항목이 생기지 않도록 합니다. 지원되지 않는 브라우저는 wrap로 대체됩니다.
  • flex-grow/flex-shrink/flex-basis를 개별적으로 설정하는 대신 항목에 flex 약식 <grow> <shrink> <basis> (예: flex: 1 1 250px)를 사용합니다.
  • 하위 여백 대신 항목 간 간격에 gap (또는 row-gap/column-gap 긴 형식)를 사용합니다.
  • 컨테이너가 콘텐츠보다 좁을 때 포커스 가능한 콘텐츠가 잘리지 않도록 위치 정렬에 safe (예: align-items: safe center)를 접두사로 추가합니다.
  • margin-inline-start: auto (또는 margin-block-start: auto)를 사용하여 단일 항목을 기본축의 맨 끝으로 밀어냅니다. 이것이 표준 이스케이프 해치입니다.
  • align-self를 사용하여 항목별로 교차축 정렬을 재정의합니다.
  • align-items를 사용하여 교차축에서 모든 항목을 중앙에 배치합니다. 단일 항목에 margin: auto를 사용하여 두 축에서 독립적으로 중앙에 배치합니다. 컨테이너가 줄바꿈되고 행에 추가 공간이 있는 경우에만 align-content를 사용합니다.
  • 끊을 수 없는 긴 콘텐츠(URL, 코드, 긴 문자열)가 포함된 flex 항목에 min-inline-size: 0(또는 min-width: 0)를 설정합니다. flex 항목은 기본적으로 콘텐츠 크기보다 작게 축소되지 않으므로 오버플로가 발생합니다.

금지사항:

  • flex 항목에는 justify-self를 사용하지 마세요. 그리드, 블록, 절대 위치 레이아웃에서만 작동합니다. 대신 자동 여백을 사용하세요.
  • order 또는 flex-direction: *-reverse를 사용하여 대화형 콘텐츠의 순서를 변경하지 마세요. 시각적 순서만 변경됩니다. DOM 순서는 여전히 순차적 포커스를 유도하므로 키보드 탭 흐름이 사용자가 보는 것과 일치하지 않습니다.
  • space-around (끝부분의 절반 간격)를 space-evenly (앞, 중간, 뒤의 동일한 간격)와 혼동하지 마세요.
  • 축 뒤집기를 잊지 마세요. flex-direction: column인 경우 justify-content는 블록 축에 정렬되고 align-items는 인라인 축에 정렬됩니다. 이는 기본값과 반대입니다.
  • 컨테이너와 하위 요소가 서로를 채우도록 크기를 지정하지 마세요. 이는 오버플로와 예상치 못한 결과의 일반적인 원인입니다. 한쪽에 명확한 크기를 지정합니다.
  • 동일한 항목에 flex-basis와 width/inline-size를 모두 설정하지 마세요. flex 컨텍스트에서는 flex-basis가 우선 적용되고 width는 무시됩니다. flex-basis (또는 flex 약식)를 가변 항목 크기 조정을 위한 단일 정보 소스로 사용합니다.
.card-grid        { display: flex; flex-flow: row wrap; gap: 1rem; }
.card-item        { flex: 1 1 250px; }                  /* grow, shrink, basis */
.card-item-action { margin-inline-start: auto; }        /* Push to main-axis end. */
.toolbar          { display: flex; align-items: safe center; }
.tag-list         { display: flex; flex-wrap: wrap; flex-wrap: balance; gap: 0.5rem; }

3 그리드 및 하위 그리드

2차원 레이아웃 — 행과 열을 명시적으로 정의하거나 엔진이 이를 도출하도록 합니다. 하위 그리드를 사용하면 중첩된 그리드가 상위 그리드의 트랙을 상속하므로 하위 요소가 동위 요소 간에 정렬됩니다.

그리드 기능 선택:

  • 필요한 열의 수를 정확히 알고 있나요?
    • 예 — 선정적인 트랙 (grid-template-columns: 200px 1fr, repeat(3, 1fr) 등)을 사용합니다.
    • 열마다 다른 크기가 필요한가요 (사이드바 + 기본, 모든 열에 걸쳐 있는 헤더)? → 이름이 지정된 읽기 가능한 리전에 grid-template-areas 사용
    • 모든 열이 균일한가요, 아니면 순전히 줄 번호로 배치되나요? → repeat(N, ...) 또는 이름이 지정된 선 사용
    • 아니요 (반응형, 알 수 없는 항목 수) — repeat(auto-fit, minmax(min, 1fr)) 사용
    • 마지막 행의 항목이 남은 공간을 채우도록 늘어나야 하나요? → auto-fit
    • 비어 있는 마지막 행 트랙이 최소 크기를 유지해야 하나요 (열 고스트 슬롯 유지)? → auto-fill
  • 특정 위치에 항목을 배치해야 하나요?
    • 예 — grid-column: <start> / <end> 또는 grid-area: <name> 사용
    • 아니요 (여러 트랙에 걸쳐 있으며 흐름 위치는 중요하지 않음) — grid-column: span <n> 사용
  • 하위 요소가 상위 그리드의 트랙 크기를 상속해야 하나요 (동위 요소 간의 불규칙한 가장자리 정렬)?
    • 예 — 영향을 받는 축에 하위 그리드 사용
    • 셀당 자녀 수가 가변적인가요? → 하위 그리드 한 축만, 다른 축에는 grid-auto-rows/grid-auto-columns 사용
    • 하위 요소 수가 고정되어 있나요? → 양쪽 축의 하위 그리드는 괜찮습니다.
    • 아니요 — 표준 그리드, 하위 그리드 필요 없음

권장사항:

  • display: grid를 사용하여 컨텍스트를 설정합니다.
  • 복잡한 페이지 수준 레이아웃에는 grid-template-areas를 사용하세요. 영역 이름은 자체 문서화되며 선언을 행과 열로 정렬하여 한눈에 읽을 수 있습니다.
  • repeat(auto-fit, minmax(200px, 1fr))를 사용하여 채워진 트랙을 늘려 행을 채우는 반응형 카드 그리드를 만들거나 auto-fill를 사용하여 비어 있는 반복 트랙을 최소 크기로 유지합니다.
  • 비례 트랙 분포에는 fr를 사용하고 유연하지만 제한된 트랙에는 minmax(min, max)를 사용합니다.
  • grid-column: span <n>를 사용하여 트랙 간 크기를 조정하고, grid-column: <start> / <end>를 사용하여 특정 선에 배치하고, grid-area: <name>를 사용하여 명명된 영역에 배치합니다.
  • 하위 그리드 (grid-template-columns: subgrid 또는 grid-template-rows: subgrid)를 사용하여 카드 목록의 '들쭉날쭉한 가장자리' 문제를 해결합니다. 제목, 메타데이터, CTA와 같은 내부 요소가 동위 요소에 걸쳐 정렬됩니다.
  • 이전 브라우저에서 동일한 캐스케이드 대체로 사용하기 위해 하위 그리드 선언을 이전의 명시적 grid-template-rows/-columns 선언과 페어링합니다.

금지사항:

  • auto-fit/auto-fill 트랙 크기가 항목 콘텐츠에서 가져올 것으로 예상하지 마세요. repeat() 크기 인수에서 가져옵니다.
  • 양방향 콘텐츠에는 grid-auto-flow: dense를 사용하지 마세요. 항목을 효율적으로 패킹하지만 시각적으로 재정렬하여 DOM 순서 키보드 탭 흐름을 중단합니다.
  • 하위 요소 수가 가변적인 경우 두 축에 하위 그리드를 적용하지 마세요. 추가 항목은 마지막 트랙에 배치됩니다. 대신 암시적 축에 grid-auto-rows/grid-auto-columns를 사용하세요.
  • justify-items/align-items(트랙 내에서 항목 콘텐츠 정렬)를 justify-content/align-content(컨테이너 내에서 그리드 트랙 정렬)와 혼동하지 마세요. 잘못된 것을 사용하면 아무런 효과가 없습니다.
  • 컨테이너에 명확한 inline-size 없이 repeat(auto-fit/auto-fill, ...)를 사용하지 마세요. display: inline-grid 또는 크기가 지정되지 않은 flex 항목 내에서 컨테이너에는 나눌 너비가 없으므로 트랙 수를 예측할 수 없습니다.

3.1 코드 예시: 그리드 및 하위 그리드

페이지 셸: <main class="page-layout">에는 <header>, <aside>, <div class="card"> 하위 요소가 있는 <section class="card-grid">, <footer>가 포함됩니다.

/* Align grid-template-areas in rows and columns for readability. */
.page-layout {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-areas:
    "header  header  header"
    "sidebar main    main"
    "footer  footer  footer";
  gap: 1.5rem;
}

header  { grid-area: header; }
aside   { grid-area: sidebar; }
footer  { grid-area: footer; }

.card-grid {
  grid-area: main;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  grid-template-rows: auto 1fr; /* title block, body block */
  gap: 1rem;
}

.card {
  grid-row: span 2;
  display: grid;
  /* Same-cascade fallback: ignored when subgrid is supported. */
  grid-template-rows: auto 1fr;
  grid-template-rows: subgrid;
}

4 컨테이너 쿼리

표시 영역이 아닌 상위 컨테이너의 크기 (또는 계산된 스타일)를 쿼리합니다. 정신 모델: 컨테이너 쿼리 = 구성요소 컨텍스트, 미디어 쿼리 = 전역 페이지 레이아웃 및 사용자 환경설정 (prefers-color-scheme, prefers-reduced-motion)

권장사항:

  • 하위 요소를 쿼리하기 전에 래퍼에서 container-type: inline-size (너비 전용 쿼리) 또는 container-type: size (두 축 모두)를 사용하여 포함 컨텍스트를 설정합니다.
  • 중첩된 컨텍스트가 충돌할 수 있는 경우 container-name (또는 container 약식: container: card / inline-size)를 사용하여 컨테이너의 이름을 지정합니다.
  • 유동 유형 및 간격을 계산할 때 컨테이너 쿼리 단위를 포함합니다. cqi/cqb (논리적 인라인/블록), cqw/cqh (물리적), cqmin/cqmax
  • container-type: size를 사용할 때마다 컨테이너에 명확한 block-size를 지정합니다. 지정하지 않으면 크기 포함으로 인해 컨테이너가 콘텐츠를 무시하므로 하위 요소가 축소됩니다.

금지사항:

  • block-size을 container-type 값으로 사용하지 마세요. 유효하지 않습니다. 두 축 모두에 size를 사용합니다.
  • container-type를 선언한 후에는 하위 요소의 고유 크기가 컨테이너에 영향을 미치지 않습니다. 포함이 활성화되면 컨테이너는 하위 요소가 없는 것처럼 계산됩니다.
  • 조건을 충족하지 않는 상위 요소의 하위 요소 내에 있는 컨테이너 쿼리 단위에 의존하지 마세요. 작은 표시 영역 (svw/svh)으로 대체됩니다.

4.1 코드 예시: 컨테이너 쿼리 단위를 사용하는 유동적 서체

.card-wrapper {
  container: card / inline-size; /* shorthand for container-name / container-type */
}

@container card (inline-size > 400px) {
  .content {
    display: flex;
    gap: 2rem;
  }
}

.title {
  /* Fluid type bound to the container width, not the viewport. */
  font-size: clamp(1rem, 4cqi, 2rem);
}

구성요소 기반 반응형 스타일 지정 패턴은 크기 인식 스타일 지정 및 유연한 확장을 참고하세요.

5 네이티브 오버레이, 앵커 위치 지정, 스태킹 컨텍스트

각 오버레이 프리미티브를 사용하는 경우:

  • 일시적인 비모달 UI (플라이아웃, 토스트 메시지, 도움말)에는 popover를 사용하세요. 최상위 레이어에 있으며 z-index 관리가 필요하지 않습니다.
  • 포커스 트래핑과 비활성 배경이 필요한 모달 상호작용에는 .showModal()과 함께 <dialog>을 사용합니다.
  • 동일한 요소에 popover와 .showModal()를 결합하지 마세요. 이 두 가지는 상호 배타적인 런타임 상태입니다.

앵커 위치 지정 (오버레이의 공간 레이아웃):

  • position-area (또는 인셋의 anchor()) 및 anchor-size()를 사용하여 트리거를 기준으로 오버레이의 위치와 크기를 지정합니다.
  • position-try-fallbacks: flip-block (또는 flip-inline)를 사용하여 오버레이가 표시 영역을 오버플로할 때 브라우저가 위치를 변경하도록 합니다.
  • 단일 position-area 값에 물리적 키워드와 논리적 키워드를 혼합하지 마세요. 하나의 좌표계를 선택하세요.
  • @supports (anchor-name: --x)로 기능을 감지하고 절대 위치 대체 기능을 제공합니다.

전체 구현 세부정보, 폴리필 전략, popover 값 참조는 선언적 대화상자 및 팝오버 컨트롤 및 위치 인식 도움말을 참고하세요. 드롭다운, 플라이아웃, 탭 표시기에 적용된 앵커 위치 지정은 표시 영역 인식 드롭다운 및 앵커 위치 지정 탭 밑줄을 참고하세요.

6 오버플로 추적 및 레이아웃 안정성

레이아웃 변경, 스크롤바, 클리핑을 예측 가능하게 관리합니다.

권장사항:

  • overflow: auto를 사용하여 콘텐츠가 실제로 오버플로될 때만 스크롤바가 표시되도록 합니다.
  • overflow: clip을 사용하여 스크롤 컨테이너를 설정하지 않고 콘텐츠를 클립합니다. overflow-clip-margin을 사용하여 오버플로를 선택합니다.
  • scrollbar-gutter: stable를 사용하여 스크롤바를 위한 공간을 예약하고 콘텐츠가 늘어날 때 레이아웃 변경을 방지합니다.
  • 중첩된 스크롤 영역(예: 사이드바, 모달, 목록)에서 overscroll-behavior: contain(또는 none)를 사용하여 스크롤 체인이 페이지로 버블링되지 않도록 하거나, 중간 스크롤 래퍼 (예: 스와이프하여 표시 트랙)에서 overscroll-behavior: chain를 사용하여 초과 스크롤이 로컬 고무 밴드 바운스를 트리거하지 않고 상위 스크롤러로 전파되도록 합니다.
  • 여러 줄을 자르려면 -webkit-line-clamp + display: -webkit-box + -webkit-box-orient: vertical 조합을 사용하세요. 이 패턴은 접두사가 있지만 완전히 지정되어 있으며 지원 중단되지 않았습니다. 접두사가 없는 line-clamp 약칭을 함께 선언합니다. 아직 지원하지 않는 브라우저는 속성을 무해하게 무시합니다. 금지사항:

  • auto로 충분한 경우 overflow: scroll를 사용하지 마세요. scroll는 스크롤할 항목이 없는 경우에도 스크롤바를 강제로 표시합니다.

  • 클립만 하려는 경우 overflow: hidden를 사용하지 마세요. hidden는 프로그래매틱 방식으로 스크롤할 수 있는 스크롤 컨테이너를 설정합니다.

.scrollable-list {
  max-block-size: 400px;
  overflow-y: auto;
  scrollbar-gutter: stable;       /* Reserve scrollbar space. */
  overscroll-behavior: contain;   /* No scroll chaining into the page. */
}

.snippet {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  line-clamp: 3;                  /* Ignored where unsupported. */
  overflow: clip;
}

overflow: clip 및 overflow-clip-margin에 관한 자세한 내용은 오버플로 클리핑 제어를 참고하세요. 스와이프하여 표시 UI에서 overscroll-behavior: chain를 사용한 중첩 스크롤 핸드오프는 스와이프하여 표시를 참고하세요. 스크롤바 색상, 크기, 테마는 스크롤바의 색상 또는 두께 맞춤설정, 어두운 모드, 고대비 환경설정에 맞게 스크롤바 조정을 참고하세요.

7 표시 영역 메커니즘 및 트랙 배포

  • 브라우저 UI 전환 (URL 표시줄 접기/펼치기)을 고려해야 하는 모바일 레이아웃 컨테이너에는 dvh/dvw를 사용합니다.
  • 전체 너비 레이아웃에는 100vw를 사용하지 마세요. 스크롤바 너비를 무시하고 가로 오버플로를 일으킵니다. 대신 100%, 100dvw 또는 100svw를 사용하세요.

반응형 디자인 진입점 (동적 표시 영역 단위, 미디어 공간 예약, 유동 타이포그래피)은 반응형 디자인을 참고하세요.

8 그리드 레인 (메이슨리라고도 함)

이 사양은 '그리드 레인' (예: display: grid-lanes)이라는 이름으로 개발 중이며, 이전 실험 프로토타입에서는 grid-template-rows: masonry를 사용했습니다.

권장사항:

  • DOM 순서가 시각적 순서와 일치하지 않을 수 있다는 점을 감안하여, 오늘부터 grid-auto-flow: dense가 있는 그리드를 사용하여 조밀하게 배치하세요.
  • 항목이 동일한 가중치의 카드가 아닌 문서 프래그먼트인 경우 콘텐츠가 많은 벽돌과 같은 흐름에 다중 열 (columns: 3; column-gap: 1rem)을 사용합니다.
  • grid-template-rows: masonry는 점진적 개선으로만 처리합니다. @supports로 기능을 감지합니다.

금지사항:

  • 기준 타겟이 따라잡을 때까지 grid-template-rows: masonry를 필수 요구사항으로 제공하지 마세요.
.gallery       { columns: 3 200px; column-gap: 1rem; }
.gallery > *   { break-inside: avoid; margin-block-end: 1rem; }

@supports (grid-template-rows: masonry) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    grid-template-rows: masonry;
    gap: 1rem;
    columns: unset;
  }
}