Skip to content

useIntersection

이토 edited this page Jun 1, 2025 · 2 revisions

문제 상황

스크롤이 특정 요소에 도달했을 때 이벤트를 트리거하는 기능은 무한 스크롤, lazy loading 등 다양한 상황에서 반복적으로 요구되었습니다. 하지만 실제로는 각 페이지나 컴포넌트에서 IntersectionObserver를 직접 구현하고 연결해야 했고, 이는 다음과 같은 문제를 야기했습니다:

  • 매번 동일한 observer 로직을 복붙해야 했고, 리팩토링 비용도 컸음
  • 로직의 미묘한 차이로 인해 일관되지 않은 관찰 방식과 메모리 누수 가능성이 존재함
  • 협업 시 초심자가 observer를 직접 다루는 진입장벽이 높음

어떻게 해결했을까?

이 문제를 해결하기 위해 다음과 같이 구조적인 개선을 진행했습니다:

  1. 공통된 IntersectionObserver 로직을 커스텀 훅으로 추출 - useIntersection이라는 훅을 만들어, callback과 deps만 넘기면 내부적으로 ref를 반환하고 observer를 자동 등록/해제하도록 구성했습니다.
  2. 의존성 배열로 동적 리렌더링 대응 - deps를 통해 외부 상태가 변경될 때도 observer가 갱신되도록 설계하여, 상태 기반 동작에도 유연하게 대응 가능하게 했습니다.
  3. 제네릭 타입으로 HTMLElement 범용 대응 - <T extends HTMLElement>로 타입을 추론 가능하게 하여, div, ul, li 등 어떤 요소든 타입 안전하게 ref 연결이 가능하게 했습니다.
// useIntersection.ts
interface UseIntersectionParams {
  callback: IntersectionObserverCallback;
  deps?: DependencyList;
}

const useIntersection = <T extends HTMLElement>({
  callback,
  deps,
}: UseIntersectionParams) => {
  const dependencies = deps ?? [];
  const targetRef = useRef<T>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(callback);
    const current = targetRef.current;
    if (current) {
      observer.observe(current);
    }

    return () => {
      if (current) {
        observer.disconnect();
      }
    };
  }, [callback, ...dependencies]);

  return targetRef;
};

export default useIntersection;
// 실제 사용 예시
function Component() {
  ...
  const targetRef = useIntersection<HTMLDivElement>({
    callback: ([entry]) => {
      if (entry.isIntersecting && !isLoading && hasNext) {
        refetch();
      }
    },
  });

  return (
    ...
      <div ref={targetRef} />
    ...
  );
}

결과

  • useIntersection 훅을 도입함으로써 모든 컴포넌트에서 observer 로직을 반복 없이 간결하게 적용 가능해졌습니다.
  • 협업 시 동료가 복잡한 IntersectionObserver를 몰라도 빠르게 기능 구현할 수 있게 되었고,
  • observer 연결/해제 관련 버그 가능성을 줄이며 코드 일관성과 유지보수성이 크게 향상되었습니다.

트러블 슈팅

컴포넌트

커스텀 훅

Clone this wiki locally