스크롤 맨 아래 감지가 안 된다면? scrollTop 소수점 처리법

목록을 끝까지 내렸는데 완료 버튼이 켜지지 않는다. 화면은 분명 맨 아래인데, 스크롤 위치를 비교하는 코드는 왜 끝이 아니라고 판단할까?

스크롤 맨 아래 감지가 안 된다면? scrollTop 소수점 처리법
세 줄 요약
  • 스크롤 끝 판정에 정확한 등식을 쓰면 소수점 좌표 때문에 조건을 놓칠 수 있다.
  • 일반적인 세로 스크롤 상자는 남은 거리의 절댓값에 작은 허용치를 두는 방법을 쓴다.
  • Firefox 158의 정밀도 변경과 별개로, 역방향 레이아웃과 Safari의 탄성 스크롤은 따로 고려해야 한다.

정확히 같은지 대신 1px 이내로 가까운지 본다

스크롤을 끝까지 내렸는데도 완료 버튼이나 다음 동작이 반응하지 않는다면, 먼저 높이를 정확한 등식으로 비교하는지 확인해야 한다. 일반적인 위에서 아래로 흐르는 스크롤 컨테이너에서 scrollHeight - clientHeight === scrollTop이라는 조건은 항상 성립하지 않는다. scrollTop에 소수점이 들어갈 수 있기 때문이다.

MDN의 끝 도달 판정 예제는 남은 거리의 절댓값이 1 이내인지 검사한다. 함수로 옮기면 다음과 같다.

function isAtBottom(element) {
  const remaining =
    element.scrollHeight - element.clientHeight - element.scrollTop;
  return Math.abs(remaining) <= 1;
}

여기서 1은 이 비교에 사용하는 픽셀 허용치다. 1px 남았을 때도 끝에 도달한 것으로 처리한다는 뜻이므로, 엄밀한 좌표 일치와는 다르다. 일반적인 스크롤 상자의 끝 감지에 쓰는 예제이며 모든 레이아웃에 그대로 적용하는 공식은 아니다.

스크롤 위치는 실수인데 높이는 정수다

계산에 들어가는 세 값의 정밀도가 다르다. CSSOM View 표준은 scrollTop을 실수형인 unrestricted double로, scrollHeight와 clientHeight를 정수형인 long으로 정의한다. MDN도 최신 브라우저의 scrollTop이 소수점 정밀도를 가질 수 있다고 설명한다.

예를 들어 전체 높이가 1000, 보이는 높이가 400, 현재 위치가 599.75라고 가정해 보자. 끝까지 남은 거리는 0.25다. 600 === 599.75는 거짓이지만 위 함수의 Math.abs(0.25) <= 1은 참이다. 이는 원리를 설명하기 위한 숫자 예시다.

값무엇을 재나비교할 때 주의할 점
scrollHeight화면 밖으로 넘친 부분을 포함한 콘텐츠 높이다.정수이며 패딩을 포함한다.
clientHeight요소 안에서 보이는 높이다.정수이며 테두리는 포함하지 않는다.
scrollTop스크롤 원점에서 이동한 세로 거리다.소수점 또는 레이아웃에 따른 음수가 가능하다.

세 값은 같은 스크롤 요소에서 읽어야 한다. 목록 안쪽 상자가 스크롤되는데 페이지 전체 높이를 섞어 계산하면, 소수점 허용치를 넣어도 대상이 틀린 문제는 해결되지 않는다. 테두리까지 포함하는 다른 높이 속성으로 바꾸기 전에 어떤 요소와 범위를 재고 있는지부터 맞춰야 한다.

158은 화면 픽셀 정수로 맞추던 반올림을 없앴다

Firefox 158은 스크롤 위치를 화면 픽셀 단위의 정수로 반올림하지 않도록 바뀌었다. 10월 13일 출시 노트는 특히 고해상도 디스플레이나 페이지 확대 상태에서 더 매끄럽고 정밀한 스크롤을 제공한다고 설명한다.

이 변경을 소수점 좌표가 처음 등장한 사건으로 받아들여서는 안 된다. scrollTop의 실수형 정의와 끝 감지의 오차 문제는 이미 존재했다. 지금 할 일은 업데이트를 되돌리는 것보다, 앱이 좌표를 정수라고 가정하는 지점을 찾는 것이다.

수정 뒤에는 실제 스크롤 상자의 세 값을 출력하고 기본 배율과 확대 상태에서 판정 결과를 확인하는 방법이 유용하다. 좌표가 소수점이라는 이유만으로 잘못된 값이라 판단하거나, 화면에 표시할 숫자의 반올림 방식을 내부 판정에도 그대로 적용하지 않아야 한다.

역방향 채팅 목록과 Safari의 튕김은 별도로 처리한다

flex-direction: column-reverse로 만든 목록은 위 예제와 좌표 기준이 다를 수 있다. MDN은 이 경우 가장 아래쪽에서 scrollTop이 0이고, 위쪽 콘텐츠로 이동하면 음수가 될 수 있다고 설명한다. 그런 구조에서는 '아래쪽'이 어떤 좌표를 뜻하는지부터 정의해야 한다.

Safari의 탄성 스크롤도 구분해야 한다. 끝을 넘어 튕기는 동안 scrollTop이 일반적인 최대 범위를 벗어날 수 있다. 반면 MDN은 Chrome과 Firefox가 같은 방식으로 초과 좌표를 반환하지 않는다고 설명한다. 따라서 위 함수가 탄성 움직임의 모든 순간에도 참이어야 한다고 기대하면 안 된다.

우선 일반 세로 상자인지 확인하고, 같은 요소의 크기와 좌표로 허용치 비교를 적용한다. 역방향 목록이나 탄성 구간을 쓰는 화면은 그 동작에 맞는 조건을 별도로 검증한다. 끝에 도달했다는 신호로 어떤 UI를 열어 줄지는 앱의 선택이며, 좌표 계산 자체가 사용자의 읽기 여부까지 확인해 주지는 않는다.