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

상품 상세 페이지에서 사용자가 스크롤을 내릴 때 현재 위치한 섹션에 맞춰 탭이 자동으로 변경되고, 반대로 탭을 클릭하면 해당 섹션으로 부드럽게 이동하는 기능을 구현했습니다.
구현 과정에서 직면한 문제들과 이를 어떻게 더 견고한 코드로 발전시켰는지 포스팅하게되었습니다.
문제점: useEffect 내에서 사용하는 의존성 배열의 값이 렌더링 시점에 따라 변할 수 있는 구조였습니다.
이는 React에서 예상치 못한 버그를 유발하거나 불필요한 경고를 발생시키는것을 발견하였습니다
해결 방안:
// 1. useRef를 활용해 리렌더링 없이 최신 탭 상태 참조
const activeTabRef = useRef(activeTab);
useEffect(() => {
activeTabRef.current = activeTab;
}, [activeTab]);
// 2. 실제 로직에서는 ref를 사용해 의존성 배열을 단순하게 유지
useEffect(() => {
// 스크롤 로직 실행 시 activeTabRef.current 사용
}, [product]); // product 변경 시에만 이벤트 재설정
의존성 배열을 관리하지않아
값이 자주 바뀌지만 이벤트를 매번 재등록하고 싶지 않을 때는
useRef가 훌륭한 탈출구가 된다는 것을 배우게 되었습니다.
문제점: 탭을 클릭해 스크롤이 이동할 때, 스크롤 이벤트가 동시에 발생하여 탭이 다시 계산되는 간섭 현상이 있었습니다.
해결 방안:
// 스크롤 중인지 확인하는 플래그
const isScrollingRef = useRef(false);
const handleScrollEnd = useCallback(() => {
isScrollingRef.current = false; // 스크롤 완료 시 플래그 해제
}, []);
// 탭 클릭 시에는 플래그를 true로 설정하고,
// 일정 시간 후에만 스크롤 로직이 동작하게 제어
사용자 액션과 시스템 액션이 충돌할 때, 이를 제어할 수 있는 상태 플래그 설계가 시스템의 안정성을 얼마나 높여야하는지 알게되었습니다.
문제점: 약 60줄에 달하는 복잡한 스크롤 계산 로직이 하나의 함수에 모여 있어 분석이 어려웠고 숫자들이 의미하는 바도 불분명했습니다.
해결 방안:
// 수치들을 상수로 정의해 의미를 명확히 함
const SCROLL_CONFIG = {
OFFSET: 150, // 상단 여백
THRESHOLD: 0.3, // 노출 비율 임계점
} as const;
// 복잡한 조건문을 작은 기능 단위의 함수로 분리
const activeSection =
findByTopThreshold(sections) ||
findByVisibility(sections) ||
defaultSection;
코드는 작성하는 시간보다 읽히는 시간이 훨씬 복잡한 조건을 함수 이름으로 분리하는 것만으로도 전체 흐름을 파악하기가 훨씬 편해졌습니다.
X 문제점: 이벤트 리스너는 제거되고 있었지만, 초기 실행을 위한 setTimeout 등의 타이머가 정리되지 않을 가능성이 있었습니다.
해결 방안:
return () => {
// 모든 이벤트와 타이머를 누락 없이 정리
window.removeEventListener("scroll", throttledScroll);
clearTimeout(timeoutId);
clearTimeout(initialTimeoutId);
};
컴포넌트가 사라질 때우리가 빌려 쓴 자원을 모두 돌려주는 습관은 서비스의 안정성과 성능에 직결된다는 점을 다시한번 생각하게 되었습니다
문제점: 단순 시간 기반의 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으로 완전히 분리하여 다른 페이지에서도 쉽게 재사용할 수 있도록 고도화할 계획입니다.