스크롤바가 없는데 화면 오른쪽에 빈 여백이 생기는 이유
웹 앱의 화면 가장자리는 평소에는 눈에 잘 들어오지 않습니다. 콘텐츠와 배경이 자연스럽게 이어지면 굳이 볼 이유가 없기 때문입니다. 그런데 여러 화면을 확인하던 중 오른쪽 끝에 비어 보이는 세로 띠가 계속 눈에 걸렸습니다. 어떤 화면에서는 나타나고, 어떤 화면에서는 덜 보였습니다.
처음에는 흔한 가로 오버플로 문제라고 생각했습니다. `100vw`를 쓴 요소가 있거나, 고정 헤더의 너비 계산이 어긋났거나, 큰 카드가 뷰포트 밖으로 밀려난 상황을 의심했습니다. 관련 스타일을 검색했지만 오른쪽으로 튀어나가는 요소는 찾지 못했습니다.
여러 앱에서 같은 한 줄을 찾았습니다
문제는 페이지 컴포넌트가 아니라 전역 스타일에 있었습니다.
html {
scrollbar-gutter: stable;
}일부 앱은 같은 설정을 ``의 유틸리티 클래스에도 한 번 더 넣고 있었습니다. 중복 선언 자체가 거터를 두 배로 만들지는 않지만, 설정의 출처를 두 군데로 늘려 원인을 찾기 어렵게 했습니다.
`scrollbar-gutter: stable`은 클래식 스크롤바가 나타났다가 사라질 때 콘텐츠 너비가 바뀌지 않도록 스크롤바 자리를 미리 확보합니다. 스크롤이 필요하지 않은 화면에서도 오른쪽에 같은 폭을 남깁니다. 반면 macOS에서 흔히 보는 오버레이 스크롤바는 콘텐츠 위에 표시되므로 거터를 만들지 않습니다. 같은 코드인데 운영체제나 스크롤바 설정에 따라 증상이 다르게 보인 이유였습니다.
여기서 한 번 잘못 고쳤습니다
처음에는 전역 `scrollbar-gutter` 선언을 제거했습니다. 오른쪽의 예약 공간이 사라지니 당장 화면은 꽉 차 보였고, 타입 검사와 빌드도 통과했습니다.
하지만 이 수정은 문제 하나를 없애면서 원래 막고 있던 문제를 다시 열어 둡니다. 내용이 짧은 화면에서 긴 화면으로 이동할 때 클래식 스크롤바가 새로 생기면 사용 가능한 너비가 줄어듭니다. 중앙 정렬한 콘텐츠, 헤더 버튼, 카드 그리드가 스크롤바 폭만큼 움직일 수 있습니다. 전역 스타일에 남아 있던 주석도 바로 그 흔들림을 막기 위해 거터를 예약한다고 설명하고 있었습니다.
빈 띠가 싫다는 이유만으로 거터를 없애는 것은 절반짜리 해결이었습니다.
공간은 유지하고 배경만 이어 붙였습니다
CSS Overflow 명세를 다시 확인했습니다. 스크롤바가 없는 거터는 패딩 배경의 연장으로 칠해집니다. 그렇다면 예약 공간을 없앨 필요가 없습니다. 루트 캔버스의 배경을 명시하면 됩니다.
제가 정리한 기본 형태는 다음과 같습니다.
html {
--app-header-height: calc(3.5rem + env(safe-area-inset-top));
--app-canvas: var(--background);
scrollbar-gutter: stable;
background: linear-gradient(
to bottom,
var(--surface) 0 calc(var(--app-header-height) - 1px),
var(--border) calc(var(--app-header-height) - 1px) var(--app-header-height),
var(--app-canvas) var(--app-header-height)
) fixed;
}위쪽은 고정 헤더와 같은 `surface` 색으로 칠하고, 헤더 아래는 앱의 기본 캔버스 색으로 이어집니다. 스크롤바가 없을 때는 예약된 오른쪽 공간까지 이 배경이 보입니다. 스크롤바가 생기면 같은 자리를 스크롤바가 사용하므로 콘텐츠 너비는 달라지지 않습니다.
앱마다 본문 가장자리 색이 다르면 `--app-canvas`만 바꿉니다. 일반적인 업무 화면은 `var(--background)`, 한 단계 옅은 작업 캔버스를 쓰는 화면은 `var(--surface-muted)`가 맞았습니다. 거터 선언은 전역 CSS 한 곳에만 두고, ``의 중복 유틸리티 클래스는 제거하는 편이 추적하기 쉬웠습니다.
이 방식은 콘텐츠를 거터 안으로 늘리는 것이 아닙니다. 클래식 스크롤바는 물리적인 너비를 차지하므로, 콘텐츠를 항상 끝까지 쓰면서 스크롤바가 나타나도 아무것도 가리지 않게 만드는 것은 불가능합니다. 콘텐츠가 줄어들거나, 스크롤바가 콘텐츠를 덮거나, 자리를 미리 비워 두는 셋 중 하나를 선택해야 합니다. 저는 세 번째를 유지하고 빈 공간을 앱 배경으로 보이게 했습니다.
오른쪽의 흰 스크롤바는 다른 문제였습니다
거터를 살펴보다 보니 내부 사이드바 스크롤바는 어두운 테마에 맞게 꾸며져 있는데, 문서 전체의 스크롤바는 운영체제 기본 색으로 남은 화면도 있었습니다. 이것은 거터의 원인과는 별개입니다. 루트 스크롤바에도 `scrollbar-color`, `scrollbar-width`, `::-webkit-scrollbar-thumb` 스타일을 적용하면 같은 시각 언어로 맞출 수 있습니다.
이번 확인에서 실제로 추적한 것은 여러 프로젝트에 같은 전역 거터 설정이 있었고, 그 설정이 오른쪽 예약 공간의 원인이었다는 점입니다. 최종 스타일을 모든 운영체제의 클래식 스크롤바 설정과 실기기에서 비교한 단계까지는 아니므로, 배포 전에는 Windows의 Chromium과 Firefox에서도 확인할 예정입니다.
다음에 비슷한 띠를 보면 가로 오버플로부터 뒤지기 전에 루트의 `scrollbar-gutter`를 확인하려고 합니다. 그리고 그 선언에 이유가 적혀 있다면 먼저 지우기보다, 예약된 공간에 어떤 배경이 칠해지는지부터 보겠습니다.
참고:
함께 읽기
- Next.js는 운영에서 멀쩡한데 로컬 개발 서버에서만 UI가 먹통이 되는 이유Next.js 애플리케이션은 운영 빌드가 안정적이면 개발 서버도 같은 방식으로 움직일 것 같지만, 둘은 클라이언트 코드의 수명이 다릅니다. 제가 확인한 환경은 Next.js 16.2.7과 Turbopack이었고, 운영 사이트에서는 잘 눌리던 상단 메뉴가 로컬 개발 서버에서만 간헐적으로 먹통이 됐습니다.
- 디자인 시안을 웹으로 옮기는 순서: 시맨틱 HTML·토큰·컴포넌트디자인 시안을 웹으로 옮길 때 핵심은 픽셀을 그대로 복사하는 것이 아니라, 콘텐츠의 의미와 반복 규칙을 브라우저가 이해할 수 있는 구조로 번역하는 것이다. 이 글에서는 시맨틱 HTML, 디자인 토큰, 컴포넌트, 반응형 규칙을 이용해 시안을 유지보수 가능한 코드로 구현하는 순서를 살펴본다.
- Google 번역을 켰더니 React 앱이 오류 화면으로 바뀐 이유한국어, 영어, 일본어, 중국어를 직접 제공하는 React 랜딩 페이지에서 예상하지 못한 문제가 생겼습니다. Chrome이 띄운 "이 페이지를 번역하시겠습니까?" 제안을 수락하면 번역이 시작되는 듯하다가, 잠시 뒤 사이트의 "일시적인 오류가 발생했습니다" 화면으로 바뀌었습니다.
- Next.js 랜딩 페이지의 Lighthouse 점수가 폰트 하나로 해결되지 않는 이유성능 최적화에서 가장 당황스러운 순간은 개발 환경에서는 빠른데 배포 후 측정값만 크게 느릴 때입니다. 이번 랜딩 페이지가 그랬습니다. 개발 환경에서 관찰한 LCP는 약 1.68초였지만 PageSpeed Insights의 첫 측정은 5.5초까지 늘어났습니다. 이후 배포본을 다시 측정했을 때도 성능 점수 72점, LCP 4…
- 9개의 글자로 만든 DUOLABS AI STUDIO 브랜드 인트로 제작기브랜드 인트로를 만들면서 출발점은 로고 모양이 아니라 문자 수였습니다. DUOLABS AI STUDIO를 적어 놓고 중복을 걷어내니 남는 글자는 D, U, O, L, A, B, S, I, T 아홉 개였습니다. 3×3 격자 하나를 채우기에 정확한 숫자였습니다.