휴대폰에서 내 화면을 위아래로 넘기다 보면 이따금 좌우로도 살짝 밀렸다. 손가락이 미끄러진 게 아니라 화면 자체가 오른쪽으로 몇 픽셀 흔들리는 것이었다. 오른쪽 끝에 정체 모를 빈 공간이 생기고, 그 공간을 향해 내용 전체가 아주 조금 밀려나 있었다. 나는 이 미세한 가로 흔들림을 잡겠다고 저녁 내내 스타일시트를 뒤졌다. 범인은 본문 어딘가에 숨어 있는 단 하나의 요소, 뷰포트보다 8 픽셀 넓은 무언가였다.
가로 스크롤은 모바일에서 겪을 수 있는 가장 짜증 나는 결함 축에 든다. 세로로 읽으려고 넘기는데 화면이 좌우로 덜컹거리면, 사용자는 자기가 뭘 잘못 만졌나 헷갈리고 이내 이 사이트는 덜 만들어졌다고 느낀다. 이번 편은 그 가로 흔들림의 정체를 밝히고 잡는 이야기다. 원인은 늘 하나로 요약된다. 어떤 요소가 화면보다 넓다. 문제는 그 어떤 요소를 찾는 일이 생각보다 고약하다는 데 있다.
1. 범인은 보이지 않는다
가로 스크롤이 고약한 이유는 원인이 눈에 안 보이기 때문이다. 화면 밖으로 삐져나간 요소는 대개 오른쪽 뷰포트 너머에 숨어 있어서, 스크롤을 오른쪽 끝까지 밀어야 겨우 정체를 드러낸다. 어떤 경우엔 투명하거나 배경색이 없어 밀어봐도 안 보인다.
나는 처음엔 이걸 눈으로 찾으려다 시간을 통째로 날렸다. 요소를 하나씩 지워보며 언제 흔들림이 멈추는지 이분법으로 좁혀가는 방식이었는데, 요소가 수십 개면 이게 고문이다. 그러다 알게 된 게 모든 요소에 임시로 테두리를 씌우는 방법이다. * { outline: 1px solid red; } 한 줄을 잠깐 넣으면, 페이지의 모든 상자에 빨간 테두리가 그려진다. 화면을 오른쪽으로 밀면서 어느 빨간 상자가 뷰포트 밖으로 튀어나가 있는지 보면, 범인이 그 자리에서 드러난다.
여기서 outline을 쓰는 게 요령이다. border는 상자 크기를 키워 오히려 없던 넘침을 만들어내지만, outline은 레이아웃에 영향을 주지 않고 위에 겹쳐 그려질 뿐이다. 디버깅 도구에 이 원리를 쓰면 원래 문제를 흐리지 않으면서 범인만 비출 수 있다. 나는 이 한 줄을 알고 나서 가로 스크롤 잡는 시간이 몇 시간에서 몇 분으로 줄었다.
개발자 도구의 다른 손도 있다. 문서 전체의 스크롤 폭과 뷰포트 폭을 비교해, 문서가 화면보다 넓은지 숫자로 확인하는 방법이다. 이 두 값이 다르면 어딘가 넘치고 있다는 확실한 신호다. 감으로 있나 없나를 다투는 대신 숫자로 못을 박으면, 다 잡았다고 착각하고 넘어가는 실수를 막는다.
범인을 좁힐 때 나는 화면을 반씩 접어보는 요령도 쓴다. 넘침이 페이지 위쪽 절반에서 오는지 아래쪽 절반에서 오는지를 큰 덩어리 단위로 먼저 가른 다음, 그 절반 안에서 다시 반으로 좁힌다. 이렇게 몇 번만 접으면 수십 개 요소 중 진짜 범인이 있는 좁은 구역이 금세 드러난다. 무작정 처음부터 하나씩 뒤지는 것보다 훨씬 빠르다. 이건 어릴 때 배운 숫자 맞히기 놀이와 똑같은 원리다.
2. 안 줄어드는 유연 상자
넘침의 단골 원인은 뜻밖에도 유연한 상자 안에 있다. 플렉스나 그리드로 나란히 놓은 자식들이, 화면이 좁아져도 도무지 줄어들지 않고 원래 폭을 고집하는 현상이다. 나는 이걸 처음 만났을 때 내가 뭘 잘못 적었나 한참을 의심했다. 분명 유연하게 짰는데 왜 안 줄어드나.
원인은 유연 상자 자식의 기본 최소 폭에 있다. 이 자식들은 기본적으로 자기 콘텐츠보다 작아지지 않으려는 성질을 갖는다. 안에 긴 글이나 큰 이미지가 들어 있으면, 그 콘텐츠의 폭이 곧 바닥이 되어 상자가 그 아래로는 줄지 않는다. 화면이 아무리 좁아져도 콘텐츠 폭만큼은 버티니, 그 초과분이 그대로 가로 스크롤로 튀어나온다.
해결은 허무할 만큼 짧다. 안 줄어드는 자식에 min-width: 0을 걸어주면, 콘텐츠보다 작아지지 못하게 막던 빗장이 풀려 상자가 제대로 줄어든다. 나는 긴 제목이 든 카드나, 안에 그래프를 담은 유연 칸에서 이 한 줄로 넘침을 여러 번 잡았다. 그리드 자식도 사정이 같아서, 같은 처방이 통한다. 이 버그는 워낙 흔해서, 나는 나란히 놓은 상자가 안 줄어들면 조건반사처럼 최소 폭부터 의심한다.
말줄임을 걸어둔 제목에서도 같은 함정이 튀어나온다. 한 줄로 자르고 넘치면 점을 찍는 처리를 해두면, 이 처리가 동작하려면 상자가 콘텐츠보다 작아질 수 있어야 하는데, 최소 폭 빗장이 걸려 있으면 점은커녕 제목이 그대로 상자를 밀어낸다. 나는 목록에서 긴 제목들이 줄줄이 화면을 밀어내는 걸 보고 원인을 못 찾다가, 결국 이 최소 폭 하나가 말줄임과 넘침 방지를 동시에 막고 있었다는 걸 알아냈다. 유연 상자를 다룰 때 이 성질을 모르면 같은 자리에서 몇 번이고 다시 넘어진다.
3. 넘치는 것들의 목록
범인을 몇 부류로 나눠두면 잡기가 쉬워진다. 내 경험상 넘침의 대부분은 이미지, 긴 문자열, 넓은 표 이 셋 안에 있다.
이미지와 영상은 원본 크기가 뷰포트보다 크면 그대로 삐져나간다. 나는 미디어에 max-width: 100%와 height: auto를 기본으로 걸어둔다. 이러면 부모 폭을 넘지 않는 선에서 비율을 지키며 줄어든다. 이 두 줄을 리셋 단계에 넣어두면, 나중에 어떤 이미지를 넣든 최소한 화면 밖으로는 안 나간다. 미디어 넘침만 막아도 가로 스크롤의 절반은 예방된다.
다음은 공백 없는 긴 문자열이다. 긴 영문 주소나 코드 한 줄, 이메일 주소 같은 것들은 중간에 띄어쓰기가 없어 줄바꿈할 자리를 못 찾고 옆으로 계속 뻗는다. 나는 본문 콘텐츠에 overflow-wrap: break-word를 걸어, 정상 단어는 경계에서 넘기되 도저히 안 되는 긴 문자열만 잘라 넘기게 한다. 인라인 코드처럼 확실히 잘라야 하는 곳에는 word-break: break-all을 더 강하게 쓰기도 한다. 앞 편에서 다룬 한글 줄바꿈과 짝을 이루는 처방이다.
마지막은 표와 넓은 다이어그램이다. 열이 많은 표는 애초에 좁은 화면에 다 안 들어간다. 이걸 억지로 욱여넣으면 표가 화면을 밀어버린다. 나는 이런 넓은 콘텐츠를 페이지 전체가 아니라 자기만의 상자 안에서 스크롤되게 격리한다. 표를 감싸는 상자에 overflow-x: auto를 주면, 페이지는 얌전히 있고 그 상자 안에서만 가로로 밀린다. 넓은 콘텐츠를 아예 없앨 수 없다면, 최소한 그 넘침을 한 칸에 가두는 것이다. 이 격리가 되고 안 되고가 페이지 전체가 흔들리느냐 표 하나만 밀리느냐를 가른다.
4. 100vw가 파놓은 함정
가장 알아채기 어려웠던 범인은 100vw였다. 배경을 화면 끝까지 꽉 채우고 싶어서, 어떤 구역의 폭을 뷰포트 전체 폭인 100vw로 잡은 적이 있다. 데스크톱에서 딱 화면만큼 넓어 보기 좋았는데, 이상하게 가로 스크롤이 생겼다.
이유를 알고 나서 허탈했다. 데스크톱 브라우저의 세로 스크롤바는 화면 폭을 조금 잡아먹는다. 그런데 100vw는 그 스크롤바까지 포함한 폭을 뜻해서, 실제 콘텐츠가 놓이는 영역보다 스크롤바 너비만큼 넓다. 그 초과분이 십몇 픽셀의 가로 스크롤로 나타난 것이다. 눈으로는 딱 맞아 보이는데 숫자로는 넘치는, 아주 얄궂은 상황이었다. 나는 그 뒤로 폭에 100vw를 함부로 쓰지 않고, 넘겨야 할 특별한 이유가 없으면 그냥 100%로 부모에 맞춘다.
음수 마진도 조용한 범인이다. 요소를 부모 밖으로 끌어내려고 왼쪽이나 오른쪽에 음수 마진을 주면, 그만큼 요소가 화면 밖으로 삐져나갈 수 있다. 절대 위치로 배치한 요소가 뷰포트 오른쪽 너머에 놓인 경우도 마찬가지다. 이런 요소들은 정상 흐름 밖에 있어 눈에 잘 안 띄지만, 문서 폭은 정직하게 늘려놓는다. 나는 화려한 배치를 시도한 구역에서 넘침이 나면, 음수 마진과 절대 위치 요소부터 되짚는다.
5. 최후의 방어선은 최후에
가로 스크롤을 검색하면 흔히 나오는 처방이 html, body { overflow-x: hidden; }다. 문서 자체가 가로로 안 밀리게 아예 막아버리는 것이다. 솔깃하다. 한 줄로 흔들림이 딱 멈추니까.
나는 이걸 오래 남용했다가 대가를 치렀다. 이 처방은 넘치는 요소를 없애는 게 아니라, 넘친 부분을 그냥 안 보이게 잘라낼 뿐이다. 근본 원인은 그대로 살아 있으니, 잘린 콘텐츠 일부가 손에 닿지 않거나, 나중에 그 요소에 기대어 뭔가를 얹을 때 이상하게 동작한다. 더 나쁜 건 이걸 켜두면 진짜 원인을 다시는 못 찾는다는 점이다. 흔들림이라는 증상이 사라져서, 어디가 넘치는지 알 길이 없어진다.
그래서 나는 순서를 지킨다. 넘치는 범인을 먼저 찾아 없애고, 그러고도 못 잡은 예외적인 넘침에 한해서만 이 잘라내기를 마지막 안전망으로 쓴다. 증상을 덮는 약을 먼저 삼키면 병의 원인을 영영 모른다. 이 순서는 가로 스크롤에만 해당하는 이야기가 아니라, 화면을 다루는 거의 모든 문제에 통하는 태도다.
여기에 하나 덧붙이면, 세로 방향으로만 잘라내는 방법도 있다는 걸 뒤늦게 알았다. 예전에는 문서에 가로 잘라내기를 걸면 그 부작용으로 요소를 화면에 붙여두는 기능이 먹통이 되는 일이 있었다. 스크롤 컨테이너가 뜻하지 않게 생겨서, 위에 붙어 따라오던 헤더가 엉뚱하게 굳어버리는 식이었다. 지금은 가로만 감추면서 세로 스크롤 동작은 건드리지 않게 값을 지정하는 방법이 자리 잡아, 이 부작용을 피할 수 있다. 나는 이걸 모르고 헤더가 안 따라온다며 한참을 헤맨 적이 있어서, 잘라내기를 쓸 때는 늘 그 부작용까지 함께 점검한다.
6. 애초에 넘치지 않게 짜기
넘침을 잡는 것보다 나은 건 넘치지 않게 짜는 것이다. 나는 몇 가지 기본기를 처음부터 깔아두는 것으로 가로 스크롤과의 싸움 자체를 크게 줄였다.
첫째는 상자 크기 계산 방식을 바꾸는 것이다. 기본 설정에서는 요소에 준 폭에 안쪽 여백과 테두리가 더해져 실제 폭이 그보다 커진다. 폭을 화면에 딱 맞게 100 퍼센트로 주고 안쪽 여백까지 얹으면, 그 여백만큼 화면을 넘어선다. 나는 초심자 시절 이 조합으로 넘침을 수도 없이 만들었다. 문서 전체에 테두리까지 폭에 포함하는 계산 방식을 걸어두면, 폭을 100 퍼센트로 주고 여백을 얹어도 요소가 부모 밖으로 안 나간다. 이 한 번의 설정이 초심자를 괴롭히던 넘침의 상당수를 조용히 없앤다.
둘째는 절대 폭을 함부로 박지 않는 것이다. 요소에 폭을 픽셀로 고정해두면, 그 숫자보다 화면이 좁아지는 순간 무조건 넘친다. 나는 웬만하면 폭을 고정하지 않고, 굳이 상한이 필요하면 최대 폭만 걸어 화면이 좁을 때는 알아서 줄어들게 둔다. 고정 폭은 그 폭보다 좁은 화면이 세상에 없다는 가정인데, 그런 가정은 언제나 배신당한다.
여기에 더해, 나는 넓은 화면에서만 테스트하는 습관을 버렸다. 데스크톱 브라우저 창을 크게 열어두고 작업하면 어지간한 넘침은 여백에 묻혀 안 보인다. 창을 좁게 줄여 손바닥만 한 폭으로 만들어두고 작업하면, 넘치는 요소가 만들어지는 순간 바로 표가 난다. 나는 아예 브라우저 창을 좁게 띄운 채로 반응형 작업을 시작하고, 넓혀가며 확인하는 쪽으로 순서를 뒤집었다. 좁은 데서 멀쩡하면 넓은 데서는 웬만해선 문제가 없다.
셋째는 뷰포트 설정을 제대로 두는 것이다. 문서 맨 위에 화면 폭에 맞추라는 뷰포트 지시가 빠지거나 잘못되면, 모바일 브라우저가 데스크톱 폭을 가정하고 화면을 통째로 축소해 보여준다. 이건 가로 스크롤과는 결이 다르지만, 좁은 화면이 이상하게 동작하는 문제의 뿌리인 경우가 많다. 나는 새 화면을 열면 이 한 줄부터 확인한다.
가로 스크롤을 잡는 일은 화려한 기술이 아니다. 빨간 테두리로 범인을 비추고, 안 줄어드는 상자에 빗장을 풀고, 넘치는 것들을 제 상자에 가두는, 지루하고 성실한 청소에 가깝다. 나는 새 화면을 만들 때마다 마지막에 휴대폰을 손에 쥐고 위아래로 몇 번 넘겨본다. 좌우로 티끌만큼도 밀리지 않을 때, 그제야 이 화면이 손안에서 단정하다고 말한다. 다음 편에서는 좁은 화면에서 가장 잘 넘치고 가장 자주 무너지는 물건, 표와 카드를 어떻게 반응형으로 다룰지 이야기한다.