유동 레이아웃과 CSS 단위: %, rem, vw, clamp()로 브레이크포인트 사이 잇기
이 글은 React로 반응형 웹 구성하기 시리즈 2편입니다.
1편에서 미디어 쿼리로 화면 폭에 따라 레이아웃을 바꾸는 법을 봤습니다. 그런데 미디어 쿼리만으로 반응형을 만들면 한 가지 아쉬운 점이 남습니다. 브레이크포인트는 "계단"이라, 767px에서 768px로 넘어가는 순간 레이아웃이 툭 바뀌고, 그 사이 구간(예: 800px)에서는 아무 변화 없이 고정되어 있습니다.
그래서 데스크톱 브레이크포인트(1024px)로 넘어가기 직전인 1000px 화면에서 여백이 어색하게 벌어지거나, 글자가 너무 크거나 작아 보이는 일이 생깁니다. 미디어 쿼리가 "계단"이라면, 그 계단 사이를 부드럽게 잇는 "경사로"가 바로 유동 단위입니다.
이 글에서는 반응형에서 쓰는 CSS 단위들을 성격별로 정리하고, 각각의 트레이드오프와 함께 clamp()로 계단 사이를 촘촘하게 잇는 방법까지 다룹니다.
한눈에 보면
- 단위는 절대 단위(px) 와 상대 단위(%, em, rem, vw 등) 로 나뉘고, 반응형은 상대 단위를 얼마나 잘 쓰느냐의 문제입니다.
rem은 루트 폰트 크기 기준이라 예측 가능하고 접근성(사용자 폰트 확대)에 강합니다.em은 부모 기준이라 중첩 시 값이 누적됩니다.vw/vh는 뷰포트에 완전히 비례하지만, 그대로 쓰면 너무 커지거나 작아지고 사용자 폰트 확대를 무시하는 접근성 문제가 있습니다.- 모바일에서
100vh가 주소창까지 포함해 커지는 문제는dvh/svh/lvh로 해결합니다. clamp(최소, 선호, 최대)는 "유동적으로 변하되 상한·하한은 지킨다"를 한 줄로 표현합니다. 반응형 단위의 핵심 도구입니다.- 미디어 쿼리(계단)와 유동 단위(경사로)는 경쟁 관계가 아니라 함께 쓰는 관계입니다.
계단과 경사로
먼저 두 방식이 폭 변화에 어떻게 반응하는지 감을 잡아 보겠습니다. 아래는 폰트 크기를 예로, 화면 폭이 커질 때 값이 어떻게 변하는지를 나타낸 것입니다.
● 미디어 쿼리만 사용 (계단)
값
| ___________
| |
| ____|
| |
|____|
+-------------------------- 화면 폭
(브레이크포인트에서만 값이 툭툭 바뀜)
● clamp() 유동 단위 (경사로)
값
| __________ <- 최대값에서 멈춤
| /
| /
| /
|____/ <- 최소값에서 시작
+-------------------------- 화면 폭
(구간 전체에서 부드럽게 변하고, 상·하한은 지킴)계단은 특정 지점에서만 변하고, 경사로는 구간 전체에서 부드럽게 변합니다. 좋은 반응형은 보통 이 둘을 섞습니다. 큰 구조 전환은 미디어 쿼리(계단)로, 그 사이의 폰트·여백·간격 미세 조정은 유동 단위(경사로)로 처리합니다.
단위를 성격으로 나눠 보기
반응형에서 마주치는 단위를 성격으로 묶으면 이렇게 정리됩니다.
| 단위 | 기준 | 성격 | 주 용도 |
|---|---|---|---|
px |
절대(고정) | 변하지 않음 | 테두리, 얇은 선 |
% |
부모 요소 | 부모에 비례 | 너비, 유동 컬럼 |
em |
현재 요소의 폰트 크기 | 부모 폰트에 누적 | 컴포넌트 내부 간격 |
rem |
루트(html) 폰트 크기 |
전역 기준, 예측 가능 | 폰트, 간격 대부분 |
vw/vh |
뷰포트 폭/높이 | 화면에 완전 비례 | 히어로, 풀블리드 |
dvh 등 |
동적 뷰포트 | 브라우저 UI 반영 | 모바일 전체 높이 |
ch |
문자 0의 폭 |
글자 수 기준 | 본문 줄 길이 제한 |
이 중 반응형에서 특히 자주 헷갈리고, 트레이드오프가 분명한 세 가지를 짚겠습니다.
em vs rem: 누적되느냐, 예측되느냐
em은 현재 요소의 폰트 크기를 기준으로 합니다. 그래서 중첩되면 값이 곱해지며 누적됩니다.
.card {
font-size: 20px;
}
.card .badge {
font-size: 0.8em; /* 20px * 0.8 = 16px */
}
.card .badge .dot {
font-size: 0.8em; /* 16px * 0.8 = 12.8px — 또 곱해짐 */
}rem은 항상 루트(html) 폰트 크기를 기준으로 하므로 중첩과 무관하게 예측 가능합니다.
html {
font-size: 16px;
}
.badge {
font-size: 1rem; /* 위치와 무관하게 항상 16px */
}트레이드오프는 이렇습니다.
rem: 전역적으로 일관되고, 사용자가 브라우저 기본 폰트 크기를 키우면 전체가 함께 커져 접근성에 강합니다. 대부분의 폰트·간격은rem이 기본값으로 좋습니다.em: "이 컴포넌트의 폰트 크기에 상대적으로" 간격을 잡고 싶을 때 유용합니다. 버튼 안 패딩을em으로 두면 폰트가 커질 때 패딩도 비례해 커집니다. 대신 중첩 누적을 항상 염두에 둬야 합니다.
실무 기준: 폰트와 대부분의 간격은
rem, 컴포넌트 내부에서 폰트에 비례해야 하는 간격만em.
vw의 함정: 접근성과 극단값
vw는 뷰포트 폭의 1%입니다. width: 50vw처럼 레이아웃 너비에 쓰면 직관적이고 좋습니다. 문제는 폰트 크기에 vw를 단독으로 쓸 때 생깁니다.
/* 위험한 패턴 */
h1 {
font-size: 5vw;
}이 코드에는 두 가지 문제가 있습니다.
- 극단값: 초광각 모니터(2560px)에서
5vw = 128px로 지나치게 커지고, 아주 좁은 화면에서는 반대로 너무 작아집니다. 상·하한이 없습니다. - 접근성:
vw는 뷰포트에만 반응하고 사용자의 브라우저 폰트 확대 설정을 무시합니다. 저시력 사용자가 글자를 키워도vw기반 글자는 그대로입니다.
그래서 폰트에 뷰포트 단위를 쓰고 싶다면 단독으로 쓰지 말고, 뒤에서 볼 clamp()로 rem과 섞어 상·하한과 접근성을 함께 챙겨야 합니다.
100vh 문제와 동적 뷰포트 단위
1편에서 잠깐 짚었듯, 모바일에서 height: 100vh는 주소창이 보이는 상태의 높이까지 포함해 실제 보이는 영역보다 커질 수 있습니다. 이걸 해결하는 게 동적 뷰포트 단위입니다.
svh(small): 브라우저 UI가 최대로 보일 때 기준 높이lvh(large): 브라우저 UI가 숨겨졌을 때 기준 높이dvh(dynamic): UI 표시 상태에 따라 실시간으로 바뀌는 높이
.hero {
min-height: 100dvh; /* 주소창 표시/숨김에 따라 실제 보이는 높이로 */
}트레이드오프도 있습니다. dvh는 스크롤로 주소창이 접힐 때 값이 바뀌면서 레이아웃이 살짝 움직일 수 있습니다. "절대 안 잘리는 게 중요하면 svh", "가득 채우는 느낌이 중요하면 dvh"로 상황에 맞게 고릅니다.
clamp(): 반응형 단위의 핵심 도구
이제 이 글의 주인공입니다. clamp()는 세 값을 받습니다.
clamp(최솟값, 선호값, 최댓값)- 선호값이 보통 뷰포트에 비례하는 유동값(
vw,%등)이고, - 그 값이 최솟값보다 작아지면 최솟값으로,
- 최댓값보다 커지면 최댓값으로 고정됩니다.
즉 "유동적으로 변하되, 너무 작아지지도 너무 커지지도 않게"를 한 줄로 표현합니다.
유동 타이포그래피
앞서 본 font-size: 5vw의 문제를 clamp()로 고쳐 보겠습니다.
h1 {
/* 최소 1.5rem, 선호 4vw, 최대 3rem */
font-size: clamp(1.5rem, 4vw, 3rem);
}이제 좁은 화면에서도 1.5rem 밑으로 안 내려가고, 넓은 화면에서도 3rem을 넘지 않습니다. 선호값에 rem이 섞인 형태(clamp(1.5rem, 1rem + 2vw, 3rem))로 쓰면 사용자 폰트 확대에도 어느 정도 반응하게 만들 수 있습니다. 타이포그래피는 7편에서 더 깊이 다룹니다.
유동 간격과 여백
clamp()는 폰트뿐 아니라 여백·간격에도 유용합니다. 브레이크포인트 사이에서 섹션 여백이 부드럽게 변하도록 만들 수 있습니다.
.section {
/* 모바일에선 좁게, 데스크톱에선 넓게, 그 사이는 부드럽게 */
padding-block: clamp(2rem, 6vw, 6rem);
}
.stack {
display: grid;
gap: clamp(1rem, 3vw, 2.5rem);
}이렇게 하면 여백을 위해 미디어 쿼리를 여러 개 두던 것을 한 줄로 대체할 수 있습니다.
min()과 max()
clamp()의 형제인 min()과 max()도 자주 쓰입니다.
.container {
/* "화면의 90%" 와 "1200px" 중 작은 값 — 큰 화면에선 1200px로 고정 */
width: min(90%, 1200px);
}
.thumb {
/* 최소 200px는 보장 */
width: max(200px, 20vw);
}특히 width: min(90%, 1200px)는 "최대 폭 1200px, 단 화면이 좁으면 좌우 여백 5%씩"을 미디어 쿼리 없이 표현하는 아주 흔한 관용구입니다.
React 예시: 유동 히어로 카드
지금까지의 단위를 하나의 컴포넌트로 모아 보겠습니다. 미디어 쿼리 없이도 폭에 따라 폰트·여백·너비가 부드럽게 변합니다.
/* HeroCard.module.css */
.card {
width: min(92%, 900px); /* 최대 900px, 좁으면 좌우 4% 여백 */
margin-inline: auto;
padding: clamp(1.25rem, 4vw, 3rem); /* 유동 패딩 */
border-radius: 16px;
}
.title {
font-size: clamp(1.5rem, 1rem + 3vw, 3rem); /* 유동 + 접근성 고려 */
line-height: 1.2;
}
.desc {
max-width: 60ch; /* 본문 줄 길이 제한 */
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
}import styles from './HeroCard.module.css';
export function HeroCard({ title, description }: { title: string; description: string }) {
return (
<section className={styles.card}>
<h1 className={styles.title}>{title}</h1>
<p className={styles.desc}>{description}</p>
</section>
);
}Tailwind CSS를 쓴다면 임의 값 문법으로 같은 표현을 할 수 있습니다.
export function HeroCard({ title, description }: { title: string; description: string }) {
return (
<section className="mx-auto w-[min(92%,900px)] rounded-2xl p-[clamp(1.25rem,4vw,3rem)]">
<h1 className="text-[clamp(1.5rem,1rem+3vw,3rem)] leading-tight">{title}</h1>
<p className="max-w-[60ch] text-[clamp(1rem,0.9rem+0.5vw,1.25rem)]">{description}</p>
</section>
);
}트레이드오프 정리
| 단위/기법 | 강점 | 주의점 |
|---|---|---|
px |
정확·예측 가능 | 사용자 폰트 확대에 반응 안 함, 반응형에 약함 |
rem |
전역 일관·접근성 강함 | 루트 폰트 크기 설계가 선행되어야 함 |
em |
컴포넌트 내부 비례에 유용 | 중첩 시 값이 누적됨 |
vw/vh 단독 |
뷰포트에 완전 비례 | 극단값·접근성 무시 (특히 폰트에 단독 금지) |
dvh/svh |
모바일 100vh 문제 해결 | dvh는 스크롤 중 값이 변할 수 있음 |
clamp() |
유동 + 상·하한을 한 줄로 | 세 값 설계에 감각이 필요 |
min()/max() |
미디어 쿼리 없이 상·하한 표현 | 남용하면 의도 파악이 어려워질 수 있음 |
정리하면
미디어 쿼리가 큰 구조를 바꾸는 계단이라면, 유동 단위는 그 사이를 부드럽게 잇는 경사로입니다.
- 폰트와 간격의 기본은
rem— 예측 가능하고 접근성에 강합니다. vw는 폰트에 단독으로 쓰지 말 것 — 반드시clamp()로 상·하한과 접근성을 챙깁니다.- 모바일 전체 높이는
dvh/svh— 상황에 따라 잘리지 않는 쪽/가득 채우는 쪽을 고릅니다. clamp()와min()은 미디어 쿼리를 줄여 주는 도구 — 여백·너비·폰트의 미세 조정을 한 줄로 처리합니다.
핵심은 미디어 쿼리와 유동 단위 중 하나를 고르는 게 아니라, 큰 전환은 계단으로, 미세 조정은 경사로로 나눠 함께 쓰는 것입니다.
다음 글에서는 이 단위들을 실제 레이아웃 엔진에 얹어, Flexbox와 Grid로 미디어 쿼리 자체를 줄이는 방법을 다룹니다.
