빌드노트
top을 줬는데 sticky가 사라지면 조상 overflow부터 확인한다
top이 적용된 sticky가 페이지 스크롤에서 사라진다면 가까운 overflow 조상을 확인한다. 같은 HTML의 hidden·visible·auto와 페이지·내부 스크롤 좌표를 비교하고, 잘림과 레이아웃을 보존할 수정 조건 및 clip의 한계를 설명한다.
position: sticky; top: 16px를 준 목차가 페이지를 내릴 때 함께 사라진다. 목차를 감싼 요소에 overflow: hidden이나 auto가 있다면, 페이지 대신 그 조상의 스크롤 영역을 기준으로 삼고 있는지 먼저 확인한다. 이 경우 top을 바꿔도 페이지 위쪽에 붙는 동작으로 바뀌지 않는다.
MDN의 position 설명 (새 탭에서 열림)은 sticky가 가장 가까운 스크롤 메커니즘을 가진 조상에 붙으며, 그 조상이 실제로 스크롤 중일 필요는 없다고 설명한다. 세로 방향에서는 top 또는 bottom 중 하나가 auto가 아니어야 한다. 여기서는 top: 16px를 유지하고 조상의 overflow와 스크롤 위치만 바꿨다.
스크롤바가 없는 조상도 기준이 됐다
원인을 구별하려고 별도 HTML을 만들었다. 실제 서비스의 장애 기록은 아니다. 목차 높이는 40px, 목차를 포함하는 본문 블록은 1,740px로 두어 붙어 있을 공간을 확보했다. 본문 바깥의 .shell에만 overflow를 바꿨다. 페이지 앞뒤에도 공간을 두어 페이지와 내부 영역을 각각 스크롤할 수 있게 했다.
처음 목차의 위쪽 좌표는 302px였다. 페이지를 700px 내린 뒤의 결과는 다음과 같다. 단위는 CSS px다.
.shell { overflow: hidden }: 목차 위쪽 -398,.shell.scrollTop0. 목차가 화면 밖으로 사라졌다..shell { overflow: visible }: 목차 위쪽 16,.shell.scrollTop0. 화면 위에서 16px 떨어진 곳에 남았다..shell { overflow: auto }: 별도 높이 제한이 없으면 목차 위쪽 -398,.shell.scrollTop0.hidden과 같은 결과였다.
위쪽 좌표는 getBoundingClientRect() (새 탭에서 열림)가 반환하는 뷰포트 기준 top이다. -398은 처음의 302에서 페이지 이동량 700을 뺀 값이다. hidden과 높이 제한 없는 auto에서는 페이지가 움직일 동안 조상의 내부 스크롤 위치는 그대로였다. 이때 .shell의 clientHeight와 scrollHeight는 모두 1,740이었다. 내부 세로 스크롤 여유가 0인 조상도 sticky의 기준 후보에서 제외하면 안 된다.
auto 비교에는 넓은 막대로 인한 가로 스크롤바가 있었다. 막대 폭을 100px로 줄여 가로·세로 넘침이 모두 없도록 만든 추가 비교에서도 목차 위쪽은 -398px였다. 스크롤바 유무만으로 조상을 건너뛰면 원인을 놓칠 수 있다.
이어서 같은 .shell에 height: 320px; overflow: auto를 적용했다. 페이지는 맨 위에 두고 내부 scrollTop만 400과 800으로 바꾸자, 두 경우 모두 목차 위쪽이 218px에 머물렀다. 이는 .shell의 화면 위쪽 200px에 위 테두리 2px와 top 16px를 더한 위치다. 목차는 컨테이너 안에서 정상적으로 붙어 있었다.
그 상태에서 페이지를 300px 내리자 목차 좌표도 218에서 -82로 바뀌었다. 컨테이너 내부에서 붙는 것과 페이지 화면에 남는 것은 다른 요구다. 내부 스크롤용 패널이라면 이 동작이 의도한 결과일 수 있다.
내 페이지에서는 가까운 조상부터 읽는다
개발자 도구의 Elements에서 목차를 고르고, Computed에서 position: sticky와 세로 방향의 top·bottom을 확인한다. 이미 top이 적용됐다면 다음 코드를 Console에서 실행한다. .sticky는 자기 페이지의 목차 선택자로 바꾼다. 이 코드는 스타일을 수정하지 않고 조상들의 계산된 overflow와 스크롤 위치를 읽는다.
(() => {
const sticky = document.querySelector('.sticky');
const ancestors = [];
for (let el = sticky.parentElement; el; el = el.parentElement) {
const css = getComputedStyle(el);
ancestors.push({
element: el.id || el.tagName,
overflow: `${css.overflowX}/${css.overflowY}`,
scrollTop: el.scrollTop,
range: el.scrollHeight - el.clientHeight
});
}
return { top: sticky.getBoundingClientRect().top,
page: document.scrollingElement.scrollTop, ancestors };
})()
목차가 보일 때와 사라진 뒤에 각각 실행한다. 반환된 조상 목록은 가까운 순서다. 일반 블록 조상에서 hidden, auto, scroll이 나타나는 가장 가까운 상자를 먼저 살핀다. 페이지의 page 값은 변하는데 그 상자의 scrollTop이 그대로이고 목차도 화면 밖으로 움직인다면, 의도와 다른 스크롤 영역을 기준으로 삼는지 확인할 근거가 된다. 목록에 html과 body도 나오므로 문서 스크롤은 반환값의 page와 함께 읽는다.
이 예제의 페이지 700px 위치에서는 top: -398, page: 700과 함께 shell 행에 overflow: "hidden/hidden", scrollTop: 0, range: 0이 나왔다. 선택한 요소가 없어 오류가 나면 .sticky 선택자부터 고친다. 이미 목차 좌표가 16에 머물러 있는데 보이지 않는다면, 좌표 이동보다 조상의 잘림이나 다른 요소의 가림을 확인한다.
CSS에 적은 값만 보지 말고 양쪽 축의 계산값을 읽어야 한다. overflow-x: hidden; overflow-y: visible을 준 비교에서 실제 값은 hidden/auto였고 목차도 -398px로 사라졌다. 한 축이 hidden이면 다른 축의 visible이 auto로 계산될 수 있다. CSS Overflow 3 작업 초안의 계산 규칙 (새 탭에서 열림)에 명시된 동작이다. 가로 넘침만 숨긴다고 생각한 선언이 세로 sticky의 기준에도 영향을 줄 수 있다.
잘림을 유지할지, 내부 스크롤을 유지할지 고른다
페이지에 붙어야 한다면 의심한 조상의 overflow를 개발자 도구에서 잠시 visible로 바꿔 비교한다. 목차가 붙기 시작하면 그 조상이 기준을 바꾸고 있었다는 근거가 된다. 다만 이 변경은 기존 잘림도 없앤다. 이번 HTML에 넣은 800px 너비의 파란 막대도 visible에서는 480px 내용 폭의 .shell 바깥으로 나왔다.
스크롤 기능 없이 넘친 부분만 잘라야 하는 일반 블록이라면 다음을 검토할 수 있다.
.shell {
overflow: clip;
display: flow-root;
}
MDN의 overflow 설명 (새 탭에서 열림)에서 clip은 스크롤 컨테이너를 만들지 않고, 프로그램을 통한 스크롤도 허용하지 않는다. 이번 비교에서는 목차가 16px에 남고 파란 막대는 계속 잘렸다. .shell의 테두리 포함 크기도 hidden 때와 같은 484×1,744px였다. 화면과 요소 좌표를 함께 확인한 결과다.
display: flow-root는 기존 일반 블록에서 hidden이 만들던 독립적인 블록 서식 맥락이 필요한 경우를 위한 선언이다. clip만으로는 그 맥락을 만들지 않는다. 같은 Overflow 작업 초안 (새 탭에서 열림)이 두 선언의 조합을 설명한다. float의 높이 포함이나 자식 여백 처리에 기대던 레이아웃인지 확인해야 하며, 원래 flex나 grid인 상자의 display를 이대로 덮어쓰면 안 된다.
반대로 패널 내부 스크롤이 필요한 UI라면 auto와 높이 제한을 유지한다. 개발자 도구에서 실제 그 패널을 스크롤하거나, document.querySelector('#shell').scrollTop = 800으로 내부 위치가 변하는지 확인한다. #shell은 자기 패널의 선택자로 바꾼다. scrollTop (새 탭에서 열림)은 요소 내용의 세로 스크롤 위치를 읽고 설정하는 값이다. hidden도 프로그램으로는 스크롤할 수 있다. 이 실험에서도 높이 320px인 hidden 상자의 scrollTop은 400으로 바뀌었다. 그런 동작에 의존한다면 clip으로 교체할 수 없다.
clip도 잘림 자체를 없애지는 않는다. 높이를 320px로 제한한 .shell을 clip으로 바꾸고 페이지를 700px 내린 비교에서는, 목차 좌표가 16px여도 화면에 보이지 않았다. 조상 상자가 이미 화면 위로 사라졌고 그 잘림이 목차에도 적용됐다. 페이지 목차는 계속 보여야 하고 안쪽 콘텐츠는 기존 상자에서 잘라야 한다면, 목차를 잘림 상자 밖의 형제로 옮기는 구조를 검토한다. 이때도 목차를 포함하는 바깥 본문 블록에는 충분한 높이가 남아야 한다.
끝까지 화면에 남지 않는다고 모두 같은 오류는 아니다. visible 상태에서도 페이지를 1,920px까지 내리자 본문 블록의 아래쪽이 22px, 높이 40px인 목차의 위쪽은 -18px가 됐다. 목차가 본문 끝을 넘어갈 수 없기 때문이다. CSS Positioned Layout 3 작업 초안의 sticky 절 (새 탭에서 열림)은 가까운 스크롤 영역과 별도로 포함 블록 안에 머물러야 한다는 제한을 설명한다. 처음부터 사라지는 현상과 본문 끝에서 풀리는 현상을 구별해야 한다.
관찰 환경은 Windows 11, headless Chromium 153.0.8010.36, 1280×720 뷰포트와 장치 픽셀 비율 1이다. 일반 블록 조상을 가진 합성 페이지에서 확인했으며 실제 앱 CSS는 바꾸지 않았다. Firefox·Safari, 모바일 장치, 중첩 flex/grid와 둥근 모서리의 잘림은 실행하지 않았다. 따라서 clip을 일괄 적용하기보다 자기 화면에서 목차 위치, 기존 잘림, 상자 크기와 필요한 내부 스크롤이 함께 유지되는지 확인한다.