모바일에서만 가로 스크롤이 생기는 이유
모바일에서 화면을 옆으로 밀었더니 페이지가 밀리고 오른쪽에 빈 여백이 보인다면, 어떤 요소가 뷰포트보다 오른쪽으로 삐져나가 있다는 뜻입니다. 흔한 문제인데 범인을 찾기가 은근히 어렵습니다. 눈에 보이는 요소가 아닌 경우가 많기 때문입니다.
감으로 찾지 않는 법
저는 처음에 플로팅 버튼을 의심했습니다. 화면 오른쪽 아래에 카카오톡 버튼이 떠 있었고, 위치가 딱 문제 영역이었고, 며칠 전에 그 버튼을 손댄 참이었습니다. 셋 다 그럴듯한 이유였는데 전부 틀렸습니다.
넘치는 요소를 그냥 다 뽑으면 목록이 수십 개 나옵니다. 대부분은 조상 요소가 overflow로 잘라주고 있어서 실제로는 문제가 아닙니다. 그래서 클리핑하는 조상이 없는 것만 걸러내야 합니다.
const cw = document.documentElement.clientWidth;
const clipped = el => {
let p = el.parentElement;
while (p && p !== document.body) {
if (/hidden|clip|auto|scroll/.test(getComputedStyle(p).overflowX)) return true;
p = p.parentElement;
}
return false;
};
const out = [];
document.querySelectorAll('body *').forEach(el => {
const r = el.getBoundingClientRect();
if (r.width > 0 && r.right > cw + 1 && !clipped(el)) {
out.push({ c: el.className, right: Math.round(r.right) });
}
});
out.sort((a, b) => b.right - a.right);
console.log(document.documentElement.scrollWidth, cw, out.slice(0, 10));브라우저를 모바일 폭으로 줄이고 콘솔에 붙여넣으면 됩니다. 제 경우 결과가 이랬습니다.
clientWidth 374
scrollWidth 411 ← 37px 넘침
범인: .c3d-card (right: 411)카카오톡 버튼은 목록에 없었습니다.
3D 변환은 보이는 것보다 넓다
.c3d-card는 3D 캐러셀의 카드였습니다. 카드를 원통형으로 배치해서 돌리는 UI입니다.
<div className="ring">
{items.map((node, i) => (
<div className="card"
style={{ transform: `rotateY(${(360/n)*i}deg) translateZ(${radius}px)` }}>링 측면에 있는 카드는 거의 옆으로 서 있어서 화면에는 얇은 조각으로 보입니다. 그런데 투영된 박스는 좌우로 크게 뻗습니다. 눈에 보이는 폭과 레이아웃이 차지하는 폭이 다릅니다.
회전이나 스케일이 들어간 요소는 다 마찬가지입니다. 원인을 못 찾겠으면 이런 요소부터 getBoundingClientRect()로 실제 폭을 재보는 게 빠릅니다.
body의 overflow-x를 믿으면 안 되는 이유
여기서 의아한 점이 있었습니다. 프로젝트에는 이미 방어가 걸려 있었습니다.
body {
overflow-x: hidden; /* clip 미지원 환경 폴백 */
overflow-x: clip;
}크롬은 이 값을 뷰포트로 전파해서 가로 스크롤을 막아줍니다. 실제로 크롬에서 window.scrollTo(200, y)를 호출해도 scrollX가 0으로 유지됐습니다. 그래서 데스크톱에서는 아무 문제가 없어 보였습니다.
iOS 사파리에서는 그렇지 않았습니다. 넘침이 그대로 가로 스크롤로 이어졌습니다.
body나 html에 overflow-x: hidden을 걸어두고 안심하는 경우가 많은데, 그건 크롬 기준의 안전망입니다. 최종 방어선으로 삼으면 안 됩니다. 넘침은 생기는 자리에서 잘라야 합니다.
hidden 말고 clip
그래서 캐러셀 뷰포트에 직접 클립을 걸었습니다.
.carousel-viewport {
perspective: 1100px;
position: relative;
overflow: clip; /* hidden 아님 */
}hidden이 아니라 clip을 쓴 데는 이유가 있습니다. overflow가 visible이나 clip 이외의 값이면 그 요소의 transform-style: preserve-3d가 flat으로 강제됩니다. 3D 연출이 들어간 컨테이너에 hidden을 걸면 입체감이 사라질 수 있습니다. clip은 스크롤 컨테이너를 만들지 않아서 그런 부작용이 없고 sticky 자식에도 영향을 주지 않습니다. 사파리는 16부터 지원합니다.
결과는 scrollWidth가 411에서 382로 줄었습니다. 정면 카드가 잘리지 않는지도 확인했습니다. 정면 카드가 75~299, 클립 박스가 20~354라 여유가 있었고, 옆면 카드만 좌우로 23px씩 덜 보이는데 원래 화면 밖에 있던 부분이라 손실이 없었습니다.
100vw는 스크롤바를 포함한다
남은 382도 짚고 넘어갈 만합니다. 풀블리드 배경에 이런 패턴을 자주 씁니다.
.full-bleed {
position: absolute;
left: 50%;
width: 100vw;
margin-left: -50vw;
}100vw는 데스크톱 클래식 스크롤바 폭을 포함합니다. 제 경우 15px이었고, 그래서 이 배경이 항상 좌우로 조금씩 넘쳤습니다. 모바일은 오버레이 스크롤바라 100vw가 clientWidth와 같으므로 실제 문제는 아닙니다.
다만 넘침을 측정하는 동안 이게 계속 노이즈로 껴서 헷갈렸습니다. scrollWidth를 재고 있다면 이 8px 남짓한 차이가 어디서 오는지 미리 알아두는 게 좋습니다.
정리
가로 스크롤이 생기면 감으로 찾지 말고 클리핑 조상이 없는 넘침 요소만 걸러내는 스니펫을 돌립니다. 3D나 회전 변환이 들어간 요소는 보이는 폭과 레이아웃 폭이 다르다는 걸 기억합니다. 그리고 body의 overflow-x는 크롬에서만 확실한 방어입니다.
여기 나온 수치는 크롬에서 잰 것입니다. 실기기 확인은 별도로 하는 게 맞습니다.
같은 작업에서 겪은 다른 문제:
함께 읽기
- iOS Safari 모바일 버그 3종 해결기: 헤더 잘림 · 흰 화면 · 가로 넘침같은 날 확인된 세 증상은 원인이 서로 달랐으며, 모두 해당 테스트 환경에서 데스크톱 Chrome과 다르게 나타난 WebKit 관련 동작이었습니다.
- 크롬에서는 멀쩡한데 아이폰에서만 흰 화면이 뜨는 이유개발하면서 가장 당황스러운 상황 중 하나가 내 브라우저에서는 되는데 다른 기기에서만 안 되는 경우입니다. 특히 화면이 아예 하얗게 나오면 어디서부터 봐야 할지 막막합니다. 콘솔도 못 보고 에러 메시지도 없습니다.
- iOS 사파리에서 고정 헤더가 사라지거나 잘리는 이유상단 고정 헤더는 거의 모든 사이트가 쓰는 패턴입니다. position: fixed 한 줄이면 끝나는 것처럼 보이고, 데스크톱에서는 실제로 그렇습니다. 그런데 iOS 사파리에서는 이 헤더가 스크롤 도중 통째로 사라지거나, 위쪽이 몇 픽셀 잘려 보이는 일이 생깁니다.
- Next.js 랜딩 페이지의 Lighthouse 점수가 폰트 하나로 해결되지 않는 이유성능 최적화에서 가장 당황스러운 순간은 개발 환경에서는 빠른데 배포 후 측정값만 크게 느릴 때입니다. 이번 랜딩 페이지가 그랬습니다. 개발 환경에서 관찰한 LCP는 약 1.68초였지만 PageSpeed Insights의 첫 측정은 5.5초까지 늘어났습니다. 이후 배포본을 다시 측정했을 때도 성능 점수 72점, LCP 4…
- Next.js 앱에서 클릭이 한 박자 느리게 느껴질 때 살펴본 것들서버가 멀면 응답이 느린 건 어쩔 수 없습니다. 제 경우 오리진 응답 지연에 CDN 우회 경로까지 겹쳐 페이지 응답에 1초 가까이 걸리는 상황이었고, 네트워크 쪽은 당장 손댈 수 없었습니다. 그래서 응답 속도 대신 "눌렀을 때 반응하는 속도"를 올리는 쪽으로 방향을 잡았습니다. 느린 것과 느리게 느껴지는 것은 생각보다 …