CSS 단위는 한동안 px, em, %, 인쇄용 pt 정도였다. 지금은 길이 단위만 세어도 마흔 개에 가깝다. rem이 들어오고, 뷰포트 단위가 들어오고, 그 뷰포트 단위가 다시 셋으로 갈라지고, 컨테이너 단위가 들어오고, 글꼴 단위마다 r 붙은 짝이 생겼다.
이걸 전부 외울 필요는 없다. 단위가 늘어난 방향은 하나다. "무엇을 기준으로 재는가"의 선택지가 늘어난 것이다. 기준을 알면 단위는 저절로 나온다.
단위는 기준으로 나뉜다
모든 길이 단위는 아래 다섯 기준 중 하나에 속한다.
- 기준 없음 —
px,pt,cm… 절대 단위 - 글꼴 — 이 요소의 글꼴(
em,ch,lh…) 또는 루트의 글꼴(rem,rch,rlh…) - 뷰포트 — 브라우저 창(
vw,vh,dvh…) - 컨테이너 — 가장 가까운 컨테이너 조상(
cqw,cqi…) - 부모 —
%
바깥일수록 화면에 따라가고, 안쪽일수록 글자에 따라간다. 어떤 값이 무엇과 함께 커져야 하는가를 정하면 기준이 정해지고, 기준이 정해지면 단위는 하나로 좁혀진다.
기준 없음 — px 하나만 기억하면 된다
절대 단위는 일곱 개다. px, pt, pc, in, cm, mm, Q. 서로 고정 비율로 묶여 있다. 1in = 96px = 72pt = 6pc = 2.54cm, 1Q = 0.25mm.
화면에서 쓰는 것은 px뿐이다. 나머지는 인쇄할 때 종이 치수와 맞추려고 있는 단위라, @media print 바깥에서 볼 일이 거의 없다.
px에 대해 하나 짚어둘 것이 있다. CSS의 픽셀은 기기의 실제 픽셀이 아니다. 1/96인치를 기준으로 한 참조 픽셀이고, 실제 화소 몇 개로 그릴지는 devicePixelRatio가 정한다. 레티나 화면에서 1px이 화소 2개로 그려지는 이유다. 그래서 px는 "절대"라기보다 "글꼴 크기나 화면 크기에 따라 변하지 않는 단위"로 이해하는 편이 맞다.
쓸 자리: 테두리, 그림자, 1px 구분선, 아이콘의 최소 크기처럼 글자가 커져도 같이 커지면 안 되는 것. 글자 크기와 여백에는 쓰지 않는다.
글꼴 기준
가장 많이 쓰고 가장 많이 헷갈리는 계열이다. 두 갈래로 나뉜다. 이 요소의 글꼴을 기준으로 하는 것과, 루트(html)의 글꼴을 기준으로 하는 것. 후자는 앞에 r이 붙는다.
em — 내 글꼴 크기
1em은 그 요소의 font-size다. font-size: 20px인 요소에서 padding: 1em은 20px이다.
함정은 복리다. font-size 자체를 em으로 주면 부모의 글꼴 크기를 기준으로 하므로, 중첩될 때마다 곱해진다.
li { font-size: 0.9em; }
/* ul > li > ul > li 는 0.9 × 0.9 = 0.81em */
/* 한 단계 더 들어가면 0.729em — 목록이 깊어질수록 작아진다 */
그래서 em은 글꼴 크기가 아니라 글꼴 크기에 비례해야 하는 다른 속성에 쓴다. 버튼의 패딩, 아이콘의 크기, 밑줄 두께. 버튼 글자를 키우면 패딩도 같이 커져야 모양이 유지되는데, em이 그걸 공짜로 해준다.
rem — 루트의 글꼴 크기
1rem은 html의 font-size다. 어디에 있든 같은 값이라 복리가 없다. 기본값은 대부분의 브라우저에서 16px이지만, 사용자가 브라우저 설정에서 바꿀 수 있는 값이다.
이 마지막 문장이 rem을 쓰는 이유의 전부다. 글자를 크게 보고 싶은 사용자가 브라우저 기본 글꼴을 20px로 올리면, rem으로 잡은 글꼴·여백·컬럼 폭이 전부 25% 커진다. px로 잡았으면 아무것도 안 바뀐다.
흔한 트릭 하나를 경계할 것. html { font-size: 62.5% }로 1rem = 10px을 만드는 방식이다. 계산이 편하긴 한데, 1rem = 16px을 전제로 만들어진 외부 컴포넌트가 전부 작아진다. 부트스트랩 같은 프레임워크를 같이 쓰면 바로 부딪힌다. 계산이 불편하면 CSS 변수로 간격 체계를 만드는 쪽이 낫다.
쓸 자리: 글꼴 크기, 여백, 간격 체계, 컬럼 폭, 브레이크포인트. 기본값으로 rem을 잡고 이유가 있을 때만 다른 단위로 바꾼다고 생각하면 대개 맞는다.
ex · cap · ch · ic — 글자 모양에서 나온 단위
이 넷은 글꼴 크기가 아니라 실제 글리프의 치수를 기준으로 한다. 같은 16px이라도 글꼴이 바뀌면 값이 달라진다.
ex— 소문자 x의 높이. 오래됐지만 거의 안 쓴다.cap— 대문자의 높이. 아이콘을 대문자 높이에 정확히 맞출 때 쓴다.ch— 숫자0의 전진 폭. "글자 몇 개 폭"이라는 뜻으로 쓴다.ic— 한자水의 전진 폭. CJK 글자 한 칸이다.
ch는 max-width: 65ch처럼 본문 한 줄의 길이를 제한할 때 널리 쓰인다. 다만 이름과 달리 "글자 한 개"가 아니라 숫자 0 한 개의 폭이라, 한글에서는 어긋난다. 한글 본문이면 ic가 맞는 단위다. 이 이야기는 따로 한 편으로 썼다.
lh · rlh — 줄 높이
1lh는 그 요소의 line-height 계산값, 1rlh는 루트의 것이다. 2023년 말에 모든 브라우저에 들어왔는데 아직 덜 알려져 있다.
쓸 자리는 "몇 줄"이라는 말이 나오는 곳 전부다.
textarea { min-height: 3lh; max-height: 20lh; } /* 세 줄에서 스무 줄 */
.icon { height: 1lh; } /* 글줄 높이에 딱 맞는 아이콘 */
p + p { margin-top: 1rlh; } /* 본문 줄 하나만큼의 문단 간격 */
line-height가 바뀌면 이 값들이 전부 따라온다. 전에는 1.5em 같은 식으로 줄 높이를 손으로 다시 계산해 넣었는데, 그 계산이 사라진다. textarea 높이 글에서 rows 대신 쓴 것이 이 단위다.
r 접두사 — rex · rcap · rch · ric
rem·rlh와 같은 원리로, 글리프 단위 넷에도 루트 기준 짝이 생겼다. 2026년 1월에 Baseline이 됐다. 컴포넌트 안에서 글꼴이 바뀌어도 본문 기준 폭을 유지하고 싶을 때 쓴다. 예를 들어 사이드바 글꼴이 작아도 컬럼 폭은 본문 기준 60rch로 고정하는 식이다.
뷰포트 기준
vw · vh · vmin · vmax
1vw는 뷰포트 폭의 1%, 1vh는 높이의 1%다. 그러니 100vw는 뷰포트 폭 전체, 100vh는 높이 전체다. vmin·vmax는 둘 중 작은 쪽·큰 쪽이다. 화면을 꽉 채우는 히어로 영역, 화면 폭에 따라 움직이는 글꼴 크기에 쓴다.
두 가지 함정이 유명하다.
100vw는 스크롤바를 포함한다. 세로 스크롤바가 자리를 차지하는 환경(윈도우 데스크톱)에서 width: 100vw는 스크롤바 폭만큼 넘쳐 가로 스크롤을 만든다. 폭은 100%나 auto로 두는 것이 안전하다. 모바일 가로 스크롤 범인 찾기에서 자주 걸리는 원인이기도 하다.
100vh는 모바일에서 화면보다 크다. 이건 절이 하나 필요하다.
100vh가 모바일에서 틀리는 이유 — svh · lvh · dvh
모바일 브라우저는 스크롤에 따라 주소창이 나타났다 사라진다. 그래서 "뷰포트 높이"가 하나가 아니다. 주소창이 보일 때의 작은 높이와, 숨었을 때의 큰 높이가 있다.
전통적인 vh는 큰 쪽으로 계산된다. 그래서 페이지를 막 열어 주소창이 보이는 상태에서 height: 100vh는 화면 아래로 삐져나가고, 하단에 둔 버튼이 주소창 뒤에 숨는다. 오랫동안 JS로 innerHeight를 읽어 CSS 변수에 넣는 우회를 썼던 문제다.
2022년 말에 단위 셋이 이 문제를 나눠 가졌다.
svh— small. 주소창이 보일 때의 높이. 항상 화면 안에 들어온다.lvh— large. 주소창이 숨었을 때의 높이. 기존vh와 같다.dvh— dynamic. 지금 이 순간의 높이. 주소창이 움직이면 값도 움직인다.
고르는 기준은 이렇다. 반드시 화면 안에 들어와야 하는 것(전체 화면 모달, 하단 고정 버튼이 있는 레이아웃)은 svh. 배경처럼 넘쳐도 되는 것은 lvh. dvh는 정확하지만 스크롤할 때마다 값이 바뀌어 레이아웃이 다시 계산된다. 높이에 따라 위치가 바뀌는 요소가 많으면 스크롤 중에 화면이 출렁인다. 채팅 입력창처럼 정말 현재 높이가 필요한 곳에만 쓴다.
같은 접두사가 vw·vmin·vmax에도 붙는다(svw, dvmax …). 실제로 쓰는 것은 높이 쪽이다.
vi · vb — 논리 방향
vi는 인라인 방향(가로쓰기에서 폭), vb는 블록 방향(높이)이다. writing-mode가 세로면 둘이 뒤바뀐다. 다국어·세로쓰기를 다루지 않으면 vw·vh와 같다.
컨테이너 기준 — cqw · cqi …
뷰포트 단위의 한계는 컴포넌트가 어디에 놓였는지 모른다는 것이다. 같은 카드가 본문 컬럼에 있을 때와 사이드바에 있을 때, 뷰포트는 같지만 쓸 수 있는 폭은 다르다.
컨테이너 단위는 가장 가까운 컨테이너 조상의 크기를 기준으로 한다. 조상에 container-type을 선언해야 한다.
.card-list { container-type: inline-size; }
.card h3 {
font-size: clamp(1rem, 4cqi, 1.5rem); /* 컨테이너 폭의 4% */
padding: 2cqi;
}
1cqw는 컨테이너 폭의 1%다. 컨테이너가 600px이면 1cqw = 6px, 300px이면 3px. 뷰포트 단위와 계산법은 같고, 재는 상자가 브라우저 창에서 담긴 곳으로 바뀐 것뿐이다. 위 코드의 padding: 2cqi는 본문 컬럼(600px)에서 12px, 사이드바(300px)에서 6px이 된다.
clamp()는 처음 보는 것일 수 있는데, 뒤의 반응형 절에서 min()·max()와 함께 설명한다. 여기서는 "컨테이너 폭의 4%로 하되 1rem에서 1.5rem 사이를 벗어나지 않게" 정도로 읽으면 된다.
cqw · cqh 와 cqi · cqb — 물리 방향과 논리 방향
단위는 여섯 개인데 실은 같은 것을 두 가지 이름으로 부르는 셋 쌍이다.
| 물리 방향 | 논리 방향 | 가로쓰기(한국어·영어)에서 |
|---|---|---|
cqw — 폭(width)의 1% | cqi — 인라인(inline) 방향의 1% | 둘이 같다 |
cqh — 높이(height)의 1% | cqb — 블록(block) 방향의 1% | 둘이 같다 |
cqmin · cqmax | — | cqi·cqb 중 작은 쪽 · 큰 쪽 |
인라인 방향은 글자가 흘러가는 방향, 블록 방향은 줄이 쌓이는 방향이다. 우리가 쓰는 가로쓰기에서는 인라인 = 가로 = 폭, 블록 = 세로 = 높이라서 cqi와 cqw는 완전히 같은 값이다. 둘이 갈리는 것은 writing-mode: vertical-rl 같은 세로쓰기뿐이다. 그때는 글자가 위에서 아래로 흐르니 인라인 방향이 세로가 되고, cqi는 높이를, cqb는 폭을 가리킨다. 앞서 본 뷰포트의 vi·vb와 vw·vh의 관계가 정확히 이것이다.
그러면 세로쓰기를 안 하는데 왜 cqi를 쓰나. container-type 값 때문이다.
container-type: inline-size— 인라인 축만 컨테이너로 만든다. 폭은 재지만 높이는 재지 않는다. 대부분의 경우 이걸 쓴다.container-type: size— 양쪽 축 다 컨테이너로 만든다. 단, 이렇게 선언한 요소는 자기 높이를 내용으로 정할 수 없게 된다(높이가 내용에 따라 변하면 그걸 기준으로 삼는 자식과 순환이 생기기 때문). 높이를 밖에서 명시해 줘야 하고, 안 주면 0이 된다.
그래서 실무에서는 inline-size를 선언하고 인라인 축 단위인 cqi를 짝으로 쓴다. 이름이 선언과 맞아떨어져서 "이 값은 폭에서 왔다"가 코드에 그대로 드러난다. cqw를 써도 값은 같지만, cqh나 cqb는 inline-size 컨테이너에서는 기준이 없어 작은 뷰포트 높이(svh)로 대체된다. 높이 기준 단위가 필요하면 size 컨테이너와 명시적 높이가 먼저다.
컨테이너 조상이 없으면 작은 뷰포트 단위(sv*)로 대체된다. 그래서 선언을 빠뜨려도 깨지지는 않고, 다만 뷰포트 기준으로 동작한다. 이게 디버깅을 헷갈리게 만드니 기억해둘 것.
% — 부모 기준, 그런데 무엇의 부모인가
%는 가장 오래된 상대 단위인데, 속성마다 기준이 다르다는 점 때문에 가장 자주 사람을 속인다.
width,margin,padding— 부모의 폭. 세로 여백도 폭 기준이다.padding-top: 56.25%로 16:9 비율을 만들던 트릭이 이 성질을 이용한 것인데, 지금은aspect-ratio: 16 / 9로 대체됐다.height— 부모의 높이. 단, 부모 높이가 확정돼 있을 때만. 부모가auto면height: 100%는 무시된다. "왜 100%가 안 먹지"의 대부분이 이것이다.font-size— 부모의 글꼴 크기.em과 같다(100% = 1em).line-height— 자기 글꼴 크기. 여기서%와 단위 없는 숫자가 갈리는데, 다음 절에서.transform: translate()— 자기 자신의 크기. 부모가 아니다.
쓸 자리: 부모에 비례하는 것이 곧 의미인 곳. 2단 레이아웃의 width: 50%, 이미지의 max-width: 100%. 그 외에는 대개 더 정확한 기준을 가진 단위가 있다.
길이가 아닌 단위들
길이 말고도 단위가 있다. 잠깐씩만.
- 각도 —
deg,rad,grad,turn.1turn = 360deg. 회전과 그라디언트 방향.0.25turn이90deg보다 읽기 쉽다. - 시간 —
s,ms. 전환과 애니메이션. - 해상도 —
dpi,dpcm,dppx(x). 미디어 쿼리(min-resolution: 2dppx)로 레티나를 구분한다. fr— 그리드에서 남는 공간의 비율. 길이가 아니라서calc()에 길이와 섞어 넣을 수 없다. 그리드 트랙 크기 전용이다.
단위 없는 숫자 — line-height는 숫자로
line-height: 1.5와 line-height: 150%는 그 요소에서는 같은 값이다. 다른 점은 자식에게 상속될 때다. 150%는 계산된 픽셀값이 상속되고, 1.5는 비율이 상속된다. 자식의 글꼴이 크면 150%로 상속받은 줄 높이는 그대로라 글자가 줄에 안 들어가고, 1.5로 상속받으면 자기 글꼴 기준으로 다시 계산된다. 그래서 line-height는 숫자로 준다.
반응형에서 단위를 조합하기
단위가 늘어난 진짜 이유는 여기에 있다. min(), max(), clamp()가 들어오면서 서로 다른 기준의 단위를 한 값 안에서 섞을 수 있게 됐다. 미디어 쿼리로 값을 갈아 끼우던 일이 상당 부분 한 줄로 줄어든다.
min() · max() · clamp() — 세 함수부터
이름이 헷갈리기 쉬운데, 뜻은 단순하다.
min(a, b)— 둘 중 작은 값. "이 값을 넘지 말 것"이라는 상한.max(a, b)— 둘 중 큰 값. "이 값 밑으로 내려가지 말 것"이라는 하한.clamp(최소, 선호, 최대)— 선호값을 쓰되 최소보다 작아지면 최소로 올리고, 최대보다 커지면 최대로 자른다.min(max(최소, 선호), 최대)를 한 번에 쓴 것이다.
앞의 컨테이너 절에 나온 font-size: clamp(1rem, 4cqi, 1.5rem)으로 보면 이렇다. 컨테이너가 300px이면 4cqi는 12px인데 최소 16px보다 작으니 16px이 된다. 500px이면 20px, 구간 안이라 그대로다. 700px이면 28px인데 최대 24px보다 크니 24px에서 잘린다.
세 함수의 진짜 값어치는 인자마다 다른 단위를 넣을 수 있다는 데 있다. 최소는 rem, 선호는 cqi나 vw, 최대는 다시 rem — 기준이 다른 단위를 한 값 안에서 섞는 것이다. 아래 예들이 전부 그 조합이다.
유동 글꼴 — 왜 rem과 vw를 섞는가
h1 {
font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
}
화면이 좁으면 1.5rem, 넓으면 3rem, 사이에서는 폭에 따라 매끄럽게 움직인다. 미디어 쿼리 두세 개가 사라진다.
가운데 값이 2.5vw가 아니라 1rem + 2.5vw인 데 이유가 있다. 순수 vw는 브라우저 확대에 반응하지 않는다. 사용자가 Ctrl + 로 200% 확대해도 뷰포트 폭은 그대로라 글자가 안 커진다. 국내 지침인 KWCAG 2.2(한국형 웹 콘텐츠 접근성 지침, KS X OT0003:2022)에는 텍스트 확대를 직접 다루는 검사항목이 없지만, 그 바탕이 된 WCAG는 1.4.4(텍스트 크기 조정)에서 200% 확대를 요구한다. 지침에 번호가 있든 없든, 확대해도 안 커지는 글자는 접근성 문제다. rem을 섞어두면 그 부분만큼은 확대에 따라오고, 양끝의 clamp 한계값도 rem이라 확대 시 같이 커진다.
폭 — 컨테이너를 넘지 않되 읽기 좋은 폭까지만
.prose {
width: min(100%, 38ic); /* 한글 본문: 한 줄 38자 */
margin-inline: auto;
}
min()은 둘 중 작은 값을 고른다. 좁은 화면에서는 100%가 이기고, 넓은 화면에서는 38ic가 이긴다. max-width와 width를 따로 쓰던 것이 한 줄이 된다.
모바일 전체 높이
.modal-fullscreen { height: 100svh; } /* 주소창이 있어도 다 보인다 */
.hero { min-height: 100lvh; } /* 배경은 넘쳐도 된다 */
컴포넌트 단위 반응형
.sidebar, .main { container-type: inline-size; }
.card { padding: clamp(0.75rem, 3cqi, 1.5rem); }
.card img { width: clamp(4rem, 30cqi, 12rem); }
같은 카드가 본문에서는 넉넉하고 사이드바에서는 촘촘하다. 카드 자신은 자기가 어디 있는지 모르고, 담긴 곳의 폭에만 반응한다.
언제 무엇을 쓰나
| 이 값은 …와 함께 커져야 한다 | 단위 | 예 |
|---|---|---|
| 아무것과도 안 커진다 | px | 테두리, 그림자, 1px 선 |
| 사용자의 글꼴 설정 | rem | 글꼴 크기, 여백, 간격 체계, 브레이크포인트 |
| 이 요소의 글꼴 | em | 버튼 패딩, 인라인 아이콘 |
| 글자 수 | ch · ic | 본문 폭, 코드 폭, 입력란 폭 |
| 줄 수 | lh · rlh | textarea 높이, 문단 간격, 줄 맞춤 아이콘 |
| 화면 크기 | vw · svh · lvh · dvh | 전체 화면 영역, 유동 글꼴의 가변 부분 |
| 담긴 곳의 크기 | cqi · cqw | 재사용 컴포넌트 내부 치수 |
| 부모 크기 | % | 컬럼 폭, 이미지 max-width |
브라우저 지원
오래된 것(px·em·rem·%·ex·ch·vw·vh)은 걱정할 것이 없다. 최근 것만 적는다.
| 단위 | Chrome | Safari | Firefox | Baseline |
|---|---|---|---|---|
ic | 106 | 15.4 | 97 | 2022-10 |
svh · lvh · dvh | 108 | 15.4 | 101 | 2022-12 |
cqw · cqi … | 105 | 16 | 110 | 2023-02 |
lh · rlh | 109 · 111 | 16.4 | 120 | 2023-11 |
cap | 118 | 17.2 | 97 | 2023-12 |
rex · rch · ric · rcap | 111 (rcap 118) | 17.2 | 147 | 2026-01 |
출처: webstatus.dev, 2026-09-04 조회. 컨테이너 단위는 컨테이너 쿼리와 함께 들어왔다.
마지막 줄의 r 접두사 넷만 올해 것이고, 나머지는 2~3년 됐다. 파이어폭스가 늦게 붙은 것이 많아 Baseline 날짜가 늦어 보이지만, 실제 방문자 기준으로는 대부분 이미 지원 상태다. 미지원 브라우저에서 새 단위는 그 선언 하나가 무시되므로, 걱정되는 곳은 구식 단위로 한 줄 먼저 쓰고 새 단위로 덮어쓰면 된다.
.modal { height: 100vh; height: 100svh; }
정리
단위는 마흔 개지만 질문은 하나다. 이 값은 무엇과 함께 커져야 하는가.
- 아무것과도 —
px - 사용자 설정과 —
rem(기본값) - 이 요소의 글자와 —
em, 글자 수는ch·ic, 줄 수는lh - 화면과 —
vw, 높이는svh·lvh·dvh중 골라서 - 담긴 곳과 —
cqi - 부모와 —
%, 단 속성마다 기준이 다르다
그리고 clamp()·min()·max() 안에서 이 기준들을 섞는 순간, 미디어 쿼리의 상당 부분이 필요 없어진다. 단위가 늘어난 것은 외울 것이 늘어난 게 아니라 미디어 쿼리 없이 말할 수 있는 문장이 늘어난 것이다.