[Refactoring] useInfiniteScroll() 관심사 분리하기

junjeong·2025년 2월 20일

Wikied

목록 보기
2/5
post-thumbnail

🐾 들어가며...

Frontend_Fundamentals에 따르면, 쉽게 변경이 가능한 클린 코드를 작성하기 위해서는 첫째로 가독성이 좋아야하고, 넷째로 함수의 결합도가 낮아야 한다고 한다. 여기서 함수의 결합도란 코드를 수정했을 때의 영향 범위이다.

가독성과 함수의 결합도만 언급한 이유는 이번 시간에 다룰 내가 링크 페이지를 구현하면서 만든 useInfiniteScroll() hook이 두가지 규칙을 위반하고 있기 때문이다.

어떻게 코드를 작성했는지 자세히 살펴보자.

🚨 발견한 문제점

먼저 "/wikilist" 페이지의 컴포넌트이다.

import { getProfiles } from "@/api/profile";
import { Profile } from "@/types/types";
import WikiListTitle from "@/components/WikiListTitle";
import WikiCardList from "@/components/WikiCardList";
import LoadingSpinner from "@/components/LoadingSpinner";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";

interface WikiListPageProps {
  initialList: Profile[];
}

export const getServerSideProps = async () => {
  const res = await getProfiles({ pageSize: 12 });
  return {
    props: {
      initialList: res,
    },
  };
};

const WikiListPage = ({ initialList }: WikiListPageProps) => {
  const { loadingRef, hasMore, list } = useInfiniteScroll(1, initialList);

  return (
    <div className="mx-auto px-[20px] Mobile:px-[100px] Tablet:px-[60px] w-full max-w-[840px] h-full">
      <WikiListTitle />
      <WikiCardList list={list} />
      {hasMore && (
        <div ref={loadingRef}>
          <LoadingSpinner />
        </div>
      )}
    </div>
  );
};

export default WikiListPage;

이전 포스팅에서도 언급했지만, FCP 속도를 개선하고자 SSR을 활용해 초기의 데이터는 12개만 불러왔고 이후 데이터 패칭은 useInfiniteScroll() hook에서 CSR해주고 있다.

문제의 useInfiniteScroll() 코드는 다음과 같다.

import { useCallback, useEffect, useRef, useState } from "react";
import { getProfiles } from "@/api/profile";
import { Profile } from "@/types/types";

const useInfiniteScroll = (initialPage: number, initialList: Profile[]) => {
  const [list, setList] = useState<Profile[]>(initialList);
  const [page, setPage] = useState(initialPage);
  const [hasMore, setHasMore] = useState(true);
  const loadingRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const currentRef = loadingRef.current;

    const loadMoreProfiles = async () => {
      const newProfiles = await getProfiles({ page: page + 1, pageSize: 12 });

      if (newProfiles.length === 0) {
        setHasMore(false);
      } else {
        setList((prev) => [...prev, ...newProfiles]);
        setPage((prev) => prev + 1);
      }
    };

    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && hasMore) {
        loadMoreProfiles();
      }
    });

    if (currentRef) {
      observer.observe(currentRef);
    }

    return () => {
      if (currentRef) {
        observer.unobserve(currentRef);
      }
    };
  }, []);

  return { loadingRef, hasMore, list };
};

export default useInfiniteScroll;

"/wikilist" 페이지에서 필요한 상태 값들(예: list, hasMore, page)을 관리하고 있으며, loadingRef를 observer에 등록하여 list를 업데이트하는 로직까지 담당하고 있다.

내가 봤을 때 useInfiniteScroll의 문제점은 다음과 같다.

1. 함수의 이름만 보았을 때에는 무한 스크롤과 관련된 기능만 할 것 같은데 실제로는 여러 상태값들까지 관리하고 있음. 함수의 예측가능성이 안 좋음.

2. 하나의 hook이 관리하고 있는 상태값들이 너무 많음. 페이지에서 자식 컴포넌트가 새롭게 생길 경우, 동일한 hook에 의존할텐데 자칫 불필요한 리렌더링이 발생할 수 있음.

💡 문제 해결하기

해결하는 방법은 간단하다. 하나의 함수에 하나의 책임만 맡게하고 이름도 기능에 맞게 지어주자.

바뀐 코드는 다음과 같다.

//index.tsx
import { getProfiles } from "@/api/profile";
import { Profile } from "@/types/types";
import WikiListTitle from "@/components/WikiListTitle";
import WikiCardList from "@/components/WikiCardList";
import LoadingSpinner from "@/components/LoadingSpinner";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
import useListState from "@/hooks/useListState";

interface WikiListPageProps {
  initialList: Profile[];
}

export const getServerSideProps = async () => {
  const res = await getProfiles({ pageSize: 12 });
  return {
    props: {
      initialList: res,
    },
  };
};

const WikiListPage = ({ initialList }: WikiListPageProps) => {
  const { list, hasMore, loadMoreProfiles } = useListState(initialList);
  const loadingRef = useInfiniteScroll(loadMoreProfiles, hasMore);

  return (
    <div className="mx-auto px-[20px] Mobile:px-[100px] Tablet:px-[60px] w-full max-w-[840px] h-full">
      <WikiListTitle />
      <WikiCardList list={list} />
      {hasMore && (
        <div ref={loadingRef}>
          <LoadingSpinner />
        </div>
      )}
    </div>
  );
};

export default WikiListPage;
//useListState.ts
import { useState } from "react";
import { getProfiles } from "@/api/profile";
import { Profile } from "@/types/types";

const useListState = (initialList: Profile[]) => {
  const [list, setList] = useState<Profile[]>(initialList);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  const loadMoreProfiles = async () => {
    const res = await getProfiles({ page: page + 1, pageSize: 12 });

    if (res.length === 0) {
      setHasMore(false);
    } else {
      setList((prev) => [...prev, ...res]);
      setPage((prev) => prev + 1);
    }
  };

  return { list, loadMoreProfiles, hasMore };
};

export default useListState;
//useInfiniteScroll.ts
import { useEffect, useRef } from "react";

const useInfiniteScroll = (loadMore: () => Promise<void>, hasMore: boolean) => {
  const loadingRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const currentRef = loadingRef.current;

    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && hasMore) {
        loadMore();
      }
    });

    if (currentRef) {
      observer.observe(currentRef);
    }

    return () => {
      if (currentRef) {
        observer.unobserve(currentRef);
      }
    };
  }, [loadMore, hasMore]);

  return loadingRef;
};

export default useInfiniteScroll;

list와 관련된 상태관리는 useWikilistPageState hook이 맡고, 업데이트 하는 loadMoreProfiles 함수를 useInfiniteScroll에게 파라미터로 전달해주기만 함으로써 loadingRef가 뷰포트에 걸렸을 때 list를 업데이트하는 "무한 스크롤 기능"만을 하는 useInfiniteScroll hook으로 재탄생한 모습이다.

📚 요약

이번 시간에 얻은 교훈은 함수를 추상화 할 때에는 꼭 신중을 기울여야 한다는 것이다. 이유는 함수를 추상화함으로써 가독성을 챙길 수는 있지만, 오히려 함수의 결합도가 상승해 유지보수성을 해치고 있을 수 있기 떄문이다.

현재 페이지에서 어떤 기능이 추가되었을 때 해당 hook에 의존해도 문제가 없을지, 물론 모든 케이스를 미리 예측할 수 없고 더구나 이제 막 개발 공부를 시작한 나로써는 불가능에 가까운 영역이겠지만 한번쯤 고민해보고 코드를 작성하는 버릇은 좋을 것이라고 생각한다.

profile
Whether you're doing well or not, just keep going👨🏻‍💻🔥

0개의 댓글