
원문: https://frontendmasters.com/blog/building-a-ui-without-breakpoints/
브레이크포인트는 우리가 웹을 만들기 시작한 이래로 반응형 디자인을 지배해왔습니다. 익숙하고, 편리하며, 가르치기 쉽고, 오늘날 프로덕션 시스템에서도 여전히 널리 쓰이고 있습니다.
브레이크포인트가 다양한 화면 크기에 대응하는 훌륭한 해법이었던 것은 사실이지만, 현대 인터페이스는 더 이상 "페이지 우선"이 아닙니다. 컴포넌트 우선이고, 중첩되며, 전혀 다른 컨텍스트에서 재사용됩니다. 그 세계에서 전역 뷰포트 너비는 로컬 레이아웃 결정에 있어 종종 잘못된 입력값입니다.
이 글은 더 현대적인 웹에 잘 맞는 접근 방식을 제안합니다. 기본적으로 적응하는 유동적이고 내재적인(intrinsic) 컴포넌트를 만들고, 조건부 규칙은 지역적이고 의도적인 예외로 취급하는 것입니다.
미디어 쿼리는 여전히 사용해야 하지만, 주로 실제 기기 기능과 사용자 환경설정에 활용하고, 레이아웃 엔진으로는 사용하지 않는 것이 좋습니다. 이에 대해서는 뒤에서 다시 살펴보겠습니다.
브레이크포인트는 고정 데스크탑 레이아웃에서 크게 발전된 방식이었습니다. 팀들이 명시적인 CSS 분기를 통해 폰, 태블릿, 데스크탑을 실용적으로 지원할 수 있게 해줬습니다.
모델은 단순했습니다.
바로 이 단순함이 브레이크포인트를 표준으로 만든 이유입니다.
오늘날도 여전히 코드베이스에서 볼 수 있는 전형적인 패턴이 있습니다.
.page {
width: 90%;
font-size: 16px;
}
.card-grid {
display: grid;
gap: 1em;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.page {
width: 920px;
font-size: 18px;
}
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1280px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
}
이 방식 자체에 문제는 없습니다. 문제는 규모에서 나타납니다. 수십 개의 컴포넌트가 반복되는 뷰포트 분기를 갖게 되면 CSS 크기가 급증하고, 오버라이드가 복잡해지며, 서로 무관한 부분들 사이의 결합도가 높아집니다.
브레이크포인트의 중요성을 충분히 인정하지만, 오늘날 우리가 구축하는 환경은 과거와 크게 다릅니다. 화면 유형, 크기, 픽셀 밀도의 다양성이 증가함에 따라 실질적인 의문이 제기됩니다. 과연 어떤 브레이크포인트를 목표로 삼아야 하며, 어떤 기존 기준값이 여전히 유효한 것일까요? 기기의 다양성이 계속 확대됨에 따라, 브레이크포인트 선택은 더 이상 안정적인 체계라기보다 끊임없이 변화하는 추측에 가까워지고 있습니다.
동시에, 레이아웃 로직이 항상 화면 너비와 가장 잘 연동되는 것은 아니라는 사실도 이제 우리는 알고 있습니다. 대개는 컴포넌트 컨텍스트와 연동될 때 더 잘 작동합니다. 동일한 컴포넌트가 전체 너비 피드, 좁은 사이드바, 모달, 대시보드 타일 등, 종종 같은 앱 내에서 다양한 형태로 나타날 수 있습니다. 컴포넌트가 서로 다른 컨테이너에 위치할 때, 뷰포트 기반 규칙은 불일치하는 동작을 유발하고, 불필요한 예외를 발생시키며, 컴포넌트 재사용의 예측 가능성을 떨어뜨릴 수 있습니다.

그리고 실제 구현 과정을 살펴보면, 대부분의 반응형 변경 사항이 진정한 구조적 전환은 아니라는 것을 알 수 있습니다. 공간 배분, 밀도, 타이포그래피, 리듬 등에서 점진적인 변화가 일어납니다. 브레이크포인트 우선의 사고방식은 이러한 변화의 상당 부분을 불연속적인 도약으로 강요하며, 이는 종종 논리의 중복, 긴 오버라이드 체인, 그리고 한 브레이크포인트에 대한 미세 조정이 예기치 않게 다른 구성 요소에 영향을 미쳐 불안정한 상호작용을 초래합니다.
다행히도 현대적인 CSS는 과거보다 더 나은 기본 요소를 제공합니다. 이러한 기본 요소를 기준으로 삼는다면, 분기 구조를 줄이고 코드를 더 깔끔하게 유지하며 시간이 지나도 훨씬 더 예측 가능한 동작을 보이는, 완벽하게 반응형인 인터페이스를 구축할 수 있습니다.
첫 번째 방법은 적응 로직을 레이아웃 원시 도구 자체에 밀어 넣는 것입니다. "X 너비에서 N개의 열을 강제하라"고 말하는 대신, 제약 조건을 정의하고 브라우저가 레이아웃을 연속적으로 유도하게 하는 것입니다.
브레이크포인트로 열 수를 하드코딩하는 대신, auto-fit과 minmax()를 사용해 최소 카드 너비를 지키면서 자연스럽게 공간을 채우는 그리드를 만들 수 있습니다.
.grid {
display: grid;
gap: 1em;
}
/* 전통적인 브레이크포인트 방식 */
.with-breakpoints {
grid-template-columns: 1fr;
@media (min-width: 720px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}
@media (min-width: 1360px) {
grid-template-columns: repeat(4, 1fr);
}
}
/* 내재적 그리드 방식 — 브레이크포인트 없음 */
.with-auto-fit {
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
(아래 데모를 새 탭에서 열고 창 크기를 조절해 그리드가 어떻게 적응하는지 확인해 보세요.)
미디어 쿼리 없이 동일한 결과를 얻습니다. 내재적 그리드를 사용함으로써 브레이크포인트를 없애고, CSS 라인을 줄이고, 오버라이드 복잡도를 낮췄습니다. 더 나아가 의도도 더 명확하게 표현됩니다. "최대한 많은 열을 만들되, 절대 320px보다 작아지지 않도록"이라는 뜻이 코드에 그대로 담깁니다. 브라우저가 나머지를 처리합니다.
비슷한 패턴을 사이드바와 메인 콘텐츠 같은 2영역 레이아웃에도 적용할 수 있습니다. 브레이크포인트에서 단일 컬럼에서 멀티 컬럼으로 전환하는 대신, 단순히 적응하는 유연한 레이아웃을 정의할 수 있습니다.
body {
display: flex;
flex-wrap: wrap;
}
main {
/* 메인 콘텐츠는 우선순위를 갖고, 최소 읽기 가능 너비 보장 */
flex: calc(infinity) 1 360px;
}
aside {
/* 사이드바 기본 크기 설정 */
flex: 1 1 240px;
}
이 패턴은 의도를 직접 인코딩하고, 하드 스위치를 더 부드러운 레이아웃 흐름으로 대체합니다.
많은 "반응형" 규칙은 사실 단순한 스칼라(scalar) 조정입니다. 타이포그래피, 간격, 반경, 컴포넌트 크기는 대부분 단계적이 아닌 연속적으로 만들 수 있습니다.
min()과 max()를 연속 값에 활용할 수 있지만, clamp()는 안전한 최솟값과 최댓값을 모두 제공하면서 그 사이를 유동적으로 스케일링하기 때문에 특히 유용합니다.
clamp()뒤의 수학은 여기서 깊이 다루지 않겠습니다. 이미 훌륭한 심화 자료들이 많이 있습니다.
핵심은 약간의 수학으로, 뷰포트 너비를 기반으로 최솟값과 최댓값 사이에서 유동적으로 스케일되는 토큰을 만들 수 있다는 점입니다.
/* 브레이크포인트 방식 — 단계적 값 */
.stepped-card {
font-size: 20px;
padding: 2em;
@media (max-width: 720px) {
font-size: 18px;
padding: 1.5em;
}
@media (max-width: 380px) {
font-size: 16px;
padding: 1em;
}
}
/* clamp() 방식 — 유동적 값 */
.fluid-card {
font-size: clamp(16px, calc(11.529px + 1.176vi), 20px);
padding: clamp(1em, calc(-0.118em + 4.706vi), 2em);
}
여러 미디어 쿼리를 단일 규칙으로 대체하면서, 더 부드러운 스케일링과 더 적은 CSS를 만들어냅니다. clamp() 값을 구하려면 온라인 clamp 계산기를 활용하세요.
여기까지는 대부분의 값이 여전히 뷰포트에 절대적으로 묶여 있었습니다. 컴포넌트 기반 시스템에서는 컴포넌트가 어디에 렌더링될지, 실제로 얼마나 많은 공간을 얻을지 미리 알 수 없습니다. 바로 이 지점에서 컨테이너 유닛이 유용해지며, 이제 브라우저 지원이 광범위해졌으므로 자신 있게 사용할 수 있습니다.
컨테이너 유닛은 화면 너비가 아닌, 컴포넌트의 실제 렌더링 크기를 기반으로 값을 스케일하고 싶을 때 이상적입니다.
.card-container {
container-type: inline-size;
}
.card {
/* 뷰포트가 아닌 카드의 실제 너비를 기준으로 폰트 크기가 스케일됨 */
font-size: 5cqi;
/* 폰트 크기가 설정되면, 다른 속성의 기본 단위로 활용 가능 */
gap: 1em;
padding: 1.5em;
/* 또는 카드 크기와 함께 스케일되는 clamp 사용 */
border-radius: clamp(4px, calc(-16.87px + 6.522cqi), 64px);
}
컨테이너 유닛을 활용해 내부 내재적 레이아웃 동작을 관리할 수도 있습니다.
.card {
display: flex;
flex-wrap: wrap;
}
.card-content {
/* 이 콘텐츠는 가능한 모든 공간을 차지하되, 40cqi 미만으로는 줄어들지 않음 */
flex: calc(infinity) 1 40cqi;
}
동일한 CSS, 완전히 다른 결과를 보여주는 세 개의 카드를 확인할 수 있습니다.
여기서는
cqi를 사용하고 있는데, 이는 뷰포트에서vi가 작동하는 방식과 마찬가지로 컨테이너의 인라인 크기의 1%를 나타내기 때문입니다. 컨테이너 단위와 컨테이너 쿼리에 대해 더 자세히 알고 싶으시다면 이 글을 읽어보시기 바랍니다.
이 접근 방식은 각 컴포넌트가 어디에 렌더링될지 미리 알 필요를 없애주고, 레이아웃, 컨텍스트, 콘텐츠 형태를 가로질러 훨씬 더 이식 가능하고 재사용 가능하게 만들어줍니다.
유동 값과 내재적 레이아웃이 많은 것을 해결하지만, 전부는 아닙니다. 때로는 진짜 구조적 전환이 필요합니다. 컨테이너 쿼리가 정확히 그 순간을 위한 것입니다.
핵심적인 변화는: "화면이 얼마나 넓은가?" 가 아니라 "이 특정 컴포넌트가 지금 얼마나 많은 공간을 가지고 있는가?" 를 묻는 것입니다. 이를 통해 동작이 사이드바, 모달, 카드, 전체 페이지 레이아웃 전반에 걸쳐 재사용 가능하고 예측 가능해집니다.
아이템 그룹이 수직 스택으로 시작하지만, 컨테이너가 좁아지면 구조가 변하는 예시가 있습니다. 그룹은 행이 되고, 간격과 아이콘 크기가 줄어들며, 보조 텍스트는 명확성을 유지하기 위해 숨겨집니다.
.container {
container-type: inline-size;
}
.group {
display: flex;
flex-direction: column;
gap: 1em;
@container (max-width: 32em) {
flex-direction: row;
gap: 0.5em;
.icon {
font-size: 1.5em;
}
p {
display: none;
}
}
}
같은 컴포넌트, 다른 컨테이너 동작. 컨테이너 쿼리는 각 컴포넌트가 자체 적응 로직을 갖게 합니다. 임의의 뷰포트 브레이크포인트가 아닌, 해당 컴포넌트에 맞는 올바른 순간에 레이아웃 변화가 일어납니다.
지금까지의 흐름은 명확합니다. 내재적 레이아웃은 구조를, 유동적 값은 크기를, 컨테이너 유닛은 국소적인 크기 조정을, 컨테이너 쿼리는 실질적인 구조적 변화를 처리합니다. 이 도구들을 종합하면, 과거 뷰포트 브레이크포인트로 해결하던 대부분의 문제를 훨씬 더 원활하고 유연하게 해결할 수 있습니다.
이는 반가운 소식입니다. 즉, 반응형 디자인의 유지보수가 더 간편해지고, 이해하기 쉬워지며, 현대적인 컴포넌트 시스템의 실제 작동 방식과 훨씬 더 잘 부합하게 된다는 의미입니다. 전역 임계값에서 끊임없이 레이아웃을 수정하는 대신, 기본적으로 유연하게 적응하며 제품이 진화함에 따라 계속 작동하는 컴포넌트를 구축할 수 있습니다.
브레이크포인트가 사라지지는 않겠지만, 더 이상 반응형 디자인의 주된 동력은 아닙니다. 모든 레이아웃 결정의 토대가 되는 대신, 컴포넌트가 동작을 전환해야 할 실질적인 이유가 있을 때만 사용하는 집중적이고 선택적인 도구가 되었습니다.
그렇다면 @media가 여전히 필요할까요? 물론입니다.
변화는 미디어 쿼리를 제거하는 것이 아닙니다. 레이아웃을 위한 픽셀 측정 대신, 기기와 사용자 환경 파악에 사용하는 것입니다.
예를 들어, hover 지원과 pointer 정확도를 감지하고, display-mode에 적응하고, update가 느릴 때 비용이 큰 효과를 줄이고, scripting이 불가능할 때 폴백 콘텐츠를 보여줄 수 있습니다.
/* hover 지원 감지 */
@media (hover: hover) {
.link:hover {
color: blue;
}
}
/* 터치 디바이스 (coarse pointer) */
button {
padding: 1em;
@media (pointer: coarse) {
padding: 2em;
}
}
/* PIP 모드 */
@media (display-mode: picture-in-picture) {
body {
border: 2px solid white;
}
}
/* 느린 업데이트 환경 */
.glass-panel {
backdrop-filter: blur(16px);
@media (update: slow) {
backdrop-filter: none;
background: rgb(255 255 255 / 0.92);
}
}
/* 스크립팅 없는 환경 */
.no-script-msg {
display: none;
@media (scripting: none) {
display: block;
}
}
@media 기능은 더 많이 탐색할 가치가 있으며, MDN 문서가 가장 좋은 레퍼런스입니다. 하나의 카테고리를 우선시한다면 사용자 환경설정부터 시작하세요. "환경설정"이라는 단어는 오해를 불러일으킬 수 있습니다. 많은 경우 이 설정들은 미적 선택이 아닌 실제 접근성 필요에서 비롯됩니다. 누군가 모션을 줄이거나, 더 강한 대비가 필요하거나, 데이터를 절약해야 하거나, 특정 색 구성표가 필요한 경우 — 우리 UI는 이를 존중해야 합니다.
/* 색 구성표 환경설정 */
:root {
color-scheme: light;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
/* 높은 대비 */
.card {
color: #333;
background: #ccc;
@media (prefers-contrast: more) {
color: #000;
background: #fff;
}
}
/* 절약 모드 */
@media (prefers-reduced-data: reduce) {
.hero-video {
display: none;
}
}
/* 모션 줄이기 */
.element {
@media (prefers-reduced-motion: no-preference) {
/* 사용자가 모션 줄이기를 설정하지 않은 경우에만 애니메이션 적용 */
animation: spin 4s linear infinite;
}
}
미디어 쿼리는 여전히 중요하지만, 그 역할은 더욱 명확해졌습니다. 레이아웃은 컨테이너 로직이 주도하게 하고, 미디어 쿼리는 기기의 기능, 제약 조건, 그리고 사용자의 요구 사항을 처리하도록 합시다.
오늘 바로 프로젝트에 적용할 수 있는 단계별 체크리스트입니다. 모든 코드베이스는 다르며 좋은 판단은 여전히 중요하지만, 몇 가지 집중적인 단계만으로도 코드 품질, 안정성, 유지보수성에서 이미 의미 있는 개선을 만들어낼 수 있습니다.
clamp(), min(), max() 토큰으로 전환한다auto-fit과 minmax()를 선호한다container-type을 추가하고 컨테이너 유닛을 도입한다hover, pointer, prefers-reduced-motion, update를 우선시한다전면 재작성할 필요는 없습니다. 한 번에 하나의 컴포넌트씩 이동하면 변화는 축적됩니다. 이 방법들을 미래 개발의 기본 기준선으로 채택하는 것이 가장 중요합니다.

반응형 디자인은 브레이크포인트에 의존하는 방식에서 의도 중심의 시스템으로 진화하고 있습니다. 이는 단순한 최적화 차원을 넘어, 인터페이스를 설계하고 구축하며 유지 관리하는 방식에 대한 사고방식의 전환입니다.
내재적 레이아웃, 유동 값, 컨테이너 단위, 컨테이너 쿼리가 기본 도구로 자리 잡게 되면, 브레이크포인트는 더 이상 가장 먼저 의존하는 대상이 아닙니다. 대신 의도적으로 사용하는 정밀한 도구가 됩니다. 그 결과 CSS는 더 깔끔해지고, 회귀 현상은 줄어들며, 제품과 콘텐츠가 진화해도 컴포넌트는 탄력성을 유지하게 됩니다.
오늘 바로 시작하고 싶다면, 현재 여러 뷰포트 브레이크포인트에 의존하는 컴포넌트 하나를 골라 이 글에서 소개한 패턴을 적용해 재구축해 보세요. 그 버전을 배포하고, 그 과정에서 배운 점을 반영하며, 이 과정을 반복하세요. 이것이 바로 팀이 '충분히 반응형'인 인터페이스에서 진정한 적응형 인터페이스로 나아가는 방법입니다.
흥미로운 글이네요. 브레이크포인트에 의존하기보다 컴포넌트 자체가 공간에 맞게 적응하도록 만드는 접근이 앞으로의 웹 개발에 더 적합해 보입니다.