SCRUM-12299 | 커밋 수: 7개 | 기간: 2025.01 ~ 2025.02


문제상황

전단 뷰어의 UX 개선이 필요했습니다. 기존에는 PanZoom 라이브러리를 사용하고 있었지만 실제로 확대/축소 기능이 필요하지 않았고, 탭과 스크롤 간의 연동이 부재하여 사용자가 전단을 탐색할 때 불편함이 있었습니다.

Before (PanZoom 라이브러리 사용)

장점

  • 확대/축소 기능 제공

고려할 점

  • 불필요한 번들 크기 증가
  • 탭-스크롤 연동 부재

After (네이티브 IntersectionObserver)

장점

  • 번들 크기 감소
  • 탭-스크롤 연동
  • 브라우저 네이티브 API

고려할 점

  • 이미지 확대/축소 필요 시 별도 구현 필요

주요 개선사항

PanZoom 라이브러리를 삭제하여 불필요한 번들 크기를 줄였습니다. IntersectionObserver를 활용하여 전단 스크롤 시 해당 탭이 자동으로 활성화되는 기능을 구현했고, 반대로 탭 클릭 시 해당 전단으로 스크롤이 이동하는 양방향 연동을 구현했습니다.

  1. PanZoom 의존성 제거
  2. IntersectionObserver 훅 구현
  3. 탭-스크롤 양방향 연동

정량적 성과

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를 우선 활용하는 것의 중요성을 체감했습니다.