개발 기술/사소하지만 놓치기 쉬운 개발 지식

[CSS] scrollIntoView를 사용하면 바운싱되는 이유(with. position의 차이)

by GicoMomg 2025. 6. 22.

0. 들어가며…

  • scrollIntoView()는 특정 요소를 화면에 보이도록 자동으로 스크롤해주는 API이다.
  • 주로 사용자가 선택한 항목이나 포커스된 입력창이 가려지지 않도록 하기 위해 사용한다.
  • 하지만 이 편리한 기능을 position: absolute로 구현된 요소에 사용하면, 의도와 달리 배경 페이지가 갑자기 튀는 듯한 '바운싱(Bouncing)' 현상을 마주할 때가 있다.
  • 가령, position: absolute로 구현된 바텀시트(화면 하단에서 올라오는 팝업)가 있다고 가정해보자.
  • 이 바텀시트 안에는 스크롤 가능한 영역이 있고, 사용자가 그 안의 입력창을 클릭하면 키보드가 올라오면서 scrollIntoView()가 호출된다.
  • 우리의 기대는 바텀시트 내부 스크롤이 입력창으로 부드럽게 이동하는 것이지만, 현실은 배경 페이지 전체가 '울컥'하고 아래로 스크롤되는 결과로 나타나곤 한다.
기대한 효과 문제 상황






 

  • 이번 시간에서는 이 현상이 왜 발생하는지, 웹 표준 명세(CSSOM View Module)를 바탕으로 차근차근 살펴보고자 한다. 글을 끝까지 읽고 나면, 이러한 현상의 원인을 정확히 이해할 수 있으며, position: fixed를 활용해 문제를 효과적으로 해결할 수 있음을 알게 될 것이다.




1. scrollIntoView를 사용하면 바운싱되는 이유

1) scrollIntoView(): 시각적 위치보다 DOM 구조에 따라 움직인다.

  • element.scrollIntoView()는 이름 그대로 특정 요소(element)를 사용자의 뷰포트 안으로 가져오기 위해 스크롤하는 메서드이다.
  • 이 메서드 동작 방식의 핵심은, 요소의 시각적 위치가 아닌 DOM(Document Object Model)의 계층 구조를 기준으로 동작한다는 것이다!
  • 브라우저가 스크롤을 실행하려면, 먼저 대상 요소의 '스크롤 가능한 조상(scroll container)'을 찾아야 한다.
  • 이때 브라우저는 다음과 같은 규칙을 따른다.

For each ancestor element or viewport that establishes a scrolling box scrolling box, in order of innermost to outermost scrolling box, run these substeps:

(번역: 가장 안쪽부터 가장 바깥쪽 스크롤 박스 순서로, 스크롤 박스를 만드는 각 조상 요소 또는 뷰포트에 대해 다음 하위 단계를 실행합니다.
CSSOM View Module 명세 6.1. Element Scrolling Members 일부

  • 이는 scrollIntoView()가 눈에 보이는 배치보다 DOM의 계층 구조를 훨씬 중요하게 생각한다는 의미이다.



2) 문제의 핵심: position과 포함 블록(Containing Block)

  • scrollIntoView()가 DOM 계층 구조에 따라 움직인다면, 왜 position 속성에 따라 동작이 달라질까?
  • 그 이유는 position 속성이 요소의 DOM 계층 구조상 위치(논리적 배치)화면에 실제 표시되는 위치(시각적 배치)를 서로 다르게 만들기 때문이다.
  • 이 현상을 이해하려면 '포함 블록(Containing Block)' 이라는 개념을 알아야 한다.

(1) 잠깐, 포함 블록(Containing Block)이란?

  • 포함 블록(Containing Block)은 요소의 위치와 크기를 계산할 때 기준이 되는 조상 요소의 영역을 의미한다.
  • 예를 들어, width: 50%를 설정하면 “무엇의 50%인가?”, left: 20px을 설정하면 “어디서부터 20px인가?”를 판단해야 하는데, 그 기준이 되는 것이 포함 블록이다.
  • 이 포함 블록은 요소의 position 속성 값에 따라 결정되며, 가장 가까이 포지셔닝된 조상 요소이거나 경우에 따라 초기 포함 블록(<html> 요소)일 수 있다.
위치 포함 블록
position: static (기본값) & relative - 가장 가까운 부모 블록 요소의 콘텐츠 영역(content-box)이 포함 블록이 됨.
- DOM상의 부모와 기준점이 거의 일치함.
position: absolute - 가장 가까운 조상 중 positionstatic이 아닌 요소(relative, absolute, fixed 등)가 포함 블록이 됨다.
- 만약 조상이 없다면, 최상위 뷰포트가 기준점이 됨.

 

(2) 문제 발생: scrollIntoView()가 absolute와 만났을 때

  • position: absolute는 요소를 원래의 문서 흐름(Flow)에서 제거한다.
  • 이 말은 해당 요소가 더 이상 형제 요소들과 나란히 배치되지 않으며, 원래 있어야 할 자리는 비어 있는 것처럼 취급된다는 의미다.
  • 요소의 위치는 자신의 포함 블록(containing block) 을 기준으로 정해지며, 포함 블록은 DOM 상 부모 요소가 아닐 수도 있다.
  • 이로 인해 DOM 구조시각적 위치 사이에 괴리가 생기게 되고, 이러한 불일치는 스크롤 동작에서도 예기치 못한 결과를 초래할 수 있다.

 

  • 바텀시트는 보통 transform 애니메이션을 사용해 화면 하단에서 위로 슬라이드되며 시각적으로 표시된다.
  • 하지만 이때 요소의 실제 레이아웃 좌표는 변하지 않기 때문에, 브라우저는 여전히 이 요소가 DOM 구조상 문서의 하단에 위치한 것으로 판단한다.
  • 이 상태에서 바텀시트 내부의 입력창에 대해 scrollIntoView()를 호출하면, 브라우저는 DOM 구조 기준으로 판단하여 <body><html> 전체를 스크롤하려 한다.
  • 결과적으로, 사용자는 바텀시트는 이미 화면에 떠 있는데도 불구하고 배경 페이지 전체가 울컥 스크롤되는 현상을 경험하게 된다.

See the Pen scrollintoview problem by KumJungMin (@kumjungmin) on CodePen.



scrollIntoView()가 호출했을 때 동작은 다음과 같다.

  1. scrollIntoView() 호출: 사용자가 바텀시트의 입력창을 클릭한다.
  2. 'DOM' 탐색: scrollIntoView()는 시각적 위치를 무시하고, 오직 DOM를 따라 조상을 찾는다.
  3. 엉뚱한 조상 발견: 입력창의 DOM상 조상을 따라 올라가다 보면, 결국 페이지 전체를 스크롤하는 <body><html>을 스크롤 컨테이너로 인식하게 된다.
  4. 스크롤 실행: 브라우저는 "이 입력창은 문서 맨 아래에 있으니, 페이지를 아래로 스크롤해야겠다!"라고 판단하고 페이지 전체를 아래로 내려버린다.
  • 이것이 바로 우리가 겪는 '바운싱' 현상의 정체이다.
  • 의도했던 바텀시트 내부 스크롤이 아니라, 엉뚱한 조상을 스크롤해서 발생한 것이다.



3) 해결책: position: fixed을 사용해보자!

  • 반면, position: fixed를 사용하면 이 문제가 해결된다.
  • 그 이유는 fixed 속성의 본질이 스크롤과 무관하기 때문이다.
  • 명세에 따르면 fixed 요소는 다음과 같은 특징을 가진다.
    • 포함 블록이 항상 뷰포트: fixed 요소의 위치 기준은 다른 요소가 아닌, 항상 브라우저 창(뷰포트) 그 자체이다.
    • 스크롤 부모가 없음: 스크롤의 영향을 받지 않으므로, 스크롤을 유발하는 부모(scroll parent)도 존재하지 않는다.

If the element’s computed value of the position property is fixed ... return null ...

(번역: 만약 요소의 position 속성값이 fixed라면, (scrollParent는) null을 반환한다.)
CSSOM View Module, scrollParent 속성 정의

 

  • 따라서 fixed로 된 바텀시트의 입력창에서 scrollIntoView()가 호출되면, 브라우저는 이렇게 판단한다.
  • "이 요소는 뷰포트에 고정되어 있네. 즉, 이미 화면 안에 보이므로 스크롤할 필요가 전혀 없군."



  • 결과적으로 아무런 스크롤도 발생하지 않아 배경 페이지는 안정적으로 유지된다.

See the Pen scrollintoview problem resolve by KumJungMin (@kumjungmin) on CodePen.





2. 마치며…

이번 시간에는 scrollIntoView()position 속성 간의 관계를 웹 표준 명세 기준으로 살펴보았다.

핵심은 scrollIntoView()가 요소의 시각적 위치가 아닌, DOM 구조를 기준으로 동작한다는 사실이다.

실무에서 스크롤 관련 이슈를 피하려면 다음 사항들을 기억하는 것이 좋다.

  • 바텀시트, 모달 등 화면 위에 고정되는 UI는 position: fixed를 사용할 것. 이것이 스크롤 컨테이너와의 혼선을 막고 가장 예측 가능한 동작을 보장하는 방법이다.
  • scrollIntoView()는 문서 흐름 안에 있는 요소에 사용하는 것이 가장 안전하다. absolute 요소에 꼭 사용해야 한다면, 이 글에서 설명한 동작 원리를 인지하고 예외 처리를 준비해야 한다.
  • transform 애니메이션을 주의할 것. transform은 시각적 위치만 바꿀 뿐, 레이아웃 좌표(DOM 위치)는 그대로 둔다. 따라서 transform으로 움직인 요소에 scrollIntoView()를 사용하면 여전히 원래 위치를 기준으로 스크롤이 동작할 수 있다.

스크롤은 사용자의 경험(UX)에 직접적인 영향을 미치는 중요한 기능이다.

그 동작 원리를 명확히 이해하고, 의도한 대로 부드럽고 안정적인 인터랙션을 구현하는 데 도움이 되길 바란다.

반응형

댓글