brunch

성능·UX 잡은 Intersection Observer

보고 있을 때만 움직여!

by 아이나비시스템즈
맵스플랫폼 사업팀_브런치 (27).png


Ep.27


유튜브나 쇼핑몰 사이트를 이용하다 보면 스크롤을 내릴 때마다 자동으로 새로운 컨텐츠나 상품이 로딩되는 무한 스크롤 기능, 보신 적 있으신가요?

출처: YouTube

위 이미지처럼 사용자가 실제로 화면에서 보는 부분에만 데이터를 불러와서, 성능을 높이고 UX를 개선하는 대표적인 방법이 바로 Intersection Observer입니다. 필자도 iMPS라는 아이나비시스템즈의 사이트를 개발·운영하면서 이 기능을 적극적으로 활용했는데요. 오늘은 그 과정에서 실제로 마주했던 문제와 Intersection Observer를 통해 어떻게 개선했는지 공유드리려 합니다.



Intersection Observer란?


먼저 이름부터 조금 어렵게 느껴질 수 있지만, 한 문장으로 요약하자면 "화면(뷰포트)에 어떤 요소가 들어오거나 나갈 때를 '감시'해주는 웹 API"입니다. 쉽게 말해, 브라우저가 “이 요소가 화면에 보이니? 안 보이니?”를 알아서 감시해주고, 그에 따라 우리가 원하는 동작(예: 재생, 로딩, 애니메이션 등)을 실행할 수 있도록 도와주는 기술입니다.


좀 더 자세히 설명을 드리자면, 웹 페이지는 사용자 화면(= 뷰포트)에 보이는 부분과, 스크롤해야만 나타나는 부분으로 나눌 수 있는데요. 기존에는 특정 요소가 화면에 보이는지 확인하려면 Scroll Events(스크롤 이벤트)를 직접 감지해서, 아래 2가지를 비교하면서 계산해야 했습니다.

현재 스크롤 위치(window.scrollY 같은 값)

요소의 위치(getBoundingClientRect() 같은 값)

Intersection Observer.png 출처 : Scroll Event 와 Intersection Observer 비교 | by 허동욱 | Medium

FPS(Frame Per Second, 초당 프레임 수)브라우저나 화면이 1초 동안 몇 번 화면을 그려서 사용자에게 부드러운 움직임을 보여줄 수 있는지를 나타내는 지표입니다.

FPS가 변동되면(특히 10~60 FPS처럼 폭넓게 오르내리면) 화면이 끊기거나, 깜빡이거나, 애니메이션이 부자연스럽게 보여서 사용자 경험(UX)이 크게 저하됩니다.

안정적인 FPS(예: 55~60 FPS)가 유지되면 화면 전환과 스크롤이 부드럽게 이어져 사용자 입장에서는 더 자연스럽고 쾌적한 느낌을 받을 수 있습니다.

Scroll Events 방식은 코드도 복잡하고, 스크롤 이벤트가 자주 발생하기 때문에 성능에 부담을 주게 되었습니다. 이러한 문제점을 해결하기 위해 2016.04 구글 개발자 페이지를 통해 Intersection Observer API(교차 관찰자 API)가 소개되었습니다.



Intersection Observer, 어떻게 사용하나요?


image4-(The Intersection Observer API).png 출처: The Intersection Observer API

예를 들어, 사용자가 .element 클래스를 가진 요소를 화면에 스크롤해서 볼 때, 애니메이션을 실행하거나 동영상을 재생하고 싶다고 가정하면, 아래와 같이 코드를 작성하면 됩니다.



// 1️⃣ 옵저버 생성

const observer = new IntersectionObserver((entries) => {

entries.forEach((entry) => {

if (entry.isIntersecting) {

// 요소가 화면에 들어옴 → 예: 애니메이션 실행, 영상 재생

console.log("보인다!");

} else {

// 요소가 화면에서 나감 → 예: 애니메이션 멈춤, 영상 일시정지

console.log("안 보인다!");

}

});

});


// 2️⃣ 관찰할 요소 등록

const target = document.querySelector(".element");

observer.observe(target);



옵저버 생성 시 두 번째 인자로 옵션을 넘겨서 더 세부적인 설정도 가능합니다.



const options = {

root: null, // 관찰 기준 요소 (null이면 뷰포트)

rootMargin: "0px", // 기준 영역에 여유를 주는 값 (ex: '100px' → 100px 더 넓게 관찰)

threshold: 0.5, // 요소가 50% 이상 보일 때 콜백 실행

};

const observer = new IntersectionObserver(callback, options);



image5-(Infinite Scrolling with the Intersection Observer).png 출처 :Infinite Scrolling with the Intersection Observer | by Akilesh Rao | JavaScript in Plain English

옵션 설명을 다시 정리하자면 다음과 같습니다.

root : 관찰할 기준이 되는 요소 (null이면 기본적으로 뷰포트를 기준으로 관찰)

rootMargin : 관찰 영역에 여유를 주는 값 (예: '100px'을 주면 요소가 뷰포트에 닿기 100px 전부터 감지)

threshold : 요소가 화면에 얼마나 보여야 콜백을 실행할지 결정 (0~1 사이의 값, 예: 0.5면 50% 이상 보여야 실행)



iMPS 사이트에서 실제로 어떻게 사용했을까요?


iMPS 사이트를 개발·운영하면서, 다음과 같은 요청이 있었습니다.

imps 사이트에서.png

처음에는 페이지 로딩과 동시에 아래처럼 구현했습니다.

유튜브 영상은 바로 재생

슬라이더는 바로 애니메이션 시작


⚠️ 그런데 곧 문제가 생겼습니다.

페이지 진입 시점부터 유튜브 영상과 슬라이더 애니메이션을 동시에 로딩·실행하다 보니, 초기 로딩 속도가 느려졌습니다. 특히 모바일 환경에서는 데이터 사용량과 렌더링 비용이 커져 UX가 크게 저하되었습니다. 또한, 사용자가 실제로 아래까지 스크롤을 내리지 않을 수도 있는데 모든 리소스를 미리 불러온다는 점도 비효율적이었습니다.



iMPS 사이트 이렇게 개선했습니다!


1. 유튜브 영상 재생

처음에는 페이지에 유튜브 영상이 있으면, 화면에 보이지 않을 때도 계속 재생되도록 구현했습니다.

그런데 이렇게 하면 아래와 같은 문제가 생겨났습니다.

사용자가 화면을 내려서 영상을 실제로 보고 있지 않아도 재생이 이어짐 → 불필요한 리소스 사용

CPU 사용량·데이터 사용량 증가 → 페이지 성능 저하


그래서 Intersection Observer를 사용해 아래처럼 개선해 보았습니다.

사용자가 화면을 내려 영상이 안 보이면 → 일시정지

다시 화면에 올라와서 영상이 보이면 → 재생


아래는 YouTubePlayer 컴포넌트 안의 예시입니다.


useEffect(() => {

if (!player) return; // 유튜브 플레이어가 없으면 종료


// IntersectionObserver 생성


const observer = new IntersectionObserver(

([entry]) => {

if (entry.isIntersecting) {

player.playVideo(); // 요소가 화면에 들어오면 재생

} else {

player.pauseVideo(); // 요소가 화면에서 나가면 일시정지

}

},


{ threshold: 0.9 } // 요소가 90% 이상 보일 때를 기준으로 판단

);


// containerRef.current: 유튜브 플레이어 DOM 요소


if (containerRef.current) {

observer.observe(containerRef.current); // 감시 시작

}


return () => {

observer.disconnect(); // 컴포넌트가 사라질 때 observer 해제

};

}, [player]);



IntersectionObserver가 요소가 화면에 얼마나 보이는지 감지해서 그에 따라 playVideo() / pauseVideo()를 호출하는 구조입니다.

threshold: 0.9는 요소가 화면의 90% 이상 보일 때를 기준으로 동작하게 설정했습니다.


<개선 전 후 비교>

아래의 내용은 크롬 DevTools(Performance 패널) 에서 측정한 결과입니다.

개선 전 개선 후.png

유튜브 영상 재생 최적화 결과, 총 소요 시간이 약 22.7초 → 19.6초로 줄어들어 영상 시작 속도가 빨라졌습니다.

자바스크립트 실행(Scripting)과 브라우저 내부 처리(System) 시간이 모두 단축되어 페이지 반응성이 향상되었습니다.

화면을 그려주는 Rendering과 Painting 시간도 줄어들어 영상 재생 시 화면 전환이 더 부드러워졌습니다.

특히 YouTube 메인스레드 점유 시간이 약 1,717ms → 1,395ms로 감소해 전체적인 성능과 사용자 경험(UX)이 개선되었습니다.


2. CI 무한 슬라이더

image13_(CI 무한 슬라이더).gif

처음에는 페이지 로딩 시점부터 슬라이더 애니메이션이 항상 실행되며, 로고 이미지도 처음부터 전부 로딩도록 구현했었습니다. 이렇게 하면 화면에 보이지 않아도 CPU가 계속 애니메이션을 돌려 불필요한 리소스가 사용됩니다. 또한, 모든 이미지를 한 번에 로딩하여 초기 렌더링이 지연되고 데이터가 낭비되었죠.


그래서 Intersection Observer를 사용해 아래와 같이 개선했습니다.

화면에 슬라이더가 보이면 → 애니메이션 재생 + 이미지 로딩 시작

화면에서 사라지면 → 애니메이션 멈춤 + 이미지 로딩 중단


또한, Lazy Load(지연 로딩)를 적용해, 사용자가 실제로 보지 않으면 로고(CI) 이미지를 아예 로딩하지 않도록 개선했습니다. 아래는 LazyLoadImage 컴포넌트의 예시입니다.



const LazyLoadImage = ({ src, alt, width }) => {

const imgRef = useRef(null); // 실제 DOM 요소를 가리키기 위한 ref

const [isLoaded, setIsLoaded] = useState(false); // 로딩 완료 여부


useEffect(() => {

if (!imgRef.current) return;


// IntersectionObserver 지원 브라우저

if ("IntersectionObserver" in window) {

const observer = new IntersectionObserver(

(entries, observer) => {

entries.forEach((entry) => {

if (entry.isIntersecting) {

imgRef.current.src = src; // 화면에 보이면 진짜 이미지 로딩

observer.unobserve(imgRef.current); // 더 이상 감지 필요 없음

}

});

},

{

rootMargin: "100px", // 살짝 미리 로딩하도록 설정

threshold: 0.1, // 이미지가 10% 이상 보이면 실행

}

);


observer.observe(imgRef.current); // 이미지 DOM 요소 감시 시작


return () => observer.unobserve(imgRef.current); // 컴포넌트 언마운트 시 해제

} else {

// 구형 브라우저에서는 바로 로딩

imgRef.current.src = src;

}

}, [src]);


// onLoad 이벤트로 로딩 상태 업데이트 (필요시 스타일 등 변경)

const onLoad = () => setIsLoaded(true);


return (

<SliderImg

ref={imgRef}

alt={alt}

src="" // 초기엔 빈 값

onLoad={onLoad}

loading="lazy" // 브라우저 기본 lazy loading도 활용

$width={width}

/>

);

};


Intersection Observer가 화면에 들어왔을 때만 src를 할당 → 안 보면 네트워크 요청 자체가 발생하지 않음!

rootMargin: "100px" 덕분에 실제로 닿기 직전에 미리 로딩 → 사용자 입장에서는 끊김 없이 부드럽게 보임

loading="lazy"는 브라우저 자체의 기본 지연 로딩 기능 → 추가 성능 향상과 호환성 확보


<개선 전 후 비교>

아래의 내용은 크롬 DevTools(Layers 패널)을 통해 측정한 결과입니다.

개선 전 개선 후2.png


*참고 : Slow scroll rects 색상 의미

색상.png

CI 무한 슬라이더를 개선한 결과, 렌더링에 필요한 레이어 수를 줄이고 메모리 사용량도 32.0 MB → 25.0 MB로 감소시켰습니다.

화면을 그릴 때 여러 겹의 투명판(레이어)을 쌓아 처리하는 구조를 간소화하여 컴퓨터가 더 가볍게 작업할 수 있게 되었습니다.

또한, 스크롤할 때 브라우저가 집중해서 계산해야 하는 Slow scroll rects 영역을 얇고 단순하게 최적화하여 스크롤이 훨씬 부드러워졌습니다.

불필요하게 복잡했던 화면의 계층 구조도 정리하여 GPU와 CPU의 부담을 줄였고, 그 결과 브라우저의 렌더링 효율과 사용자 경험(UX)이 전반적으로 크게 향상되었습니다.



성능 테스트 방법


성능 개선 효과를 객관적으로 검증하기 위해 Puppeteer를 사용해 개선 전·후 두 URL을 자동으로 로드하고, 10초간 스크롤하며 실제 사용자 환경을 시뮬레이션했습니다. 수집한 Chrome Trace 데이터를 분석한 결과, 개선 전에는 Scripting 2,545ms, Rendering 709ms, Painting 225ms가 소요되었고, 총 처리 시간은 13,575ms였습니다. 개선 후에는 Scripting이 897ms로 약 64.7% 감소했고, Rendering도 431ms로 약 39.2% 감소하여, 총 처리 시간도 10,287ms로 줄어 성능이 크게 개선되었습니다.


최종 개선 전 개선 후 3.png

Intersection Observer 도입으로 스크립트 실행 시간은 약 64.7% 감소, Rendering 비용은 약 39.2% 감소, 총 처리 시간은 약 24% 감소하여 성능 향상을 수치로 확인할 수 있었습니다.



마치며


처음 이 문제를 마주했을 땐 정말 막막했지만, Intersection Observer를 적용한 뒤 성능과 사용자 경험이 모두 눈에 띄게 좋아져서 큰 보람을 느꼈습니다. 여기서 더 나아가, 동일한 테스트 환경에서 실제 사용자 환경 시뮬레이션을 해본 결과도 기대 이상으로 개선된 것을 확인할 수 있었습니다.


혹시 페이지 버벅임이나 무한 스크롤 문제로 고민하고 계시다면, 꼭 한 번 Intersection Observer를 시도해 보시길 추천드립니다. 저처럼 비슷한 문제를 겪고 있는 개발자분들께 이 글이 조금이나마 도움이 되었으면 좋겠습니다.


by 아이나비시스템즈 응용기술개발팀 이예지


pJXLOCU5eJaeH2dtOP14OXBfX4U.png

국내 가격 경쟁력 1위! 아이나비가 만든 차별화된 지도 솔루션, iMPS 바로가기


#UXUI #IntersectionObserver #iMPS #UXUIdesigner #위치기반솔루션 #지도솔루션 #아이나비 #아이나비시스템즈 #아이나비내비게이션 #API

keyword
작가의 이전글주니어 개발자가 바라본 리팩터링과 테스트 코드의 중요성