단위를 유연하게 쓰는 법을 익히고 나니, 이제는 화면 크기 자체에 따라 배치를 다르게 하고 싶어졌다. 좁은 휴대폰 화면에서는 세로로 쌓이던 요소가 넓은 데스크톱에서는 나란히 놓이는, 그런 반응형 화면 말이다. 유연한 단위가 값의 크기를 부드럽게 조절하는 것이라면, 반응형은 아예 배치의 구조를 바꾸는 일이다. 그 구조 변경의 스위치가 바로 미디어 쿼리였다.
이번 편은 반응형 웹의 기초인 미디어 쿼리를 다룬다. 미디어 쿼리가 무엇이고 어떻게 조건을 거는지, 모바일을 먼저 생각하는 접근이 왜 유리한지, 사용자의 취향과 환경을 읽는 쿼리에는 어떤 것이 있는지, 그리고 화면이 아닌 부모 크기에 반응하는 컨테이너 쿼리까지 살펴본다. 이 도구들을 익히면 하나의 코드로 모든 크기의 화면을 감당할 수 있다.
1. 미디어 쿼리라는 조건 스위치
미디어 쿼리는 특정 조건이 참일 때만 스타일을 적용하도록 감싸는 장치다. 화면 폭이 일정 값 이상일 때, 혹은 이하일 때처럼 조건을 걸고, 그 조건이 맞을 때만 안에 담긴 규칙이 살아난다. 나는 이 조건 스위치를 처음 봤을 때, 하나의 스타일 파일로 여러 화면을 감당할 수 있다는 사실에 감탄했다. 화면마다 다른 페이지를 만들 필요가 없어진 것이다.
가장 많이 쓰는 조건은 화면의 너비다. 화면이 어느 폭 이상이면 이렇게, 그 미만이면 저렇게 배치하라는 식으로 조건을 나눈다. 나는 좁은 화면에서 한 줄에 하나씩 쌓이던 카드가, 넓은 화면에서는 여러 열로 펼쳐지도록 미디어 쿼리로 구분한다. 같은 요소가 화면에 따라 전혀 다른 모습으로 재배치되는 것이다.
조건을 나누는 경계값을 브레이크포인트라고 부른다. 이 경계를 넘는 순간 배치가 바뀌므로, 어디에 경계를 둘지가 반응형 설계의 핵심 결정이다. 나는 처음에 특정 휴대폰이나 태블릿의 정확한 해상도를 경계로 삼으려 했는데, 기기 종류가 너무 많아 금세 감당이 안 됐다. 특정 기기를 좇는 방식은 유지보수의 늪이었다.
더 나은 방법은 콘텐츠가 깨지는 지점을 경계로 삼는 것이다. 화면을 서서히 좁히다 보면 어느 순간 배치가 어색해지거나 넘치는 지점이 온다. 바로 그 지점이 브레이크포인트가 되어야 한다. 나는 특정 기기 대신 내 콘텐츠 자체를 기준으로 경계를 잡은 뒤로, 새로운 기기가 나와도 흔들리지 않는 튼튼한 반응형을 얻었다.
경계값도 단위 선택이 중요하다. 픽셀 대신 상대 단위로 경계를 잡으면, 사용자가 브라우저 기본 글자 크기를 키웠을 때 경계도 함께 반응한다. 나는 접근성을 위해 브레이크포인트를 상대 단위로 지정한다. 글자를 키운 사용자에게는 실질적으로 화면이 좁아진 것과 같으므로, 경계가 그에 맞춰 앞당겨지는 것이 자연스럽기 때문이다.
미디어 쿼리는 너비 말고도 다양한 조건을 걸 수 있다. 화면의 방향이 가로인지 세로인지, 화면의 해상도가 높은지, 심지어 인쇄용인지까지 구분한다. 나는 인쇄할 때만 불필요한 요소를 숨기는 규칙을 미디어 쿼리로 따로 두기도 한다. 조건의 종류가 다양하다는 걸 알면, 화면 폭 외에도 상황에 맞춘 세밀한 대응이 가능해진다.
2. 모바일을 먼저 생각하기
반응형을 짜는 방향에는 두 가지가 있다. 넓은 화면을 기본으로 두고 좁은 화면을 덧붙이는 방식과, 좁은 화면을 기본으로 두고 넓은 화면을 덧붙이는 방식이다. 나는 여러 번의 시행착오 끝에 좁은 화면, 즉 모바일을 먼저 생각하는 방식이 훨씬 낫다는 결론에 이르렀다. 이걸 모바일 우선 접근이라고 부른다.
모바일 우선은 아무 조건 없는 기본 스타일을 모바일 기준으로 작성하는 데서 시작한다. 그리고 화면이 일정 폭 이상으로 넓어질 때만 배치를 더하는 규칙을 얹는다. 스타일이 위에서 아래로, 좁은 화면에서 넓은 화면으로 자연스럽게 쌓여 올라간다. 나는 이 누적되는 흐름이 코드를 읽기 쉽게 만든다는 점이 마음에 든다.
반대 방향인 데스크톱 우선은 넓은 화면을 기본으로 두고, 좁아질 때마다 배치를 되돌리는 규칙을 덧붙인다. 그런데 이 방식은 좁은 화면에서 원래 스타일을 지우고 다시 쓰는 되돌리기 코드가 자꾸 늘어난다. 나는 이 방식으로 짰다가 규칙끼리 충돌하며 디버깅이 어려워지는 걸 겪고, 모바일 우선으로 방향을 통일했다.
모바일 우선에는 성능상의 이점도 있다. 저사양 휴대폰은 기본 스타일만 처리하면 되고, 넓은 화면 전용 규칙은 조건이 맞을 때만 계산한다. 데스크톱 우선이라면 휴대폰이 넓은 화면용 규칙까지 모두 읽은 뒤 되돌려야 하니 비효율적이다. 나는 성능이 예민한 프로젝트일수록 이 차이가 쌓여 무시할 수 없게 된다는 걸 배웠다.
중요한 것은 두 방향을 한 프로젝트에서 섞지 않는 것이다. 어떤 규칙은 넓은 화면에서, 어떤 규칙은 좁은 화면에서 걸면 경계 근처에서 규칙들이 서로 엉킨다. 나는 초기에 두 방식을 무심코 혼용했다가, 특정 폭에서 스타일이 이상하게 겹치는 문제를 겪었다. 방향을 하나로 정하고 끝까지 지키는 것이 혼란을 막는 길이다.
모바일 우선은 사고방식의 훈련이기도 하다. 좁은 화면을 먼저 설계하면 정말 중요한 것만 남기고 군더더기를 덜어내게 된다. 그렇게 추린 핵심 위에 넓은 화면의 여유를 더하는 것이다. 나는 이 순서로 작업하면서 화면의 우선순위를 더 냉정하게 판단하게 되었다. 제약이 오히려 더 좋은 설계를 이끌어낸다는 걸 반응형에서 배웠다.
3. 사용자의 환경과 취향을 읽기
미디어 쿼리는 화면 크기만이 아니라 사용자의 설정과 취향도 읽을 수 있다. 그중 하나가 밝은 화면과 어두운 화면 중 무엇을 선호하는지를 알려주는 쿼리다. 운영체제나 브라우저에서 어두운 화면을 택한 사용자에게는 어두운 배경과 밝은 글자를 자동으로 내줄 수 있다. 나는 이 쿼리를 알고 나서 다크 모드를 손쉽게 지원하게 되었다.
어두운 화면 선호를 감지하면, 배경과 글자, 테두리 색을 어두운 환경에 맞게 바꾼다. 사용자가 별도로 버튼을 누르지 않아도 시스템 설정에 맞춰 화면이 알아서 어두워지는 것이다. 나는 색을 변수로 정의해두고, 이 쿼리 안에서 변수 값만 바꾸는 방식으로 다크 모드를 구현한다. 그러면 규칙을 두 벌 쓰지 않고도 두 가지 화면을 지원할 수 있다.
또 하나 중요한 것은 움직임을 줄이고 싶어 하는 사용자를 위한 쿼리다. 화면의 큰 움직임이나 전환 효과에 어지럼증이나 불편을 느끼는 사람들이 있고, 이들은 시스템 설정에서 움직임 최소화를 켠다. 나는 이 설정을 감지하면 애니메이션과 전환을 꺼서, 움직임에 민감한 사용자를 배려한다. 화려함보다 편안함이 우선인 사용자가 분명히 존재한다.
움직임을 줄이는 대응은 접근성의 중요한 부분이다. 멋진 전환 효과가 누군가에게는 고통이 될 수 있다는 걸 알고 나서, 나는 새 애니메이션을 넣을 때마다 이 설정을 감지하는 대비를 함께 둔다. 움직임을 완전히 없애기보다 아주 짧게 줄이는 절충도 가능하다. 중요한 건 사용자의 명시적 선호를 무시하지 않는 것이다.
이 밖에도 사용자의 환경을 읽는 쿼리는 계속 늘고 있다. 화면의 대비를 높이고 싶어 하는지, 데이터 사용을 아끼고 싶어 하는지 같은 선호까지 감지할 수 있다. 나는 이런 쿼리들을 볼 때마다, 웹이 획일적인 화면을 강요하는 대신 각 사용자의 사정에 맞추는 방향으로 발전하고 있다고 느낀다. 반응형은 화면 크기를 넘어 사람에 반응하는 것으로 넓어졌다.
사용자 선호 쿼리의 좋은 점은 조용히 작동한다는 것이다. 설정을 켠 사용자에게만 효과가 나타나고, 그렇지 않은 사용자는 기존 화면을 그대로 본다. 나는 이런 배려를 부담 없이 얹을 수 있는 점진적 향상으로 받아들인다. 지원하면 누군가에게 큰 도움이 되고, 그렇지 않아도 아무 문제가 없다. 배려의 비용이 이토록 낮은 경우는 드물다.
4. 화면이 아닌 부모에 반응하기
미디어 쿼리에는 근본적인 한계가 하나 있었다. 항상 화면 전체의 크기만 볼 수 있다는 것이다. 그런데 같은 컴포넌트가 넓은 본문에 놓일 때와 좁은 사이드바에 놓일 때는 다르게 배치되어야 한다. 화면은 넓어도 그 컴포넌트가 놓인 자리는 좁을 수 있는데, 미디어 쿼리는 이 국소적인 사정을 알지 못했다. 나는 이 한계 때문에 오래 답답했다.
이 문제를 푸는 것이 컨테이너 쿼리다. 컨테이너 쿼리는 화면 전체가 아니라 요소를 감싼 부모의 크기에 반응한다. 그래서 같은 컴포넌트라도 넓은 자리에 놓이면 가로로 펼쳐지고, 좁은 자리에 놓이면 세로로 쌓이게 만들 수 있다. 나는 컨테이너 쿼리를 처음 써본 순간, 진정으로 재사용 가능한 컴포넌트를 만들 수 있게 되었다고 느꼈다.
컨테이너 쿼리를 쓰려면 먼저 어떤 요소가 크기의 기준이 될지를 지정해야 한다. 부모 요소를 크기 질의의 대상으로 선언해두면, 그 안의 자식들이 부모 폭을 기준으로 배치를 바꿀 수 있다. 나는 카드를 감싸는 래퍼를 기준 컨테이너로 삼고, 카드 내부가 그 폭에 따라 반응하도록 짠다. 카드가 어디에 놓이든 자기 자리 크기에 맞춰 스스로 적응한다.
컨테이너 쿼리에는 전용 단위도 있다. 기준 컨테이너의 크기에 대한 비율을 나타내는 단위여서, 부모 폭에 비례하는 값을 자연스럽게 표현한다. 나는 컨테이너 안에서 글자나 간격을 이 단위로 주어, 부모가 넓어지면 함께 커지도록 한다. 화면 전체가 아니라 놓인 자리에 비례하는 크기라는 점이 컴포넌트 설계에 딱 맞는다.
컨테이너 쿼리와 미디어 쿼리는 경쟁하는 게 아니라 역할이 나뉜다. 나는 페이지 전체의 큰 골격, 이를테면 사이드바를 접거나 펼치는 결정은 미디어 쿼리로 하고, 그 안에 들어가는 개별 컴포넌트의 배치는 컨테이너 쿼리로 맡긴다. 큰 틀은 화면에 반응하고, 작은 조각은 자기 자리에 반응하는 이 분업이 반응형을 훨씬 견고하게 만든다.
컨테이너 쿼리는 비교적 최근에 표준으로 자리 잡은 기능이지만, 이제는 실무에서 안심하고 쓸 만큼 널리 지원된다. 나는 새로 만드는 컴포넌트라면 처음부터 컨테이너 쿼리를 염두에 두고 설계한다. 어디에 갖다 놔도 알아서 적응하는 컴포넌트는 재사용성이 압도적으로 높기 때문이다. 반응형의 단위가 화면에서 컴포넌트로 내려온 셈이다.
5. 무너지지 않는 반응형의 습관
반응형을 오래 다루며 얻은 첫 번째 습관은 화면을 천천히 늘였다 줄였다 하며 직접 확인하는 것이다. 브레이크포인트만 점검하는 게 아니라, 경계 사이의 모든 폭에서 배치가 자연스러운지 살핀다. 나는 특정 경계에서만 잘 보이고 그 사이 어중간한 폭에서 깨지는 화면을 여러 번 겪은 뒤로, 연속적으로 크기를 바꿔가며 점검하는 습관을 들였다.
두 번째는 브레이크포인트를 최소한으로 유지하는 것이다. 경계를 잘게 나눌수록 관리할 상태가 늘고 예외가 많아진다. 나는 유연한 단위와 자동으로 줄바꿈되는 레이아웃을 최대한 활용해서, 꼭 필요한 곳에만 경계를 둔다. 좋은 반응형은 경계가 많은 게 아니라, 경계 없이도 대부분의 폭을 매끄럽게 소화하는 것이다.
세 번째는 가로 스크롤을 절대 만들지 않는 것이다. 좁은 화면에서 어떤 요소가 화면 폭을 넘어서면 원치 않는 가로 스크롤이 생겨 사용 경험을 크게 해친다. 나는 반응형을 점검할 때 가장 좁은 화면에서 가로로 넘치는 요소가 없는지 반드시 확인한다. 긴 단어나 고정 폭 요소가 넘침의 흔한 원인이므로 이런 부분을 특히 눈여겨본다.
네 번째는 이미지와 미디어를 유연하게 두는 것이다. 고정 크기의 이미지는 좁은 화면에서 쉽게 넘친다. 나는 이미지의 최대 폭을 부모에 맞춰 제한해서, 화면이 좁아지면 이미지도 함께 줄어들게 한다. 이 간단한 처리 하나만으로 이미지 때문에 생기는 넘침 문제의 대부분이 사라진다. 미디어는 반응형에서 특히 조심해야 할 요소다.
다섯 번째는 실제 기기에서 확인하는 것이다. 개발 도구의 화면 축소 기능은 편리하지만 실제 기기의 감각을 완전히 대신하지는 못한다. 나는 중요한 화면은 실제 휴대폰으로 열어보며, 손가락으로 누를 요소가 충분히 큰지, 글자가 답답하지 않은지 확인한다. 화면 크기뿐 아니라 손으로 만지는 경험까지 챙겨야 반응형이 완성된다.
정리하면 반응형의 기초는 미디어 쿼리로 조건을 걸고, 모바일을 먼저 생각하며, 사용자의 환경과 취향까지 읽고, 컴포넌트는 컨테이너 쿼리로 자기 자리에 반응하게 하는 것이다. 나는 이 도구들을 조합해 하나의 코드로 모든 화면을 감당한다. 화면 크기에 맞서 싸우기보다 흐르게 두는 감각이 반응형의 핵심이다. 다음 편에서는 상태가 바뀔 때 그 변화를 부드럽게 이어주는 트랜지션을 다룬다.