6. 레이아웃과 반응형
클래스 이름과 선언에 배치 의도를 드러내고, 폭에 따른 변경은 한곳에 모읍니다. 브레이크포인트를 추가하기 전에 고유 크기 지정으로 해결할 수 있는지 확인합니다. 뷰포트 브레이크포인트는 파일 아래 한곳에 모으고 데스크톱 퍼스트로 정한 세 값만 씁니다. 컴포넌트가 받은 폭에 따라 구조를 바꿔야 하면 컨테이너 쿼리를 씁니다.
6.1 @media 브레이크포인트는 파일 맨 아래에 모읍니다
각 브레이크포인트에서 달라지는 스타일을 한 블록에서 확인합니다.
브레이크포인트 재선언은 파일 맨 아래 @media 블록에 모으고 클래스 블록 안에 중첩하지 않습니다.
같은 폭에서 툴바, 패널, 사이드바가 어떻게 달라지는지 한 블록에서 읽도록 합니다.
조건 방향과 값은 규칙 6.2를 따릅니다.
| 반복되거나 별도로 판단할 것 | 처리 |
|---|---|
| 같은 역할에서 값만 파일마다 다름 | 토큰 파일에서 값을 나눕니다 |
| 선택자와 선언까지 같은 배치 책임이 여러 파일에 반복됨 | 배치를 컴포넌트 하나로 모을지 검토하고 브레이크포인트를 그 파일에만 둡니다 |
| 조건 숫자만 같고 역할은 다름 | 컴포넌트를 합치지 않습니다 |
| 브레이크포인트 없이 배치할 수 있음 | 규칙 6.4 규칙을 먼저 적용합니다 |
prefers-color-scheme 테마 조건 | 이 규칙의 대상이 아닙니다 |
테마 조건은 규칙 5.4에 따라 토큰 파일의 최상위 @media에 둡니다.
클래스 블록 안에 중첩해서 브레이크포인트가 흩어집니다.
.pg_products__toolbar {
display: flex;
gap: 24px;
@media (width < 1024px) {
flex-direction: column;
}
}
.pg_products__layout {
display: grid;
grid-template-columns: 280px 1fr;
@media (width < 1024px) {
grid-template-columns: 1fr;
}
}
선언은 위에 두고 브레이크포인트는 파일 아래 한곳에 모읍니다.
.pg_products__toolbar {
display: flex;
gap: 24px;
}
.pg_products__layout {
display: grid;
grid-template-columns: 280px 1fr;
}
@media (width < 1024px) {
.pg_products__toolbar {
flex-direction: column;
}
.pg_products__layout {
grid-template-columns: 1fr;
}
}
같은 @media 블록을 파일마다 복사합니다.
@media (width < 1024px) {
.pg_products__filterBar {
flex-direction: column;
}
}
/* src/page/orders/pg-orders.css */
@media (width < 1024px) {
.pg_orders__filterBar {
flex-direction: column;
}
}반복되는 배치를 컴포넌트 하나로 만들고 브레이크포인트는 그 파일에만 둡니다.
@media (width < 1024px) {
.ui_filterBar__root {
flex-direction: column;
}
}함께 보기
- 6.2 브레이크포인트는 데스크톱 퍼스트로 세 값만 씁니다
- 6.4 브레이크포인트보다 고유 크기 지정을 먼저 검토합니다
- 4.4 한 클래스의 선언은 한 블록에 모읍니다
- 5.4 테마는 토큰 값만 바꿔서 전환합니다
6.2 브레이크포인트는 데스크톱 퍼스트로 세 값만 씁니다
넓은 화면부터 좁은 화면 순서로 덮어쓰고 프로젝트 전체에서 세 기준 폭을 공유합니다.
기준 폭 세 값
기본 선언은 1440px 이상인 가장 넓은 화면을 기준으로 하고, 좁아질 때만 덮어씁니다.
브레이크포인트는 아래 순서로 쓰며 (width >= ...) 방향과 섞지 않습니다.
| 조건 | 구간 이름 | 대상 |
|---|---|---|
(width < 1440px) | ~lg | 좁은 데스크톱 |
(width < 1024px) | ~md | 가로 태블릿, 좁은 노트북 |
(width < 640px) | ~sm | 세로 태블릿 아래 |
숫자는 이 셋만 쓰며 이름은 경계 아래의 구간을 가리킵니다.
좁은 화면에서 여러 조건이 함께 맞으면 뒤의 좁은 조건이 앞의 넓은 조건을 덮습니다.
@media 조건에는 var()를 쓸 수 없으므로 이 숫자를 토큰으로 만들지 않습니다.
범위 표기
조건은 범위 표기로 씁니다.
max-width: 1024px과 min-width: 1024px은 경계를 함께 포함하지만 <와 >=는 같은 경계를 소수 보정 없이 나눕니다.
블록 위치는 규칙 6.1을 따릅니다.
기본 선언을 중간 폭에 맞추고 넓고 좁은 방향을 함께 씁니다.
.pg_products__layout {
display: grid;
grid-template-columns: 220px 1fr;
}
@media (width >= 1440px) {
.pg_products__layout {
grid-template-columns: 280px 1fr;
}
}
@media (width < 1024px) {
.pg_products__layout {
grid-template-columns: 1fr;
}
}
기본 선언은 가장 넓은 화면에 맞추고 좁아질 때만 덮어씁니다.
.pg_products__layout {
display: grid;
grid-template-columns: 280px 1fr;
}
@media (width < 1440px) {
.pg_products__layout {
grid-template-columns: 220px 1fr;
}
}
@media (width < 1024px) {
.pg_products__layout {
grid-template-columns: 1fr;
}
}
max-width를 소수로 보정하고 좁은 쪽부터 씁니다.
@media (max-width: 639.98px) {
.pg_products__layout {
padding: 12px;
}
}
@media (max-width: 1023.98px) {
.pg_products__layout {
padding: 20px;
}
}
범위 표기로 적고 넓은 쪽부터 좁혀 갑니다.
@media (width < 1024px) {
.pg_products__layout {
padding: 20px;
}
}
@media (width < 640px) {
.pg_products__layout {
padding: 12px;
}
}
함께 보기
6.3 레이아웃의 기준과 역할을 클래스 이름과 선언에 드러냅니다
조상 DOM을 찾아보지 않고 sticky, fixed의 기준과 각 요소의 배치 역할을 파악합니다.
레이아웃의 기준과 역할은 클래스 이름과 선언에서 드러나야 합니다. 크기 고정 여부는 규칙 6.4를 따릅니다.
| 선언이나 요소 | 요구 사항 | 이유 |
|---|---|---|
z-index | 숫자 대신 층 토큰을 씁니다. 목록과 쌓임 맥락은 규칙 5.3를 따릅니다 | 토큰 이름으로 쌓임 순서를 읽습니다 |
sticky, fixed | 기준 컨테이너를 주석 한 줄로 남깁니다 | 조상 조건은 해당 선언만으로 알 수 없습니다 |
| 로딩 대체 화면 | 실제 내용과 같은 컨테이너 클래스 안에 넣습니다 | 대체 화면에만 높이를 주면 교체 시 그 높이가 사라져 레이아웃이 튈 수 있습니다 |
fixed는 transform이 적용된 조상 아래에서 뷰포트 기준을 잃습니다.
sticky는 중간 조상에 overflow: hidden이나 auto가 있으면 그 조상이 기준이 되어 뷰포트에 붙지 않습니다.
층 숫자를 직접 적고 기준 컨테이너 설명이 없습니다.
.pg_productDetail__toolbar {
position: sticky;
top: 0;
z-index: 9999;
}
기준 컨테이너와 의도를 드러냅니다.
.pg_productDetail__toolbar {
/* 스크롤 컨테이너는 .pg_productDetail__content */
position: sticky;
top: 0;
z-index: var(--app-z-index-sticky);
}
.pg_productDetail__content {
display: grid;
min-height: 0;
overflow-y: auto;
}
로딩 대체 화면에만 높이를 따로 적습니다.
<Suspense fallback={<UiLineChartSkeleton className={clsx("pg_productDetail__chartSkeleton")} />}>
<PgProductDetailChartSection />
</Suspense>
.pg_productDetail__chartSkeleton {
height: 320px;
}
대체 화면을 실제 내용과 같은 컨테이너 클래스 안에 넣습니다.
<div className={clsx("pg_productDetail__chart")}>
<Suspense fallback={<UiLineChartSkeleton />}>
<PgProductDetailChartSection />
</Suspense>
</div>
.pg_productDetail__chart {
/* 로딩 중과 실제 차트에 같은 최소 높이 확보 */
min-height: 320px;
}
함께 보기
6.4 브레이크포인트보다 고유 크기 지정을 먼저 검토합니다
컴포넌트가 배치된 폭에 맞춰 크기를 조정해 위치가 바뀌어도 CSS 수정을 줄입니다.
고유 크기 먼저 보기
브레이크포인트를 추가하기 전에 고유 크기 지정만으로 배치할 수 있는지 확인합니다.
@media는 뷰포트 폭을 보므로 같은 컴포넌트를 본문에서 좁은 사이드바로 옮겨도 실제 슬롯 폭을 반영하지 못합니다.
flowchart LR
q1{"고유 크기 지정만으로<br>배치할 수 있는가?"} -- 아니요 --> q2{"실제 슬롯 폭에 따라<br>구조를 바꾸는가?"} -- 아니요 --> r3("@media")
q1 -- 예 --> r1("flex-wrap, auto-fit, clamp")
q2 -- 예 --> r2("@container")| 필요한 변화 | 사용할 방법 |
|---|---|
| 한 줄에 안 들어가 줄을 바꿈 | flex-wrap: wrap + flex: 1 1 <기준폭> |
| 폭에 따라 열 개수가 달라짐 | repeat(auto-fit, minmax(min(100%, <기준폭>), 1fr)) |
| 슬롯을 채우되 최대 폭에서 멈춤 | flex: 1 1 <기준폭> + max-width |
| 여백이나 글자 크기가 조금씩 달라짐 | clamp(<최소>, <선호>, <최대>) |
| 뷰포트 폭에 따라 사이드바를 숨기거나 표를 카드 목록으로 바꿈 | @media. 위치는 규칙 6.1을 따릅니다 |
| 실제 슬롯 폭에 따라 구조를 바꿈 | @container |
minmax(240px, 1fr)의 고정 최솟값도 슬롯보다 크면 넘치므로 min(100%, 240px)로 제한합니다.
컨테이너 쿼리 자리
@container 작성 위치 | 기준 |
|---|---|
| 크기를 제공하는 조상 | container-type: inline-size를 선언합니다 |
| 크기 조건으로 바꿀 요소 | 해당 조상의 자손입니다. 자기 크기를 조건으로 자신을 바꾸지 않습니다 |
| 조건 숫자와 블록 위치 | 내용이 깨지는 컨테이너 폭으로 정합니다. 뷰포트용 세 값을 강제로 재사용하지 않고 소유자 파일의 기본 선언 뒤에 모읍니다 |
폭의 소유
버튼과 입력은 padding, min-height, 글자 크기까지만 소유하고 폭은 사용처가 정합니다.
폭을 고정한 이유가 클래스 이름과 선언에서 드러나는지는 규칙 6.3을 따릅니다.
열 개수를 브레이크포인트마다 직접 지정합니다.
.pg_products__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
@media (width < 1440px) {
.pg_products__grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (width < 1024px) {
.pg_products__grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (width < 640px) {
.pg_products__grid {
grid-template-columns: 1fr;
}
}
사용 가능한 폭에 따라 열 개수를 조정합니다.
.pg_products__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
gap: 16px;
}
버튼이 자기 폭을 뷰포트로 정합니다.
.ui_button__root {
display: inline-flex;
min-height: 40px;
padding: 0 var(--app-space-control-inline);
width: 300px;
}
@media (width < 1024px) {
.ui_button__root {
width: 200px;
}
}
@media (width < 640px) {
.ui_button__root {
width: 100%;
}
}
버튼은 폭을 정하지 않고 모양만 소유합니다.
/* ui-button.css: 폭 지정 없음 */
.ui_button__root {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
padding: 0 var(--app-space-control-inline);
}
폭은 사용처가 자기 배치 안에서 정합니다.
/* ui-form-footer.css: 한 번 쓰고 여러 화면에서 그대로 사용 */
.ui_formFooter__root {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--app-space-inline);
}
.ui_formFooter__action {
flex: 1 1 200px;
max-width: 300px;
}
값이 매끄럽게 변하면 clamp를 씁니다.
.pg_products__hero {
padding-block: clamp(24px, 4vw, 64px);
font-size: clamp(1.5rem, 1rem + 2vw, 2.5rem);
}
함께 보기