
getBoundingClientRect()는 문서 절대 위치가 아닌 viewport 기준 상대값을 돌려준다.우선 글을 작성하기 전에 getBoundingClientRect()에 대해 먼저 정리해보겠다.
getBoundingClientRect()는 요소가 지금 보이는 viewport 안에서 어디에 위치해 있는지를 알려준다.
const rect = el.getBoundingClientRect();
rect.top; // viewport 맨 위에서 요소 위쪽까지의 거리
여기서 top은 문서 전체가 아닌 지금 보이는 viewport 기준이다. 스크롤 위치에 따라 top 값은 계속 변하고, 스크롤을 내려서 요소가 viewport 밖으로 벗어나면 음수가 된다.
getBoundingClientRect()를 이용하여 viewport와 요소 사이의 거리, 요소의 너비와 높이를 알 수 있다.

const rect = el.getBoundingClientRect();
rect.top; // 화면 맨 위에서 요소 위쪽까지의 거리
rect.left; // 화면 왼쪽에서 요소 왼쪽까지의 거리
rect.bottom; // 화면 맨 위에서 요소 아래쪽까지의 거리
rect.width; // 요소 너비
rect.height; // 요소 높이
반면, 현재 스크롤 위치를 가져오거나 설정할 때 사용하는 scrollTop은 스크롤 컨테이너를 기준으로 얼마나 스크롤되었는지를 나타내는 값이다.
정리하자면, getBoundingClientRect()는 스크롤 기준 상대값을, scrollTop은 문서 전체 기준 절대값을 제공한다.
내가 작업 중인 서비스는 하나의 서비스 안에 정말 많은 조직이 각각 다른 메뉴를 맡아서 관리하고 있다. A 메뉴 안에 A-1, A-2, A-3 메뉴를 모두 각각 조직이 관리하기도 한다. 굉장히 큰 서비스이기 때문에, 사이트의 틀이 되는 헤더, 메뉴 등의 바깥 페이지는 또 별도 조직에서 관리하고, 각 조직에서 담당하는 화면은 iframe 안에 들어가게 된다.
그래서 내가 작업하는 환경은 local에서는 바깥 페이지 없이 내부 콘텐츠만 작업한 후, 실제로 배포를 하면 iframe에 적용되어 화면에 나오는 구조이다.

정리하면, 실제 내가 개발하는 파일은 내부 콘텐츠만이기 때문에 실제 iframe에 적용해서 개발 중에 확인하는 건 불가능한 환경이다. iframe으로 인해 콘텐츠가 다르게 적용된다거나 했던 경험이 없었는데, 이번 작업에서 처음으로 불편함을 발견하게 되었다.
이번 이슈는 다른 분 작업에 대해 코드 리뷰를 하다가 발견하게 되었다. 가이드를 제공하는 안내 페이지인데, 6개의 섹션으로 나뉘어 있고 각 섹션 하단에는 각 섹션으로 이동하는 동일한 6개의 버튼이 있었다. 아래 버튼들을 통해서 페이지 내 다른 섹션으로 편하게 이동할 수 있는 구조였다.
코드 리뷰를 위해 작업 내용을 받고 로컬에서 확인해보았는데, 버튼을 눌렀을 때 엉뚱한 위치로 이동하는 걸 발견했다. 맨 위에 있는 버튼을 누르면 정확한 위치로 이동하지만, 아래 섹션에 있는 버튼을 누르면 엉뚱한 위치로 이동하는 것이었다.
맨 위 버튼만 잘 된다는 것에서 힌트를 얻어, 왜 아래 버튼들이 이상하게 움직이는지 고민해보았다.
스크롤 이동 코드를 확인해보니 아래와 같은 구조로 되어있었다.
function scrollToSection(targetEl: HTMLElement) {
const container = getScrollContainer();
const top = targetEl.getBoundingClientRect().top;
animateScroll(container, top, 400);
}
function animateScroll(container: HTMLElement, targetPos: number, duration: number) {
const initial = container.scrollTop; // 현재 스크롤 위치
const diff = targetPos - initial; // 목표 - 현재
// ...
// 진행률(progress)에 따라 initial에서 목표까지 조금씩 이동
container.scrollTop = progress * diff + initial;
}
getBoundingClientRect().top 값을 top에 담고, 현재 스크롤 위치(initial)와의 차이(diff)만큼 스크롤을 이동시키고 있다.
여기서 initial은 scrollTop, 즉 문서 기준 절대 위치이고, targetPos는 getBoundingClientRect().top, 즉 viewport 기준 상대 위치이다.
즉, viewport 기준 좌표를 문서 기준 좌표처럼 사용하고 있었던 것이다.
해당 코드를 보니 왜 맨 위에 있는 버튼을 눌렀을 땐 제대로 잘 이동하고, 스크롤이 내려간 상황에서는 그렇지 않았는지 이해할 수 있었다.
페이지 맨 위에서는 scrollTop이 0이기 때문에 상대 좌표와 절대 좌표를 섞어 계산해도 문제가 드러나지 않는다. 하지만 스크롤이 내려간 이후에는 scrollTop은 계속 증가하는 반면 getBoundingClientRect().top은 viewport 기준으로 계속 변하기 때문에, 서로 다른 기준의 값을 계산하면서 엉뚱한 위치로 이동하게 된 것이다.
코드 리뷰를 달기 전, 혹시 로컬에서는 제대로 동작하지 않아도 iframe 환경 때문에 이렇게 코드를 짜신 건가 싶어서 실제 배포 화면을 확인해보았다.
그랬더니 iframe 안에서는 버튼을 눌렀을 때 모두 올바른 위치로 이동하고 있었다.
분명 로컬에서는 깨져보이던 기존 코드가, iframe이 적용된 실제 배포 화면에서는 정상적으로 나오고 있었던 것이다.
코드를 다시 확인해보니 아래와 같은 주석이 있었다.
// iframe에서 작동하지 않을때는 정확한 위치가 아님.
const top = div.getBoundingClientRect().top;
왜 iframe이 없을 땐 엉뚱한 위치로, iframe이 적용된 실제 화면에서는 정확한 위치로 이동하는 것일까? 이유가 궁금해서 가설을 세우고 직접 검증해보기로 했다.
현재 서비스에서는 iframe 여부에 따라 실제 스크롤되는 주체가 다른 것이 아닐까?
로컬에서는 콘텐츠 자체가 스크롤되지만, iframe이 적용된 배포 환경에서는 내부 콘텐츠는 자체적으로 스크롤되지 않고 실제로 스크롤되는 건 바깥 셸인 것 아닐까?
만약 그렇다면 iframe 내부 문서에서는 scrollTop이 항상 0이다.
그 상태에서는 getBoundingClientRect().top이 viewport 기준 상대값이더라도 문서 안에서의 위치처럼 사용할 수 있게 된다.
그렇다면 이 코드는 현재 서비스의 iframe 구조를 전제로 작성된 것이 아닐까 하는 가설을 세웠다.
직접 코드를 수정하며 개발 서버에 배포해 확인해보고 싶었지만 불가능한 상황이었다.
그래서 콘솔을 이용해 가설을 검증해보기로 했다.
1. 요소가 iframe 안에 있는지 확인
window.parent === window;
콘솔 실행 컨텍스트를 바깥 셸 프레임으로 두고 찍으면 true, iframe 내부 문서 프레임으로 두고 찍으면 false가 나온다.
iframe 내부 문서와 바깥 셸이 서로 다른 window에서 실행되고 있다는 것을 확인할 수 있었다.
2. 실제로 스크롤되는 컨테이너 찾기
스크롤을 내린 상태에서 scrollTop을 찍어보았다.
여기서 실제 값이 증가하는 쪽이 실제로 스크롤을 담당하는 컨테이너다.
iframe 내부 콘텐츠 기준
document.documentElement.scrollTop; // 0
document.body.scrollTop; // 0
스크롤을 아무리 내려도 scrollTop이 계속 0으로 찍혔다.
즉, 현재 서비스에서는 iframe 내부 문서가 스크롤을 담당하지 않고 있다는 것을 확인할 수 있었다.
바깥 셸 기준
document.documentElement.scrollTop; // 1234
document.body.scrollTop; // 0
반대로 바깥 셸에서는 스크롤 위치에 따라 값이 계속 증가했다.
가설이 맞았다.
현재 서비스에서는 iframe 내부 콘텐츠는 자체적으로 스크롤되지 않고, 실제 스크롤은 바깥 셸이 담당하고 있었다.
드디어 기존 코드를 이해할 수 있는 실마리가 잡혀갔다.
콘솔로 검증할 때는 실행 중인 프레임(Frame)을 잘 확인해야 한다.
처음에는 결과가 예상과 다르게 나와서 한참 헤맸는데, 알고 보니 개발자도구 콘솔이 다른 프레임에서 실행되고 있었던 것이다.
개발자도구 콘솔은 기본적으로 최상위 페이지 기준으로 실행된다.
코드가 iframe 안에 있어도 콘솔이 바깥 프레임에 붙어 있으면 바깥 페이지 기준으로 계산된 값이 나온다.
콘솔 좌측 상단의 Frame(실행 컨텍스트)을 확인하고, 검사하려는 iframe을 선택해야 정확한 값을 확인할 수 있다는 것을 이번에 알게 되었다.
확인 결과, 현재 서비스에서는 iframe 내부 문서가 스크롤을 담당하지 않고, 바깥 셸이 스크롤을 담당하는 구조라는 것을 확인할 수 있었다.
정리하면 아래와 같다.
scrollTop이 항상 0이다.getBoundingClientRect().top이 viewport 기준 상대값이더라도 문서 안에서의 위치처럼 사용할 수 있다.정리하자면, 현재 이 코드는 iframe이라는 실제 동작 환경에 맞게 작성된 코드였다. 그렇다면 로컬과 iframe 적용 이후 화면을 모두 맞출 수는 없을지 궁금해서 담당자분께 직접 여쭤보았다.
답변은 "하려면 할 수는 있다"였다.
예를 들어 로컬에서도 가상의 외부 셸을 만들어 iframe 환경을 흉내 내는 방식으로 구현할 수도 있다. 다만 그렇게 되면 외부 셸 구조가 변경될 때마다 함께 관리해야 하고, 개발 환경도 복잡해진다.
현재 프로젝트에서는 이러한 관리 비용보다 실제 배포 환경을 기준으로 작업하는 것이 더 적합하다고 판단하여, 스크롤을 외부 셸에 위임하는 현재 구조를 선택하셨다고 한다.
그래서 이러한 내부 콘텐츠에서 스크롤 이동 작업을 할 때는 로컬 화면만 확인하는 것이 아니라, 실제 iframe이 적용된 화면에 배포해서 함께 확인하며 작업해야 한다는 가이드를 받을 수 있었다.
잘 이해되지 않는 코드였는데, 직접 하나씩 뜯어보며 분석해보고 작업자분께 직접 질문까지 드려보니 이제야 해당 코드가 왜 그렇게 작성되었는지 이해할 수 있었다.
이번 경험을 통해 동일하게 동작할 것이라고 예상했던 코드도 실행 환경에 따라 전혀 다른 결과를 만들 수 있다는 것을 배웠다. 특히 iframe처럼 내가 직접 제어하지 않는 외부 환경이 영향을 줄 수 있다는 점도 새롭게 알게 되었다.
앞으로도 작업하면서 이해되지 않는 현상을 발견하면 그냥 지나치지 않고, 직접 뜯어보며 가설을 세우고 검증해보는 습관을 계속 가져가려고 한다.
웹 언어를 잘 모르는 저로썬 JS의 뷰포트 원점이 왼쪽 상단에 있다는 점이 글 초반부터 흥미로웠습니다.
제가 최근 다룬 그래픽스에서는 뷰포트의 원점이 왼쪽 하단에 있었거든요.
언어적인 관점에서는 등호를 3개 사용하는 === 표현도 신기하게 느껴졌습니다.
문제를 관찰로부터 인식하고 원인을 역추적해나가는 과정으로 글이 쓰여 있어서, 웹 언어를 잘 몰라도 흥미롭게 읽을 수 있었습니다.
디버깅은 어느 분야에서든 여러 방식으로 필요하군요.
(저는 C/C++을 다루다보니, gdb나 strace와 같은 프로그램의 도움을 받는데요. 보통은 원시적으로 cout이나 printf로 찍어봅니다....)
토요일 제대로 참여 못했지만 뒤늦게 즐겁게 읽었습니다!