사용자 경험을 높이는 상품 상세 페이지 스크롤 탭 구현 개선하기

궁금하면 500원·2026년 1월 26일

미생의 개발 이야기

목록 보기
63/63

이미지 출처: 제미나이 나노바나나

상품 상세 페이지 스크롤 및 탭 연동 로직 개선기

상품 상세 페이지에서 사용자가 스크롤을 내릴 때 현재 위치한 섹션에 맞춰 탭이 자동으로 변경되고, 반대로 탭을 클릭하면 해당 섹션으로 부드럽게 이동하는 기능을 구현했습니다.
구현 과정에서 직면한 문제들과 이를 어떻게 더 견고한 코드로 발전시켰는지 포스팅하게되었습니다.


주요 개선 사항 및 느낀 점

1. React 규칙 useEffect 의존성 관리

문제점: useEffect 내에서 사용하는 의존성 배열의 값이 렌더링 시점에 따라 변할 수 있는 구조였습니다.
이는 React에서 예상치 못한 버그를 유발하거나 불필요한 경고를 발생시키는것을 발견하였습니다

해결 방안:

// 1. useRef를 활용해 리렌더링 없이 최신 탭 상태 참조
const activeTabRef = useRef(activeTab);
useEffect(() => {
  activeTabRef.current = activeTab;
}, [activeTab]);

// 2. 실제 로직에서는 ref를 사용해 의존성 배열을 단순하게 유지
useEffect(() => {
  // 스크롤 로직 실행 시 activeTabRef.current 사용
}, [product]); // product 변경 시에만 이벤트 재설정

의존성 배열을 관리하지않아
값이 자주 바뀌지만 이벤트를 매번 재등록하고 싶지 않을 때는
useRef가 훌륭한 탈출구가 된다는 것을 배우게 되었습니다.


2. 스크롤-탭 클릭 충돌 방지

문제점: 탭을 클릭해 스크롤이 이동할 때, 스크롤 이벤트가 동시에 발생하여 탭이 다시 계산되는 간섭 현상이 있었습니다.

해결 방안:

// 스크롤 중인지 확인하는 플래그
const isScrollingRef = useRef(false);

const handleScrollEnd = useCallback(() => {
  isScrollingRef.current = false; // 스크롤 완료 시 플래그 해제
}, []);

// 탭 클릭 시에는 플래그를 true로 설정하고, 
// 일정 시간 후에만 스크롤 로직이 동작하게 제어

사용자 액션과 시스템 액션이 충돌할 때, 이를 제어할 수 있는 상태 플래그 설계가 시스템의 안정성을 얼마나 높여야하는지 알게되었습니다.


3. 가독성을 위한 로직 분리 및 상수화

문제점: 약 60줄에 달하는 복잡한 스크롤 계산 로직이 하나의 함수에 모여 있어 분석이 어려웠고 숫자들이 의미하는 바도 불분명했습니다.

해결 방안:

// 수치들을 상수로 정의해 의미를 명확히 함
const SCROLL_CONFIG = {
  OFFSET: 150,      // 상단 여백
  THRESHOLD: 0.3,   // 노출 비율 임계점
} as const;

// 복잡한 조건문을 작은 기능 단위의 함수로 분리
const activeSection = 
  findByTopThreshold(sections) || 
  findByVisibility(sections) || 
  defaultSection;

코드는 작성하는 시간보다 읽히는 시간이 훨씬 복잡한 조건을 함수 이름으로 분리하는 것만으로도 전체 흐름을 파악하기가 훨씬 편해졌습니다.


4. 철저한 자원 해제

X 문제점: 이벤트 리스너는 제거되고 있었지만, 초기 실행을 위한 setTimeout 등의 타이머가 정리되지 않을 가능성이 있었습니다.

해결 방안:

return () => {
  // 모든 이벤트와 타이머를 누락 없이 정리
  window.removeEventListener("scroll", throttledScroll);
  clearTimeout(timeoutId);
  clearTimeout(initialTimeoutId);
};

컴포넌트가 사라질 때우리가 빌려 쓴 자원을 모두 돌려주는 습관은 서비스의 안정성과 성능에 직결된다는 점을 다시한번 생각하게 되었습니다


5. rAF와 Throttle의 브라우저 최적화 처리

문제점: 단순 시간 기반의 Throttle은 브라우저의 화면 갱신 주기와 맞지 않아 미세한 끊김이 발생할 수 있습니다.

해결 방안:

// 브라우저의 다음 프레임에 맞춰 실행하는 최적화 기법
const rafThrottle = (callback: () => void) => {
  let rafId: number | null = null;
  return () => {
    if (rafId) return; // 이미 대기 중인 프레임이 있으면 무시
    rafId = requestAnimationFrame(() => {
      callback();
      rafId = null;
    });
  };
};

requestAnimationFrame을 활용하니 스크롤 감지 로직이 훨씬 부드러워졌지만 브라우저의 렌더링 원리를 이해하고 코드를 작성하는 것의 얼마나 중요한지 다시한번
반성하는 시간이 되었습니다.


결론 및 향후 계획

이번 작업을 통해 단순히 기능을 구현하는 것을 넘어 "어떻게 하면 더 안전하고 성능이 좋은 코드를 작성할 것인가"를 깊이 고민했습니다.

다음 단계로는 현재의 스크롤 계산 로직을 더 선언적인 방식인 Intersection Observer API로 교체하여 브라우저의 부하를 줄이고, 이 로직을 Custom Hook으로 완전히 분리하여 다른 페이지에서도 쉽게 재사용할 수 있도록 고도화할 계획입니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글