데스크톱에서 완벽하게 다듬은 에디터를 모바일에서 열어 보면, 전혀 다른 세상이 펼쳐져요. 가상 키보드가 화면 절반을 덮어 버리고, 입력칸이 키보드 뒤로 숨고, 툴바가 엉뚱한 데 붙어 있죠. 저는 모바일 에디터를 처음 만들 때 이 가상 키보드와 뷰포트 때문에 진땀을 뺐어요. 오늘은 모바일에서만 터지는 문제들을, 역시 상황과 코드로 풀어 볼게요.
25.1 모바일 에디터가 왜 더 어려워요?
가장 큰 이유는 가상 키보드예요. 데스크톱 키보드는 화면을 안 가리지만, 모바일 키보드는 화면 아래 절반을 덮고 올라와요. 문제는 이 키보드가 올라와도 브라우저가 아는 화면 크기는 그대로일 때가 많다는 거예요. 그래서 화면 맨 아래에 붙여 둔 툴바가 키보드 뒤에 파묻혀 안 보이게 돼요.
또 하나는 뷰포트(viewport, 화면에 보이는 영역)가 두 종류라는 거예요. 페이지 전체 기준의 영역과, 키보드를 뺀 실제로 보이는 영역이 따로 노는데, 이 둘을 헷갈리면 위치 계산이 전부 어긋나요. 이 차이를 아는 게 모바일 에디터의 출발점이에요.
여기에 기기마다 사정이 조금씩 달라서 더 까다로워요. 안드로이드는 키보드가 올라오면 화면을 실제로 줄여 주는 편이라 그나마 다루기 쉬운데, 아이폰은 화면 크기를 그대로 둔 채 덮기만 해서 우리가 직접 위치를 계산해 줘야 하거든요. 그래서 저는 모바일 에디터를 만들 땐 안드로이드와 아이폰을 따로 확인해요. 한쪽에서 멀쩡한 코드가 다른 쪽에선 툴바를 엉뚱한 데 붙여 놓기 일쑤예요.
25.2 키보드가 가린 영역을 어떻게 알아내요?
다행히 브라우저가 실제로 보이는 영역을 알려 주는 창구가 있어요. 바로 window.visualViewport예요. 여기엔 키보드를 뺀 실제 height(높이)가 담기고, 이 영역이 바뀔 때 resize(크기 변경) 이벤트도 줘요. 키보드가 올라오면 이 높이가 확 줄어들죠.
const vv = window.visualViewport;
vv.addEventListener("resize", () => {
const keyboard = window.innerHeight - vv.height;
toolbar.style.bottom = keyboard + "px";
});
window.innerHeight는 키보드를 포함한 전체 높이고, vv.height는 키보드를 뺀 실제 보이는 높이예요. 둘을 빼면 키보드가 차지한 높이가 나오죠. 그만큼 툴바를 위로 올리면, 툴바가 키보드 바로 위에 딱 붙어요. 키보드가 내려가면 resize가 다시 와서 툴바도 제자리로 돌아오고요.
25.3 입력칸이 키보드에 가려질 땐요?
사용자가 아래쪽 입력칸을 탭하면, 키보드가 올라오면서 그 입력칸을 덮어 버리는 일이 자주 있어요. 자기가 뭘 치는지 안 보이니 답답하죠. 그래서 입력칸에 포커스가 가면 그 칸이 보이는 자리로 스크롤해 줘야 해요.
input.addEventListener("focus", () => {
setTimeout(() => {
input.scrollIntoView({ block: "center" });
}, 300);
});
여기서 setTimeout으로 잠깐 기다리는 게 핵심이에요. 포커스가 가는 즉시 스크롤하면 아직 키보드가 다 안 올라와서 계산이 어긋나거든요. 0.3초쯤 기다렸다가 scrollIntoView로 입력칸을 화면 가운데로 올리면, 키보드가 자리를 잡은 뒤라 정확히 보이는 자리에 놓여요. 이 짧은 지연 하나가 모바일에선 큰 차이를 만들어요.
25.4 터치로 글자를 못 고르겠다는 문의가 와요
모바일에선 글자를 길게 눌러 선택하잖아요. 그런데 편집 영역에 스크롤이나 다른 터치 처리를 얹으면, 이 롱프레스 선택이 방해받아 사용자가 글자를 못 고르겠다고 해요. 저는 이럴 때 CSS로 선택이 되는지부터 확인해요.
.editor {
-webkit-user-select: text;
user-select: text;
touch-action: manipulation;
}
user-select: text는 이 영역의 글자를 선택할 수 있게 열어 둬요. 모바일 사파리는 접두어가 붙은 -webkit-user-select도 함께 챙겨야 하고요. touch-action: manipulation은 두 번 탭하면 확대되는 동작을 꺼서, 사용자가 글자를 고르려다 화면이 확대돼 버리는 짜증을 줄여 줘요. 터치 문제는 이 CSS 몇 줄에서 시작하는 경우가 많아요.
25.5 예측 변환이 글자를 마음대로 바꿔요
모바일 키보드는 사용자가 치는 동안 다음 단어를 예측하거나 자동으로 고쳐 줘요. 이 예측 변환이 편집 내용을 우리도 모르게 바꿔 놓아서, 데스크톱에선 멀쩡하던 에디터가 모바일에서만 이상해지곤 해요. 이런 변화는 beforeinput(입력 직전) 이벤트로 미리 잡을 수 있어요.
el.addEventListener("beforeinput", (e) => {
if (e.inputType === "insertReplacementText") {
console.log("자동 고침이 들어옴:", e.data);
}
});
beforeinput 이벤트의 inputType(입력 종류)을 보면 이게 어떤 입력인지 알 수 있어요. 예측 단어나 자동 고침으로 글자가 교체될 때는 insertReplacementText라는 종류로 와요. 이걸 미리 알아채면, 우리 처리가 그 교체와 부딪히지 않게 피할 수 있죠. 여기서도 원칙은 한글 편과 같아요. 키보드가 내용을 바꾸는 중엔 우리가 끼어들지 말고 기다리는 거예요.
25.6 키보드를 닫았는데 화면이 안 내려와요
모바일에서 특히 애를 먹인 게 이거예요. 아이폰 사파리에서 입력칸을 탭하면 키보드를 띄우면서 페이지 전체를 위로 밀어 올리는데, 키보드를 닫아도 그 밀려 올라간 화면이 제자리로 안 내려올 때가 있어요. 화면 위쪽에 정체 모를 빈 공간이 생기거나, 스크롤이 어중간한 자리에 멈춰 버리죠.
저는 포커스가 빠질 때 스크롤을 한 번 바로잡아 이 문제를 달래요.
input.addEventListener("blur", () => {
setTimeout(() => {
window.scrollTo(0, window.scrollY);
}, 100);
});
입력칸에서 포커스가 빠지는 blur(포커스 잃음) 순간에, 키보드가 다 내려가길 살짝 기다렸다가 스크롤을 현재 위치로 다시 지정해 줘요. 이렇게 한 번 건드려 주면 브라우저가 밀려 있던 화면을 정상 위치로 다시 잡아요. 근본은 브라우저 쪽 동작이라 완벽한 해법은 아니지만, 이 재조정 하나로 대부분의 어긋난 스크롤이 제자리를 찾아요. 실제 아이폰에서 키보드를 여닫아 가며 확인하는 게 필수고요.
25.7 오늘 정리
모바일 에디터의 어려움은 대부분 가상 키보드와 뷰포트에서 나왔어요. 키보드가 가린 영역은 window.visualViewport의 높이로 알아내, 그만큼 툴바를 올려 키보드 위에 붙였죠. 키보드에 가려진 입력칸은 포커스 뒤 살짝 기다렸다가 scrollIntoView로 끌어올렸고요. 터치 선택은 user-select와 touch-action 같은 CSS로 열어 줬어요. 예측 변환은 beforeinput의 insertReplacementText로 미리 알아채 부딪힘을 피했죠. 무엇보다 중요한 건 진짜 폰으로 직접 써 보는 거예요. 개발자 도구의 모바일 흉내로는 가상 키보드가 안 뜨거든요. 실제 기기에서 키보드를 올리고 내려 봐야 이 문제들이 비로소 보여요.