반응형 디자인을 검색해보면 반복해서 등장하는 몇 가지 키워드가 있다. 미디어 쿼리(Media query), 뷰포트(Viewport), 분기점(Breakpoints), 모바일 같은 단어들이다. 그리고 반응형 디자인을 소개하는 글은 대체로 비슷한 흐름을 따른다. 뷰포트에 따라 분기점를 나누고, @media를 사용해 레이아웃을 변경한다. 많은 글들이 이 지점에서 설명을 끝맺는다.
이 방식으로도 화면 크기에 따라 레이아웃은 분명 '반응'하지만 여기서 한 가지 질문을 던져볼 수 있다. 단순히 레이아웃이 바뀌는 것만으로 그것을 진정한 의미의 '반응형'이라고 볼 수 있을까?
반응형 디자인을 구현하는 문제는 생각보다 단순하지 않다. 이러한 접근은 결국 디자인을 정해진 결과에 맞추는 방식에 가깝다. 특정 화면 크기에 맞춰 미리 준비된 레이아웃을 끼워 넣는 식이다. 이는 유연한 UI를 설계하는 것과는 거리가 있다. 진정한 의미의 반응형 디자인은 단순히 화면 크기에 대응하는 것을 넘어서 콘텐츠와 레이아웃, 그리고 인터랙션을 포함한 웹의 모든 요소가 변화에 자연스럽게 적응하도록 만들어야 한다.
그렇다면 반응형 디자인은 무엇을 지향해야 할까? 반응형 디자인은 특정 화면에 맞추는 기술이 아니라, 변화에 자연스럽게 적응하는 인터페이스를 설계하는 방식이다. 이런 관점에서 보면 반응형 디자인은 세 가지 특성으로 나눠볼 수 있다. 바로 가변성(Fluid), 유연성(Flexible), 그리고 **맥락 인지(Context-aware)**다. 이 세 가지는 서로 독립적인 개념이라기보다는 반응형 디자인이 얼마나 깊이 있게 구현되었는지를 보여주는 단계적 설계에 가깝다.
그렇다면 각각의 원칙을 어떻게 실현할 수 있을까? 미디어 쿼리에만 의존하지 않고 더 유연하게 반응하는 인터페이스를 만들기 위해 사용할 수 있는 방법들을 단계별로 살펴보자.
끊기지 않는 레이아웃
반응형 디자인의 기본 설계 원칙은 유동성이다. 강풍과 지진에도 유연하게 버티는 고층 건물처럼 웹 또한 변화에 저항하기보다 자연스럽게 흘러가야 견고하게 설계할 수 있다. 크기가 고정된 값을 사용하는 대신 레이아웃이 자연스럽게 늘어나고 줄어들도록 만드는 것이 가장 기본적인 수준의 반응형 디자인이다. 화면 크기가 변할 때마다 특정 지점에서 구조 자체를 바꾸기보다는 변화를 흡수하도록 설계하는 것이 핵심이다. 이를 위해 CSS에서는 고정된 값 대신 상대 단위를 활용할 수 있다.
%, vw, vh
%는 부모 요소를 기준으로, vw와 vh는 뷰포트를 기준으로 크기를 계산한다. 이러한 단위를 사용하면 요소의 크기가 특정 값에 고정되지 않고, 주변 환경에 따라 자연스럽게 변한다. 중요한 점은 "정확한 크기"를 지정하는 것이 아니라, "어떻게 변할지"를 정의한다는 것이다. 이는 고정된 결과를 만드는 방식이 아니라, 변화에 대응하는 규칙을 만드는 접근에 가깝다.
max-width, min-width
max-width와 min-width도 이러한 유동성을 제어하는 장치다. 요소의 넓이가 무한정 커지거나 작아지는 것을 방지하면서도, 일정 범위 내에서는 자유롭게 변화할 수 있도록 한다. 예를 들어 width: 50%와 max-width를 함께 사용해서 어떤 요소의 넓이를 지정해보자. 해당 요소의 넓이는 기본적으로 부모 요소의 1/2 비율의 넓이를 가지지만 계산된 넓이값이 max-width로 지정한 넓이 이상으로 넓어질 수는 없다. 화면이 작아지거나 주변의 다른 요소의 넓이에 따라 요소의 크기가 자연스럽게 줄어들고, 일정 크기 이상에서는 더 이상 확장되지 않도록 제한할 수 있다.
규칙에 따라 스스로 자리를 찾기
레이아웃이 끊기지 않고 자연스럽게 흐르도록 구성했다면 이제는 요소들이 스스로 배치되고 정렬될 수 있어야 한다. 유동적인 요소들이 특정 조건에 따라 배치되는 것이 아니라, 미리 정의된 규칙 안에서 자연스럽게 자리를 찾아가야 한다. 개별 요소가 상황에 맞게 행동해야 할 '규칙'을 설계하는 것이다.
예를 들어 화면이 줄어들 때마다 특정 지점에서 레이아웃을 강제로 바꾸는 대신, 요소들이 남는 공간에 따라 줄을 바꾸거나 크기를 조절하도록 만들 수 있다. Flexbox나 Grid 레이아웃은 이러한 방식을 가능하게 해준다. 요소의 개수가 변하거나 콘텐츠의 길이가 달라지더라도 레이아웃은 별도의 분기 없이도 스스로 균형을 맞춰서 변화한다.
두 레이아웃의 가장 큰 차이는 '차원'에 있다. Flexbox는 하나의 축을 기준으로 요소를 정렬하는 1차원 레이아웃인 반면, Grid는 행과 열을 동시에 다루는 2차원 레이아웃이다. Flexbox는 요소들이 하나의 방향으로 흐르며 정렬되는 경우에 적합하다. 네비게이션 바의 버튼이나 카드 리스트처럼 하나의 방향축에서 흐름을 제어할 때 유용하다. Grid는 레이아웃의 구조 자체를 정의할 때 적합하다. 배치할 공간을 먼저 만들고 그 위에 요소를 채우는 방식이다. 대시보드나 행과 열을 동시에 다루는 복잡한 레이아웃에 더 자연스럽다.
한편 Flexbox를 사용하다 보면 flex-wrap을 통해 요소를 다음 줄로 넘기는 방식으로 레이아웃을 구성하는 경우가 많다. 인스타그램의 피드를 Flexbox 레이아웃으로 바둑판처럼 정렬해보자.
.container {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.feed {
flex: 1 1 200px;
}
결과적으로는 아이템들이 여러 줄에 걸쳐 배치되기 때문에 2차원 레이아웃처럼 보이기도 한다. 그러나 Flexbox는 각 줄(row)을 독립적으로 정렬하기 때문에, 줄 간의 정렬이 맞지 않거나 전체적인 균형이 깨지는 문제가 발생할 수 있다. 특히 전체 레이아웃을 중앙으로 정렬한 상태에서 마지막 행의 요소 개수가 다른 행의 요소 개수와 다르다면, 마지막 행의 정렬된 결과가 의도된 정렬 결과와 다르게 위치한다. 일반적으로는 마지막 요소가 첫 번째 열에 맞춰 정렬되는 것이 더 자연스럽지만, Flexbox에서 이 문제를 해결하려면 특정 요소에 별도의 정렬을 적용해야 한다. 레이아웃이 아닌 개별 요소 수준에서 예외 처리를 해야 한다.
이는 단순히 줄 바꿈의 문제가 아니라 레이아웃 자체가 2차원 구조를 요구하고 있다. 이런 경우 처음부터 Grid 레이아웃을 사용하는 것이 더 자연스러운 해결 방법이 된다. Grid 레이아웃을 사용해서 불필요한 미디어 쿼리와 분기점 설정을 줄일 수 있고 더 간결하고 선언적인 방식으로 레이아웃을 구성할 수 있다.
환경을 읽는 설계
유동적이고 유연한 설계가 레이아웃이 자연스럽게 '흐르게' 만들고 '정렬'하는 단계라면, 맥락을 인지하는 설계는 그 레이아웃이 놓인 '환경'을 이해하는 단계다. 지금까지의 반응형 디자인을 구현하는 방식은 대부분 뷰포트의 크기를 기준으로 반응하여 동작한다. 여기서 한 걸음 더 나아가면, 환경과 사용자 설정까지 고려하는 설계를 생각해볼 수 있다. 콘텐츠의 길이, 컨테이너의 크기, 사용자가 설정한 기본 폰트 크기, 그리고 페이지의 확대/축소 비율까지. 웹은 생각보다 다양한 맥락을 가진다.
예를 들어, rem 단위로 폰트의 크기를 지정하는 것은 사용자가 설정한 브라우저의 폰트 크기에 반응한다는 점에서 의미 있는 선택이다. 사용자가 브라우저의 기본 폰트 크기를 변경하면 UI 역시 그 변화에 맞춰 조정된다. 브라우저의 폰트 크기를 줄이거나 늘리면 이때부터 더 이상 1rem이 16px이 아니게 된다.
동일한 컴포넌트라도 어떤 컨테이너 안에 배치되느냐에 따라 전혀 다른 레이아웃이 필요할 수도 있다. 이러한 문제를 해결하기 위해 등장한 것이 Container Query다. 더 나아가 콘텐츠 자체도 중요한 변수가 된다. 텍스트의 길이나 데이터의 양에 따라 레이아웃이 자연스럽게 변화하지 않는다면 아무리 정교하게 분기점을 설정하더라도 결국 특정 지점에서 레이아웃이 깨지게 된다.
반응형 디자인은 더 이상 viewport 뿐만 아니라 다양한 맥락에 동시에 반응해야 하는 문제다. 따라서 분기점은 디바이스의 스크린 크기가 아니라, 콘텐츠가 깨지는 지점을 기준으로 설정해야 한다. 디자인은 화면이 아니라 콘텐츠에서 시작되기 때문이다.
모두를 위한 기본, 그리고 점진적으로 나아가기
사용자의 환경과 웹을 사용하는 맥락에 동시에 반응해야 한다면 모든 변수를을 완벽하게 고려해서 대응하는 일은 현실적으로 불가능하다. 디바이스, 브라우저, 사용자 설정, 네트워크 환경까지 포함해서 고려해야 할 경우의 수는 무한에 가깝기 때문이다. 그렇기 때문에 모든 환경의 사용자에게 동일한 경험을 제공하려는 대신, 모든 사용자에게 기본적인 기능을 제공하고 가능하다면 더 나은 환경의 사용자에게 점진적으로 더 나은 경험을 추가하는 방식을 고려해볼 수 있다. 이는 단순히 구형 브라우저를 대응하는 전략이 아니다. 다양한 맥락을 가진 사용자에게 일관된 접근성을 보장하기 위한 설계 방식이다.
모든 브라우저에서 동일한 CSS 속성을 제공하지 않는다. 심지어 같은 브라우저라고 할지라도 디바이스 종류와 버전에 따라 사용 가능한 CSS 속성이 다르다. 예를 들어, 아직 실험 단계에 있는 corner-shape CSS 속성은 다양한 모양의 border를 설정할 수 있지만 Safari와 Firefox에서 지원하지 않는다. 이 경우 다음과 같이 @supports를 활용해 점진적으로 사용자의 경험을 향상시킬 수 있다.
css
.item {
border: thick double red;
}
@supports (corner-shape: notch) {
.item {
corner-shape: notch;
}
}
또 다른 예로, JavaScript 없이도 기본적인 기능이 동작하도록 설계하는 것도 점진적 향상의 중요한 원칙이다. 반응형 디자인과 관련해서 자주 보이는 안티패턴도 JavaScript로 스타일을 통제하려는 경우이다. 화면 크기를 동적으로 감지해서 레이아웃을 다시 계산하거나 클래스를 추가하는 패턴을 자주 볼 수 있다. 스크롤 위치에 따라 특정 요소의 위치를 고정시키는 접근도 이와 같은 경우다. 물론 이런 접근 방식이 가장 먼저 손이 가고 쉽고 간단하게 문제를 해결할 수 있지만 CSS만으로 충분히 해결할 수 있는 문제이다. 레이아웃은 선언으로 다루고 동작은 점진적으로 추가하는 것이 더 견고한 설계다.
최종 병기 Media Query
반응형 디자인를 구현하는 설계에서 미디어 쿼리를 완전히 배제할 수는 없다. 유연한 설계 원칙에도 불구하고 특정 분기점에서 레이아웃이 변경되는 방식은 반응형 디자인에서 여전히 중요한 역할을 한다. 다만 그것이 기본 전략이 되는 것은 바람직하지 않다. 미디어 쿼리는 꼭 필요한 순간에 사용하는 보조적인 수단에 가깝다. 앞서 언근한 세 단계의 원칙을 기반으로 반응형 다지인을 설계했음에도 해결되지 않는 지점에서 미디어 쿼리를 사용하는 순서를 지향해야 한다. 아래의 경우 미디어 쿼리의 사용을 고민해볼만 한다.
- 레이아웃의 구조 자체가 변경되는 경우. 화면 크기에 따라 사이드바가 등장하거나 사라져야 한다면 구조가 변경되는 명확한 분기점이 필요하다.
- UX 패턴이 변경되는 경우. 특정 지점에서 네비게이션 바(Navigation Bar)가 햄버거 메뉴(Hamburger Menu)로 변경되거나 탭(Tab)에서 드롭다운(Dropdown)으로 변경되는 경우는 단순히 레이아웃의 문제가 아니다. 사용자가 웹을 사용하는 모델 자체가 달라지기 때문에 이런 경우 연속적으로 문제를 해결할 수 없다.
- 화면 크기 대비 폰트 크기가 작아서 사용자의 가독성을 침해하는 경우.
rem같은 상대 단위로 폰트 크기를 지정할 수 있지만 특정 임계정에서 가독성이 떨어진다면 폰트 크기를 키우거나 늘어나는 속도를 다르게 지정할 수 있다. - 성능/리소스 대응 전략. 화면 크기에 따라 이미지의 크기를 다르게 로딩하여 불필요한 데이터 사용을 줄일 수 있다.
대부분의 변화는 연속적이고 이에 유연하게 반응하여 문제를 해결할 수 있지만 모든 변화가 연속적인 것은 아니다. 레이아웃 구조가 바뀌거나, 인터랙션 방식이 달라지는 순간처럼 ‘명확한 전환’이 필요한 지점에서는 여전히 미디어 쿼리가 필요하다. 그렇다고 미디어 쿼리를 남용하는 경우 변화는 연속적이지 않고 계단식으로 사용자 경험도 끊기게 된다. 미디어 쿼리는 변화가 단절된 지점에서 안정적으로 사용자 경험을 제공하기 위한 도구일 뿐, 반응형 디자인의 출발점이 되어서는 안 된다.
사실 반응형 디자인을 완벽하게 구현하는 일은 쉽지 않다. 고려해야 할 조건이 많고, 각각의 변수를 독립적으로 다룰 수도 없기 때문이다.
하지만 도구나 구현 방법을 익히는 것보다, 반응형 디자인을 바라보는 관점을 바꾸는 것이 더 중요하다는 것을 깨달았다.
그동안 나는 반응형 디자인을 구현할 때 뷰포트와 디바이스에만 지나치게 집중해 문제를 해결하려고 했다. 하지만 디바이스의 화면 크기와 실제 뷰포트의 크기는 반드시 일치하지 않는다. 같은 노트북이라도 창 크기를 줄이면 모바일보다 더 작은 환경이 될 수 있다. 이런 단순한 사실을 뒤늦게 인지했다.
결국 특정 디바이스나 정해진 뷰포트에 맞추는 방식은 많은 문제를 해결해 주지만, 근본적인 해결책은 아니었다. 오히려 그 과정에서 더 중요한 것들을 놓치고 있었다. 브라우저 확대에도 깨지지 않는 UI, 콘텐츠 길이에 따른 자연스러운 흐름, 사용자 설정에 반응하는 타이포그래피, 그리고 다양한 입력 방식에 대응하는 인터랙션까지.
돌이켜보면 나는 그동안 모든 변화를 미디어 쿼리와 JavaScript로만 ‘대응’하려고 했다. 문제는 계속 달라졌지만, 해결 방식은 늘 같았다.
하지만 반응형 디자인은 조건에 대응하는 문제가 아니라, 변화에 적응하는 문제다.
조건을 나누고 분기하는 방식으로는 결국 한계에 부딪힐 수밖에 없다. 중요한 것은 더 많은 분기를 만드는 것이 아니라, 변화가 자연스럽게 흘러갈 수 있도록 설계하는 것이다.