✨ 대부분의 웹 애플리케이션에서 핵심 아이디어는 DB에 저장된 데이터를 가져와 사용자에게 가장 좋은 방식으로 보여주는 것이다.
화면에 표시된 데이터를 사용자가 탐색할 수 있는 방법 중 가장 흔한 2가지로는 페이지네이션과 무한 스크롤이 있다. 상황에 따라 페이지네이션을 사용하는게 더 적합할 수도 있고 무한 스크롤이 더 적합할 수도 있기 때문에 충분히 상황을 고려해서 선택하는게 좋다.
나의 경우에는 사용자가 상품 목록을 탐색하는 상황에서 별도의 추가 동작(페이지 버튼 클릭 등) 없이 스크롤 만으로 모든 상품을 볼 수 있도록 하고 싶었기 때문에 무한 스크롤 기능을 선택했다.
Javascript에서 무한 스크롤을 구현하는 잘 알려진 방법으로는 addEventListener의 scroll 이벤트를 사용하는 방법이 있다. 하지만 이 방법은 단시간에 수 백번, 많게는 수 천번까지 호출이 된다는 성능 문제가 있다.
이를 보완하기 위해 등장한 Intersection Observer API는 루트 요소와 타겟 요소의 교차점을 관찰하여 타겟 요소가 루트 요소와 교차하는지 아닌지를 구별하는 기능을 제공한다.

let observer = new IntersectionObserver(callback, options); // Intersection Observer 인스턴스 생성하기
observer.observe(element); // 타겟 요소 관찰 시작
Target의 관찰이 시작되는 상황에 대한 옵션을 설정할 수 있다.
root : target의 가시성을 확인할 때 사용되는 루트 요소이다. target의 조상 요소여야 한다.rootMargin : root 요소의 범위를 확장/축소하는데 사용된다. ex) "15px 20px 40px 50px".threshold : target 요소의 가시성 퍼센티지가 얼마일 때 콜백 함수를 실행할 것인지 설정하는데 사용된다. 예를 들어 threshold를 0.5로 설정하면 target이 절반만큼 보여졌을 때 콜백이 호출되고 0으로 설정하면 target이 1px이라도 보이면 콜백이 호출된다.Target의 관찰이 시작되거나 가시성에 변화가 생기면 실행된다.
콜백함수는 파라미터로 entries와 observer를 받는다.
let observer = new IntersectionObserver((entries, observer) => {}, options)
entries : IntersectionObserverEntry 인스턴스를 담은 배열이다. root 요소와 target 요소의 교차 상황을 묘사한다.
콘솔창에서 찍어보면 아래와 같이 다양한 속성을 갖는 것을 볼 수 있다..
여기서 내가 사용할 속성은 target 요소와 root 요소가 교차 상태인지 아닌지를 알려주는 isIntersecting 속성이다.
observer : 콜백 함수가 호출되는 IntersectionObserver 를 가리킨다.
Intersection Observer에서 자주 사용되는 메서드
observe() : target의 관찰을 시작할 때 사용된다.disconnect() : IntersectionObserver 인스턴스가 관찰하는 모든 요소의 관찰을 중지할 때 사용된다.// 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() 를 실행하여 상품을 추가로 불러옴.
<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>
<script setup>
const target = ref(null);
</script>
<template>
<span ref="target" />
</template>
ref 는 특별한 속성으로, 특정 DOM 요소나 자식 컴포넌트 인스턴스에 대해 직접 참조를 얻을 수 있게 해준다. 따라서 ref를 사용해서 template의 span 요소와 const target 을 연결할 수 있다.
onMounted(() => {
observer.value = new IntersectionObserver((entries) => {
const firstEntry = entries[0];
if (entries && firstEntry.isIntersecting) {
emit("show");
}
}, options.value);
observer.value.observe(target.value);
});
show 이벤트를 부모 컴포넌트로 emit 해서 부모 컴포넌트에서 데이터를 추가로 불러오도록 한다.onUnmounted(() => {
observer.value.disconnect();
});
컴포넌트 마운트 해제 시 target의 관찰을 멈춘다.
<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()가 무한으로 호출된다.