IOS에서 페이지 뒤로가기
들어가며
오늘은 26일이라.. 연차를 쓰신 분들이 많기도 하고.. 연말이니 정리겸 오랜만에 개발 블로그를 적어보려 한다 ㅎ
커뮤니티를 개발하면서 AI의 도움을 많이 받았으나, AI가 아직 웹뷰 환경에서의 디버깅은 잘 못 도와주는 것 같다는 생각을 많이 받았었다. 여러 일들(바탐시트와 헤더의 z-index 문제라든가, AOS에서 최근 검색어 버튼이 눌리지 않는 현상, 앱브릿지 연동 …etc)이 있었지만 오늘은 그 중 IOS 환경에서 커스텀 모달 창의 확인 버튼을 누를 시 페이징 뒤로가기 동작이 안 됐던 사건에 대해 풀어보려 한다.
문제 상황
커스텀 모달 창에 있는 확인 버튼을 누르면 이전 페이지로 이동이 되어야 하는데 이게 Chrome이나 Android 환경에서는 잘 동작했는데, Safari(iOS)에서만 뒤로가기가 동작하지 않는 문제가 발생했다.
핵심 문제: WebKit vs Blink 엔진의 차이
문제의 원인은 WebKit 엔진(Safari)과 Blink 엔진(Chrome)의 popstate 이벤트 처리 순서가 다르기 때문이었다.
| 이벤트 | Blink | WebKit |
|---|---|---|
| hashchange → history 스택 적용 | 거의 즉시 | 렌더 안정되기 전엔 이벤트/스택 업데이트를 멈춤 |
| popstate 처리 우선순위 | 렌더링과 별도 스케줄 | 렌더 완료 이후로 밀릴 수 있음 |
무슨 일이 일어났나?
- Confirm modal이 오픈되면
window.location.hash에#confirm-open을 붙인다 hashchange이벤트가 발생하면서 history 스택에 새 항목이 쌓인다- iOS 환경에서는
hashchange이벤트가popstate이벤트로 즉시 연결되지 않는다 - UI 업데이트와
history.back명령이 한 이벤트 안에서 동시에 수행되면history.back블록이 무시된다
왜 그런가?
- Safari는 WebKit 엔진을 사용하고 있기에
hashchange이벤트는 즉시 처리되지만,popstate는 히스토리가 안정됐을 때만 발생한다 - UI 변경과 히스토리 변경이 같은 이벤트 루프에서 일어나면
popstate동작이 스킵되면서 뒤로가기가 안 일어나는 것이다
해결 방법 1: setTimeout
첫 번째 해결 방법은 setTimeout을 사용하는 것이다.
1
2
3
4
window.close();
setTimeout(() => {
window.history.back();
}, 100);
setTimeout을 사용하면 history.back을 이벤트 큐에 넣었다가 콜스택이 비었을 때 실행시킨다. 고로 이렇게 하면 UI 렌더링과 history 조작 동작이 겹치지 않아 정상적으로 동작한다.
근데… 다들 알다시피 setTimeout은 남용하면 위험하다. 그 이유는 다음과 같다.
- setTimeout(fn, 100)은 0.1s 뒤에 해당 함수가 무조건 실행되게 하는게 아니라 최소 0.1s 뒤에 실행 되는걸 보장하는 함수다.
- 왜냐면 자바스크립트는 싱글 스레드 언어이므로 하나의 콜스택 안에서 이벤트들이 순차적으로 실행된다.
- 근데 내가 이 컨펌창에서 setTimeout을 쓰고, 다른 컴포넌트에서도 setTimeout을 쓰면 여러 타이밍들이 겹칠 수도 있고,
- 만약 콜스택에서 0.1s 이상이 걸리는 무거운 연산을 (cpu 사용량이 많은 작업) 진행하고 있다면
- setTimeout의 콜백 함수는 타이머 만료 후 태스크 큐에 들어가고, 무거운 연산이 끝나 콜스택이 비었을 때 이벤트 루프가 큐에서 콜스택으로 옮기므로 0.1s 이상의 시간이 걸린 뒤에 페이지가 이동될 수 있다.
해결 방법 2: requestAnimationFrame
고로 더 좋은? 안전한? 방법은 requestAnimationFrame을 활용하는 것이다!
1
2
3
4
5
6
7
8
9
onConfirm: () => {
// 모달 닫기
closeModal();
requestAnimationFrame(() => {
overlay.unmountAll();
// 페이지 뒤로 가기
window.history.back();
});
}
requestAnimationFrame이란?
브라우저는 초당 60번 정도(화면 주사율에 따라 다르지만) 빠르게 그림을 계속 바꾸며 화면을 보여준다. 이때 화면이 바뀌기 직전에 실행될 함수를 콜백에 넘겨줄 수 있는데 이것이 바로 requestAnimationFrame이다.
브라우저의 렌더링 과정
브라우저에서 UI 변경은 크게 아래의 두 단계로 나뉜다
- Layout 또는 Style 업데이트를 준비하는 단계
- 실제로 화면에 그리는 Paint 단계
따라서 requestAnimationFrame을 사용하면
overlay.close()- DOM 변경requestAnimationFrame(callback)등록overlay.close()- 리플로우requestAnimationFrame(history.back)실행overlay.close()- 페인팅
이렇게 렌더링 사이클을 명확히 분리할 수 있어 setTimeout보다 더 예측 가능하고 정확한 타이밍 제어가 가능하다.
번외: Safari에서의 _blank
Safari에서는 _blank로 연 새 창의 히스토리 스택이 1이 아닌 2부터 시작한다
참고: https://github.com/whatwg/html/issues/6491
번외 2: ai를 잘 쓴다는건 뭘까..
요근래 ai를 잘 쓴다는게 대체 뭘 의미하는걸까 곰곰히 생각하게 되는데.. 내가 모르는 도메인의 영역까지 빠르게 이해하고 구현해서 업무 효율을 높이는게 지금 세대에서 ai를 잘 쓴다는 의미인 것 같다 흠..