들어가며
안드로이드 기기에서 키보드가 올라오면 입력창이 가려지는 이슈를 만났다. 결론을 먼저 이야기하면 내가 직접 해결을 하지는 못했다. 하지만 문제를 해결하기까지 어떤 과정을 거쳤고, 어떤 것을 배웠는지, 그리고 느낀 점을 중심으로 이야기를 해보려 한다.
이슈를 해결하며
이슈 발생
이슈가 발생했다. 사용자가 입력창을 누르면 문자 입력을 위해 키보드가 올라오는데, 이 키보드가 화면을 가려버린 것이다.
상황 분석
가장 먼저 상황 분석을 했다. ‘어떤 상황에서 발생하는 걸까?’
PC로 해당 페이지에 접속하면 당연하게도 잘된다. PC에는 키보드 창이 열리지 않기 때문이다.
아이폰 13인 내 기기에서 테스트를 해봤지만 키보드가 올라감에 따라 웹뷰도 함께 올라가며 입력창이 잘 보였다. 여기서 ‘특정 기기에서만 이슈가 발생하는구나’라는 걸 깨달았고, 한숨 돌릴 수 있었다. 모든 기기에서 발생하는 건 아니고 특수한 상황, 혹은 특정한 기기에서만 발생하는구나라는 것이라 생각했기 때문이다.
재현
그럼 이제 위 이슈가 재현이 되는 기기를 먼저 찾아야 한다. 재현이 되는 기기를 QA팀에 요청했다.
해당 기기를 가져와 재현을 해봤다. 앱 내 웹뷰에서 해당 이슈가 재현이 된다. 하지만 크롬에서 웹사이트에 접속하면 이슈가 재현이 안 되는 거다. 즉, 키보드가 올라오면 정상적으로 웹 화면이 위로 올라가는 거다. 여기서 예상을 했다. ‘크롬에서는 잘 되는거 보니깐, 앱의 설정 문제가 아닐까?’
그 다음으로는 디버깅을 위해 내 PC에 기기를 USB로 연결했다. 그리고 안드로이드 기기의 디버깅이기 때문에 기기의 디버깅 모드를 활성화한 후 chrome://inspect에 접속했다.
범위 좁히기
웹뷰의 개발자 도구를 띄우자, 비로소 맨손에서 벗어나 연장을 쥔 기분이었다. 이제 요소 탭에서 웹뷰의 돔 트리 구조도 살펴볼 수 있고, 조작할 수도 있고, 콘솔 탭에서 JavaScript 코드를 실행해 어떤 시도든 해볼 수 있기 때문이다.
가장 먼저 측정한 것은 Visual Viewport였다. Visual Viewport는 브라우저에서 사용자가 실제로 보고 있는 화면 영역이다. 뷰포트를 기준으로 하는 Layout Viewport와 달리 Visual Viewport는 사용자가 실제로 보고 있는 화면 영역이기에 키보드가 올라오면 키보드 영역만큼 브라우저를 가리기 때문에 줄어드는게 정상이다. 그래서 사용자가 보고 있는 화면에서 키보드를 제외한 영역을 구할 때 다음과 같이 계산하기도 한다.
const keyboardHeight = window.innerHeight - window.visualViewport.innerHeight
측정 결과 키보드를 열기 전과 후 Visual Viewport의 높이 변화는 없었다.
이 부분에서 의심한 것은 ‘웹이 기기에서 키보드가 올라온 것을 인식하지 못하나?’였다. ‘키보드’는 네이티브의 영역이고, 이를 브라우저가 인식하지 못하면 웹에서 할 수 있는 것이 없을 것이다.
그 다음으로는 viewport meta에 interactive-widget이 명시되어 있는지 체크했다.
interactive-widget 메타 태그는 키보드와 같은 인터랙티브 위젯이 화면에 나타날 때 뷰포트가 어떻게 반응할지에 대한 제어이다. 설정된 값은 resizes-content로, 키보드가 올라오면 레이아웃 자체가 줄어든 옵션이다. 웹뷰에 맞는 옵션으로 잘 설정되어있음을 확인했다.
근데 여기서 좀 이상했다. resizes-content는 키보드가 올라오면 레이아웃 자체가 줄어드는 옵션인데, 정작 Visual Viewport의 높이는 변하지 않았기 때문이다. 옵션은 제대로 걸려있는데 왜 반응이 없을까? 한참을 헤매다 내린 결론은 이랬다. interactive-widget 옵션은 어디까지나 ‘키보드가 올라왔다’는 정보를 웹뷰가 시스템으로부터 전달받았을 때 비로소 동작한다. 그런데 구버전 안드로이드 웹뷰에서는 이 키보드 inset 정보 자체가 시스템에서 웹뷰로 넘어오지 않았다. 웹은 키보드가 올라온 사실조차 모르고 있으니, 옵션을 뭘로 걸어두든 리사이즈가 일어날 리가 없던 것이다. 앞서 ‘웹이 기기에서 키보드가 올라온 것을 인식하지 못하나?’라고 의심했던 게 정확히 이 지점이었다.
여러가지 시도
위에서 크롬에서는 재현되지 않지만(키보드가 올라오면 화면이 위로 밀린다) 앱 내 웹뷰에서만 재현된다는 점, Visual Viewport 높이의 차이가 없다는 점을 통해서 앱의 문제라는 것으로 추정을 해볼 수 있었다. 그럼에도 이슈가 웹 팀으로 먼저 접수된 만큼, 앱 팀에 넘기기에 앞서 웹에서 할 수 있는 시도들을 최대한 해본 후 전달을 하는 것이 적절하다고 생각했다. 그래서 웹에서 할 수 있는 모든 시도들을 추가적으로 해보았다.
position:fixed 속성으로 입력창을 하단에 고정시켜 우회해보려했으나 이 또한 키보드에 가려져서 할 수 없었다. 그리고 키보드가 열리면 스크롤을 내리는 방식으로 우회를 시도했으나 뷰포트(window.innerHeight)와 스크롤 가능한 높이(document.documentElement.scrollHeight)가 변하지 않았기에 이 방법 또한 불가능했다.
상황 보고
지금까지의 상황을 노션의 작업 카드에 정리하고, PM님과 파트장님에게 보고를 했다. 현재 상황, 그리고 이슈의 원인으로 추정되는 것들을 함께 정리해 작성했다.
그리고 PM님은 안드로이드 팀에 해당 이슈를 전달해주셨다.
그 결과
그리고 1주일 뒤 ..
안드로이드 팀에서 문제를 해결했다는 이야기를 전해들었다.
원인은 안드로이드 OS 15 이상부터는 키보드 영역 정보를 웹뷰에 시스템이 자동으로 전달해주지만, 그 이전 버전에서는 그렇지 못하다. 그래서 네이티브에서 키보드가 올라올 때 직접 입력 UI가 함께 위로 밀려 올라가도록 해주어야 하는데, 그 로직이 빠져있던 것이다.
조금 더 풀어서 말하면, 안드로이드 15부터는 앱이 화면 전체를 쓰는 edge-to-edge가 기본이 되면서 키보드 영역 같은 시스템 UI 정보를 WindowInsets라는 형태로 웹뷰까지 자동으로 흘려보내준다. 그래서 웹이 별다른 처리를 안 해도 키보드에 맞춰 화면이 밀려 올라간다. 반면 그 이전 버전에서는 이 inset을 네이티브가 직접 챙겨서 웹뷰에 전달하고, 입력 UI를 밀어 올리는 로직까지 손수 넣어줘야 했던 것이다.
내가 한몫 거들었다는 게 꽤 뿌듯했다.
이슈를 돌아보며
느낀 점
회사에 입사해 처음으로 QA팀에서 재현 기기를 빌려와 다양한 시도를 해보고 디버깅과 업무 상황을 보고하며, 정말 다양한 것을 경험한 순간들이었다. 그 과정에서 든든함을 느꼈다. 이슈가 발생하면 가장 먼저 알려주는 QA팀, 앱 영역에서 이슈가 발생하면 이를 대응해주는 네이티브 팀, 프로젝트를 총괄해주고 조율해주는 기획팀. 각자 전문성을 가진 팀원들이 있고, 막히면 언제든 도움을 요청할 수 있다는 점에서 안도감이 들었다.
이러한 체계 속에서 회사가 굴러가는구나 느꼈고, 나도 웹에서 이슈가 생기면 정확하고 신속하게 대응할 줄 아는 동료가 되어야겠다.
아쉬웠던 점
이번 이슈에서 가장 크게 아쉬웠던 점은 처음 하는 것들이 많아서 중간중간에 발이 자주 묶였다는 점이다. 내 휴대폰에서는 재현이 안되는데 재현되는 기기는 어디서 구하지? 웹뷰 디버깅은 어떻게 하지? 앱 이슈라고 예상이 되면 어디까지 해보고 어떻게, 언제, 누구에게 전달하지? 모르는 것들 투성이였다. 사수님은 바빠보여서 하나하나 물어보기에 부담이 되었다. 그래서 최대한 나 혼자서 슬랙과 노션을 부지런히 오가며 답을 찾아 헤맨 시간이 길었다. 여기서 내가 챙겨야 할 점은 ‘경계점’이라는 걸 깨달았다. 내가 직접 시도해서 해결할 수 있는 것, 혹은 시간이 조금 걸리더라도 해결할 수 있는 것과 vs 내가 직접 시도해서 해결할 수 없는 것, 혹은 직접 해결하면 시간이 오래 걸릴 거라 예상이 들지만 도움을 요청했을 때 더 빠르게 해결할 수 있을 거같은 것. 이 둘 사이의 경계점 말이다. 전자는 내가 직접 시도해서 해결을 하고, 후자는 질문을 하는 것이 바람직하다. 물론 전자로 시도하다가, 후자로 넘어가는 경우도 있을 것이다. 위 내용에서 ‘내 휴대폰에서는 재현이 안되는데 재현되는 기기는 어디서 구하지?’는 먼저 질문하는 것이 적절하고, ’웹뷰 디버깅은 어떻게 하지?’는 내가 직접 해결하는 것이 적절할 것이다. 그 경계는 ‘구글에 검색/AI에 질문했을 때, 혹은 슬랙/노션에 검색했을 때 해결이 되느냐’의 차이도 있다. 그렇다면 ‘앱 이슈라고 예상이 되면 어디까지 해보고, 어떻게, 언제, 누구에게 전달하지? ‘도 직접 해결하는 쪽에 더 가깝다고 할 수 있다. 왜냐하면 슬랙에 이전 사례들을 살펴보면 되기 때문이다. 더 나아가서, 이를 이제는 직접 살펴보지 않아도 된다. Slack MCP를 연결한 AI에게 물어보면 된다. 물론 검증은 직접 해야겠지만.
아쉬웠던 점 두 번째는 급한 마음으로 디버깅에 임한 것이다. 차분함은 항상 중요하지만 특히 디버깅을 할 때 중요성이 부각된다. 마음이 급하면 체계가 무너지고 촉박한 마음에, 이전에 했던 시도를 반복하며 시간이 오히려 더 오래 걸리게 된다. 항상 차분한 마음으로 범위를 좁혀나가며 문제를 해결하는 것이 중요하다.
세 번째는 AI에 의존한 것이다. AI의 한계를 느끼고 있는 요즘이다. AI는 발산에는 능하지만, 수렴에는 약하다. 이번 이슈에서도 AI에게 ‘이 문제 원인과 해결 방법을 알려줘’했는데 수많은 추정되는 원인과 해결 방법을 알려줬다. 알려준 해결 방법은 전부 안되어서 또 다른 해결 방법을 요구했다. 그 방법도 안되었다. 이 상황에서 나는 어떠한 시도도 해보지 못한 꼴이 되어버렸다. 어떤 시도를 왜 해보았는지, 이 시도가 어떤 경우의 수를 줄이기 위해서 시도한 것인지 맥락을 전혀 모르기 때문이다. 결국 같은 자리를 맴도는 도르마무 신세가 된 셈이다. 그렇게 시간을 허비한 것이 아쉬웠다. 또한 만약에 우연히 AI가 알려준 해결 방법이 먹혔다고 해보자. 그렇다 하더라도 그 해결 방법이 최적의 해결 방법이었는가, 근본 원인을 해결한 것인가에 대한 판단은 또 다시 인간의 몫이다. AI는 도구로서 활용해야 한다. 그리고 어떤 상황에 쓸 지와 안 쓸지에 대한 본인만의 명확한 기준이 있어야 한다. 이 내용은 길어질 것 같아 다음 글에서 작성하도록 하겠다.
끝내 내 손으로 매듭짓진 못했지만, 한 매듭을 함께 풀었다는 사실만으로도 두고두고 곱씹게 될 시간이었다
앞으로도 더 많은 문제들을 해결하고 싶다. 우리 서비스의 모든 문제들을 해결해보고 싶다.
나는 문제 해결사가 되고 싶다.
문제 해결은 언제나 짜릿하고 즐겁다.