[JavaScript] 바닐라 자바스크립트로 무한 스크롤을 구현해보자! (feat. IntersectionObserver, Event Delegation)
글 작성자: 망고좋아
반응형
🎯 무한 스크롤 구현 (feat. IntersectionObserver, Event Delegation)
- TMDB API를 이용한
Vanilla JS SPA
홈페이지를 구현하고 있었다. - 메인 페이지에는 Upcoming 영화 1개를 가져오고 아래에는 각 장르별 추천 영화 5개씩 가져오고 있었다.
- 무언가 부족해서 좀 더 많은 장르의 영화를 가져오려고 시도했지만 많은 데이터 요청으로 인하여 로딩 속도가 너무 느려졌다.
- 개선점을 찾으려고 고민하던 끝에 스크롤이 맨 밑으로 내려갔을 때 api를 요청해서 다음 장르의 추천 영화를 가져오는 무한 스크롤을 적용하기로 결정했다.
- 깃허브 코드 보기
🔍 결과 미리 보기
기존 화면
무한 스크롤 적용 화면
💡 생각
- 무한 스크롤 구현에 대표적인 방법에는
스크롤 이벤트
orIntersectionObserver API
가 있다. - 그중 debounce, throttle를 사용하지 않아도 되며 reflow 현상이 발생되지 않는
IntersectionObserver API
를 사용해서 구현하기로 결정했다.
📝 추천 영화 템플릿
import { getUpcomingMovie, getMovieDetail, getSearchMovie, getMovieList } from "../api/api.js"
import clkRoute from "../functions/handleClickRoute.js";
const genereMovieView = async () => {
const section = document.createElement('section');
section.classList.add('genere__section')
const genreName = ['액션', '어드벤처', '코미디', '애니메이션', '드라마', '로맨스', '공포', '스릴러', 'SF', '코미디', '범죄'];
const generApiName = ['Action', 'Adventure', 'Comedy', 'Animation', 'Drama', 'Romance', 'Horror', 'Thriller', 'SF', 'Comedy', 'Crime'];
let cnt = 0;
const pageStart = async (cnt) => {
let genreMovieList = [];
const movieList = await getMovieList(generApiName[cnt]);
let genreMovieTemplate = `
<div class="my-0 mx-auto w-90vw xl:w-1220">
<div>
<h2 class="text-3xl font-bold my-9">추천 ${genreName[cnt]} 영화!</h2>
<ul class="flex justify-between">
{{__movie_list_}}
</ul>
</div>
</div>
`;
const movieLi = [];
movieList.forEach(element => {
const year = element.release_date.substring(0, 4);
const vote_average = String(element.vote_average).substring(0, 3);
const posterPath = `https://image.tmdb.org/t/p/original/${element.poster_path}`;
movieLi.push(`
<li class="movie-detail cursor-pointer" id="${element.id}" route="/detail/${element.id}">
<a route="/detail/${element.id}">
<!-- <div class= "w-44 h-60 bg-left bg-no-repeat bg-contain" style="background-image: url(${posterPath})"></div> -->
<img src="${posterPath}" class= "w-44 h-60 bg-left bg-no-repeat bg-contain"></img>
<strong class="block w-44">${element.title}</strong>
<span class="block" class="inline-block">${year}</span>
<span class="block">⭐${vote_average}</span>
</a>
</li>
`);
});
genreMovieTemplate = genreMovieTemplate.replace("{{__movie_list_}}", movieLi.join(''));
genreMovieList.push(genreMovieTemplate);
if (cnt === 0) {
section.innerHTML = genreMovieList.join('');
document.querySelector('main').appendChild(section);
}
return genreMovieList;
}
await pageStart(cnt);
- 메인 페이지에서 장르별 추천 영화를 제공해주는
gener.js
컴포넌트이다. getMovieList
는 api에 요청해서 해당 장르에 대한 영화 리스트를 배열 형태로 가져온다.- 시작과 동시에 1개의 장르는 화면에 보여줘야 되기 때문에
await pageStart(cnt);
을 실행시켜 준다. cnt === 0
일 때만main
에 appendChild를 해준다. 그다음 리스트들은section
아래로 들어가야 되기 때문에!
📝 이벤트 위임(Event Delegation)
- 무한 스크롤을 구현하면서 마주한 첫 번째 에러는 DOM으로 생성한 요소에 이벤트 핸들러가 적용되지 않는 현상이었다.
- 이벤트 핸들러는 당시에 생성된 dom 객체에만 적용되지만 추후 생성된 요소에는 적용되지 않는다.
- 이 점을 해결하기 위해서 이벤트 위임을 사용하면 된다.
- 이벤트 위임이란 하위 요소에 각각 이벤트를 붙이지 않고 상위 요소에서 하위 요소의 이벤트들을 제어하는 방식이다.
- 즉, 이벤트 버블링을 이용해서 상위 요소에 이벤트 위임을 해주면 하위에 DOM으로 생성한 요소에 이벤트 핸들러를 적용시킬 수 있다.
handleClickRoute.js
const genereClkRoute = () => {
const changePage = document.querySelector('.genere__section');
changePage.addEventListener("click", event => {
event.preventDefault();
if (event.target.parentNode.matches("[route]")) {
const pagePath = event.target.parentNode.getAttribute("route")
navigateTo(pagePath);
}
})
}
- 전체 추천 영화를 감싸고 있는
section
태그에 이벤트 위임을 해준다. - 그리고 클릭한 요소에 대한 정보는
event.target.parentNode
로 영화 정보를 감싸고 있는li
태그 아래a
태그에 접근해준다. 접근 후 route 정보를 빼와서 navigateTo로 넘겨서 영화 상세 페이지를 라우팅 해준다.- 인라인 태그들을 블록 태그로 변환시켜주면 동일한 parentNode에 접근할 수 있다. (여기서 삽질한 건 안 비밀..)
📝 IntersectionObserver
- 브라우저의 viewport와 설정한 요소(element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 않는지, 사용자 화면에 지금 보이는 요소인지 아닌지를 구별하는 기능을 제공한다.
const io = new IntersectionObserver(callback, options) // 인터섹션 옵저버를 생성
io.observe(element) // 관찰할 대상(요소) 등록
- callback함수는 관찰한 대상이 등록되거나 가시성에 변화가 생기면 콜백 함수를 실행
- 콜백은 2개의 매개변수를 가진다. (entries, observer)
const io = new IntersectionObserver((entries, observer) => {}, options)
io.observe(element)
📕 entries
boundingClientRect
: 관찰 대상의 사각형 정보(DOMRectReadOnly)intersectionRect
: 관찰 대상의 교차한 영역 정보(DOMRectReadOnly)intersectionRatio
: 관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율, Number)isIntersecting
: 관찰 대상의 교차 상태(Boolean)rootBounds
: 지정한 루트 요소의 사각형 정보(DOMRectReadOnly)target
: 관찰 대상 요소(Element)time
: 변경이 발생한 시간 정보(DOMHighResTimeStamp)
📕 options
- root
- 타겟의 가시성을 검사하기 위해 뷰포트 대신 사용할 요소 객체(루트 요소)를 지정한다
{ root: document.getElementById('my-viewport') }
- rootMargin
- 바깥 여백(Margin)을 이용해 Root 범위를 확장하거나 축소할 수 있다.
(callback, { rootMargin: '200px 0px'})
- threshold
- 옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한지 백분율로 표시한다.
- 0 -> 타겟의 가장자리 픽셀이 root 범위를 교차하는 순간 옵저버 실행
- 0.3 -> 타겟의 가시성이 30%일 때 옵저버 실행
- [0, 0,3, 1] -> 타겟의 가시성이 0%, 30%, 100%일 때 모두 옵저버 실행
(callback, { threshold: 0.3})
📕 Methods
- observe()
- 대상 요소의 관찰을 시작
- unobserve()
- 대상 요소의 관찰을 중지
- disconnect()
- IntersectionObserver 인스턴스가 관찰하는 모든 요소의 관찰을 중지
📝 무한 스크롤 구현(Infinite Scroll)
const lastSection = document.querySelector('.genere__section');
const io = new IntersectionObserver((entries, observer) => {
entries.forEach(async entry => {
if (entry.isIntersecting) {
io.unobserve(lastSection);
cnt++;
if (cnt === genreName.length) {
observer.disconnect();
} else {
const div = document.createElement('div');
div.innerHTML = await pageStart(cnt);
document.querySelector('.genere__section').appendChild(div);
io.observe(div);
}
}
},
{
threshold: 0.3
}
);
});
io.observe(lastSection);
lastSection
는 관찰할 대상 -> 화면 마지막에 있는 영화 리스트- 관찰 대상이
threshold: 0.3
으로lastSection
의 가시성이 30% 일 때isIntersecting
이 되면 관찰하고 있는 대상을unobserve
해주고await pageStart(cnt)
으로 가져온 영화 리스트를appendChild
해준다. - 그리고 새롭게
appendChild
된 항목에 대해서 다시observe
시작! - 그리고 배열에 넣어준 장르를 모두 순회했을 때
observer.disconnect();
으로 무한 스크롤을 종료시켜준다.
🏷 후기
- 처음에는 IntersectionObserver을 복잡하게 생각했는데 무한 스크롤 구현 후에는 복잡하지 않고 편리한 api라고 느껴진다.
- 추후에 IntersectionObserver를 사용해서 이미지 레이지 로딩을 구현해보려고 한다.
- 공부하면서 정리한 내용으로 틀린 내용이 있을 수 있으므로 댓글 부탁드립니다!
📌 참고
반응형
'프로그래밍 > Project' 카테고리의 다른 글
[회고] 엘리스에서 2차 프로젝트를 마치고 작성하는 회고록 (feat. 유지 보수 및 리팩터링) (0) | 2022.03.25 |
---|---|
[회고] 엘리스에서 1차 프로젝트를 마치고 작성하는 회고록 (0) | 2022.03.22 |
[React] 리액트 웹폰트 적용 방법과 최적화 방법 (feat. 2022년) (0) | 2022.02.25 |
댓글
이 글 공유하기
다른 글
-
[회고] 엘리스에서 2차 프로젝트를 마치고 작성하는 회고록 (feat. 유지 보수 및 리팩터링)
[회고] 엘리스에서 2차 프로젝트를 마치고 작성하는 회고록 (feat. 유지 보수 및 리팩터링)
2022.03.25 -
[회고] 엘리스에서 1차 프로젝트를 마치고 작성하는 회고록
[회고] 엘리스에서 1차 프로젝트를 마치고 작성하는 회고록
2022.03.22 -
[React] 리액트 웹폰트 적용 방법과 최적화 방법 (feat. 2022년)
[React] 리액트 웹폰트 적용 방법과 최적화 방법 (feat. 2022년)
2022.02.25