CSS 그리드가 모바일에서 페이지를 740px로 밀어붙인 이유 — min-width:auto 함정
홈페이지를 2단 레이아웃으로 리뉴얼해서 배포한 뒤, 모바일에서도 제대로 보이는지 직접 확인하는 중이었다. 홈 화면과 카테고리 화면은 문제가 없었는데, 특정 글의 본문 페이지를 열자 화면이 이상했다. 글자가 다 작게 눌려 있고, 화면 오른쪽이 잘려 나가 있었다.
innerWidth가 375가 아니라 740
모바일 프리셋(375×812)으로 열어놓은 페이지인데, 실제로는 훨씬 넓게 렌더링되고 있는 것 같았다. 브라우저가 그걸 억지로 화면에 맞추려고 축소해서 보여주니 전체적으로 작게 보이는 것이었다. 콘솔에서 직접 확인했다.
window.innerWidth // 740
window.visualViewport.width // 375
window.screen.width // 375
innerWidth만 740이고 나머지는 정상적으로 375였다. 이 조합이 중요한 단서였다. visualViewport와 screen이 정상이라는 건 기기 자체나 뷰포트 메타 태그(width=device-width) 문제가 아니라는 뜻이다. 반면 innerWidth는 실제로 렌더링된 문서(레이아웃 뷰포트)의 폭을 반영한다 — 즉 문서 자체가 740px로 그려지고 있었다.
그리드 트랙에도 “최소 크기”가 있다
이 사이트의 2단 레이아웃은 이렇게 짜여 있다.
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--sidebar-w);
gap: 36px;
}
@media (max-width: 1079px) {
.layout {
grid-template-columns: 1fr; /* ← 문제의 지점 */
}
}
CSS 그리드에서 1fr 트랙은 남는 공간을 나눠 갖는 유연한 단위다. 그런데 스펙상 1fr 트랙의 기본 최소 크기(min-width)는 0이 아니라 auto다. auto는 그 트랙 안에 있는 콘텐츠의 min-content 크기로 정해진다 — 즉 “이 안의 내용이 더 줄어들 수 없는 최소 폭”이 트랙의 하한선이 된다는 뜻이다.
보통은 이게 문제가 안 된다. 문단 텍스트는 어절 단위로 줄바꿈되므로 min-content 폭이 좁다. 문제는 줄바꿈될 지점이 없는 콘텐츠가 하나라도 있으면, 그 콘텐츠의 전체 폭이 그대로 트랙의 최소 폭이 된다는 점이다.
범인은 인용구 안의 URL
문제가 된 글에는 이런 인용구가 있었다.
> 아래 명령어·정책은 [code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp),
[modelcontextprotocol.io](https://modelcontextprotocol.io) ... 확인한 값이다.
렌더링되면 <blockquote> 안에 code.claude.com/docs/en/mcp라는 링크 텍스트가 그대로 들어간다. 이 문자열에는 공백이 없다 — 하이픈이나 슬래시가 있어도 브라우저는 기본적으로 도메인·경로 문자열을 통째로 하나의 단어로 취급해 줄바꿈하지 않는다. 이 하나의 “단어”가 375px보다 넓었고, .layout이 1fr(=minmax(auto, 1fr))이었던 모바일에서는 그 폭이 그대로 그리드 트랙의 최소 폭이 됐다. 트랙이 넓어지니 .layout 전체가 넓어지고, 그 부모인 <body>까지 740px로 밀렸다.
데스크톱 그리드가 안전했던 이유는 단순했다 — 처음부터 minmax(0, 1fr)을 썼기 때문이다. minmax(0, 1fr)은 트랙의 최소 크기를 명시적으로 0으로 고정하므로, 콘텐츠가 얼마나 넓든 트랙 자체는 넓어지지 않고 콘텐츠가 트랙 안에서 넘쳐나거나(overflow) 줄바꿈된다. 모바일 미디어쿼리를 나중에 추가하면서 이 minmax(0, ...)를 빠뜨리고 그냥 1fr만 쓴 게 이번 버그의 직접 원인이었다.
고친 것
@media (max-width: 1079px) {
.layout {
- grid-template-columns: 1fr;
+ grid-template-columns: minmax(0, 1fr);
}
}
.prose {
font-size: 1.05rem;
word-break: keep-all;
+ overflow-wrap: break-word;
}
두 줄을 같이 고쳤다. minmax(0, 1fr)은 이번에 실제로 문제를 일으킨 지점을 고친 것이고, overflow-wrap: break-word는 같은 종류의 콘텐츠(공백 없는 긴 문자열)가 다른 글에 또 나왔을 때를 대비한 것이다. word-break: keep-all은 한글 단어가 잘리지 않게만 막을 뿐 영문 URL 같은 토큰에는 적용되지 않으므로, 최후 수단으로 글자 단위 줄바꿈을 허용하는 규칙을 별도로 추가했다.
검증
로컬에서 같은 페이지를 다시 열어 확인했다.
window.innerWidth // 375
blockquote.scrollWidth // 332
blockquote.clientWidth // 332
scrollWidth === clientWidth라는 건 더 이상 가로 오버플로가 없다는 뜻이다. 배포 후에는 문제가 있던 글과 다른 글 하나를 라이브에서 각각 확인해, 둘 다 innerWidth: 375로 정상 렌더링되는 걸 확인했다. 콘텐츠 하나에서 재현된 버그였지만, 고친 지점은 레이아웃(그리드 최소폭)과 콘텐츠 스타일(줄바꿈 규칙) 양쪽이라 다른 글에서 비슷한 콘텐츠가 나와도 재발하지 않는다.
자주 묻는 질문
왜 데스크톱에서는 이 문제가 없었나요?
이 사이트의 데스크톱 그리드(1080px 이상)는 처음부터 grid-template-columns: minmax(0, 1fr) var(--sidebar-w) 형태로 짜여 있었습니다. minmax(0, 1fr)은 트랙의 최소 크기를 0으로 강제하기 때문에 콘텐츠가 아무리 넓어도 트랙 자체가 넓어지지 않고, 넘치는 내용은 트랙 안에서 줄바꿈되거나 스크롤됩니다. 문제는 1080px 미만에서 1열로 접히는 모바일 미디어쿼리가 이 minmax를 빼고 그냥 1fr만 썼다는 점이었습니다.
word-break: keep-all이 이미 있었는데 왜 안 막았나요?
word-break: keep-all은 한글 단어가 중간에서 잘리지 않게(예: '홈페이지'가 '홈페' + '이지'로 갈라지지 않게) 막는 규칙입니다. 공백이 없는 영문 URL 같은 문자열에는 적용 대상이 아니라서, 그 문자열은 여전히 하나의 끊을 수 없는 토큰으로 남아 있었습니다. overflow-wrap: break-word는 그런 토큰도 필요하면 글자 단위로 끊는, 별도의 최후 수단 규칙입니다.
이 버그를 발견한 게 자동 테스트가 아니라 실제 모바일 확인이었던 이유는?
타입체크나 유닛 테스트는 CSS 레이아웃이 실제로 어떻게 그려지는지는 검증하지 않습니다. 이 문제는 특정 글의 본문에 특정 형태(공백 없는 긴 URL)의 콘텐츠가 있어야만 재현되는, 콘텐츠와 레이아웃의 조합 버그였습니다. 배포 후 실제 화면에서 직접 확인하는 과정이 아니었다면 다음에 비슷한 콘텐츠가 들어간 글이 나올 때까지 발견되지 않았을 가능성이 있습니다.