iOS 사파리 탭이 반복해서 죽을 때: 폰 시스템 로그로 WebContent 메모리 읽기
아이폰에서 랜딩 페이지 하나를 가로로 눕혀 스크롤하면 탭이 죽었습니다. 사파리는 「문제가 반복적으로 발생했습니다」를 띄웠고, 크롬은 빈 화면을 남겼습니다. 세로로는 멀쩡했고, 가로에서도 가끔 죽었으며, 크게 확대하면 더 잘 죽었습니다. 서버는 200을 돌려주고 있었습니다.
이런 증상은 원인 후보가 둘로 갈립니다. WebKit이 특정 CSS나 기능에서 실제로 충돌하는 경우와, 페이지가 메모리 한도를 넘어 강제로 끝나는 경우입니다. 맥에서 아이폰 화면을 흉내 내 레이어 크기를 어림해 봤지만, 그 숫자로는 어느 쪽인지 가를 수 없었습니다. 그래서 폰이 남긴 기록을 직접 읽었습니다.
충돌 로그로 먼저 가르기
폰이 맥과 짝지어져 있으면 Xcode의 devicectl로 충돌 로그를 가져올 수 있습니다.
xcrun devicectl list devices
xcrun devicectl device copy from --device <UDID> \
--domain-type systemCrashLogs --source / --destination ./crashlogs처음에는 「The device is currently locked」로 실패했습니다. 폰 잠금을 풀어 두면 받아집니다.
볼 파일은 둘입니다. WebKit이 정말 충돌했다면 com.apple.WebKit.WebContent-….ips가, 시스템 전체 메모리가 모자랐다면 JetsamEvent-….ips가 남습니다. 받은 127개 중에 그날 밤 것은 둘 다 없었습니다.
없다는 것도 단서였습니다. 페이지를 그리는 WebContent 프로세스가 자기 메모리 한도를 넘어 끝나면 충돌 파일이 남지 않고 시스템 로그에만 기록됩니다.
폰의 시스템 로그를 맥으로 가져오는 방법
맥의 log collect가 연결된 폰의 로그를 아카이브로 떠 옵니다.
sudo /usr/bin/log collect --device-udid <UDID> --last 60m --output ./phone.logarchive두 가지에 걸렸습니다. zsh에는 log라는 내장 명령이 있어서 /usr/bin/log로 전체 경로를 써야 합니다. 또 root 권한이 필요한데, 제 환경에서는 비밀번호를 입력할 터미널이 없어 sudo가 실패했습니다. 그래서 osascript -e 'do shell script "…" with administrator privileges'로 돌려 맥의 비밀번호 창을 띄웠습니다. 한 시간 분량이 432MB였습니다.
걸러 내는 조건
처음에는 jetsam, memory, terminat 같은 낱말로 넓게 걸렀습니다. 그랬더니 위젯과 시스템 서비스의 메모리 설정 줄이 수백 개 쏟아져 정작 필요한 줄이 묻혔습니다. 웹 페이지를 띄운 앱의 줄만 남기고, WebKit이 메모리에 대해 남기는 메시지 넷으로 좁히니 원하는 것이 나왔습니다.
/usr/bin/log show phone.logarchive --style compact --start "2026-10-27 23:30:00" \
--predicate '(process == "MobileSafari" OR process == "Chrome") AND
(eventMessage CONTAINS "phys_footprint_mb"
OR eventMessage CONTAINS "measurePostLoadMemoryUsage"
OR eventMessage CONTAINS "Received memory pressure event"
OR eventMessage CONTAINS "processDidTerminate:")'iOS의 크롬도 WebKit 위에서 돌기 때문에 같은 메시지가 남습니다.
로그가 보여 준 숫자
WebKit은 페이지 로드를 시작할 때, 로드가 끝난 뒤, 메모리 경고를 받을 때마다 WebContent 프로세스의 phys_footprint_mb를 남깁니다. 한 번 죽을 때를 시간순으로 놓으면 이렇습니다.
| 시각 | phys_footprint |
|---|---|
| 페이지 로드 시작 | 629MB |
| 로드 10초 뒤 | 573MB |
| 메모리 경고 | 1,864MB |
| 2초 뒤 | processDidTerminate: reason=Crash |
30분 동안 같은 일이 열 번 넘게 반복됐습니다. 끝나는 지점은 매번 1,647MB에서 1,893MB 사이였습니다. 로그에는 reason=Crash라고 찍히지만 충돌 파일이 없으니, 이 근처가 이 기기에서 탭 하나가 쓸 수 있는 한도로 보입니다. 첫 질문의 답은 메모리 쪽이었습니다.
가장 많은 것을 알려 준 것은 다시 열린 직후의 기록이었습니다. 탭이 죽으면 브라우저가 같은 위치로 페이지를 다시 엽니다. 이때 631MB이던 프로세스가 1초 뒤 1,521MB가 됐습니다. 확대도 스크롤도 하지 않고 그 위치를 그리기만 했는데 한도 근처까지 찼습니다. 확대는 마지막 한 방이었을 뿐이고, 페이지가 원래 한도 바로 밑에서 돌고 있었습니다.
1.8GB 중 160MB만 보이는 이유
메모리 경고 때 WebKit은 VM 태그별 내역도 남깁니다.
phys_footprint_mb: 1864
javascript_gc_heap_size_mb: 15
bmalloc: 128 MB
malloc (small): 11 MB합해도 160MB 정도이고, 나머지 1.7GB는 목록에 나오지 않습니다. 레이어를 그려 두는 그래픽 버퍼(IOSurface)는 프로세스 안의 일반 메모리가 아니라 태그 목록에서는 빠지지만, 그 프로세스의 footprint에는 들어갑니다. 스크립트가 아니라 그리는 쪽이 메모리를 쓴다는 뜻입니다. 이 구분이 다음에 볼 곳을 정합니다. JS 힙이나 DOM 노드를 줄여서는 풀리지 않는 문제입니다.
로그를 잘못 읽은 한 번
같은 30초 동안 resourceID가 6,360에서 12,802로 뛰어 있었습니다. 20초에 요청이 6천 건 나간 줄 알고 초 단위로 셌습니다. 실제로 기록된 요청은 52건이었습니다. 이 번호는 요청이 아닌 것에도 매겨지는 카운터였습니다. 로그에서 숫자가 크게 뛰면, 그 숫자가 무엇을 세는지부터 확인해야 합니다.
이 로그로 알 수 있는 것과 없는 것
이 방법으로 가른 것은 셋입니다. WebKit 충돌이 아니라 메모리 한도 초과라는 것, 한도가 이 기기에서 1.7~1.9GB 근처라는 것, 그 대부분이 그래픽 메모리라는 것입니다. 맥에서 흉내 내 어림한 숫자(약 150MB)가 실제보다 열 배쯤 작았다는 것도 이 로그로 알았습니다.
어느 레이어가 그 메모리를 쓰는지는 이 로그에 나오지 않습니다. 그건 맥 사파리의 웹 속성 창을 폰 사파리에 붙여, 레이어 탭에서 레이어별로 봐야 합니다. 시스템 로그는 무엇을 볼지 정해 주고, 레이어 탭은 그중 어느 것인지 짚어 줍니다.
우리 서비스에 적용할 계획이 있으신가요?
홈페이지·웹서비스 개발 살펴보기 →함께 읽기
- iOS Safari 모바일 버그 3종 해결기: 헤더 잘림 · 흰 화면 · 가로 넘침같은 날 확인된 세 증상은 원인이 서로 달랐으며, 모두 해당 테스트 환경에서 데스크톱 Chrome과 다르게 나타난 WebKit 관련 동작이었습니다.
- iOS 사파리에서 고정 헤더가 사라지거나 잘리는 이유상단 고정 헤더는 거의 모든 사이트가 쓰는 패턴입니다. position: fixed 한 줄이면 끝나는 것처럼 보이고, 데스크톱에서는 실제로 그렇습니다. 그런데 iOS 사파리에서는 이 헤더가 스크롤 도중 통째로 사라지거나, 위쪽이 몇 픽셀 잘려 보이는 일이 생깁니다.
- 크롬에서는 멀쩡한데 아이폰에서만 흰 화면이 뜨는 이유개발하면서 가장 당황스러운 상황 중 하나가 내 브라우저에서는 되는데 다른 기기에서만 안 되는 경우입니다. 특히 화면이 아예 하얗게 나오면 어디서부터 봐야 할지 막막합니다. 콘솔도 못 보고 에러 메시지도 없습니다.
- 모바일에서만 가로 스크롤이 생기는 이유모바일에서 화면을 옆으로 밀었더니 페이지가 밀리고 오른쪽에 빈 여백이 보인다면, 어떤 요소가 뷰포트보다 오른쪽으로 삐져나가 있다는 뜻입니다. 흔한 문제인데 범인을 찾기가 은근히 어렵습니다. 눈에 보이는 요소가 아닌 경우가 많기 때문입니다.
- 역방향 SSH 터널이 반쯤 죽을 때: ServerAlive 가 8시간 동안 못 끊은 이유역방향 SSH 터널로 오리진을 내보내는 구성에서, 터널 너머의 서비스들이 한꺼번에 「응답 없음」이 된 날의 기록입니다. 프로세스도 소켓도 살아 있다고 말하는데 서비스만 죽어 있었고, 원인은 제가 「죽은 연결은 90초 안에 정리된다」고 믿고 있던 설정에 있었습니다.