Skip to content

노가리하우스의 hooks : useFetchItems

iHoHyeon edited this page Mar 23, 2022 · 3 revisions

🖌️Custom Hook : useFetchItems

노가리 하우스는 SNS로 여러 형태의 데이터를 사용자가 페이지 이동없이 항상 제공받을 수 있어야합니다. 이러한 사용자 경험은 서비스 만족도를 높일 뿐더러 일관성있는 화면 구성을 제공할 수도 있습니다.

따라서 노가리하우스의 모든 화면에서 무한 스크롤로 데이터를 받아오는 로직을 커스텀 훅으로 분리해서 필요한 view에서 모두 사용할 수 있도록 구현했습니다.

소개

1. 상태 관리

// @atom/main-section-scroll.ts
import { atom } from 'recoil';

export const nowFetchingState = atom<boolean>({
  key: 'nowFetchingState', // 해당 atom의 고유 key
  default: false, // true : scroll 바가 데이터를 받아올 정도로 내려옴
});

export const nowItemsListState = atom<any[]>({ // any대신 item들의 타입이 들어가야함
  key: 'nowItemsCountState',
  default: [],

export const nowCountState = atom<number>({
  key: 'nowCountState',
  default: 0,
});

nowFetchingState 는 현재 사용자가 새로운 데이터를 불러오고 있는지 알려주는 상태입니다. 이 상태를 이용해서 로딩화면 제공, view 간의 이동에서 부작용 감소 등의 효과를 거둘 수 있습니다.

또한 nowFetchingStatefetch 요청을 담고있는 useEffect 의 의존성에 들어가게 되어서 추가로 데이터를 불러오고싶은 상황에서 해당 상태를 변경해주면 fetch 요청을 보내는 트리거 역할을 수행하게 됩니다.

nowItemsListState 는 현재 사용자의 화면에 보여지는 아이템 (User, Room, Event 등등)을 각 형태에 맞게 배열로 담고있는 전역변수입니다. 서로 렌더링 영향이 없는 view에서 이 상태를 구독함으로 불필요한 상태 선언을 줄일 수 있고 여러개의 view를 하나의 커스텀 훅으로 연결해주는 상태가 되었습니다.

nowCountState 는 데이터 요청을 보낼 때 조회 할 데이터의 skip (offset) 을 제어하기 위한 변수입니다.

2. Hook 로직

// @hooks/useFetchItems.tsx
import { nowCountState, nowFetchingState, nowItemsListState } from '@src/recoil/atoms/main-section-scroll';
import { useEffect, useRef } from 'react';
import { useRecoilState, useResetRecoilState } from 'recoil';

const useFetchItems = <T extends {}>(apiPath : string, nowItemType: string)
: [T[], string] => {
  const [nowItemsList, setNowItemsList] = useRecoilState(nowItemsListState);
  const [nowFetching, setNowFetching] = useRecoilState(nowFetchingState);
  const resetItemList = useResetRecoilState(nowItemsListState);
  const nowItemTypeRef = useRef<string>('');
  const [nowCount, setNowCount] = useRecoilState(nowCountState);

  useEffect(() => {
    resetItemList();
    setNowCount(0);
    setNowFetching(true);

    return () => {
      resetItemList();
      setNowCount(0);
    };
  }, []);

  useEffect(() => {
    if (nowFetching) {
      const fetchItems = async () => {
        try {
          const newItemsList = await fetch(`${process.env.REACT_APP_API_URL}/api${apiPath}?count=${nowCount}`, {
            credentials: 'include',
          })
            .then((res) => res.json())
            .then((json) => json.items);
          setNowItemsList([...nowItemsList, ...newItemsList]);
          nowItemTypeRef.current = nowItemType;
          setNowCount((count) => count + Math.min(newItemsList.length, 10));
          setNowFetching(false);
        } catch (e) {
          console.log(e);
        }
      };
      fetchItems();
    }
  }, [nowFetching]);

  return [nowItemsList, nowItemTypeRef.current];
};

export default useFetchItems;

파라미터로 요청을 보낼 api 경로와 받아올 데이터 type을 입력받습니다.

view 컴포넌트에서 사용하게 되는데, view 간의 이동을 하면 보여주고 있던 itemList를 초기화해주는 useEffect를 아래와 같이 작성하였습니다.

    useEffect(() => {
    resetItemList();
    setNowCount(0);
    setNowFetching(true);

    return () => {
      resetItemList();
      setNowCount(0);
    };
  }, []);

어디서든 전역변수인 nowFetching 을 원하는 상황에 true 로 변경하면 새로 아이템 fetch 요청을 보낼 수 있는 useEffect 를 아래와 같이 작성했습니다.

  useEffect(() => {
    if (nowFetching) {
      const fetchItems = async () => {
        try {
          const newItemsList = await fetch(`${process.env.REACT_APP_API_URL}/api${apiPath}?count=${nowCount}`, {
            credentials: 'include',
          })
            .then((res) => res.json())
            .then((json) => json.items);
          setNowItemsList([...nowItemsList, ...newItemsList]);
          nowItemTypeRef.current = nowItemType;
          setNowCount((count) => count + Math.min(newItemsList.length, 10));
          setNowFetching(false);
        } catch (e) {
          console.log(e);
        }
      };
      fetchItems();
    }
  }, [nowFetching]);

nowFetching 에 의존하여 useEffect 의 효과가 발생하게 되며 원하는 시점에 nowFetchingtrue 로 세팅하기만 하면 현재 type에 맞는 아이템을 불러와 보여주게 됩니다.

간단하죠??

우리가 구현한 데이터 fetch 시점은 intersection observer 가 등록된 아이템 요소가 어느정도 노출되었을 경우 미리 데이터를 불러와서 사용자가 끊임없이 화면에 집중할 수 있도록 합니다.

// @hooks/useItemFetchObserver.tsx
import { RefObject, useEffect, useRef } from 'react';
import { useRecoilState } from 'recoil';

import { nowFetchingState } from '@atoms/main-section-scroll';

const useItemFecthObserver = (loading: boolean): [RefObject<HTMLDivElement>] => {
  const [nowFetching, setNowFetching] = useRecoilState(nowFetchingState);
  const targetRef = useRef<HTMLDivElement>(null);

  const onIntersect = async (entries: IntersectionObserverEntry[]) => {
    if (entries[0].isIntersecting && !nowFetching) {
      setNowFetching(true);
    }
  };

  useEffect(() => {
    let observer: IntersectionObserver;
    if (targetRef.current) {
      observer = new IntersectionObserver(onIntersect, {
        threshold: 0.4,
      });
      observer.observe(targetRef.current);
    }
    return () => observer?.disconnect();
  }, [targetRef.current, loading]);

  return [targetRef];
};

export default useItemFecthObserver;

위는 intersection observer 등록을 위한 hook 입니다.

여기서 중요한 점은 우리가 원하는 intersection , 즉 사용자의 화면에 교차가 발생한 시점에

  const onIntersect = async (entries: IntersectionObserverEntry[]) => {
    if (entries[0].isIntersecting && !nowFetching) {
      setNowFetching(true);
    }
  };

아래와 같은 단순한 콜백을 등록해서 아이템을 fetch 하도록 만들 수 있습니다. 단순히 nowFetching 을 true 로 세팅해주기만 하면 알아서 새로운 데이터를 fetch 하도록 구현되었기 때문입니다.

SNS인 노가리 하우스에서 새로운 형태의 데이터를 다루게 되었을 때 확장이 매우 쉬워질 것을 기대할 수 있습니다!

3. 적용 사례

// @views/activity-view.tsx
...
function ActivityView() {
  const [nowItemList, nowItemType] = useFetchItems<ActivityCardProps>('/activity', 'activity');
  const [loading, setLoading] = useState(true);
  const nowFetching = useRecoilValue(nowFetchingState);
  const [targetRef] = useItemFecthObserver(loading);

  useEffect(() => {
    if (nowItemList && nowItemType === 'activity') {
      setLoading(false);
    }
  });

  if (loading) {
    return <LoadingSpinner />;
  }
  return (
    <>
      <ActivityCardList activityList={nowItemList} />

      <ObserverBlock ref={targetRef}>
        {nowFetching && <LoadingSpinner />}
      </ObserverBlock>
    </>
  );
}

위는 최근 알림 내역을 나타내주는 view 컴포넌트 입니다.

Untitled

가운데 나타내는 아이템 목록을 무한 스크롤로 제공하기 위한 코드를 hooks 로 모두 감출 수 있게 되어 view 컴포넌트의 역할에 조금 더 충실한 코드를 만들 수 있게 되었습니다!

노가리의 모든 view에서 위에 소개된 hook을 이용해주세요!

개선 방향이나 질문은 언제든지 환영입니다.