문제 상황
캐시워크 내 동네생활 서비스에 화면 확대가 안 된다는 CS 문의가 여럿 들어왔다. 시력이 약하거나 고령인 사용자들이었다. 원인은 viewport meta 태그의 user-scalable=no 옵션이었다. 네이티브 앱 영역은 사용자가 기기의 글씨 크기를 키우면 글씨가 커지지만, 웹뷰 영역은 그렇지 않다. iOS 웹뷰는 Dynamic Type을 자동으로 따라가지 않기 때문이다.
<!-- before -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- after -->
<meta name="viewport" content="width=device-width, initial-scale=1">
사실 iOS 10부터 Safari는 user-scalable=no를 무시한다. 접근성을 위해 그렇게 바꿨다. (WebKit 블로그 · New Interaction Behaviors in iOS 10) 하지만 WKWebView는 다르다. WKWebViewConfiguration의 ignoresViewportScaleLimits가 기본값 false라서, 웹뷰는 여전히 user-scalable=no를 그대로 따른다. 브라우저에서는 이미 무력화된 설정이 웹뷰에서만 살아있었던 것이다.
그렇다면 웹뷰에서 이 설정(user-scalable=no)을 하게 된 배경으로 거슬러 올라가보자. 이 설정은, 웹뷰 내 입력창을 누르면 자동으로 화면이 확대(줌 인)되는 현상을 막기 위해 설정한 값이었다. 입력창을 누르면 화면이 확대되는데, 사용자는 그 사실을 모르니 좌우가 잘려 보인다고 느끼는 경우가 많았다. 다만 이 자동 확대는 iOS에서만 일어나는 동작이다. 결국 iOS 한 곳의 동작을 막으려고 모든 사용자의 확대 기능을 통째로 꺼둔 셈이었다.
해결 방안
입력창의 폰트 크기를 16px로 설정한다.
입력창이 포커스되었을 때 화면이 확대되는 근본적인 원인은 폰트 크기가 16px보다 작기 때문이다. 그러니 해결책은 폰트 크기를 16px 이상으로 두는 것뿐이다. 단순하다.
정말 우회해서, 평소에는 16px 미만으로 두고 입력창을 포커스 했을 때만 16px로 바꾸는 방법도 떠올렸다. 하지만 포커스 시점에 폰트 크기가 바뀌면 그 순간 레이아웃이 흔들려 보여서 제외했다.
그럼 CSS상 폰트 크기는 16px로 두되, 화면에는 기존처럼 16px보다 작게 보이게 하는 방법은 없을까. 두 가지가 떠올랐다.
zoom: n/16(n은 기존 폰트 크기)transform: scale(n/16)
둘은 비슷한 해결책처럼 보이지만, 동작 방식은 전혀 다르다. zoom은 레이아웃 이전에 적용된다. 길이 속성의 사용값(used value)에 배율을 곱해버리기 때문에, 폰트 크기도 padding도 줄어든 값으로 레이아웃이 계산된다. 즉, 브라우저 입장에서 “이 박스는 원래 그 크기”라고 인지하게 된다. 하지만 transform은 Paint/Composite 단계에 개입한다. 레이아웃 박스는 원래 크기 그대로 두고, 다 그려놓은 결과물을 확대/축소해서 화면에 얹는다.
이렇듯 브라우저 렌더링 단계에서 언제 개입하느냐에 따라 브라우저가 인식하는 입력창의 폰트 크기가 달라진다. zoom:n/16으로 폰트 크기를 줄인 경우, 브라우저는 줄어든 크기로 폰트 크기를 인식한다. 따라서 입력창 포커스 시 화면 확대가 여전히 발생한다. 하지만 transform:scale(n/16)은 레이아웃에서 박스 계산을 마친 후 Paint/Composite 단계에 개입하기 때문에 브라우저는 이미 폰트 크기를 16px로 인식하고 있는 것이다. 따라서 화면이 확대되는 이슈는 발생하지 않는다.
단, transform에는 부작용이 두 가지 따라온다. 하나는 transform이 레이아웃에 반영되지 않아서, 화면에서는 작아졌는데 요소가 차지하는 공간은 원래 크기 그대로 남아 주변 요소와의 간격이 어긋난다는 것. 다른 하나는 transform이 폰트 크기에만 적용되는 게 아니라 그려진 결과 전체에 곱해진다는 것이다. 기존 폰트가 14px이었다면 padding, border, border-radius, box-shadow가 전부 87.5%(14/16)가 된다. 또한 1px 테두리가 비율에 맞게 얇아지면서 기기에 따라 흐릿해지거나 얇아 보인다.
이를 해결하기 위한 방법이 하나 있다. 폰트 크기 외의 속성들은 크기가 줄어든 만큼 늘려주는 것이다.
// 선언처
export const zoomSafeInputStyle = (
visualFontSize: number,
layoutHeight: number
): FlattenSimpleInterpolation => {
const scale = visualFontSize / IOS_ZOOM_SAFE_FONT_SIZE;
return css`
/* 1. 줌 방지: 폰트는 16px로 고정 */
font-size: ${IOS_ZOOM_SAFE_FONT_SIZE}px;
/* 2. 시각적 축소: scale로 줄어들 만큼 미리 부풀린 뒤 전체를 축소
(폰트뿐 아니라 padding·border를 포함한 요소 전체가 줄어든다) */
width: ${100 / scale}%;
height: ${layoutHeight / scale}px;
transform: scale(${scale});
/* center면 좌·상단에도 여백이 생겨 아래 음수 마진 계산과 어긋난다 */
transform-origin: left top;
/* 3. 레이아웃 보정: transform은 레이아웃에 반영되지 않아
점유 공간은 부풀린 그대로 남는다. 그 차이를 음수 마진으로 회수 */
margin-right: ${100 - 100 / scale}%;
margin-bottom: ${layoutHeight - layoutHeight / scale}px;
/* 위 계산의 전제 조건 */
/* content-box면 부풀린 크기 위에 padding·border가 얹혀 화면 밖으로 뻗는다 */
box-sizing: border-box;
/* flex 컨테이너 안에서는 shrink가 가로 보정폭을 되돌리므로 고정 */
flex-shrink: 0;
`;
};
// 사용처
const StyledWrapper = styled.div`
/* 렌더 높이 = 기존 content-box 40px + border 2px */
${zoomSafeInputStyle(14, 42)}
`
최종 선택
위와 같이 폰트 크기를 16px로 두고 transform: scale(n/16)으로 보이는 크기를 되돌린 뒤, zoomSafeInputStyle 같은 함수로 함께 줄어든 padding·border와 레이아웃 공간을 보정하는 방법이 있다. 하지만 이 방식은 유지보수가 힘들다는 단점이 크다. 입력창의 폰트 크기가 16px보다 작다면 매번 그 박스의 크기를 직접 계산(content-box, border 등을 합산)해서 zoomSafeInputStyle 함수의 인자로 전달해야 한다.
그래서 가장 쉬운 해결 방법은 폰트 크기를 16px로 유지하는 것이라 생각했다. 애초에 iOS가 16px 미만 입력창을 확대하는 건 그 크기가 읽기에 너무 작다고 보기 때문이다. 처음 CS가 ‘글씨가 작아서 확대하고 싶다’는 문의였던 걸 떠올리면, 입력창을 16px로 키우는 건 이 규칙의 취지와도 같은 방향이었다. 먼저, 16px로 적용한 후 기획자와 디자이너에게 공유했다. 이질감이 느껴질 것이라 우려했던 것과 달리, 기존 화면에서 크게 이질감이 없었고 최종적으로는 16px로 변경하는 것으로 해결을 할 수 있었다.
걷어내기 전에 확인한 것
확대를 여는 건 되돌리기 까다로운 변경이다. 막아둘 때는 사용자가 확대를 못 하는 것으로 끝나지만, 열어두면 사용자가 확대한 상태 그대로 화면을 계속 쓰게 된다. 그래서 걷어내기 전에 확대한 화면에서 무엇이 어긋나는지부터 봐야 했다.
가장 먼저 본 건 position: fixed로 화면에 붙여둔 요소들이다. 확대를 하면 사용자가 보는 영역만 좁아지고 레이아웃 뷰포트는 그대로라, fixed 요소는 화면에 붙어 있는 게 아니라 확대된 문서 어딘가에 남는다. 하단 고정 버튼이나 상단 헤더가 화면 밖으로 밀려나면, 확대한 사용자는 그 버튼을 영영 누르지 못한다. 다행히 확대해서 돌아봤을 때 밀려나는 곳은 없었다.
입력창 자체도 확인 대상이었다. 폰트가 16px이 되면서 글자가 커지니, 박스 높이를 고정해둔 곳은 위아래가 빡빡해지고 placeholder가 길었던 곳은 말줄임으로 잘릴 수 있다. 기획자와 디자이너가 이질감이 없다고 한 건 전체적인 인상이었지, 이런 경계는 따로 봐야 하는 것이었다. 이쪽도 결국 손볼 곳은 없었다. 걱정했던 것에 비하면 확대를 열어주는 쪽이 훨씬 조용한 변경이었던 셈이다.
여전히 존재하는 문제점
위 user-scalable=no를 제거해서 얻는 줌인/줌아웃 효과는 iOS에서만 적용된다는 한계가 있다. AOS 웹뷰는 builtInZoomControls가 기본값 false라, 뷰포트에서 무엇을 풀어주든 핀치 줌이 동작하지 않는다. 따라서 네이티브에서 webView.settings.builtInZoomControls를 true로 직접 켜줘야 AOS에서도 해결할 수 있다. 특히 고령의 사용자는 AOS 사용자 층이 많을 것이라 iOS에서 적용되는 것보다 더 효과가 클 것이라 생각한다.
아직 웹 쪽 변경(user-scalable=no 제거, 입력창 16px)만 적용했고, AOS 네이티브의 builtInZoomControls 설정은 검토 전이라 실제로 언제 적용될지는 미지수이다.
맺으며
결론만 놓고 보면 모든 입력창(input, textarea, contenteditable 요소)의 폰트 크기를 16px로 변경한 참 간단한 해결 방법이었다. 그럼에도 그 사이에 정말 많은 시행착오가 있었다. ‘기존 16px 미만의 보여지는 폰트 크기를 유지하면서, 화면 확대가 되지 않도록 하는 방법은 없을까?’ 이 소명 의식을 가지고 zoom, transform 등의 CSS 속성을 활용하며 해결해보려 노력했다. 결국 최종적으로 transform을 적용하는 대신 유지보수 비용이 증가하는 것을 감안하느냐 vs transform 적용 없이 보여지는 폰트 크기를 16px로 키우느냐로 귀결이 되었다. 그리고 크게 이질감이 없어 후자로 결정이 되었다.
‘처음부터 16px로 적용해서 이질감이 느껴지는지 기획자와 디자이너에게 확인을 받았더라면 어땠을까?’라는 생각이 조금은 든다. 그럼 나 혼자서 ‘이 문제를 어떻게 해결해야하지’라고 고민을 할 필요가 없었던 것이다. 이건 해결해야 하는 문제가 아니었던 것이다. 뭐 그럼에도 이런 시행착오들이 있었기에, 다른 해결 방법(transform)이 있지만 감수해야 할 것(유지보수 비용)도 있다는 걸 알았고, 트레이드오프를 비교했을 때 폰트 크기를 16px로 키우는 게 최선이라는 것도 알게 된 게 아닐까 하는 생각이 든다.
이 경험은 사내에 공유해 하나의 선례로 남겼다. 이러한 내 시행착오가 단순히 ‘시간 낭비’가 아니라 ‘우리 팀도 이렇게 해결하면 되겠구나’하는 하나의 실마리가 되었으면 하는 생각이 든다.