지난 편까지 유연한 격자와 이미지를 다루며 반응형의 몸통을 세웠다. 그 모든 전환을 실제로 지휘하는 것이 미디어 쿼리인데, 나는 오랫동안 이 도구를 화면 폭에 따라 스타일을 갈아 끼우는 단순한 스위치 정도로만 여겼다. 폭이 얼마 이상이면 이렇게, 이하면 저렇게, 딱 그 수준으로만 썼던 것이다.
이번 편은 그 미디어 쿼리를 한 겹 더 파고드는 이야기다. 겹침과 틈을 없애는 범위 구문, 조건을 엮고 뒤집는 논리 연산, 마우스와 손가락을 구분하는 입력 특성, 그리고 사용자의 취향을 존중하는 선호 쿼리까지 내 경험과 함께 정리한다. 이 도구들을 알고 나면 미디어 쿼리가 단순한 폭 스위치가 아니라 환경 전체를 읽는 감각기관이라는 걸 알게 된다. 나는 이 감각기관을 갖추고 나서야 반응형이 화면 폭에 반응하는 것을 넘어, 입력 방식과 사용자의 취향에까지 반응하는 넓은 개념이라는 걸 실감했다.
1. 겹치지도 벌어지지도 않는 범위 구문
나는 오랫동안 최소 폭과 최대 폭만으로 조건을 나눴다. 어떤 폭 이상이면 한쪽 규칙을, 그 아래면 다른 규칙을 적용하는 방식이었다. 이 방식 자체는 문제가 없지만, 한 구간의 끝과 다음 구간의 시작을 같은 숫자로 맞추려다 자주 어긋났다.
문제의 핵심은 경계에서 벌어지는 미세한 어긋남이었다. 최대 폭과 최소 폭을 같은 숫자로 두면 그 정확한 폭에서 두 규칙이 동시에 걸려 충돌하고, 겹침을 피하려고 한쪽을 일 픽셀 줄이면 이번엔 그 한 폭에서 어느 규칙도 안 걸리는 틈이 생겼다. 나는 이 한 픽셀의 함정을 몰라 경계 근처에서 스타일이 튀는 걸 한참 헤맸다. 화면을 아주 천천히 넓히다 보면 그 한 폭에서만 배경이 깜빡이거나 여백이 어긋났는데, 원인을 엉뚱한 곳에서 찾느라 시간을 많이 버렸다. 소수점 폭을 다루는 고해상도 화면에서는 이 어긋남이 더 자주 드러나기도 했다.
이 오래된 골칫거리를 깔끔하게 없앤 것이 범위 구문이다. 크거나 같다, 작다 같은 비교 기호를 그대로 써서 width >= 768px처럼 조건을 적으면, 이 폭 이상이라는 뜻이 수식 그대로 읽힌다. 최소니 최대니 하는 접두어를 머릿속에서 뒤집어 해석할 필요가 사라진 것이다.
한 구간만 정확히 겨냥할 때 이 구문은 더 빛난다. 400px <= width <= 900px처럼 양쪽 경계를 한 줄에 담으면, 이 폭부터 저 폭까지라는 범위가 부등식 하나로 또렷이 드러난다. 나는 태블릿 구간처럼 중간 폭만 손봐야 할 때 이 형식을 쓰고, 두 조건을 각각 적던 예전보다 실수가 크게 줄었다.
범위 구문은 픽셀뿐 아니라 다른 단위와도 잘 어울린다. 나는 브레이크포인트를 루트 폰트 배수인 단위로 잡는 편인데, 이 단위로 48em <= width처럼 적으면 사용자가 기본 글자를 키웠을 때 경계도 함께 밀려나 접근성에 유리하다. 절대 픽셀만 고집하지 않게 된 것도 이 구문을 익힌 덕이다.
물론 아주 오래된 환경에서는 이 구문을 못 읽을 수 있다. 그래서 나는 지원이 걱정되는 상황에서는 예전의 최소 폭 방식으로 폴백을 함께 두거나, 이 구문이 널리 자리 잡았음을 확인한 뒤에 쓴다. 어쨌든 경계에서 튀던 오래된 버그를 없애준 것만으로도 나에게는 큰 도약이었다.
2. 조건을 엮고 뒤집는 논리 연산
미디어 쿼리는 한 조건만 걸 때보다 여러 조건을 엮을 때 진짜 표현력이 나온다. 나는 폭 하나만 보던 시절에는 이 결합을 잘 안 썼는데, 방향이나 다른 특성까지 함께 따져야 할 때 논리 연산의 필요를 절감했다.
여러 조건을 모두 만족해야 할 때는 and로 잇는다. 예를 들어 폭이 넉넉하면서 동시에 화면이 가로로 누운 상황만 겨냥하고 싶으면, 폭 조건과 방향 조건을 and로 묶는다. 두 조건이 모두 참일 때만 규칙이 걸리니, 특정한 상황을 정밀하게 조준할 수 있다. 나는 넓은 화면이면서 정밀한 포인터를 쓰는 환경처럼, 여러 특성이 겹쳐야만 의미가 있는 스타일을 줄 때 이 결합을 자주 쓴다.
둘 중 하나라도 만족하면 될 때는 쉼표로 나열한다. 쉼표는 곧 또는이라는 뜻이어서, 폭이 넓거나 아니면 화면이 세로로 선 경우처럼 서로 다른 조건 여럿을 한 규칙에 묶을 수 있다. 나는 성격이 비슷한 여러 상황에 같은 스타일을 줄 때 이 쉼표 나열을 즐겨 쓴다.
조건을 통째로 뒤집을 때는 not을 쓴다. 정밀한 포인터가 아닌 경우처럼, 어떤 특성을 가진 상황을 빼고 나머지를 겨냥하고 싶을 때 유용하다. 나는 특정 환경만 예외로 두고 그 밖의 모든 곳에 규칙을 적용하고 싶을 때 이 부정을 떠올린다.
아주 오래된 브라우저를 걸러내는 only라는 키워드도 있다. 이것은 미디어 쿼리를 이해하는 브라우저에만 규칙을 노출하고, 구문을 모르는 옛 브라우저는 통째로 건너뛰게 하는 안전장치다. 요즘은 그런 브라우저가 거의 없어 쓸 일이 줄었지만, 이 키워드의 의도를 알아두면 옛 코드를 읽을 때 헷갈리지 않는다.
이 연산들을 엮으면 조건을 문장처럼 또렷하게 쓸 수 있다. 나는 조건이 복잡해질수록 한 줄에 억지로 우겨넣기보다, 목적이 다른 조건은 규칙을 나눠 적어 읽기 쉽게 유지한다. 논리 연산은 강력하지만, 남이 읽을 때 뜻이 한눈에 들어오는 선에서 쓰는 것이 오래가는 코드를 만든다.
3. 마우스와 손가락을 구분하는 입력 특성
미디어 쿼리가 폭만 본다고 생각하던 시절, 나는 마우스로 쓰는 화면과 손가락으로 쓰는 화면을 구분할 방법이 없어 답답했다. 넓은 화면이라고 다 마우스인 것도 아니고, 좁은 화면이라고 다 터치인 것도 아니기 때문이다. 이 구분을 가능하게 해주는 것이 입력 특성 쿼리다.
가장 자주 쓰는 것이 호버 가능 여부를 묻는 쿼리다. hover: hover 조건은 포인터를 올려두는 동작이 실제로 가능한 환경에서만 참이 된다. 나는 이 조건 안에서만 마우스를 올렸을 때의 강조 효과를 주고, 손가락으로 쓰는 화면에는 그 효과를 아예 걸지 않는다.
포인터의 정밀도를 묻는 쿼리도 요긴하다. pointer: fine은 마우스나 펜처럼 정밀한 입력을, pointer: coarse는 손가락처럼 뭉툭한 입력을 뜻한다. 나는 뭉툭한 입력이 감지되면 버튼과 누름 영역을 손가락에 맞게 키우는데, 이 한 줄 덕분에 터치 환경의 조작감이 크게 좋아졌다.
왜 이 구분이 중요한가 하면, 호버에만 의존한 메뉴는 손가락으로 열 방법이 없기 때문이다. 마우스라면 올리기만 하면 열리지만, 터치에는 올리는 동작 자체가 없다. 나는 예전에 호버로만 펼쳐지는 메뉴를 만들었다가, 휴대폰에서 그 메뉴가 아예 안 열리는 걸 보고 이 함정을 뼈저리게 배웠다. 데스크톱에서는 완벽하게 동작하던 기능이 손가락 앞에서 무용지물이 되는 경험은, 화면 폭만 보던 나에게 입력 방식이라는 새로운 축을 각인시켰다.
여기서 주 입력만 보는 쿼리와 보조 입력까지 보는 쿼리의 차이도 알아둘 만하다. 앞의 것은 기기의 대표 입력만 따지고, any-hover나 any-pointer처럼 어느라는 말이 붙은 것은 연결된 모든 입력을 통틀어 본다. 마우스와 터치를 겸하는 기기가 늘면서, 나는 이 겸용 상황을 놓치지 않으려고 두 종류를 함께 고려한다.
정리하면 입력 특성 쿼리는 화면의 크기가 아니라 사용자가 화면과 어떻게 닿는지를 묻는다. 나는 폭으로만 나누던 습관을 버리고, 강조 효과는 호버 가능한 곳에만, 큰 누름 영역은 뭉툭한 입력에만 주는 식으로 조건을 다시 짰다. 그러자 같은 화면 폭이라도 입력 방식에 맞는 경험이 나오기 시작했다.
4. 사용자의 취향을 존중하는 선호 쿼리
미디어 쿼리로 읽을 수 있는 것은 기기 특성만이 아니다. 사용자가 운영체제에 설정해둔 취향까지 읽을 수 있는데, 나는 이 선호 쿼리를 알고 나서 반응형의 범위가 화면 너머 사람에게까지 넓어졌다고 느꼈다.
가장 널리 쓰이는 것이 밝은 화면과 어두운 화면에 대한 선호다. prefers-color-scheme 쿼리로 사용자가 어두운 테마를 택했는지 읽어, 배경과 글자색을 그 취향에 맞춰 바꾼다. 나는 색을 변수에 담아두고 이 쿼리 안에서 값만 갈아 끼우는데, 그러면 어두운 화면을 얹는 일이 훨씬 수월해진다.
움직임에 민감한 사용자를 배려하는 쿼리도 중요하다. prefers-reduced-motion이 참이면 사용자가 화면의 큰 움직임을 줄여달라고 요청한 것이므로, 나는 애니메이션과 전환을 거의 없애거나 아주 짧게 줄인다. 화려한 효과가 누군가에게는 어지럼증을 부를 수 있다는 걸 알고 나서 이 배려를 습관으로 삼았다. 시선을 크게 끄는 미끄러짐이나 확대 효과는 전정기관이 예민한 사람에게 실제로 메스꺼움을 일으키기도 하므로, 나는 이 쿼리 안에서 그런 큰 움직임부터 걷어낸다.
명암 대비에 대한 선호를 읽는 쿼리도 있다. prefers-contrast가 높은 대비를 원한다고 알려주면, 나는 흐릿한 회색 톤 대신 또렷한 색과 진한 경계를 써서 글과 요소가 배경에서 확실히 떨어져 보이게 한다. 잘 안 보이는 사람에게는 이 대비가 읽고 못 읽고를 가르는 차이가 된다. 흐릿한 저채도 배색이 세련돼 보인다는 이유로 대비를 낮췄다가, 정작 그 화면을 읽어야 하는 사람을 배제하는 일이 없도록 나는 이 신호를 늘 살핀다.
이 선호 쿼리들의 공통점은 사용자가 이미 내려둔 결정을 사이트가 존중한다는 데 있다. 나는 사용자에게 굳이 다시 묻지 않고, 운영체제에 표현된 취향을 그대로 받아들이는 것을 기본으로 삼는다. 물론 사이트 안에서 따로 바꿀 수 있는 스위치를 더할 수도 있지만, 아무것도 안 해도 시스템 설정을 따르는 것이 좋은 출발점이다.
다만 선호를 반영하되 최소한의 품질은 늘 지켜야 한다. 어두운 화면에서도 글이 충분히 읽혀야 하고, 움직임을 줄여도 상태 변화가 사용자에게 전달돼야 한다. 나는 취향을 존중하는 것과 기능을 온전히 유지하는 것을 함께 챙겨야 진짜 배려가 완성된다고 여긴다.
5. 미디어 쿼리를 설계로 다루기
도구를 익히고 나면 남는 것은 이 조건들을 어떻게 배치할지에 대한 설계다. 나는 미디어 쿼리를 아무 데나 흩뿌리기보다, 기본을 먼저 세우고 필요한 곳에만 조건을 더하는 순서를 지킨다. 이 순서가 무너지면 조건이 조건을 덮으며 코드가 얽힌다.
내가 지키는 기본 원칙은 좁은 화면을 먼저 세우고 넓어질 때 조건을 더하는 방향이다. 좁은 화면용 스타일을 조건 없이 기본으로 깔아두고, 폭이 넉넉해지는 지점에서만 규칙을 얹으면, 조건이 겹겹이 쌓이지 않고 한 방향으로만 흐른다. 이 방향을 지키면 유지보수가 눈에 띄게 쉬워진다.
브레이크포인트는 특정 기기 크기가 아니라 배치가 실제로 무너지는 지점에서 잡는다. 나는 화면을 조금씩 넓혀보며 레이아웃이 어색해지는 폭을 찾아 거기에 조건을 두는데, 유명한 기기 해상도에 억지로 맞추던 예전보다 결과가 훨씬 자연스럽다. 콘텐츠가 브레이크포인트를 정하게 하는 것이다.
미디어 쿼리와 컨테이너 쿼리의 역할을 나누는 것도 중요하다. 페이지 전체의 골격은 화면 폭을 보는 미디어 쿼리로 잡고, 어디에나 놓일 수 있는 재사용 부품 내부는 부모 폭을 보는 컨테이너 쿼리로 다룬다. 나는 이 둘을 경쟁 관계가 아니라 층위가 다른 도구로 보고 함께 쓴다.
조건이 늘어날수록 값을 한곳에서 관리하는 습관도 든다. 나는 반복되는 브레이크포인트 숫자와 색 같은 값을 변수로 모아두고, 여러 규칙이 같은 값을 참조하게 한다. 그러면 나중에 한 곳만 고쳐도 관련된 조건들이 함께 따라와, 값이 서로 어긋나 생기는 사고를 막는다.
정리하면 미디어 쿼리 심화는 경계에서 튀지 않는 범위 구문에서 시작하고, 논리 연산으로 조건을 문장처럼 엮으며, 입력 특성으로 마우스와 손가락을 구분하고, 선호 쿼리로 사용자의 취향까지 존중한다. 여기에 모바일 우선의 순서와 콘텐츠 기반 브레이크포인트라는 설계 원칙을 더하면 미디어 쿼리가 환경을 읽는 감각기관이 된다. 다음 편에서는 이 입력 특성 이야기를 이어받아, 손가락으로 쓰는 화면을 위한 터치와 포인터를 더 깊이 살핀다.