크롬에서는 멀쩡한데 아이폰에서만 흰 화면이 뜨는 이유
개발하면서 가장 당황스러운 상황 중 하나가 내 브라우저에서는 되는데 다른 기기에서만 안 되는 경우입니다. 특히 화면이 아예 하얗게 나오면 어디서부터 봐야 할지 막막합니다. 콘솔도 못 보고 에러 메시지도 없습니다.
저는 얼마 전에 이걸 겪었습니다. 맥 크롬에서는 멀쩡한 페이지가 아이폰 사파리에서만 흰 화면으로 덮였습니다. 원인은 폰트 로딩 API였는데, 거기까지 가는 데 몇 번 헛짚었습니다.
흰 화면부터 분류하기
흰 화면은 한 가지가 아닙니다. 크게 셋으로 나뉩니다.
HTML 자체가 안 내려오는 경우는 서버 에러나 라우팅 문제입니다. 소스 보기를 하면 비어 있습니다. HTML은 왔는데 아무것도 안 그려진 경우는 하이드레이션 실패나 렌더 단계 예외입니다. 소스 보기에는 내용이 있는데 화면만 비어 있습니다. 마지막으로 뭔가가 화면을 덮고 있는 경우가 있습니다. 스플래시, 인트로, 로딩 오버레이가 안 걷힌 상태입니다.
이걸 먼저 가르지 않으면 엉뚱한 데를 팝니다. 저는 첫 번째부터 의심했다가 시간을 썼습니다.
헛짚은 것: dev 서버가 막는 줄 알았다
로컬 개발 서버를 다른 기기에서 여는 상황이었기 때문에 Next.js가 외부 origin을 차단하는 줄 알았습니다. Next에는 allowedDevOrigins 설정이 있고, 여기 등록되지 않은 origin에서 오는 개발용 요청을 막습니다. 설정이 없었으니 그럴듯했습니다.
curl 두 줄로 아니라는 게 나왔습니다.
curl -o /dev/null -w "%{http_code}\n" http://<개발 서버>/
# 200
curl -o /dev/null -w "%{http_code}\n" "http://<개발 서버>/_next/static/chunks/<...>.js"
# 200HTML도 JS 청크도 다 내려옵니다. 첫 번째 유형이 아니었습니다.
진짜 원인은 폰트 API였다
저희 랜딩에는 인트로 오버레이가 있습니다. 로고가 떴다가 막이 위로 걷히는 연출인데, 라이트 테마에서 이 막이 흰색입니다. 그리고 이 막은 JS 타이머가 걷어냅니다.
즉 클라이언트 JS가 죽으면 막이 영원히 남습니다. 세 번째 유형이었습니다.
JS가 왜 죽었는지가 남았습니다. 인트로에 폰트 로딩을 기다리는 코드가 있었습니다.
const fontFamily = window.getComputedStyle(document.body).fontFamily;
document.fonts.load(`850 1em ${fontFamily}`, "DUOLABS");폰트가 로드된 다음에 애니메이션을 시작해서 글자가 튀는 걸 막으려는 의도입니다. 문제는 getComputedStyle이 돌려주는 값입니다.
pretendard, "pretendard Fallback", -apple-system, "system-ui", "Segoe UI", sans-serifWebKit은 font 단축 문법 파서에서 -apple-system 같은 시스템 폰트 키워드를 받지 않습니다. 그래서 사파리에서 document.fonts.load()는 거부된 프로미스를 돌려주는 대신 동기 예외를 던집니다. 스펙과 다른 동작입니다.
useEffect 안에서 던져진 예외가 밖으로 나가면 React 트리를 통째로 언마운트시킵니다. 남는 건 흰 인트로 막뿐입니다. 크롬은 같은 문자열을 그대로 받아주기 때문에 맥에서는 아무리 봐도 재현되지 않았습니다.
고친 방법
두 가지를 같이 했습니다.
let started = false;
const startOnce = () => { if (started) return; started = true; start(); };
// 폰트가 어떻게 되든 800ms 안에는 무조건 시작
setTimeout(startOnce, 800);
try {
const fontFamily = window.getComputedStyle(document.body).fontFamily;
document.fonts?.load(`850 1em ${fontFamily}`, BRAND)?.then(startOnce, startOnce);
} catch {
startOnce();
}try/catch가 동기 예외를 잡습니다. 그런데 더 중요한 건 두 번째, 폰트와 무관한 타이머입니다. 폰트 API가 앞으로 또 무슨 짓을 하든 인트로는 800ms 뒤에 걷힙니다.
남는 것
화면 전체를 덮는 오버레이를 JS로 걷어내는 구조라면, JS가 죽었을 때 무슨 일이 생기는지 한 번은 따져봐야 합니다. 스플래시, 인트로, 로딩 막이 다 해당됩니다. 오버레이 해제는 JS 성공에 의존하지 않는 경로를 하나 갖고 있는 편이 안전합니다.
document.fonts.load()에 getComputedStyle로 뽑은 font-family를 그대로 넣는 것도 피하는 게 좋습니다. 시스템 폰트 키워드가 하나만 섞여 있어도 사파리가 던집니다. 확인하려는 폰트 이름만 직접 지정하면 됩니다.
그리고 모바일 문제를 크롬 재현 여부로 판단하면 안 됩니다. 이 문제는 크롬에서 절대 재현되지 않습니다. 실기기에서 실제 에러를 보려면 아이폰 설정에서 사파리 > 고급 > 웹 속성을 켜고 맥 사파리의 개발자용 메뉴로 연결하면 됩니다.
덧붙이면, 제가 원인을 특정한 건 코드 분석까지이고 실기기에서 예외 로그를 직접 확인한 건 아닙니다. 같은 증상이라면 웹 인스펙터로 첫 번째 에러를 보는 게 확실합니다.
같은 작업에서 겪은 다른 문제:
함께 읽기
- 모바일에서만 가로 스크롤이 생기는 이유모바일에서 화면을 옆으로 밀었더니 페이지가 밀리고 오른쪽에 빈 여백이 보인다면, 어떤 요소가 뷰포트보다 오른쪽으로 삐져나가 있다는 뜻입니다. 흔한 문제인데 범인을 찾기가 은근히 어렵습니다. 눈에 보이는 요소가 아닌 경우가 많기 때문입니다.
- iOS 사파리에서 고정 헤더가 사라지거나 잘리는 이유상단 고정 헤더는 거의 모든 사이트가 쓰는 패턴입니다. position: fixed 한 줄이면 끝나는 것처럼 보이고, 데스크톱에서는 실제로 그렇습니다. 그런데 iOS 사파리에서는 이 헤더가 스크롤 도중 통째로 사라지거나, 위쪽이 몇 픽셀 잘려 보이는 일이 생깁니다.
- iOS Safari 모바일 버그 3종 해결기: 헤더 잘림 · 흰 화면 · 가로 넘침같은 날 확인된 세 증상은 원인이 서로 달랐으며, 모두 해당 테스트 환경에서 데스크톱 Chrome과 다르게 나타난 WebKit 관련 동작이었습니다.
- Google 번역을 켰더니 React 앱이 오류 화면으로 바뀐 이유한국어, 영어, 일본어, 중국어를 직접 제공하는 React 랜딩 페이지에서 예상하지 못한 문제가 생겼습니다. Chrome이 띄운 "이 페이지를 번역하시겠습니까?" 제안을 수락하면 번역이 시작되는 듯하다가, 잠시 뒤 사이트의 "일시적인 오류가 발생했습니다" 화면으로 바뀌었습니다.
- Next.js 랜딩 페이지의 Lighthouse 점수가 폰트 하나로 해결되지 않는 이유성능 최적화에서 가장 당황스러운 순간은 개발 환경에서는 빠른데 배포 후 측정값만 크게 느릴 때입니다. 이번 랜딩 페이지가 그랬습니다. 개발 환경에서 관찰한 LCP는 약 1.68초였지만 PageSpeed Insights의 첫 측정은 5.5초까지 늘어났습니다. 이후 배포본을 다시 측정했을 때도 성능 점수 72점, LCP 4…