본문으로 건너뛰기

브라우저에서는 되는데요 - 웹뷰는 브라우저가 아니다 ep.00

같은 HTML이다. 같은 CSS다. 브라우저에서는 멀쩡하고 앱 안에서만 깨진다. 그 경계에 무엇이 있는가.

이 문장이 반복되는 이유

앱에서 화면이 깨졌다는 제보가 들어옵니다. 웹 개발자가 브라우저에서 열어봅니다. 멀쩡합니다. 앱 개발자가 네이티브 코드를 봅니다. 웹 화면을 띄우는 코드 몇 줄뿐이고, 그것도 멀쩡합니다.

양쪽 다 자기 영역에서는 정상입니다. 그래서 공이 오갑니다.

실무에서 이런 모양으로 올라옵니다.

  • 새 창으로 열리는 링크를 눌렀는데 아무 반응이 없습니다.
  • 첨부 PDF 다운로드가 안 됩니다.
  • 인증번호가 자동으로 안 채워집니다.
  • 전송 버튼을 눌렀더니 마지막 글자가 이상하게 붙었습니다.
  • 키보드가 올라오면 하단 버튼이 가려집니다.
  • 다크모드가 아닌데 화면이 어둡게 뒤집혔습니다.
  • 어제 배포했는데 앱에서는 아직도 그제 화면입니다.

공통점이 있습니다. 전부 브라우저에서는 재현되지 않습니다.


웹뷰는 브라우저에서 무엇을 뺀 것인가

흔한 설명은 “주소창과 탭을 뗀 브라우저”입니다. 틀린 말은 아니지만, 이 설명으로는 위 증상 중 어느 것도 예측하지 못합니다.

정확히 보면 이렇습니다.

브라우저는 렌더링 엔진 위에 브라우저 벤더가 만든 껍데기를 얹은 완성품입니다. 주소창·탭 같은 눈에 보이는 부분만이 아니라, 새 창을 어떻게 열지, 다운로드를 어디에 저장할지, 쿠키를 얼마나 유지할지, 파일 선택 창을 어떻게 띄울지 같은 수백 개의 결정이 이미 내려져 있습니다. 그 결정은 벤더가 수십 년 다듬은 기본값입니다.

웹뷰는 같은 렌더링 엔진에 앱 개발자가 조립한 껍데기를 얹은 것입니다. 엔진은 같습니다. 그런데 그 수백 개의 결정이 비어 있습니다. 앱이 채워 넣지 않으면 아무 일도 일어나지 않습니다.

브라우저와 웹뷰의 구조를 나란히 비교한 다이어그램. 왼쪽은 브라우저로, 아래에서 위로 렌더링 엔진 계층, 그 위에 벤더가 구현한 정책 계층(새 창 처리·다운로드·파일 선택·쿠키 정책·확대 허용), 맨 위에 브라우저 UI 계층(주소창·탭·메뉴)이 쌓여 있고 정책 계층의 각 항목에 체크 표시가 붙어 있다. 오른쪽은 웹뷰로, 같은 렌더링 엔진 계층 위에 같은 이름의 정책 계층이 놓여 있으나 각 항목이 점선 테두리의 빈 슬롯으로 표시되고 그 옆에 '앱이 구현해야 함'이라는 라벨이 붙어 있으며, 브라우저 UI 계층 자리는 비어 있다. 두 스택 하단을 잇는 화살표에 '엔진은 같다'는 문구가, 정책 계층을 잇는 화살표에 '껍데기가 다르다'는 문구가 표시되어 있다.

그래서 이 시리즈의 축이 되는 문장은 이렇습니다.

웹뷰에서 안 되는 것의 대부분은 지원하지 않는 것이 아니라, 아직 켜지 않은 것입니다.

관점을 이렇게 바꾸면 대응이 달라집니다. “웹뷰에서는 원래 안 됩니다”라고 닫고 우회 구현을 시작하는 대신, 무엇을 켜달라고 요청할지 특정할 수 있게 됩니다.


두 종류의 문제를 먼저 가릅니다

증상은 하나여도 원인은 두 갈래입니다. 이 구분을 못 하면 고칠 수 없는 쪽을 붙들고 시간을 씁니다.

앱 설정으로 켜면 되는 것: 웹 코드를 아무리 고쳐도 안 고쳐집니다. 새 창 허용, 파일 선택 처리, 다운로드 수신, 인라인 재생 같은 것들입니다. 여기서 웹 개발자가 할 일은 구현이 아니라 정확한 요청입니다.

웹 쪽에서 다르게 짜야 하는 것: 앱에 요청해도 안 고쳐집니다. 뷰포트 단위 선택, 조합 중 상태 처리, 스크롤 가능 여부를 알리는 시각 장치 같은 것들입니다. 여기서는 앱을 기다릴 이유가 없습니다.

둘 다인 것도 있습니다: 앱이 확대를 막아뒀고, 웹은 확대를 전제로 글자 크기를 잡았다면 양쪽이 같이 움직여야 합니다.

웹뷰 이슈의 담당을 가르는 판단 흐름도. 시작 지점은 '앱에서만 재현되는 증상'이고, 첫 분기는 '브라우저에서도 재현되는가'로 예이면 '일반 웹 이슈'로 빠진다. 아니오이면 두 번째 분기 '웹 코드 수정으로 동작이 바뀌는가'로 이어지고, 예이면 '웹 담당', 아니오이면 세 번째 분기 '앱 설정 변경으로 동작이 바뀌는가'로 이어진다. 여기서 예이면 '앱 담당: 요청 항목 특정', 아니오이면 '공동 대응: 합의 필요'로 귀결된다. 각 종착 상자에는 담당 주체가 색으로 구분되어 표시되어 있다.

진단이 먼저인 이유가 여기 있습니다. 그래서 ep.01이 증상이 아니라 진단부터 다룹니다.


이 시리즈에서 다룰 것들

증상은 흩어져 보이지만 원인은 아홉 갈래로 모입니다. 각 편은 그중 하나에 답합니다.

  • ep.01 웹뷰 판별과 진단: 내 화면이 지금 어떤 웹뷰에서 돌고 있는가. 업데이트 경로가 플랫폼마다 달라서 “특정 기기에서만”이라는 말이 나옵니다. 나머지 편을 읽기 전에 읽으세요.
  • ep.02 내비게이션 경계: 링크·스킴·다운로드·인쇄 요청은 어디서 삼켜지는가. 넷 다 웹뷰가 요청의 행선지를 정하는 한 지점을 함께 통과합니다.
  • ep.03 폼 컨트롤: 네이티브로 치환되는 컨트롤을 어디까지 되찾을 수 있는가. CSS가 닿지 않는 자리를 커스텀으로 메우다 접근성이 무너집니다.
  • ep.04 조합 중 상태: 한글이 덜 조합된 채로 넘어가는 이유는 무엇인가. 한국어권에서만 터지는 문제라 영어권 자료로는 해결되지 않습니다.
  • ep.05 뷰포트와 제스처: 화면이 잘리고 손이 미끄러지는 자리는 어디인가. 뷰포트는 하나가 아니고, 키보드가 올라올 때 무엇이 줄어드는지가 환경마다 다릅니다.
  • ep.06 스타일 개입: 내가 쓰지 않은 스타일은 어디서 들어오는가. 강제 다크와 글꼴 배율, 확대 차단이 내 CSS 위에 덧씌워집니다.
  • ep.07 저장소와 오리진: 브라우저에서 유지되던 세션이 왜 앱에서 사라지는가. 오리진이 무너지면 쿠키와 교차 출처 정책, 클립보드가 함께 무너집니다.
  • ep.08 캐시와 수명: 웹뷰의 수명은 누가 정하는가. 사용자에게는 강제 새로고침 수단이 없고, 백그라운드에 다녀오면 입력 중이던 폼이 사라집니다.
  • ep.09 담당 매핑: 웹이 고칠 것과 앱만 고칠 수 있는 것을 어떻게 가르는가. 앞 여덟 편을 증상에서 담당으로 가는 표로 재배열합니다.

이 시리즈를 읽는 법

각 편은 독립적으로 읽을 수 있게 했습니다. 증상을 검색해서 들어온 개발자가 그 편만 읽고 해결하는 걸 목표로 썼습니다. 다만 ep.01에서 진단 도구를 붙이는 절차는 어느 편을 읽든 필요합니다.

한 가지 주의를 미리 드립니다. 웹뷰 동작은 플랫폼 버전과 앱 설정에 따라 갈립니다. 이 시리즈는 각 편에서 근거가 되는 공식 문서를 함께 표기하지만, 표기 시점 이후 바뀔 수 있습니다. 결론을 그대로 옮기기보다 자기 프로젝트의 실기기에서 한 번 확인하시는 편이 안전합니다.

웹뷰는 배포 주기와 인력 구조 면에서 충분히 합리적인 선택이고, 실제로 많은 서비스가, 제가 만든 수많은 서비스들 조차도 웹뷰 위에서 돌아가고 있습니다. 다만 이 선택에는 브라우저가 대신 내려주던 결정을 우리가 직접 내려야 한다는 비용이 따라옵니다. 이제 그 비용의 목록을 만드려고 합니다.


참고 자료


전체 목차


다음 편 예고

ep.01 - 같은 엔진, 다른 껍데기

내 화면이 지금 어떤 웹뷰에서 돌고 있는지 모르면 아무것도 진단할 수 없습니다. 사용자 에이전트에서 읽어도 되는 것과 읽으면 안 되는 것을 가르고, 실기기에 디버거를 붙여 실제 콘솔을 여는 데까지 갑니다.