모바일에서 가로 스크롤이 생겼다 — 콘솔 한 줄로 화면 밖으로 넘치는 범인 찾기

  • 20th August 2026
  • 5 min read

모바일에서 화면을 확인하다 보면 손가락이 살짝 옆으로 미끄러졌을 뿐인데 페이지 전체가 좌우로 흔들리는 경우가 있다. 분명 레이아웃은 전부 화면 안에 들어와 있는 것처럼 보이는데 가로 스크롤이 생긴다. 스크롤이 생기니 세로로 넘겨야 할 상황에서 화면이 옆으로 밀리고, 고정해둔 헤더는 왼쪽으로 잘려 나간다.

이럴 때 가장 먼저 떠오르는 처방은 하나다.

body { overflow-x: hidden; }

그런데 이건 해결이 아니라 은폐다. 넘친 요소는 그대로 넘친 채 잘려서 안 보일 뿐이고, 무엇보다 조상 요소에 overflow-x: hidden이 걸리면 그 안의 position: sticky가 조용히 죽는다. 헤더가 안 붙거나 사이드가 안 따라오는 증상의 절반은 여기서 나온다. 원인을 찾아서 그 요소를 고치는 게 맞다.


먼저 누가 스크롤되는지부터 확인한다

문서 자체가 넘치는 것인지, 안쪽 컨테이너가 넘치는 것인지 구분해야 한다. 콘솔에서 이것부터 찍어본다.

const de = document.documentElement;
console.log(de.scrollWidth, de.clientWidth, document.body.scrollWidth);

scrollWidthclientWidth보다 크면 문서가 넘치는 것이고, 이때부터 범인 찾기가 시작된다. 값이 같은데도 스크롤이 되면 특정 내부 컨테이너가 자기 안에서 스크롤되고 있는 것이니 그 컨테이너를 따로 봐야 한다.


넘치는 요소를 한 번에 훑는다

DevTools를 열어 요소를 하나씩 클릭해보는 방법도 있지만, 모바일 화면을 원격 디버깅으로 붙여놓고 DOM을 타고 내려가는 건 시간이 많이 든다. * { outline: 1px solid red } 같은 트릭도 자주 쓰이는데, 어떤 박스가 넘쳤는지는 눈에 보여도 그게 부모 때문인지 자식 때문인지는 결국 다시 확인해야 한다.

그래서 콘솔에 붙여넣는 스크립트 쪽이 훨씬 빠르다.

const W = document.documentElement.clientWidth;
document.querySelectorAll('body *').forEach(el => {
  const r = el.getBoundingClientRect();
  if (r.width && r.right > W + 1 && !el.closest('.sidenav-menu'))
    console.log(Math.round(r.left), Math.round(r.right), W, el);
});

짧지만 조건 하나하나가 이유가 있다.

  • document.documentElement.clientWidth를 기준으로 쓴다. window.innerWidth는 데스크톱에서 스크롤바 폭까지 포함하기 때문에 몇 px 차이로 오탐이 생긴다.
  • r.width가 0인 요소는 건너뛴다. display: none이거나 크기가 없는 요소는 위치값이 의미가 없다.
  • W + 1로 1px 여유를 둔다. 소수점 레이아웃에서 right375.328처럼 잡히는 경우가 흔한데, 이걸 다 잡으면 목록이 노이즈로 가득 찬다.
  • closest('.sidenav-menu')로 예외를 뺀다. 오프캔버스 메뉴는 원래 화면 밖에 대기시켜 두는 요소다. 이런 의도된 요소를 걸러내지 않으면 진짜 범인이 묻힌다.

출력은 left, right, 화면폭, 요소 순서다. right가 화면폭보다 얼마나 큰지를 보면 몇 px이 넘쳤는지 바로 감이 온다. 콘솔에 찍힌 요소에 마우스를 올리면 실제 화면에서 해당 영역이 하이라이트되니 확인도 빠르다.


진짜 범인만 남기기

위 스크립트를 그대로 돌리면 보통 결과가 여러 줄 나온다. 한 요소가 넘치면 그 안의 자식들도 같이 넘친 좌표를 갖기 때문이다. 넘침의 시작점은 그중 가장 바깥쪽 요소다. 그래서 다른 후보에 포함되는 요소는 빼버리면 목록이 한두 개로 줄어든다. 왼쪽으로 넘치는 경우도 같이 잡도록 조건을 추가했다.

(() => {
  const W = document.documentElement.clientWidth;
  const IGNORE = '.sidenav-menu';           // 의도적으로 화면 밖에 두는 요소
  const hits = [];

  document.querySelectorAll('body *').forEach(el => {
    const r = el.getBoundingClientRect();
    if (!r.width) return;
    if (IGNORE && el.closest(IGNORE)) return;
    if (r.right > W + 1 || r.left < -1) hits.push({ el, r });
  });

  // 이미 걸린 요소의 자식은 제외 — 넘침이 시작된 지점만 남긴다
  const roots = hits.filter(h => !hits.some(o => o !== h && o.el.contains(h.el)));

  console.table(roots.map(({ el, r }) => ({
    tag: el.tagName.toLowerCase(),
    id: el.id || '',
    cls: (typeof el.className === 'string' ? el.className : '').slice(0, 40),
    left: Math.round(r.left),
    right: Math.round(r.right),
    over: Math.round(r.right - W),
    el
  })));
  console.log('viewport', W, 'candidates', roots.length);
})();

console.table로 뿌리면 over 열만 봐도 어느 요소가 몇 px 밀려났는지 한눈에 들어온다. 이 상태에서 후보가 두세 개면 그때부터는 눈으로 확인하는 게 빠르다.


자주 걸리는 것들

범인을 찾고 나면 원인은 대체로 몇 가지 패턴 안에 있다.

  • 고정 px 폭. width: 480px 같은 값이 남아 있으면 좁은 화면에서 그대로 넘친다. 데스크톱 기준으로 잡아둔 이미지나 테이블이 흔하다.
  • 100vw. vw는 세로 스크롤바 폭을 포함한 값이라 스크롤바가 있는 환경에서 항상 몇 px 넘친다. 전체 폭이 필요하면 width: 100%를 쓰는 편이 안전하다.
  • 줄바꿈되지 않는 긴 문자열. 공백 없는 URL이나 해시값, 코드 조각 하나가 컨테이너를 통째로 밀어낸다.
  • flex / grid의 최소 크기. flex 아이템의 min-width 기본값은 auto라서 내용물보다 작아지지 않는다. 안에 긴 텍스트나 <pre>가 있으면 컨테이너를 밀어버린다.
  • 음수 마진과 translateX. 배너나 캐러셀 애니메이션이 화면 밖까지 이동하는 경우. 이건 의도된 것이라면 예외 목록에 넣는다.
  • 테이블과 iframe. 임베드된 지도, 유튜브, 광고 프레임은 자기 폭을 스스로 정한다.

고치는 쪽

처방은 원인별로 다르지만 실제로 자주 쓰는 건 이 정도다.

img, video, iframe, table, pre { max-width: 100%; }

/* 긴 URL·해시 강제 줄바꿈 */
.post-body { overflow-wrap: anywhere; word-break: break-word; }

/* flex/grid 아이템이 내용물 때문에 안 줄어드는 문제 */
.flex-item { min-width: 0; }
.grid-col  { min-width: 0; }

/* 넓은 표는 감싸서 그 안에서만 스크롤 */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

표나 코드블록처럼 원래 넓을 수밖에 없는 콘텐츠는 억지로 줄이지 말고 감싸는 요소 안에서 스크롤되게 두는 편이 낫다. 문서 전체가 흔들리는 것과, 표 하나가 자기 영역 안에서 옆으로 밀리는 것은 사용감이 완전히 다르다.

그래도 마지막 방어선이 필요하다면 hidden 대신 clip을 쓴다.

body { overflow-x: clip; }

overflow: clip은 스크롤 컨테이너를 만들지 않기 때문에 안쪽의 position: sticky가 그대로 살아 있다. hidden으로 막아둔 곳들을 clip으로 바꾸는 것만으로 이유를 알 수 없던 sticky 미동작이 풀리는 경우도 있다. 물론 이것도 원인을 고친 다음에 얹는 안전장치지, 원인 대신 쓰는 게 아니다.


한 번 찾고 끝낼 일이 아니다

가로 스크롤은 새 컴포넌트를 붙일 때마다 다시 생긴다. 그래서 위 스크립트는 브라우저 개발자도구의 Snippets에 저장해두고, 화면을 크게 손댄 날 배포 전에 좁은 폭으로 줄여놓고 한 번씩 실행한다. 320px, 360px, 390px 정도만 확인해도 대부분 걸린다.

결국 요령이랄 게 없는 작업이지만, 손으로 DOM을 타고 내려가던 걸 콘솔 한 줄로 바꾸면 십 분 걸리던 일이 십 초로 끝난다. 그거면 충분하다.