디자인에서 두 가지 조건을 받았다고 가정해 봅시다. 제목은 320px 화면에서 16px, 1440px 화면에서 32px여야 합니다. 의도는 분명하지만 구현 방법은 그렇지 않습니다. 미디어 쿼리로 두 크기 사이를 전환할 수도 있고, clamp(1rem, 2.5vw, 2rem)처럼 그럴듯한 값을 직접 쓸 수도 있습니다. 하지만 이렇게 만든 식이 두 디자인 목표점을 정확히 지나는 것은 아닙니다.
정확한 유동형 크기에는 추측이 필요하지 않습니다. 두 쌍의 “뷰포트 너비—속성 값”이 하나의 직선을 결정하고, 그 직선을 고정 경계가 있는 CSS 뷰포트 식으로 표현할 수 있습니다.
이 글에서는 수식을 유도하고 검증하는 방법, 증가·감소 범위, rem과 px의 선택 기준을 설명합니다. JavaScript, Python, Bash로 작성한 작은 생성기도 함께 제공합니다. 목표는 계산기에서 알 수 없는 숫자를 복사하는 것이 아니라 코드 리뷰에서 근거를 설명할 수 있는 CSS를 만드는 것입니다.
clamp()가 실제로 하는 일
CSS clamp()는 쉼표로 구분된 세 개의 계산식을 받습니다.
property: clamp(minimum, preferred, maximum);
브라우저는 가운데 선호 표현식을 계산한 뒤 결과를 하한과 상한 사이로 제한합니다. 공식적으로는 다음 식과 같습니다.
max(minimum, min(preferred, maximum))
이 정의가 중요한 이유는 가운데 값이 “기본값”이 아니기 때문입니다. 현재 뷰포트에서 브라우저가 계산하는 반응형 규칙이며, 그 결과가 허용 범위를 벗어날 때만 양쪽 경계가 대신 적용됩니다.
예를 들어 다음 선언을 살펴봅시다.
font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);
동작은 세 구간으로 나뉩니다.
- 좁은 화면에서는 선호 값이
1rem보다 작아지므로 최솟값이 적용됩니다. - 선택한 두 끝점 사이에서는 가운데 식이 뷰포트에 따라 선형으로 변합니다.
- 넓은 화면에서는 선호 값이
2rem보다 커지므로 최댓값이 적용됩니다.
MDN의 clamp() 문서는 이 기능을 최신 브라우저에서 널리 사용할 수 있는 것으로 안내합니다. 비교 함수의 공식 동작은 CSS Values and Units Level 4에 정의되어 있습니다.
clamp(1rem, 2.5vw, 2rem)만으로 부족한 이유
단순한 뷰포트 항이 유용한 경우도 있지만, 여기에는 기울기만 있고 독립적으로 정한 절편이 없습니다. 따라서 임의의 두 디자인 목표점을 동시에 정확히 통과하기 어렵습니다.
흔히 볼 수 있는 다음 선언을 확인해 보겠습니다.
font-size: clamp(1rem, 2.5vw, 2rem);
320px 뷰포트에서 2.5vw는 8px이므로 최솟값에 의해 16px이 됩니다. 1440px에서는 2.5vw가 36px이므로 최댓값에 의해 32px이 됩니다. 결국 양쪽 경계에는 도달하지만 실제 유동 구간은 320px1440px가 아니라 640px1280px입니다. 880px에서는 22px이 나오지만, 디자인 끝점 사이를 직선으로 보간하면 24px이어야 합니다.
빠진 것은 절편입니다. 두 끝점을 정확히 통과하려면 선호 표현식에 고정 항과 뷰포트 항이 함께 필요한 경우가 많습니다.
fixed-intercept + viewport-slope
이는 직선 방정식 y = mx + b와 같습니다.
두 끝점에서 수식 유도하기
먼저 네 가지 입력을 정의합니다.
v₁: 최소 뷰포트 너비v₂: 최대 뷰포트 너비s₁: 최소 뷰포트에서의 값s₂: 최대 뷰포트에서의 값
기울기는 값의 변화량을 뷰포트 변화량으로 나눈 값입니다.
m = (s₂ - s₁) / (v₂ - v₁)
절편은 이 직선을 너비가 0인 뷰포트까지 연장했을 때의 값입니다.
b = s₁ - m × v₁
CSS에서 1vw는 뷰포트 너비의 1%이므로 픽셀당 기울기에 100을 곱하면 vw 계수가 됩니다.
preferred = b + (m × 100)vw
계산 예시: 320px에서 16px, 1440px에서 32px
먼저 기울기를 계산합니다.
m = (32 - 16) / (1440 - 320)
= 16 / 1120
= 0.0142857143
다음으로 절편을 계산합니다.
b = 16 - 0.0142857143 × 320
= 11.4285714px
기울기를 vw로 변환합니다.
m × 100 = 1.42857143
따라서 px 결과는 다음과 같습니다.
font-size: clamp(16px, 11.4286px + 1.4286vw, 32px);
루트 글꼴 크기가 16px이라면 고정 길이를 16으로 나눕니다. vw 계수는 바뀌지 않습니다.
font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);
두 끝점 검증하기
320px에서 1vw는 3.2px입니다.
11.4286px + 1.4286 × 3.2px ≈ 16px
1440px에서 1vw는 14.4px입니다.
11.4286px + 1.4286 × 14.4px ≈ 32px
이 끝점 검사만으로도 대부분의 수동 계산 실수를 찾을 수 있습니다. 반올림을 마지막에 해야 하는 이유도 드러납니다. 절편을 계산하기 전에 기울기를 반올림하면 오차가 누적되어 한쪽 끝점이 눈에 띄게 달라질 수 있습니다.
실용적인 계산기 사용 흐름
ZeroTool CSS clamp() 계산기의 입력 방식은 디자인 전달 자료에 가깝게 구성되어 있습니다.
font-size,padding,margin,gap,width또는 사용자 정의 속성을 선택합니다.- 최소 및 최대 뷰포트 너비를 픽셀로 입력합니다.
- 각 뷰포트 끝점에서 필요한 속성 값을 입력합니다.
- px 또는 rem 출력을 선택합니다. rem이라면 루트 글꼴 크기도 확인합니다.
- 다섯 개의 샘플 값을 확인하고 미리보기 슬라이더를 움직입니다.
- 전체 CSS 선언이나 생성된 값만 복사합니다.
두 점이 결정하는 것은 한 개의 직선이므로 곡선은 의도적으로 선형입니다. 샘플 표에는 중간점과 사분점도 표시되어 수학적으로는 맞지만 디자인 감각에는 맞지 않는 범위를 쉽게 찾을 수 있습니다.
계산기는 단순한 코드 조각이 자주 놓치는 세 가지 경우도 처리합니다.
- 역방향 범위: 뷰포트가 넓어질수록 값이 작아집니다.
- 고정 범위: 두 끝점 값이 같으면 하나의 고정 길이로 단순화합니다.
- 음수 값: 음수 길이에 의미가 있을 수 있는 margin과 사용자 정의 속성에서 허용합니다.
계산과 상태 저장은 모두 브라우저 안에서 이루어집니다. 초기화하면 기본값으로 돌아가고 저장된 폼 항목도 삭제됩니다.
rem과 px 선택하기
수식에서 단위를 섞어 사용해도 됩니다. 이는 편법이 아니라 호환 가능한 길이를 조합하도록 설계된 CSS 수학 함수의 정상적인 사용법입니다.
텍스트와 사용자 설정을 따라야 하는 시스템에는 rem
rem 출력을 사용하면 최솟값, 최댓값, 고정 절편이 루트 글꼴 크기에 대한 상대값이 됩니다.
.hero-title {
font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);
}
따라서 수식의 일부가 사용자의 글꼴 크기 설정과 관계를 유지합니다. 타이포그래피와 글꼴 시스템에 맞춰 확대되어야 하는 간격 토큰에는 대체로 rem이 더 나은 시작점입니다.
변환에는 여전히 명확한 루트 크기가 필요합니다. 프로젝트에서 html { font-size: 62.5%; }를 사용하지만 계산기를 16px로 설정하면 해당 코드베이스와 맞지 않는 rem 값이 나옵니다. 계산기의 루트 입력을 실제 디자인 시스템이 가정하는 값과 맞추세요.
끝점이 실제 고정 치수라면 px
정확한 CSS 픽셀 끝점에 맞춰야 하는 시각적 치수에는 px 출력이 유용합니다.
.content-shell {
padding-inline: clamp(20px, 12px + 2.5vw, 48px);
}
CSS 픽셀은 물리적 기기 픽셀과 같지 않지만 CSS 레이아웃 안에서는 고정 길이입니다. 텍스트에 적용하면 사용자 글꼴 설정과의 관계가 rem보다 약해질 수 있습니다.
vw 항은 계속 뷰포트에 상대적입니다
rem을 선택해도 전체 식이 글꼴 상대값으로 바뀌지는 않습니다. 가운데 식에는 여전히 vw가 포함되며 뷰포트 너비에 반응합니다. 고정 항과 뷰포트 상대 항은 모든 사용자 설정에 똑같이 반응하지 않으므로 확대와 텍스트 크기 변경 테스트가 여전히 필요합니다.
끝점 변환을 별도로 확인하려면 CSS 단위 변환기를 사용하세요.
역방향 유동 스케일
화면이 넓어질수록 값을 줄여야 할 때도 있습니다. 작은 모바일 카드에는 터치 편의를 위해 더 큰 세로 패딩이 필요하지만, 밀도가 높은 데스크톱 레이아웃에는 더 작은 패딩이 적합할 수 있습니다. 320px에서 32px, 1440px에서 16px을 입력하면 음의 기울기가 나옵니다.
padding-block: clamp(1rem, 2.2857rem - 1.4286vw, 2rem);
작은 뷰포트에서 선호 식은 2rem, 큰 뷰포트에서는 1rem으로 계산됩니다.
여기에는 중요한 구현 세부 사항이 있습니다. CSS는 여전히 작은 경계를 먼저, 큰 경계를 마지막에 요구합니다. 스케일 방향이 반대라고 해서 clamp(2rem, ..., 1rem)을 출력하면 안 됩니다. 경계는 1rem, 2rem 순으로 유지하고 음의 vw 계수가 방향을 표현합니다.
CSS 명세에서는 작성자가 충돌하는 경계를 제공했을 때 최소 계산이 우선한다고 정의합니다. 경계를 정렬하면 이 함정을 피하고 의도대로 동작하게 할 수 있습니다.
고정 값도 올바른 결과입니다
두 끝점이 모두 16px이라면 기울기는 0입니다. 기술적으로는 다음과 같이 긴 형태도 가능합니다.
font-size: clamp(1rem, 1rem + 0vw, 1rem);
하지만 추가 정보를 전달하지 않습니다. 가장 작고 올바른 결과는 다음과 같습니다.
font-size: 1rem;
모든 입력 쌍에 반응형 계산이 필요한 것처럼 보이게 하지 말고 이 경우는 단순화해야 합니다. 디자인 시스템에서도 보간이 실제 레이아웃 목적을 가질 때만 유동형 토큰을 사용하세요.
유동형 텍스트가 자동으로 접근성을 보장하지는 않습니다
유동형 타이포그래피가 해결하는 것은 시각적 보간입니다. 이것만으로 접근성 요구 사항을 충족하지는 못합니다.
WCAG 2.2 성공 기준 1.4.4는 명시된 예외를 제외하고 콘텐츠나 기능 손실 없이 텍스트를 200%까지 확대할 수 있어야 한다고 요구합니다. MDN의 clamp() 접근성 지침은 텍스트 확대를 너무 일찍 제한하지 않는 한 가지 방법으로, 상대 길이 최댓값을 최솟값의 두 배 이상으로 두는 방식을 권합니다.
이 비율은 유용한 경고일 뿐 준수 인증이 아닙니다. 실제 동작을 다음과 같이 확인해야 합니다.
- 페이지를 200%로 확대합니다.
- 텍스트를 읽을 수 있고 어떤 컨트롤도 사라지지 않는지 확인합니다.
- 레이블이 잘리지 않고 줄바꿈되는지 확인합니다.
- 고정 높이 컨테이너가 콘텐츠를 가리지 않는지 확인합니다.
- 텍스트를 확대한 상태에서 좁은 레이아웃을 다시 테스트합니다.
W3C G179 기법도 같은 결과에 초점을 둡니다. 텍스트가 커져도 모든 콘텐츠와 기능을 사용할 수 있어야 합니다.
font-size 범위가 2대 1보다 좁으면 ZeroTool 계산기가 안내 메시지를 표시합니다. 제품마다 제약이 다르고 실제 컴포넌트 테스트만으로 적합성을 판단할 수 있으므로 CSS 생성을 막지는 않습니다.
흔한 실수와 원인
1. 가운데 값을 평균으로 생각하기
선호 값은 표현식이지 최솟값과 최댓값의 중간값이 아닙니다. clamp(1rem, 1.5rem, 2rem)처럼 가운데가 고정 값이라면 뷰포트 너비에 반응하지 않고 항상 1.5rem이 됩니다.
2. 끝점을 확인하지 않고 vw만 사용하기
2vw는 한 화면 너비에서 좋아 보이더라도 다른 모든 너비에서 디자인과 어긋날 수 있습니다. 배포 전에 절편을 계산하거나 두 끝점을 모두 검증하세요.
3. 기울기를 너무 일찍 반올림하기
표시되는 CSS는 소수점 네 자리만 사용해도 되지만 계산은 직렬화 직전까지 전체 정밀도를 유지해야 합니다. 이른 반올림은 절편을 이동시켜 끝점 검사를 실패하게 합니다.
4. + 또는 - 주변의 공백 생략하기
CSS 수학 구문에서는 이항 덧셈과 뺄셈 연산자 양쪽에 공백이 필요합니다. 다음처럼 작성하세요.
clamp(1rem, 0.7143rem + 1.4286vw, 2rem)
다음 형식은 피해야 합니다.
clamp(1rem, 0.7143rem+1.4286vw, 2rem)
5. 뷰포트 구간을 0으로 만들기
최소 뷰포트와 최대 뷰포트 너비가 같으면 기울기를 계산할 때 0으로 나누게 됩니다. 고정 값이 필요했던 것인지, 서로 다른 두 뷰포트 끝점을 입력해야 하는지 결정하세요.
6. 모든 루트 글꼴이 16px이라고 가정하기
16px은 흔한 기본값이지만 모든 프로젝트의 계약은 아닙니다. px 끝점을 rem으로 변환하기 전에 루트 스타일과 사용자 글꼴 설정을 확인하세요.
7. 컨테이너 크기에 따라야 하는 컴포넌트에 뷰포트 보간 사용하기
넓은 데스크톱 뷰포트 안에서도 카드는 좁을 수 있습니다. vw는 카드가 아니라 페이지에 반응합니다. 독립적으로 재사용되는 컴포넌트에는 직접 작성한 컨테이너 쿼리와 컨테이너 상대 단위가 더 적합한지 검토하세요. 이 계산기는 의도적으로 뷰포트 기반 CSS를 생성합니다.
8. 모든 토큰에 유동 값을 적용하기
테두리, 아이콘 선, 기준선 그리드처럼 정확해야 하는 세부 요소에는 고정된 값이 더 적합한 경우가 많습니다. 유동화는 디자인 결정이지 기본 리팩터링이 아닙니다.
작은 글꼴 및 간격 시스템 만들기
끝점이 승인되면 수식을 컴포넌트 곳곳에 흩어 놓지 말고 사용자 정의 속성으로 저장하세요.
:root {
--font-body: clamp(1rem, 0.9643rem + 0.1786vw, 1.125rem);
--font-title: clamp(2rem, 1.5714rem + 2.1429vw, 3.5rem);
--space-section: clamp(3rem, 2.1429rem + 4.2857vw, 6rem);
--space-card: clamp(1rem, 0.8571rem + 0.7143vw, 1.5rem);
}
body {
font-size: var(--font-body);
}
h1 {
font-size: var(--font-title);
}
.section {
padding-block: var(--space-section);
}
토큰은 역할에 따라 이름을 붙이고, 디자인 시스템 문서에 끝점 가정을 기록하며, 생성된 값은 한곳에서 관리하세요. 수식이 확정되면 CSS 변수 생성기로 최종 :root 블록을 정리할 수 있습니다.
디자인 검토 없이 한 쌍의 끝점만으로 전체 모듈형 글꼴 스케일을 생성하지 마세요. 선형 보간은 “이 토큰 하나가 두 점 사이에서 어떻게 움직이는가”에 답합니다. 모든 제목 단계가 같은 비율이나 끝점을 공유해야 하는지는 결정하지 않습니다.
JavaScript로 clamp 값 생성하기
일회성 작업에는 브라우저 기반 계산기가 가장 빠릅니다. 빌드 도구나 토큰 파이프라인에서는 같은 계산을 의존성 없는 작은 함수로 유지할 수 있습니다.
function clampFromPoints({
minViewport,
maxViewport,
minValue,
maxValue,
unit = 'rem',
root = 16,
}) {
if (!(maxViewport > minViewport)) {
throw new Error('maxViewport must be greater than minViewport');
}
if (unit === 'rem' && !(root > 0)) {
throw new Error('root must be greater than zero');
}
const round = value => Number(value.toFixed(4));
const length = px =>
unit === 'rem' ? `${round(px / root)}rem` : `${round(px)}px`;
if (Math.abs(maxValue - minValue) < 1e-9) {
return length(minValue);
}
const slope = (maxValue - minValue) / (maxViewport - minViewport);
const interceptPx = minValue - slope * minViewport;
const intercept = unit === 'rem' ? interceptPx / root : interceptPx;
const vw = slope * 100;
const sign = vw < 0 ? '-' : '+';
const lower = Math.min(minValue, maxValue);
const upper = Math.max(minValue, maxValue);
return `clamp(${length(lower)}, ${round(intercept)}${unit} ${sign} ${round(Math.abs(vw))}vw, ${length(upper)})`;
}
console.log(clampFromPoints({
minViewport: 320,
maxViewport: 1440,
minValue: 16,
maxValue: 32,
}));
// clamp(1rem, 0.7143rem + 1.4286vw, 2rem)
계산에는 반올림하지 않은 숫자를 사용하고 CSS 문자열을 만들 때만 반올림하세요. 토큰 파이프라인이 소수를 문자열로 받는다면 두 끝점, 역방향 범위, 고정 범위에 대한 테스트를 추가하세요.
Python으로 같은 값 생성하기
Python 버전은 디자인 토큰 스크립트나 정적 사이트 빌드 단계에서 유용합니다.
def clamp_from_points(v_min, v_max, s_min, s_max, root=16):
if v_max <= v_min:
raise ValueError("v_max must be greater than v_min")
if root <= 0:
raise ValueError("root must be greater than zero")
if abs(s_max - s_min) < 1e-9:
return f"{s_min / root:.4f}".rstrip("0").rstrip(".") + "rem"
slope = (s_max - s_min) / (v_max - v_min)
intercept_rem = (s_min - slope * v_min) / root
vw = slope * 100
lower, upper = sorted((s_min, s_max))
sign = "-" if vw < 0 else "+"
return (
f"clamp({lower / root:.4f}rem, "
f"{intercept_rem:.4f}rem {sign} {abs(vw):.4f}vw, "
f"{upper / root:.4f}rem)"
)
print(clamp_from_points(320, 1440, 16, 32))
실무에서는 프로젝트 규칙에 따라 끝의 불필요한 0을 일관되게 정리하세요. 중요한 것은 계산이며 포맷 정책은 다른 생성 CSS와 맞아야 합니다.
Bash/awk로 CI 검사하기
작은 CI 검사 하나 때문에 별도 언어 런타임을 추가하고 싶지 않을 수 있습니다. 다음 셸 함수는 awk를 사용해 px 기반 값을 출력합니다.
clamp_px() {
awk -v v1="$1" -v v2="$2" -v s1="$3" -v s2="$4" 'BEGIN {
if (v2 <= v1) exit 2
if (s1 == s2) {
printf "%.4gpx\n", s1
exit
}
m = (s2 - s1) / (v2 - v1)
b = s1 - m * v1
lo = s1 < s2 ? s1 : s2
hi = s1 > s2 ? s1 : s2
printf "clamp(%.4gpx, %.4gpx %s %.4gvw, %.4gpx)\n", \
lo, b, m < 0 ? "-" : "+", (m < 0 ? -m : m) * 100, hi
}'
}
clamp_px 320 1440 16 32
이 코드는 완전한 토큰 컴파일러가 아니라 의도적으로 검증 도우미에만 집중합니다. 여러 단위, 스키마 검증, 파일 출력이 필요해지면 테스트된 JavaScript나 Python 스크립트가 더 유지하기 쉽습니다.
병합 전에 CSS 테스트하기
짧은 체크리스트만으로 수식과 레이아웃 문제를 함께 찾을 수 있습니다.
- 끝점 검사: 최소 및 최대 뷰포트 너비에서 계산된 값을 확인합니다.
- 중간점 검사: 가운데 값이 의도한 보간과 일치하는지 확인합니다.
- 범위 밖 검사: 더 좁고 더 넓은 뷰포트에서 경계가 유지되는지 확인합니다.
- 확대 검사: 실제 콘텐츠로 브라우저를 200% 확대해 테스트합니다.
- 현지화 검사: lorem ipsum이 아니라 지원 언어 중 가장 긴 실제 문구를 사용합니다.
- 컨테이너 검사: 텍스트가 줄바꿈되고 카드가 세로로 늘어날 수 있는지 확인합니다.
- 토큰 검사: 루트 글꼴 크기 가정이 프로젝트와 일치하는지 확인합니다.
- 구문 검사: 선호 표현식의
+와-양쪽에 공백을 둡니다.
계산기의 곡선과 다섯 행 샘플 표는 숫자 검사를 담당합니다. 레이아웃 검사는 브라우저 개발자 도구와 컴포넌트 테스트로 수행해야 합니다.
가장 단순하면서 확실한 규칙
실제 두 끝점을 사용해 한 직선을 유도하고, 경계를 올바른 순서로 유지한 뒤, 그 값을 사용하는 컴포넌트에서 테스트하세요. 방법은 이것이 전부입니다.
clamp()의 가치는 모든 CSS 길이를 수식으로 바꾸는 데 있지 않고 불필요한 브레이크포인트 점프를 없애는 데 있습니다. 정확한 유동 값은 대체하는 미디어 쿼리보다 이해하기 쉬워야 합니다. 결과를 설명하는 데 예외 문단이 필요하다면 해당 레이아웃에는 명시적인 브레이크포인트가 더 적합할 수 있습니다.
더 읽을 자료
- MDN:
clamp()— 구문, 브라우저 지원, 예제, 접근성 지침. - CSS Values and Units Level 4 —
min(),max(),clamp()의 공식 정의. - W3C: 성공 기준 1.4.4 텍스트 크기 조정 이해하기 — 200% 크기 조정 요구 사항과 목적.
ZeroTool 관련 도구
- CSS 단위 변환기 — px/rem 변환과 뷰포트 단위를 검증합니다.
- CSS 변수 생성기 — 완성된 수식을 사용자 정의 속성으로 정리합니다.
- Cubic Bezier 생성기 — 반응형 길이가 아니라 애니메이션 이징 곡선을 만듭니다.