03 — CSS Units

CSS 단위는 측정값이 아니라 “무엇을 기준으로 잴 것인가”의 선언이다. px은 화면 픽셀, rem은 루트 폰트, em은 자신의 폰트, dvh는 동적 뷰포트, cqi는 컨테이너 너비 — 단위를 바꾸면 디자인 시스템의 좌표계가 통째로 바뀐다.


Why — 왜 단위가 중요한가

width: 200pxwidth: 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 — 환경과 무관

단위정의실무
pxCSS 픽셀 (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 vhviewport width / height의 1% (CSS2, 정적)
vmin vmaxmin/max(vw, vh)
vi vbinline·block 축 (logical)
svw svhSmall viewport (UI 최대 펼친 상태)
lvw lvhLarge viewport (UI 최대 숨긴 상태)
dvw dvhDynamic viewport (현재 상태)

모바일 Safari의 100vh 문제

iOS Safari는 스크롤하면 주소창이 숨겨지고, 뷰포트가 늘어난다. 100vh언제나 큰 쪽 (lvh) 기준이라, 주소창이 보이는 상태에서는 화면 아래가 잘린다.

.hero {
  /* 과거: 항상 잘림 */
  height: 100vh;
 
  /* 모던: 현재 뷰포트에 따라 동적 */
  height: 100dvh;
 
  /* 점진 향상 */
  height: 100vh;
  height: 100dvh;
}
시나리오100vh100svh100lvh100dvh
주소창 보임877px740px877px740px (현재)
주소창 숨음877px740px877px877px (현재)
회전변할 수 있음변할 수 있음변할 수 있음즉시 반영

(아이폰 16 기준 예시 값 — 디바이스마다 다름)

Baseline: 2022년 8월 (Safari 15.4, Chrome 108, Firefox 101).

4) Container Query Units — 컨테이너 기준 (2023+)

단위의미
cqw cqh가장 가까운 containment context의 width/height의 1%
cqi cqbinline·block 축 (logical, 권장)
cqmin cqmaxmin/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은 부모 너비 기준).