Vue.js에서 무한 스크롤 구현하기(ft. Intersection Observer API)

beenvyn·2024년 11월 15일

Vue.js

목록 보기
3/3

🔎 들어가기에 앞서...

✨ 대부분의 웹 애플리케이션에서 핵심 아이디어는 DB에 저장된 데이터를 가져와 사용자에게 가장 좋은 방식으로 보여주는 것이다.

화면에 표시된 데이터를 사용자가 탐색할 수 있는 방법 중 가장 흔한 2가지로는 페이지네이션과 무한 스크롤이 있다. 상황에 따라 페이지네이션을 사용하는게 더 적합할 수도 있고 무한 스크롤이 더 적합할 수도 있기 때문에 충분히 상황을 고려해서 선택하는게 좋다.

나의 경우에는 사용자가 상품 목록을 탐색하는 상황에서 별도의 추가 동작(페이지 버튼 클릭 등) 없이 스크롤 만으로 모든 상품을 볼 수 있도록 하고 싶었기 때문에 무한 스크롤 기능을 선택했다.

❓Intersection Observer API란?

Javascript에서 무한 스크롤을 구현하는 잘 알려진 방법으로는 addEventListenerscroll 이벤트를 사용하는 방법이 있다. 하지만 이 방법은 단시간에 수 백번, 많게는 수 천번까지 호출이 된다는 성능 문제가 있다.

이를 보완하기 위해 등장한 Intersection Observer API루트 요소와 타겟 요소의 교차점을 관찰하여 타겟 요소가 루트 요소와 교차하는지 아닌지를 구별하는 기능을 제공한다.

📌 Intersection Observer API 사용법

🔤 기본 문법

let observer = new IntersectionObserver(callback, options); // Intersection Observer 인스턴스 생성하기

observer.observe(element); // 타겟 요소 관찰 시작

Options

Target의 관찰이 시작되는 상황에 대한 옵션을 설정할 수 있다.

  • root : target의 가시성을 확인할 때 사용되는 루트 요소이다. target의 조상 요소여야 한다.
    - 기본값 : 브라우저의 뷰포트
  • rootMargin : root 요소의 범위를 확장/축소하는데 사용된다. ex) "15px 20px 40px 50px".
    - 기본값 : 0
  • threshold : target 요소의 가시성 퍼센티지가 얼마일 때 콜백 함수를 실행할 것인지 설정하는데 사용된다. 예를 들어 threshold를 0.5로 설정하면 target이 절반만큼 보여졌을 때 콜백이 호출되고 0으로 설정하면 target이 1px이라도 보이면 콜백이 호출된다.
    - 기본값 : [0]

Callback

Target의 관찰이 시작되거나 가시성에 변화가 생기면 실행된다.
콜백함수는 파라미터로 entriesobserver를 받는다.

let observer = new IntersectionObserver((entries, observer) => {}, options)
  • entries : IntersectionObserverEntry 인스턴스를 담은 배열이다. root 요소와 target 요소의 교차 상황을 묘사한다.
    콘솔창에서 찍어보면 아래와 같이 다양한 속성을 갖는 것을 볼 수 있다..
    여기서 내가 사용할 속성은 target 요소와 root 요소가 교차 상태인지 아닌지를 알려주는 isIntersecting 속성이다.

  • observer : 콜백 함수가 호출되는 IntersectionObserver 를 가리킨다.

Methods

Intersection Observer에서 자주 사용되는 메서드

  • observe() : target의 관찰을 시작할 때 사용된다.
  • disconnect() : IntersectionObserver 인스턴스가 관찰하는 모든 요소의 관찰을 중지할 때 사용된다.

📐 Vue.js에서 Intersection Observer API 사용하기

1. ProductItem 컴포넌트 세팅하기

// ProductItem.vue
<script setup>
	const items = ref([]);
    const lastKey = ref("");
    
	const fetch = async () => {
     // 초반에 데이터를 가져오는 로직 구현
     const result = ~~
     
      if (result?.success) {
    	items.value = result.data.results;
    	lastKey.value = result.data.lastKey;
  		}
    }
    
    const fetchMore = async () =>
     // 데이터를 추가로 가져오는 로직 구현
     const result = ~~
     
     if (result?.success) {
    	items.value = [...items.value, ...result.data.results];
    	lastKey.value = result.data.lastKey;
  		}	
     
    }
</script>

<template>
	<ProductItem
	:product="product"
	v-for="product in items"
	:key="product.id"
	/>
</template>

추가로 불러올 데이터가 있는지 판단하는 기준 ❓
초기 fetch 과정에서 서버로부터 lastKey를 전달받을 경우 추가로 불러올 상품들이 남아있다고 판단
fetchMore() 를 실행하여 상품을 추가로 불러옴.

2. Observer 컴포넌트 세팅하기

한 눈에 보기

<script setup>
import { ref, toRefs, onMounted, onUnmounted } from "vue";

const props = defineProps({
  options: {
    type: Object,
    default: () =>
      Object.assign({
        root: null,
        threshold: 0,
      }),
  },
});
const { options } = toRefs(props);
const emit = defineEmits(["show"]);

const observer = ref(null);
const target = ref(null);

onMounted(() => {
  observer.value = new IntersectionObserver((entries) => {
    const firstEntry = entries[0];
    if (entries && firstEntry.isIntersecting) {
      emit("show");
    }
  }, options.value);

  observer.value.observe(target.value);
});

onUnmounted(() => {
  observer.value.disconnect();
});
</script>

<template>
  <span ref="target" />
</template>

2-1. Target 설정

<script setup>
const target = ref(null);
</script>

<template>
  <span ref="target" />
</template>

ref 는 특별한 속성으로, 특정 DOM 요소나 자식 컴포넌트 인스턴스에 대해 직접 참조를 얻을 수 있게 해준다. 따라서 ref를 사용해서 template의 span 요소와 const target 을 연결할 수 있다.

2-2. IntersectionObserver 인스턴스 생성

onMounted(() => {
  observer.value = new IntersectionObserver((entries) => {
    const firstEntry = entries[0];
    if (entries && firstEntry.isIntersecting) {
      emit("show");
    }
  }, options.value);

  observer.value.observe(target.value);
});
  • 컴포넌트 마운트 시 IntersectionObserver의 인스턴스를 생성해서 target의 관찰을 시작한다.
  • entries 배열의 첫 번째 요소가 root 요소와 교차됐을 때 show 이벤트를 부모 컴포넌트로 emit 해서 부모 컴포넌트에서 데이터를 추가로 불러오도록 한다.

2-3. Target 관찰 해지

onUnmounted(() => {
  observer.value.disconnect();
});

컴포넌트 마운트 해제 시 target의 관찰을 멈춘다.

ProductItem 컴포넌트에 Observer 연결하기

<script setup>
import Observer from "./Observer.vue";

const hasNextPage = computed(() => !!lastKey.value);
</script>

<template>
	<ProductItem
	:product="product"
	v-for="product in items"
	:key="product.id"
	/>
   <Observer v-if="hasNextPage" @show="fetchMore" />
</template>
  • 뷰 포트에 도달하면 페이지를 늘리는 방식으로 사용하기 위해 목록 맨 아래에 배치시킨다.
  • show 이벤트 발생시 fetchMore() 를 실!
    행한다.
  • 불러올 상품이 더 존재 할 때만 Observer 컴포넌트가 존재하게 한다. 이걸 빠뜨리면 fetchMore()가 무한으로 호출된다.

🪄 결과물

profile
୧ʕ•̀ᴥ•́ʔ୨

0개의 댓글