무한 스크롤 구현

지인혁·2023년 11월 16일
0
post-thumbnail

🤔 들어가며

데브코스 강의에서 무한 스크롤 구현하는 방법을 알려주었다.

사실 데브코스 입교하기 전에 react로 무한 스크롤을 구현한 적이 있었다. 하지만 제대로 이해하고 사용한 건 아니였다.

단순히 구글링을 통해 구현만 되는 것을 확인하고 제대로 이해하지는 않았다. 이 기회에 무한 스크롤 구현 하는 방법을 어느정도 이해하고 다음 기회가 있다면 무한 스크롤을 자유자제로 다루고 싶어서 포스팅하게 되었다.


무한 스크롤

무한 스크롤은 사용자가 웹 페이지의 하단에 도다랗면 자동으로 새로운 데이터를 불러와 페이지의 끝이 없는 것처럼 보이게 하는 기술이다.

무한 스크롤 로직은 다음과 같다.

  1. 사용자가 페이지 하단에 도달하면
  2. 추가 데이터를 서버에서 불러오고
  3. 받아온 데이터를 현재 페이지에 추가한다.

무한 스크롤 구현할 수 있는 방법은 2가지가 있다.

  1. 스크롤 이벤트를 활용
  2. Intersection Observer를 활용

스크롤 이벤트로 구현

스크롤 이벤트 로직은 생각보다 간단하다.

먼저 window 객체에 scroll 이벤트 핸들러를 부착시킨다.

그리고 현재 높이 + 현재 스크롤 된 값이 웹 전체 높이보다 크다면 우리는 추가 데이터를 불러와 받아온 데이터를 추가하면 된다.

window.addEventListener('scroll', (e) => this.handleScroll(e));

handleScroll(e) {
    // 전체 높이
    const fullHeight = document.body.offsetHeight;
    // 현재 높이
    const currentHeight = window.innerHeight;
    // 현재 스크롤 된 값
    const scrollValue = window.scrollY;
    const isScrollEnded = currentHeight + scrollValue >= fullHeight;
    const { isLoading, totalCount, photos } = this.state;
  
    if (isScrollEnded && !isLoading && photos.length < totalCount) {
        this.onScrollEnded();
    }
}

document.body.offsetHeihgt로 전체 높이를 구하고
window.innerHeihgt로 현재 보이는 화면의 높이를 구하고
window.scrollY 현재 스크롤 된 값을 구한다.

isScrollEnded은 현재 사용자가 화면 맨 하단에 위치한지 boolean 값이다. 그리고 isScrollEnded가 true일 때 데이터를 받아오면 된다.

하지만 주의할 점이 있다. 만약 사용자가 맨 하단에 있고 스크롤을 계속해서 내리면 onScrollEnded 함수가 계속해서 호출되고 데이터를 계속해서 불러오는 문제가 발생한다.

이때 사용할 수 있는 것이 쓰로틀링 기법이다.

데이터를 불러오는 onScrollEnded가 호출되고 데이터를 불러오는 중인 isLoading 값이 false, true 인 값을 활용할 수 있다.

맨 하단에서 스크롤이 연속적으로 발생해도 isLoading 값을 활용하여 이미 데이터를 불러오는 시간일때는 로직을 무시하며 데이터를 올바르게 불러올 수 있다.

문제점

window 객체에 스크롤 이벤트 핸들러를 부착해서 어디에서나 매번 스크롤이 발생하면 이벤트가 계속해서 호출되는 문제가 발생해 메모리 문제가 발생한다.


Intersection Observer로 구현

우선 옵저버의 IntersectionObserver 생성자 함수를 통해 구현할 수 있다.

IntersectionObserver를 통해 원하는 요소를 옵저버가 주시하고 있다가 만약 현재 보이는 뷰 화면에 포착이되면 지정 로직을 수행하는 방식으로 동작한다.

옵저버 방법은 스크롤과 다르게 매번 불 필요하게 이벤트가 발생하지 않고 뷰 포트에서 해당 요소를 주시하고 있다가 포착이되면 로직이 수행되는 장점이 있다.

init() {
    // entries는 옵저버로 지정한 entry 모음
    this.observer = new IntersectionObserver(
        (entries) => {
            entries.forEach((entry) => {
                // entry가 화면에 포착됬는가??
                if (entry.isIntersecting && !this.state.isLoading) {
                    console.log('확인');
                    // 새로운 entry를 가자기 위해 기존 entry를 삭제
                    if (this.state.totalCount > this.state.photos.length) {
                        this.onScrollEnded();
                        this.observer.unobserve(entry.target);
                    }
                }
            });
        },
        {
            // root: null, // 대상 요소를 감시할 상위요소, null이면 최상위 문서(document)의 뷰포트를 사용
            threshold: 0, // 대상이 얼마나 노출되어있는지 설정할 수 있다.
            rootMargin: '0px 0px -200px 0px', // 루트의 범위를 효과적으로 늘리거나 줄인다.
        }
    );
    this.$photoList = document.createElement('div');
    this.$target.appendChild(this.$photoList);
    this.render();
}

우선 IntersectionObserver를 생성해 로직을 작성하는 부분이다. new 키워드로 IntersectionObserver 객체를 생성하고 2개의 인자를 담을 수 있다.

첫 번째 인자는 콜백 함수를 전달하며 매개변수로 entries라는 우리가 주시하는 요소들을 모두 담은 객체다. 즉 하나의 요소가 entry다.

이 entries를 순회하면서 isIntersecting 프로퍼티 값으로 지정한 entry 즉 요소가 화면에 포착되었는지 판별할 수 있다. 포착되면 true, 포착되지 않으면 false. 이때 데이터 통신이 계속해서 발생할 수 있기 때문에 isLoading 값으로 데이터 통신 주기를 제어할 수 있다.

그 후 데이터를 불러오는 로직을 수행하고 새로운 데이터를 추가적으로 렌더링을 한다. 그리고 기존 entry를 제거해줘야 새로 불러운 데이터의 맨 마지막 요소를 새 entry로 지정할 수 있다.

두 번째 인자는 entry에 대한 설정 옵션 객체다.

  • root

entry를 감시할 상위 요소다.

null이면 최상위 문서 document가 지정되고 document면 전체 뷰포트에서 해당 entry를 감시한다는 뜻이다.

  • threshold

대상이 얼마나 노출되어있는지 설정할 수 있다.

0.0은 대상 요소가 한 픽셀이라도 노출되면 콜백을 실행하며, 1.0은 대상 요소가 전체적으로 노출될 때 콜백을 실행한다. 또한, 여러 개의 임계값을 배열로 지정하여 각 임계값에 도달할 때마다 콜백을 실행할 수 있다.

  • rootMargin

root의 마진을 설정한다. root의 관찰 영역을 효과적으로 늘리거나 줄일 수 있다.

rootMargin은 CSS의 margin 속성과 유사하게 동작하며, top, right, bottom, left 순서로 값을 지정한다.. 값은 픽셀(px) 또는 백분율(%)로 지정할 수 있다.

rootMargin: '0px 0px -200px 0px'라는 설정은 관찰 영역의 하단을 200픽셀 줄이는 것을 의미하며, 대상 요소가 뷰포트에 200픽셀 진입하기 전에 콜백이 실행됩니다. 이렇게 하면 사용자가 스크롤하여 대상 요소가 뷰포트에 완전히 보이기 전에 미리 작업을 처리할 수 있다.

render() {
    const { photos } = this.state;
    if (!this.isInitalize) {
        this.$photoList.innerHTML = `
        <ul class="photo-list__photos">
            ${photos
                .map(
                    (photo) =>
                        `<li style="list-style: none;min-height: 500x;"><img width="100%" src="${photo.imagePath}" /></li>`
                )
                .join('')}
        </ul>
        `;
        this.isInitalize = true;
    }
    const $photos = this.$photoList.querySelector('.photo-list__photos');
  
    photos.forEach((photo) => {
        if ($photos.querySelector(`li[data-id="${photo.id}"]`) === null) {
            const $li = document.createElement('li');
            $li.setAttribute('data-id', photo.id);
            $li.style = 'list-style: none;min-height: 500x;';
            $li.innerHTML = `<img width="100%" src="${photo.imagePath}" />`;
            $photos.appendChild($li);
        }
    });
  
    const $lastLi = $photos.querySelector('li:last-child');
  
  	// 마지막 요소 옵저버가 주시하도록 등록
    if ($lastLi !== null) {
        this.observer.observe($lastLi);
    }
}

이 부분은 상태 값을 가지고 렌더링을 수행하는 로직이다.

마지막에 보면 젤 마지막 요소를 $lastLi로 구하고 이 요소를 observe 메소드를 통해 entry를 등록하여 옵저버가 주시할 수 있다. 마지막 요소를 새로운 entry로 지정하고 또 화면에 포착되면 데이터를 불러와 다시 entry를 갱신해주고 이렇게 해서 무한 스크롤을 구현할 수 있다.


👏 마치며

바닐라 자바스크립트로 무한 스크롤을 구현해봤으며 React에서도 동일하게 구현하면 될 것 같다.

2가지 방법이 있었는데 스크롤 이벤트로 구현하는 방법은 로직이 간단하지만 이벤트가 불 필요하게 무수히 발생하는 문제가 있었고

Intersection Observer로 구현하는 방법은 처음에 로직이 쉽지는 않았지만 다시 공부하면서 이전보다는 이해가 더욱 더 되었다.

개인적으로는 Intersection Observer로 구현하는 방법이 더 깔끔하다는 생각이 든다.

profile
대구 사나이

0개의 댓글