원저자의 허락을 받아 <Beyond CSS Media Queries> 아티클을 한국어로 번역한 글입니다. 🙂
📝 요약: 최근 몇 년간 출시된 많은 CSS 기능들이 특정 조건에서 적용되는 스타일 코드를 "똑똑하게" 작성하도록 고안되었습니다. 또한 그 중 상당수는 반응형 디자인과 직접적인 관련이 있습니다. 이것이 미디어 쿼리에 어떤 의미가 있을까요? Juan Diego Rodríguez(원저자)는 여전히 미디어 쿼리가 반응형 레이아웃에서 중요하게 여겨지지만, 이제는 여러 도구들 중 하나로써 다른 최신 기술들과 함께 사용할 때 가장 효과적임을 설명합니다.
미디어 쿼리는 CSS만큼이나 오래 사용되어 왔습니다. flex, grid, 반응형 단위, 수학 함수가 없는 상황에서 미디어 쿼리는 반응형 웹사이트를 만드는 가장 실용적인 선택지였습니다.
2010년대 초, 모바일 디바이스의 확산과 함께 때마침 Ethan Marcotte의 고전 아티클 "반응형 웹디자인"이 출간됨에 따라 여러 화면과 디바이스에서 변형되는 레이아웃을 만들기 위해 미디어 쿼리의 필요성이 대두되었습니다. 심지어 CSS Flexbox와 Grid 사양이 출시되었음에도, 여전히 크기를 조정하는 미디어 쿼리를 사용하고 있습니다.
미디어 쿼리의 실제 사용량 데이터는 파악하기 어렵지만, 단순히 뷰포트 조정을 넘어 사용자 경험을 개선하는 추가적인 기능과 함께 성장해 왔고 이에 따라 반응형 디자인의 필수 요소로 자리 잡고 있습니다.
오늘날 CSS에는 뷰포트 크기 외에도 페이지 요소를 다양한 조건에 맞게 레이아웃을 조정하는 많은 선택지와 도구가 있습니다. 그중에는 널리 사용되고 있는 Flexbox와 Grid 뿐만 아니라, 반응형 길이 단위나 컨테이너 쿼리와 같은 기능도 있습니다. 이에 대해 밑에서 더 살펴보겠습니다.
하지만 미디어 쿼리는 여전히 개발자가 사실상 가장 흔히 사용하는 도구입니다. 아마도 습관, 일관되지 않은 브라우저 지원, 또는 우리의 고정관념 때문에 반응형 인터페이스에 대한 최신 접근 방식이 더디게 도입되는 것 같습니다.
분명히 하자면, 저는 미디어 쿼리를 지지합니다. 미디어 쿼리는 뷰포트 크기를 고려하는 것을 넘어, 사용자의 OS 선호도 및 입력 장치의 종류 등을 기반으로 더 나은 접근성과 사용자 경험을 제공하는 데 중요한 역할을 합니다.
하지만 계속 미디어 쿼리가 반응형 레이아웃의 표준으로 사용되어야 할까요? 언제나 그렇듯이 상황에 따라 다릅니다만,
"미디어 쿼리가 접근성을 높이는 해결책으로 진화하면서, 다른 CSS 기능들이 반응형 디자인을 책임질 수 있게 된 것은 분명합니다."
미디어 쿼리는 반응형에 대한 문제를 해결하는 가장 좋은 해결책처럼 보이지만, 웹이 점점 더 크고 복잡한 레이아웃으로 성장하면서 미디어 쿼리의 한계가 그 어느 때보다 분명해졌습니다.
레이아웃의 기준이 되는 미디어 쿼리 중단점을 작성할 때, width
나 orientation
과 같은 뷰포트 속성에만 접근할 수 있습니다. 폰트 크기만 조정하면 되는 경우에 뷰포트가 가장 좋은 도구가 되기도 하지만, 대부분의 경우, 컨텍스트가 중요합니다.
컴포넌트는 다른 컴포넌트와 공간을 공유하며 일반적인 문서 흐름에 따라 상대적인 위치에 배치됩니다. 뷰포트 너비만을 기준으로 특정 중단점을 정확히 설정하기는 어렵습니다. 일부 컴포넌트는 조정된 레이아웃에 잘 반응하는 반면 다른 컴포넌트는 해당 중단점에서 또다른 조정이 필요할 수 있기 때문입니다.
그래서 우리는 브라우저 크기를 조정하고 콘텐츠 레이아웃이 깨지는 정확한 지점을 찾습니다.
다음 예제는 한동안 볼 수 있는 최악의 CSS일지도 모르지만, 미디어 쿼리의 문제를 이해하는 데 도움이 됩니다.
꽤 당혹스러운 예제이긴 하지만, 정확히 무엇이 문제인 걸까요?
위 CSS를 문맥 그대로 변환하면, 페이지 너비가 600px
보다 작으면 이 요소는 커지고 하나의 열로 접힌다 는 의미가 됩니다. 이는 요소의 콘텐츠나 형제 요소와는 무관합니다. 형제 요소가 더 많거나 하나만 있는 경우엔 어떻게 될까요? 혹은 해당 요소가 더 작은 컨테이너 내에 있다면 어떻게 될까요? 미디어 쿼리는 이러한 컨텍스트를 설명하는 정보가 부족하므로 두 번째 문제점이 발생합니다.
요즘은 모든 것이 컴포넌트입니다. 컴포넌트는 유목민처럼 돌아다니며 다른 컴포넌트와 공간을 공유하고 끊임없이 변화하는 콘텐츠를 가져옵니다. 다시 강조하지만, 미디어 쿼리는 컴포넌트를 둘러싼 컨텍스트를 전혀 알지 못하므로 개발자는 모든 경우에 맞는 최적의 지점을 찾아야 한다는 부담을 가집니다.
미디어 쿼리의 이상적인 중단점은 페이지 크기를 조정하며 찾은 하드코딩된 매직 넘버이며, 컴포넌트를 둘러싼 컨텍스트에 따라 여러 미디어 쿼리가 필요하므로 굉장히 까다로운 작업이 됩니다. 요소의 콘텐츠나 컨테이너를 변경한다면 미디어 쿼리 또한 변경되어야 합니다.
그렇다면 스타일시트에서 미디어 쿼리는 어디서 관리할까요? 마지막 부분에 넣는 개발자도 있고, 일부 파일에서 관리하며 전처리기로 컴파일하는 개발자들도 있습니다.
최근 CSS 중첩 기능으로 동일한 스타일 규칙의 컴포넌트에 미디어 쿼리를 추가할 수 있어 작업이 깔끔해지기는 했습니다. 하지만 시스템의 모든 컴포넌트마다 이 작업을 수행해야 하므로 스타일을 편집할 때 고려할 인스턴스의 수가 점점 더 증가합니다. 이는 미디어 쿼리의 또 다른 문제점으로 이어집니다.
웬만하면 요소의 크기가 화면에 따라 유동적으로 조정됐으면 하지만, 특정 중단점에서 컴포넌트가 "깨질" 때마다 새로운 미디어 쿼리를 작성하는 것은 관리할 일이 너무 많습니다. 뷰포트가 960px
에서 970px
사이일 때, 좁은 화면에서 컴포넌트의 높이가 너무 커지면 이제 새로운 스타일 집합을 또 작성해야 하는 걸까요?
이는 반응형 디자인이라기보다는, 매우 구체적인 상황에만 고정된 수치를 기반으로 하는 일종의 적응형 디자인이라고 할 수 있습니다. 여기에는 유동성이 없습니다.
다행히도 우리는 더 이상 2012년에 살고 있지 않습니다. 미디어 쿼리보다 훨씬 더 나은 선택지들이 있으며, 대부분이 널리 채택되고 지원되고 있습니다. Flexbox와 Grid, 반응형 단위(responsive units)와 수학 함수 등이 대표적인 예입니다. 컨테이너 쿼리와 같은 다른 기술들도 최근 등장했지만, 아직 초기 단계에 있습니다.
제가 이러한 최신 CSS 기능이 존재하며 현재 거의 모든 CSS 개발자가 사용하는 일반적인 도구라는 점을 새로 발견했다고 말하려는 건 아닙니다. 그러나 미디어 쿼리는 여전히 크기를 조정하기 위해 CSS에서 사용되고 있습니다. clamp()
함수나 반응형 단위는 그 목적을 위해 설계되었기 때문에 미디어 쿼리보다 더 나은 선택지이기도 합니다.
그래서 이미 존재하는 CSS 기능들을 모두 가르치기보다는 (여러분은 훌륭하며, 이미 잘 알고 있을 것이라고 믿고 있습니다) 기존의 미디어 쿼리를 현대적인 반응형 기술로 대체하는 것에 중점을 두려고 합니다.
Flexbox와 미디어 쿼리는 주로 같이 사용되며 Flexbox는 특정 방향으로 레이아웃을 설정하고 미디어 쿼리는 특정한 뷰포트 너비일 때 방향을 변경하는 역할을 합니다.
매우 간단하면서도 일반적인 이 패턴은 앞에서 설명한 세 가지 문제와 관련이 있습니다.
700px
라는 것을 알아냈으므로 이 지점에서 새로운 미디어 쿼리를 설정해야 합니다.700px
에서 중단점을 가지므로 이보다 좁은 화면을 가진 디바이스에서는 콘텐츠가 깨질 수 있습니다.미디어 쿼리를 더 추가하여 문제를 해결하려고 하면 두 번째 문제를 다시 마주하게 됩니다.
이때 가장 좋은 해결책은 미디어 쿼리를 아예 사용하지 않는 것입니다. 400px
로 설정된 특정 지점까지 사용 가능한 공간에 따라 <article>
요소를 늘리거나 줄이는 flex
속성으로 대체할 수 있습니다.
main {
display: flex;
flex-flow: row wrap;
}
main article {
flex: 1 1 400px;
}
미디어 쿼리를 사용한 이전 예제의 CSS 코드를 풀어보면 다음과 같습니다. '뷰포트가 700px
보다 작을 때 요소들이 한 줄씩 차지하게 할 거야. 왜냐고? 나도 잘 몰라.' 또 한번 강조하자면 쿼리는 아티클 요소의 컨텍스트를 알지 못합니다. 그럼 수정한 예제를 풀어볼까요? '요소가 어디에 있든 400px
의 너비를 가지도록 최선을 다하겠지만 사용할 수 있는 공간에 따라 조정할 수도 있어.'
아래 데모에서 화면 크기를 조정해 보세요. 화면 크기에 따라 아티클들의 레이아웃이 멋지게 변경되지 않나요?
심지어 매직 넘버를 사용하지 않으면서 더 적은 코드로 이 모든 걸 해냈습니다.
위 예제는 좋지만, 마지막 flex 아이템이 형제 아이템과 같은 크기를 유지하지 않고 마지막 행의 사용 가능한 공간을 모두 차지하고 있습니다. 모든 flex 아이템을 같은 크기로 만들려면 너비를 조정하고 미디어 쿼리를 다시 사용해야 할 수도 있습니다. flex 아이템에 width
를 지정해야 한다면 Grid로 전환하는 것이 더 나을 수 있습니다. 열과 행에 대한 특정 트랙을 설정할 수 있기 때문입니다.
다행히 단 두 줄의 CSS만으로 바꿀 수 있습니다.
main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
}
간단히 설명하면 다음과 같습니다.
auto-fit
: 가능한 많은 열을 맞추고 남은 공간이 있으면 열을 확장합니다.minmax
: 열의 최소 너비를 지정하는데 이 코드에서는 500px
이 최솟값입니다.참고: Sara Soueidan이 쓴 글에서 위 접근 방식에 대한 가장 좋은 설명을 찾아볼 수 있으며, 꼭 읽어보길 바랍니다. 여러분의 마음에 드는 새로운 CSS 스니펫을 발견할지도 모릅니다.
수학 함수와 반응형 단위는 요소의 크기 조정과 관련된 대부분의 문제를 해결합니다. 특정한 중단점을 어렵게 정의하지 않고도 반응형 제한을 설정합니다. 최신 브라우저에서 완전히 지원되며 이미 널리 사용되고 있으므로 사용할 수 있는 기능이 무엇인지, 왜 사용해야 하는지를 요약해 보겠습니다.
min()
함수를 사용하면, 백분율과 같은 상대 단위나 뷰포트 너비(vw) 같은 반응형 단위에 따라 요소 크기를 조정할 수 있습니다. 요소가 너무 커지지 않도록 상한값을 설정할 수 있습니다. 아래 코드는 요소가 부모의 전체 너비를 차지하면서도 300px
을 넘어서지 않도록 합니다.
.min {
height: 400px;
width: min(100%, 300px);
}
너비에 따른 높이를 조정할 때는 aspect-ratio
속성을 활용합니다.
.min-and-aspect-ratio {
aspect-ratio: 1/1; /* or 1 */
width: min(100%, 300px);
}
max()
함수를 사용하면 하한값을 적용할 수 있습니다. 다음 CSS 코드는 요소의 크기를 부모 요소의 절반 너비를 가지면서도 300px
보다 작지 않도록 합니다.
.max {
height: 400px;
width: max(50%, 300px);
}
조금 헷갈리기도 하죠? min()
으로 최대 너비를 설정하고, max()
로 최소 너비를 설정합니다.
매우 인기 있는 clamp()
함수는 최댓값과 최솟값을 모두 설정할 수 있으며 그 가운데에 "이상적인" 크기에 대한 인수를 받습니다. 이상적인 목표에 도달하려 하면서도 조건 범위 내로 값을 "고정"하는 것입니다.
다음 데모에서 요소의 너비는 부모 요소의 사용 가능한 전체 너비를 덮고자 하면서도 200px
이상 300px
이하의 값을 가집니다.
어떤 디바이스에서든 멋진 웹을 만들려면 반응형 단위나 수학 함수만 사용하는 것으론 부족합니다. 매끄러운 반응형 경험을 제공하려면 모든 기술을 조합해서 사용해야 합니다. 성능 작업을 위해 함께 동작하는 표준 그룹의 자바스크립트 성능 API와 비슷합니다.
반응형 디자인을 중심으로 구축된 CSS 사양 그룹이 있습니다. 이 사양이 반드시 CSS 미디어 쿼리를 대체하는 건 아니지만, 추가적인 기능을 제공하며 최상의 적용 범위를 위해 함께 작동하도록 설계되었습니다.
예를 들어, 뷰포트 너비에 따라 font-size
값을 늘리고 줄이고 싶을 때가 있습니다. 미디어 쿼리로도 매우 쉽게 처리할 수 있지만 프로젝트에 따라 더 효율적이거나 유지보수가 용이한 방법도 있습니다.
물론 미디어 쿼리로 브라우저의 특정 너비에 따라 font-size
값을 변경할 수 있습니다. 각 중단점에서 적절한 값을 얻기 위해 하나 이상의 미디어 쿼리를 작성해야 할지도 모르지만, 분명히 실행 가능하고 유효한 방법입니다.
여러 중단점에 대한 고정된 픽셀값으로 font-size
를 변경하는 대신 반응형 길이 단위로 접근해 볼 수 있습니다. 예를 들어, vw
단위는 뷰포트 너비를 기준으로 하며, 1vw는 현재 브라우저 너비의 1%를 나타냅니다.
하지만 뷰포트 단위만으로는 컨텍스트에 비해 너무 작거나 큰 폰트 크기를 방지하기 어렵습니다. 이를 clamp()
함수와 결합하여 최소 및 최대 제한과 함께 이상적인 크기를 설정해 보겠습니다.
잠시만요! <html>
요소의 font-size
에 직접 선언하여 다른 모든 요소의 폰트 크기를 같은 비율로 조정한다면 이 문제를 더욱 개선할 수 있습니다. 그런 다음 rem
단위를 사용하여 각 요소의 font-size
값을 작성하거나, clamp()
또는 특정 요소의 고정 픽셀 단위를 사용할 수도 있습니다.
rem
단위는<html>
과 같은 "루트" 요소에 상대적인 값이며,em
단위는 부모 요소에 상대적인 값이라는 차이가 있습니다.
따라서 각 기능은 단독으로 사용하거나 미디어 쿼리를 일대일로 대체할 순 없습니다. 한 아이를 키우려면 온 마을이 필요하듯이 반응형 인터페이스를 잘 구축하려면 다양한 도구를 조화롭게 활용해야 합니다.
미디어 쿼리는 페이지 전체의 레이아웃을 수정하는 데 유용합니다. 장바구니 페이지를 생각해 보세요. 뷰포트 너비가 충분히 넓을 때는 장바구니 속 제품들을 넓은 <table>
에 표시하여 여유롭게 배치할 수 있습니다.
동일한 레이아웃이 모바일 디바이스에서는 제대로 작동하지 않습니다. 해결이 불가능한 건 아니지만 테이블은 그 자체의 반응형 문제를 안고 있습니다. 그렇다면 더 적은 노력으로 최신 기술을 사용하는 다른 레이아웃을 고려해 볼 수 있습니다.
우리는 단순히 요소의 너비나 높이를 변경하는 것 이상의 작업을 하고 있습니다! 테두리 색상, 요소 가시성, flex 방향도 변경해야 하는데, 미디어 쿼리를 통해서만 변경할 수 있는 걸까요? 뷰포트 크기에 따라 레이아웃을 완전히 바꿔야 하는 경우에도 컨테이너 쿼리를 사용하면 더 나은 결과를 얻을 수 있습니다.
다시 강조하지만, 미디어 쿼리의 첫 번째 문제점은 무언가를 결정할 때 요소의 주변 컨텍스트를 완전히 무시하고 오로지 뷰포트 크기만을 고려한다는 점입니다.
전체 페이지 너비는 뷰포트 크기와 매우 밀접한 관련이 있습니다. 전체 페이지 너비를 차지하는 요소에 대해서는 큰 문제가 되지 않아 이런 경우에는 미디어 쿼리로 조정할 수 있습니다.
하지만 동일한 요소를 <main>
요소가 포함된 큰 열 옆에 <aside>
로 좁은 열에 포함해 다중 열 레이아웃으로 보여주어야 한다면 어떨까요? 이제 문제가 모습을 드러냅니다.
더욱 관습적인 해결책은 요소가 사용되는 위치와 콘텐츠가 끊기는 위치에 따라 일련의 미디어 쿼리를 작성하는 것입니다. 그러나 미디어 쿼리는 <main>
과 <aside>
요소 사이의 관계를 완전히 놓치는데, 이는 일반적인 웹 문서 흐름에 따라 한 요소의 크기가 다른 요소의 크기에 영향을 미치므로 중요한 문제가 됩니다.
.cards
요소는 <aside>
요소 컨텍스트 내에 있고 좁은 열에 위치하여 UI가 깨집니다. 뷰포트가 특정 크기일 때가 아니라 .cards
가 특정 크기에 도달할 때 .card
컴포넌트의 레이아웃을 변경하는 것이 좋아 보입니다.
여기서 컨테이너 쿼리를 사용함으로써 요소의 크기에 따른 조건부 스타일을 적용할 수 있습니다. 현재 예시에서는 일련의 .card
컴포넌트를 포함하는 정렬되지 않은 목록인 요소를 "컨테이너"로 등록합니다. 기본적으로 현재 레이아웃에 영향을 줄 수 있는 막강한 권한을 부모 선택기에 부여합니다.
.cards {
container-name: cards;
}
컨테이너 쿼리는 요소의 크기를 추적하므로, 브라우저가 어떻게 크기를 해석할지 정확히 알려주려면 .cards
에 container-type
을 지정해야 합니다. 이때 값은 컨테이너의 size
(block, inline 방향 크기) 또는 inline-size
(inline 방향의 전체 길이)가 될 수 있습니다. 크기 고려 사항을 제거하고 스타일로 요소를 쿼리하는 normal
값이 있습니다.
.cards {
container-name: cards;
container-type: inline-size;
}
container
라는 축약형의 속성을 사용하여 좀 더 간단하게 나타낼 수도 있습니다.
.cards {
container: cards / inline-size;
}
이제 .cards
컨테이너가 특정 인라인 크기일 때 .card
컴포넌트들의 레이아웃을 조정할 수 있게 됐습니다. 컨테이너 쿼리는 미디어 쿼리와 동일한 문법을 사용하지만 @media
가 아닌 @container
at-규칙을(@-규칙) 사용합니다.
.cards {
container: cards / inline-size;
}
@container cards (width < 700px) {
.cards li {
flex-flow: column;
}
}
각 .card
컴포넌트는 유연한 컨테이너가 되어, .cards
컨테이너의 너비가 700px
보다 작을 때 column
방향으로 정렬됩니다. 반대로 그 이상일 때 row
방향으로 배치됩니다.
스타일 쿼리는 컨테이너의 스타일을 쿼리하고 조건부로 자식 요소의 스타일을 변경할 수 있다는 점에서 컨테이너 쿼리와 유사한 부분이 많습니다. 예를 들어 컨테이너의 background-color
이 어두운색으로 설정되면 자식 요소의 color
를 흰색으로 변경하는 것이죠. 아직 초기 단계지만 스타일 쿼리와 브라우저 지원은 계속 발전하고 있습니다.
이러한 패턴이 컨텍스트를 인식하며 반응형 레이아웃을 설정할 수 있다는 점에서 얼마나 놀라운지 이해할 수 있기를 바랍니다. (비록 오랫동안 "부모 요소"라는 용어로 사용해 왔지만) 컨테이너는 CSS에서 완전히 새로운 개념으로 아주 참신하고 우아한 아이디어입니다.
그렇지 않습니다! 반응형 디자인에서 미디어 쿼리는 자주 사용하는 해결책이었지만, 미디어 쿼리의 문제를 해결하도록 설계된 더 강력한 CSS 도구가 등장하면서 그 한계가 분명해졌습니다.
그렇다고 해서 미디어 쿼리가 쓸모없어지는 것은 아닙니다. 미디어 쿼리는 반응형 인터페이스를 구축하기 위한 여러 도구 중 하나일 뿐입니다. 게다가 운영 체제 수준에서 사용자의 시각 및 동작 기본 설정을 인식하는 기능 덕분에 여전히 중요한 접근성 문제를 해결합니다.
그러니 미디어 쿼리를 계속 사용하셔도 좋습니다! 하지만 CSS가 제공하는 기능이 훨씬 많으니 미디어 쿼리에만 의존하지 말고 적절히 사용하시기를 바랍니다.