SCRUM-12299 | 커밋 수: 7개 | 기간: 2025.01 ~ 2025.02
문제상황
전단 뷰어의 UX 개선이 필요했습니다. 기존에는 PanZoom 라이브러리를 사용하고 있었지만 실제로 확대/축소 기능이 필요하지 않았고, 탭과 스크롤 간의 연동이 부재하여 사용자가 전단을 탐색할 때 불편함이 있었습니다.
Before (PanZoom 라이브러리 사용)
장점
- 확대/축소 기능 제공
고려할 점
- 불필요한 번들 크기 증가
- 탭-스크롤 연동 부재
After (네이티브 IntersectionObserver)
장점
- 번들 크기 감소
- 탭-스크롤 연동
- 브라우저 네이티브 API
고려할 점
- 이미지 확대/축소 필요 시 별도 구현 필요
주요 개선사항
PanZoom 라이브러리를 삭제하여 불필요한 번들 크기를 줄였습니다. IntersectionObserver를 활용하여 전단 스크롤 시 해당 탭이 자동으로 활성화되는 기능을 구현했고, 반대로 탭 클릭 시 해당 전단으로 스크롤이 이동하는 양방향 연동을 구현했습니다.
- PanZoom 의존성 제거
- IntersectionObserver 훅 구현
- 탭-스크롤 양방향 연동
정량적 성과
PanZoom 라이브러리를 완전히 제거하여 번들 크기를 줄였습니다. IntersectionObserver 기반의 탭-스크롤 연동 기능을 새로 구현하여 전단 탐색 경험을 개선했습니다.
번들 최적화
PanZoom 라이브러리 완전 제거
탭-스크롤 연동
IntersectionObserver 기반 양방향 동기화
네이티브 API
외부 의존성 없는 브라우저 표준 활용
비즈니스 임팩트
전단 탐색 UX가 향상되어 사용자가 원하는 전단을 빠르게 찾을 수 있게 되었습니다. 외부 라이브러리 제거로 번들 크기가 감소하여 페이지 로딩 성능도 개선되었습니다.
기술적 구현 상세
AS-IS
기존에는 PanZoom 라이브러리를 사용하여 전단 이미지를 확대/축소했습니다.
<!-- 기존 구조 (더미 예시) -->
<script setup lang="ts">
import PanZoom from 'panzoom';
onMounted(() => {
const element = document.getElementById('flyer-image');
PanZoom(element);
});
</script>
문제점:
불필요한 라이브러리 의존성이 번들 크기를 증가시키고 있었습니다. 탭과 스크롤 간 연동이 없어서 사용자가 여러 전단을 탐색할 때 현재 위치를 파악하기 어려웠습니다.
TO-BE
PanZoom을 제거하고, 탭-스크롤 연동 기능을 구현했습니다.
핵심 구현
1. 탭-스크롤 연동
<!-- views/product/FlyerViewer.vue -->
<script setup lang="ts">
import { useIntersectionObserver } from '@/hooks/common';
const props = defineProps<{
flyers: Flyer[];
}>();
const activeTab = ref(0);
const flyerRefs = ref<HTMLElement[]>([]);
// 스크롤 시 해당 탭 활성화
const { observe } = useIntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const index = flyerRefs.value.indexOf(entry.target as HTMLElement);
if (index >= 0) {
activeTab.value = index;
}
}
});
}, {
threshold: 0.5,
});
// 탭 클릭 시 해당 전단으로 스크롤
const scrollToFlyer = (index: number) => {
const target = flyerRefs.value[index];
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
};
onMounted(() => {
flyerRefs.value.forEach((ref) => {
if (ref) {
observe(ref);
}
});
});
</script>
<template>
<div class="flyer-viewer">
<!-- 탭 메뉴 -->
<nav class="flyer-tabs">
<button
v-for="(flyer, index) in flyers"
:key="flyer.id"
:class="{ active: activeTab === index }"
@click="scrollToFlyer(index)"
>
{{ flyer.name }}
</button>
</nav>
<!-- 전단 목록 -->
<div class="flyer-list">
<section
v-for="(flyer, index) in flyers"
:key="flyer.id"
:ref="(el) => flyerRefs[index] = el as HTMLElement"
class="flyer-section"
>
<img :src="flyer.imageUrl" :alt="flyer.name" />
</section>
</div>
</div>
</template>
2. IntersectionObserver 훅
// hooks/common.ts
export const useIntersectionObserver = (
callback: IntersectionObserverCallback,
options?: IntersectionObserverInit
) => {
const observer = ref<IntersectionObserver | null>(null);
onMounted(() => {
observer.value = new IntersectionObserver(callback, {
threshold: 0.5,
...options,
});
});
onUnmounted(() => {
observer.value?.disconnect();
});
const observe = (element: Element) => {
observer.value?.observe(element);
};
const unobserve = (element: Element) => {
observer.value?.unobserve(element);
};
return { observe, unobserve };
};
3. PanZoom 라이브러리 제거
// package.json 변경사항
{
"dependencies": {
// "panzoom": "^9.4.3" // 제거됨
}
}
회고
잘한 점
불필요한 라이브러리를 제거하여 번들 크기를 줄였습니다. IntersectionObserver를 활용하여 효율적인 스크롤 감지를 구현했고, 외부 의존성 없이 브라우저 표준 API만으로 기능을 완성했습니다.
개선할 점
향후 이미지 확대/축소 기능이 필요해질 경우 별도의 경량 구현 또는 대안을 검토해야 합니다.
배운 점
IntersectionObserver를 활용한 스크롤 연동 패턴을 익혔습니다. 라이브러리 의존성을 최소화하고 네이티브 API를 우선 활용하는 것의 중요성을 체감했습니다.