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 |
- 가장 가까운 조상 중 position이 static이 아닌 요소(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()가 호출했을 때 동작은 다음과 같다.

scrollIntoView()호출: 사용자가 바텀시트의 입력창을 클릭한다.- 'DOM' 탐색:
scrollIntoView()는 시각적 위치를 무시하고, 오직 DOM를 따라 조상을 찾는다. - 엉뚱한 조상 발견: 입력창의 DOM상 조상을 따라 올라가다 보면, 결국 페이지 전체를 스크롤하는
<body>나<html>을 스크롤 컨테이너로 인식하게 된다. - 스크롤 실행: 브라우저는 "이 입력창은 문서 맨 아래에 있으니, 페이지를 아래로 스크롤해야겠다!"라고 판단하고 페이지 전체를 아래로 내려버린다.
- 이것이 바로 우리가 겪는 '바운싱' 현상의 정체이다.
- 의도했던 바텀시트 내부 스크롤이 아니라, 엉뚱한 조상을 스크롤해서 발생한 것이다.
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)에 직접적인 영향을 미치는 중요한 기능이다.
그 동작 원리를 명확히 이해하고, 의도한 대로 부드럽고 안정적인 인터랙션을 구현하는 데 도움이 되길 바란다.
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
| [JS] 모바일 웹뷰에서 가상 키보드 감지하는 법: visualViewport·디바운스·rAF (8) | 2025.08.17 |
|---|---|
| [JS] Array.map() vs Iterator Helper API: 어떤 방식이 더 빠를까? (2) | 2025.07.21 |
| [JS] 예외 처리와 성능: throw를 언제, 어떻게 사용해야할까? (1) | 2025.06.08 |
| [React] onBlur/onFocus가 버블링되는 이유와 PrimeReact 메뉴 버그 해결기 (0) | 2025.05.11 |
| [JS] Delete vs Backspace: 키 이벤트 처리 시 주의사항 (with. OTP 입력기) (2) | 2025.03.29 |




댓글