03 — CSS Units
CSS 단위는 측정값이 아니라 “무엇을 기준으로 잴 것인가”의 선언이다.
px은 화면 픽셀,rem은 루트 폰트,em은 자신의 폰트,dvh는 동적 뷰포트,cqi는 컨테이너 너비 — 단위를 바꾸면 디자인 시스템의 좌표계가 통째로 바뀐다.
Why — 왜 단위가 중요한가
width: 200px과 width: 12rem은 같은 결과를 낼 수도 있지만, 사용자가 환경을 바꾸는 순간 다르게 동작한다.
- 사용자가 브라우저 폰트 크기를 200%로 키운다 →
px은 변하지 않고rem만 늘어난다 (접근성). - 모바일 Safari에서 주소창이 사라진다 →
100vh는 그대로지만100dvh는 늘어난다. - 부모 컨테이너가 800px → 400px로 줄어든다 →
vw는 그대로지만cqi는 줄어든다 (컨테이너 쿼리). - 다크 모드 토글로 폰트 크기가 바뀐다 →
em은 따라가지만px은 안 따라간다.
단위는 “이 값이 어떤 변화에 반응할 것인가” 를 정한다. 잘못 고르면 접근성·반응형·다크모드가 한꺼번에 깨진다.
How — CSS 단위의 4가지 부류
┌────────────────────────────────────────────┐
│ Absolute — 환경과 무관 (px, in, mm) │
│ Font-relative — 폰트 크기 기준 (em, rem, ch)│
│ Viewport — 뷰포트 기준 (vw, vh, dvh) │
│ Container — 컨테이너 기준 (cqw, cqi) │
└────────────────────────────────────────────┘각 부류는 무엇을 기준으로 삼느냐가 다르다. 기준이 바뀌면 값도 바뀐다.
What — 단위 카탈로그
1) Absolute Units — 환경과 무관
| 단위 | 정의 | 실무 |
|---|---|---|
px | CSS 픽셀 (1/96 inch에 정의됨, 실제 디바이스 픽셀은 DPR이 결정) | border, 1px 미세 조정 |
in cm mm pt pc | 인쇄 단위 | @media print에서만 |
px은 물리 픽셀이 아니다. CSS Specification상 1px = 1/96in으로 정의되어 있고, 디바이스 픽셀과의 비율은 devicePixelRatio가 결정한다 (레티나 화면에서는 1 CSS px = 2 device px).
2) Font-Relative Units — 폰트 기준
| 단위 | 기준 | 비고 |
|---|---|---|
em | 자신의 font-size | 누적 함정 있음 |
rem | 루트(<html>) 의 font-size | 가장 흔히 쓰임, 기본 16px |
ch | ”0” 글자의 너비 | 텍스트 컬럼 너비에 유용 |
ex | 소문자 “x”의 높이 | 거의 쓰지 않음 |
cap ic lh rlh | 캡 높이·표의문자 너비·line-height·root line-height | 모던, 일부 미지원 |
rem vs em의 누적 함정
html { font-size: 16px; }
.card { font-size: 1.2em; } /* 19.2px */
.card .title { font-size: 1.2em; } /* 23.04px ← 누적! */
.card .title .badge { font-size: 1.2em; } /* 27.65px ← 더 누적! */em은 부모의 폰트를 기준 삼으므로 중첩될수록 곱해진다. 반면:
.card { font-size: 1.2rem; } /* 19.2px */
.card .title { font-size: 1.2rem; } /* 19.2px ← 항상 루트 기준 */규칙: 모듈러 스케일·전역 토큰은 rem. 컴포넌트 내부의 비율 관계 (예: 버튼의 padding이 자신의 폰트에 비례)는 em.
.button {
font-size: 1rem;
padding: 0.5em 1em; /* 버튼이 커지면 padding도 비례 */
border-radius: 0.25em;
}ch — 가독성 컬럼
.prose { max-width: 70ch; } /* 일반 본문 최적 너비 약 65~75ch */전통적 타이포그래피 권장 행 길이 (45~75 글자)를 CSS로 표현하는 단위.
3) Viewport Units — 뷰포트 기준
| 단위 | 의미 |
|---|---|
vw vh | viewport width / height의 1% (CSS2, 정적) |
vmin vmax | min/max(vw, vh) |
vi vb | inline·block 축 (logical) |
svw svh | Small viewport (UI 최대 펼친 상태) |
lvw lvh | Large viewport (UI 최대 숨긴 상태) |
dvw dvh | Dynamic viewport (현재 상태) |
모바일 Safari의 100vh 문제
iOS Safari는 스크롤하면 주소창이 숨겨지고, 뷰포트가 늘어난다. 100vh는 언제나 큰 쪽 (lvh) 기준이라, 주소창이 보이는 상태에서는 화면 아래가 잘린다.
.hero {
/* 과거: 항상 잘림 */
height: 100vh;
/* 모던: 현재 뷰포트에 따라 동적 */
height: 100dvh;
/* 점진 향상 */
height: 100vh;
height: 100dvh;
}| 시나리오 | 100vh | 100svh | 100lvh | 100dvh |
|---|---|---|---|---|
| 주소창 보임 | 877px | 740px | 877px | 740px (현재) |
| 주소창 숨음 | 877px | 740px | 877px | 877px (현재) |
| 회전 | 변할 수 있음 | 변할 수 있음 | 변할 수 있음 | 즉시 반영 |
(아이폰 16 기준 예시 값 — 디바이스마다 다름)
Baseline: 2022년 8월 (Safari 15.4, Chrome 108, Firefox 101).
4) Container Query Units — 컨테이너 기준 (2023+)
| 단위 | 의미 |
|---|---|
cqw cqh | 가장 가까운 containment context의 width/height의 1% |
cqi cqb | inline·block 축 (logical, 권장) |
cqmin cqmax | min/max |
.card-container { container-type: inline-size; }
.card-title { font-size: clamp(1rem, 5cqi, 2rem); }
/* 부모 컨테이너 너비에 따라 폰트가 동적 */이게 진짜 컴포넌트 단위의 반응형이다. vw는 뷰포트에 묶이지만 cqi는 컴포넌트가 놓인 자리에 묶인다. Baseline 2023.
5) % — 컨텍스트 의존
%는 무엇의 %인지가 속성마다 다르다.
| 속성 | %의 기준 |
|---|---|
width | 부모의 content box width |
height | 부모의 명시적 height (없으면 auto로 무시되거나 0) |
margin padding | 부모의 width (top/bottom도!) |
font-size | 부모의 font-size |
line-height | 자신의 font-size |
transform: translate() | 자신의 width/height |
padding-top: 50%이 부모 너비의 50%인 것이 — 과거 aspect-ratio hack의 기반이었다.
6) calc(), min(), max(), clamp() — 단위 연산
.container {
width: min(100%, 1200px); /* 더 작은 쪽 */
padding: max(16px, 5vw); /* 더 큰 쪽 */
font-size: clamp(1rem, 2.5vw, 1.5rem); /* min ≤ pref ≤ max */
margin-top: calc(2rem + env(safe-area-inset-top));
}clamp(MIN, PREFERRED, MAX)는 모던 fluid typography의 핵심.
What-if — 잘못 쓰면
1) px로 모든 것을 잡으면 접근성이 깨진다
사용자가 브라우저 폰트 크기를 키워도 (브라우저 설정 → “글자 크기”), px은 변하지 않는다. WCAG 1.4.4는 200%까지 확대해도 콘텐츠가 깨지지 않아야 한다고 요구한다 — rem 기반 설계가 이를 자동으로 만족한다.
2) 100vh만 쓰면 iOS에서 잘린다
위 표 참조. dvh 또는 100svh(보수적) 또는 progressive enhancement.
3) em을 깊은 트리에 누적
ul { font-size: 0.9em; }
/* 중첩된 ul ul ul ul → 0.9^4 = 65.6%로 작아짐 */해결: rem 또는 :is(ul ul) { font-size: 1em }.
4) width: 100% + padding을 content-box로
→ 이미 01-box-model에서 다룬 가로 스크롤 문제.
5) Container Query 단위를 컨테인먼트 없이 쓰면 0
.title { font-size: 5cqi; } /* 부모에 container-type 없으면 0이거나 viewport fallback */부모에 반드시 container-type: inline-size 또는 container: <name> / inline-size를 줘야 한다.
Insight — dvh는 iOS Safari가 만든 단위다
2020년경 — 트위터에 한 글이 떴다: “왜 모바일 웹사이트의 hero가 항상 잘려 보이지?”
원인은 iOS Safari의 뷰포트 변동. 주소창 영역이 스크롤 상태에 따라 나타났다 사라지면서,
100vh가 의미하는 “뷰포트 높이”가 실제 뷰포트와 어긋났다. 당시 Safari는 항상 가장 큰 상태를vh로 보고했다.
웹 개발자들은 --vh: ${window.innerHeight / 100}px JS hack을 썼다 — resize 이벤트마다 다시 계산하고 CSS 변수를 갱신하는. 이 hack은 수년간 사실상 표준이었다.
2021년 CSS Values 4 명세에 dvh, svh, lvh가 정의되었고, 2022년 Safari 15.4가 가장 먼저 출시했다. 흥미로운 점은 — 문제를 일으킨 브라우저가 해결책도 가장 먼저 표준화했다는 것이다. Apple의 Webkit 팀은 이 변동이 디자인 결정 (스크롤 시 더 많은 컨텐츠 노출)이라 고집했지만, 동시에 개발자에게 4가지 모드를 제공하는 방향을 택했다.
또 하나의 진화는 컨테이너 쿼리 단위 (cqi) 다. 2022년 Chromium이 컨테이너 쿼리를 처음 출시했을 때, 컨테이너의 크기를 단위로 쓰는 아이디어는 거의 동시에 나왔다. 이는 컴포넌트 시스템 시대의 답이다 — “내 폰트 크기는 내가 놓인 곳에 따라 정해진다”. Tailwind v4가 컨테이너 쿼리 단위를 1st-class로 다루는 이유다.
CSS 단위는 세상이 변하면 단위도 추가된다. px만 있던 1995년에서 rem, vh, dvh, cqi까지 — 매번 새 단위는 그 시대의 새 변동을 다룬다.
요약 + Mermaid
- 단위는 “무엇을 기준 삼느냐”의 선언.
rem: 루트 폰트 (전역 토큰).em: 자신 폰트 (컴포넌트 비율).dvh/svh/lvh: 모바일 뷰포트 변동에 대응 (2022 Baseline).cqi/cqw: 컨테이너 기준 — 컴포넌트 반응형의 진짜 답 (2023).clamp(MIN, PREF, MAX): 모던 fluid typography의 표준.%는 속성마다 기준이 다르다 (padding은 부모 너비 기준).