디자이너라면 누구나 'breakpoint를 뭘로 잡아야할까?'에 대한 고민이 있을거라고 생각한다. iOS 같은 경우는 가로/세로에 대한 size class가 있어서 4가지 케이스만 대응하면 되는데, AOS나 웹 같은 경우는 권장 사이즈가 있을 뿐 뾰족하게 정답이라고 정의내리기는 힘들다.
모바일
,타블렛
,노트북
,PC
등으로 구분한다고 치면, 그럼 테블릿
-노트북
을 구분짓는 viewport width는 전세계 노트북 중에 제일 작은걸로 최소값을 정하면 깔끔하게 해결될 거 같다가도, 제일 큰 테블릿이 제일 작은 노트북보다 크다면 어떻하지?라는 물음에 게슈탈트 붕괴가 오기도 한다. 우리는 무의식 중에 타블렛-노트북을 터치 유/무로 구분하고는 있는데, 점점 터치가 되는 대형화면과 블루투스 마우스 연결이 가능한 타블렛들이 지구상에 많이 존재하고 있어서 사실상 viewport width로는 구분의 의미가 크게 없어진 상황이다. (더군다나 split, pip(창모드) 모드처럼 큰 유형의 디바이스가 보다 작은 유형의 디바이스 사이즈를 활용하고 있기도 하니까 말이다)
결국 의식의 흐름상 애초에 '디바이스의 유형을 viewport width를 기준으로 잡는게 맞는걸까?'라는 생각이 든다. 디자이너의 의도에 따라 크게 커서 사용성과 터치 사용성에 대해서 레이아웃상 분기가 필요하다면 사용자의 디바이스 유형이 뭔지를 먼저 필터링하고, 그 이후에 필요에 따라서 fluid layout 방식을 적용하거나, viewport width에 따른 분기를 진행하는 것이 좋을 것 같다는 생각에 다다른다.
CSS에서 @키워드
를 at-rule(규칙)이라고 하고, @import
, @font-face
, @key-frames
, @media
등이 있다. CSS 스타일를 적용하기 이전에 필요한 사전 규칙들을 정의할 수 있는 CSS Statement이다.
@import
다른 CSS를 가져와서 같이 씀
@font-face
폰트를 불러와서 정의함
@key-frames
사전 정의된 에니메이션값을 만듬
@media
특정 조건에 따라서 스타일을 분기함
@media
를 미디어쿼리라고 부르는데, 쉽게 생각해서 if문이라고 생각하면 좋을 것 같다.
아래 CSS 코드 스니펫을 참고해보면 viewport의 width를 구간을 나눠줄 수 있다는 걸 알 수 있다.
@media (max-width: 639px) {
// 최대 639px일 때 모바일 레이아웃
}
@media (min-width: 640px) and (max-width: 1279px) {
// 640px - 1279px 사이일 때 테블릿 레이아웃
}
@media (min-width: 1280px) {
// 1280 이상일 때 PC 레이아웃
}
이 방식의 문제점은 639.5px
이거나 1279,5px
일 때인데, @media (max-width: 639.99999)
로 하더라도 결국 찝찝해지는건 어쩔 수 없다.
그래서 한쪽 방향(이상 or 이하)으로만 조건을 만들어주는게 좋은데, 예를 들어서 우리가 만드는 서비스가 모바일이 더 우선이라고 생각해보자. 기본적으로 A 레이아웃을 만들고, 640이상이면 B, 1280이상이면 C 레이아웃을 구성하게 되는 방식이기 때문에 더 직관적이게 된다.
// 모바일(A) 레이아웃 (default)
@media (min-width: 40rem) {
// 40rem(640px)이상일 때 B 레이아웃
}
@media (min-width: 80rem) {
// 80rem(1280px) 이상일 때 C 레이아웃
}
여기서 단위를 px
대신 rem
으로 쓴 이유는 시력이 안좋은 사용자가 브라우저의 텍스트 크기를 200% 이상으로 키웠을 때, breakpoint도 동적으로 늘어나는 편이 유리하기 때문이다. 레이아웃이나 공간 변화없이 텍스트만 커지게 된다면 가독성이 많이 떨어지게 되는 현상을 아래 참고 이미지에서 볼 수 있다.
우리의 사고방식이 아직 스크린 터치와 커서 사용성 정도에 머물러 있지만, 생각보다 다양한 제스쳐를 활용한 디바이스들이 많다. 최근에 나온 애플 비전 프로만 보더라도 eye-tracking과 vision에 의한 손동작 감지를 사용하고 있다. 디바이스 타입을 대략적으로 정리해보자면 아래 이미지와 같은데, 이 구분들은 미디어쿼리에서 사용할 수 있고 Interaction Media Features라는 이름으로 비교적 최근에 추가된 사양이라고 할 수 있다. 그리고 테블릿에서 블루투스 마우스를 연결하는 것처럼 인풋 방식이 바뀌는 것을 동적으로 감지할 수 있기 때문에, 동적 쿼리라고도 부른다.
hover는 마우스/트랙패드처럼 pointer(cursor)를 click/tap(touch) 이벤트 없이 엘리먼트 위로 움직일 수 있는지에 대한 여부를 나타낸다. 쉽게 말해서 항상 커서가 떠있는지 유/무라고 할 수 있다.
@media (hover: hover) {
// hover가 가능한 디바이스에서 CSS 스타일 정의하기
}
그리고 버튼이나 링크처럼 인터렉티브한 엘리먼트 위에 pointer가 올라가 있는 상태를 엘리먼트의 Pseudo-classes(가상 클래스)로 정의할 수 있다.
button:hover {
// 버튼이 hover 상태일 때 CSS 스타일 정의하기
}
pointer는 click/tap(touch) 제스쳐를 얼마나 정밀하게 위치 제어 할 수 있는지를 구분한다.
fine: 매우 정밀
coarse: 그렇게 정밀하지는 않음
none: tap를 통한 focus 이동 정도만 가능함
터치 기반 디바이스(pointer: coarse) 같은 경우에 최소 터치 영역을 44pt이상 x 44pt이상
으로 권장하고 있기 때문에, 엘리먼트 사이즈를 실제로 더 키우거나 Pseudo-elements(가상 엘리먼트)를 통해서 터치 영역만 키워주는 방법이 필요하다.
.touch-area::after {
--touch-area-minimum: 44px;
--inset-by: min(0px, calc((100% - var(--touch-area-minimum)) / 2));
// 각 상하좌우 절반 크기가 22px보다 작으면 작은만큼을 음수로 뱉어낸 값
content: ""; // 빈 공간으로 만들기
position: absolute;
top: var(--inset-by); // 그 값만큼 위로 커지게 하기
left: var(--inset-by); // 그 값만큼 왼쪽으로 커지게 하기
right: var(--inset-by); // 그 값만큼 오른쪽으로 커지게 하기
bottom: var(--inset-by); // 그 값만큼 아래로 커지게 하기
}
click, tap, touch의 구분
click: 마우스나 트랙패드처럼 hover가 가능한 기기에서 엘리먼트를 active하는 이벤트
tap: 손가락이나 스타일러스와 같은 터치 기반 디바이브에서 엘리먼트를 active하는 이벤트
touch: tap은 하나의 결과만 있다면 touch는 touchStart, touchEnd처럼 터치가 일어나는 상황을 구간별로 정의할 수 있음
개발적으로는 click이나 tap 중에서 어떤게 상위 개념인지는 모르겠지만, 디자이너들이 제스쳐를 설명할 때 '모바일앱에서 버튼을 click한다'거나 하는 잘못된 용어 사용은 지양했으면 하는 작은 소망이 있다
button {
// 버튼의 기본 스타일 정의
}
button:active {
// 버튼을 눌렀을 때 스타일 정의
}
@media (hover: hover) and (pointer: fine) {
button:hover {
// 버튼이 hover 상태일 때 스타일 정의
}
}
viewport width만으로 디바이스 유형을 나누고 레이아웃을 구성하려고 했던 지점 때문에 늘 정답이 없었던 것처럼 느껴졌다면, 이제는 뭔가 hover나 pointer의 상태에 대한 미디어쿼리로 잘 버무려보면 정답을 찾을 수 있을 것만 같은 느낌이 든다.
viewport view
로 구분하는 것은 디자이너의 의도나 텍스트의 가독성과 주로 연관이 있을 것 같다.
hover
나pointer
로 구분하는 것은 디바이스의 물성에 따른 사용성에 연관이 있을 것 같다.
미디어 쿼리는 if문처럼 중첩 사용할 수 있는데, 그렇다면 viewport view, hover, pointer이 3가지의 우선 순위를 어떻게 구성해야할까? 정답은 없겠지만, 내가 생각한 방식은 pointer > (viewport width) > hover인데 가급적이면 viewport width는 지양하고 fluid 방식으로 처리하면 좋을 듯 하다.
@media (min-width: 40rem)
@media (pointer:fine)
@media (min-width: 40rem)
@media (hover: hover)
터치 디바이스 기본 스타일
@media (min-width: 40rem){
터치 디바이스 큰 해상도 스타일
}
@media (pointer:fine){
포인터 디바이스 기본 스타일
@media (min-width: 40rem){
포인터 디바이스 큰 해상도 스타일
@media (hover: hover){
hover 상태 정의
}
}
}
화면의 비율이 가로인지 세로인지 판단@media (orientation: portrait)
해서 스타일을 정의하는 방식도 있는데, 아직은 유의미하게 쓸만한 용도를 찾지 못했다.
지금까진 조건이 먼저 있고 조건에 따라서 엘리먼트들이 동시에 변경되는 부분을 다뤘었는데, react + styled-components 같은걸 사용하면 엘리먼트 내부에 미디어 쿼리를 넣을 수 있기 때문에 각 엘리먼트마다의 조건을 다르게 지정할 수 있다.
예를 들어 viewport width가 80rem(1280px)을 넘어가면 모든 엘리먼트들이 동시에 바꼈다고 한다면, 컴포넌트 기반인 react 라이브러리 같은걸 쓰면 어떤건 40rem을 넘어갔을 때 바뀌고, 어떤건 60rem이 넘어갔을 때 바뀌게 할 수도 있다.
디바이스 유형이나 뷰포트의 상태에 따라서 동적으로 페이지를 구성해야겠다는 의지치를 갖는게 정말 중요할 것 같다. 이 시야를 가지고 평소에 쓰던 서비스들을 둘러보면 생각보다 엉망인 서비스들도 많기 때문이다. '점점 모니터가 커지고 있는 추세니까 이 사이즈 정도로 디자인하면 되지 않을까'라는 정도로 사고가 갇힌 사람들도 있어서, 창크기를 줄여보면 컴포넌트들이 다 잘려서 보이는 페이지들도 숱하게 많이 봐왔는데, 안타까울 따름이었다.
앞선 포스팅이랑 묶어서 동적인 웹페이지를 구성하는 흐름에 대해서 정리를 해보자면 다음과 같은 순서가 될 것 같다.
사실 눈치가 빠른 디자이너들은 알겠지만, 쓰는 용어가 다를 뿐 figma에서 웬만한 기능들은 다 대응해주고 있고 css로 handoff도 잘 해준다. 그렇기 때문에 auto layout을 잘 활용해서 fixed, fill container, hug contents, min/max widht, top/right/bottom/left 등을 의도에 따라서 잘 설정해주기만해도 개발자분들과 커뮤니케이션 비용을 꽤 많이 줄일 수 있다.
미디어 쿼리로 대응이 가능할 지, 아니면 차라리 적응형처럼 도메인을 분리하는게 좋을지는 개발자분들과 잘 논의해보면 좋을 것 같다.
미디어 쿼리로 충분히 대응가능한 케이스가 있는 반면에
- 작은 해상도에서는 A컴포넌트(메뉴 drawer)가 보여지고, 큰 해상도에서는 B컴포넌트(GNB)가 보여진다. (display: none;)
- 작은 해상도에서는 flex layout을 큰 해상도에서는 grid layour을 적용한다.
기능상 변경되는 지점이 많거나 개발 구현 방식이 전혀 다르다면 아예 적응형처럼 도메인을 분리하는 것이 방법이 될 수도 있다.