-
Notifications
You must be signed in to change notification settings - Fork 8
노가리하우스의 hooks : useFetchItems
노가리 하우스는 SNS로 여러 형태의 데이터를 사용자가 페이지 이동없이 항상 제공받을 수 있어야합니다. 이러한 사용자 경험은 서비스 만족도를 높일 뿐더러 일관성있는 화면 구성을 제공할 수도 있습니다.
따라서 노가리하우스의 모든 화면에서 무한 스크롤로 데이터를 받아오는 로직을 커스텀 훅으로 분리해서 필요한 view에서 모두 사용할 수 있도록 구현했습니다.
// @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 간의 이동에서 부작용 감소 등의 효과를 거둘 수 있습니다.
또한 nowFetchingState
는 fetch
요청을 담고있는 useEffect
의 의존성에 들어가게 되어서 추가로 데이터를 불러오고싶은 상황에서 해당 상태를 변경해주면 fetch
요청을 보내는 트리거 역할을 수행하게 됩니다.
nowItemsListState
는 현재 사용자의 화면에 보여지는 아이템 (User, Room, Event 등등)을 각 형태에 맞게 배열로 담고있는 전역변수입니다. 서로 렌더링 영향이 없는 view에서 이 상태를 구독함으로 불필요한 상태 선언을 줄일 수 있고 여러개의 view를 하나의 커스텀 훅으로 연결해주는 상태가 되었습니다.
nowCountState
는 데이터 요청을 보낼 때 조회 할 데이터의 skip (offset) 을 제어하기 위한 변수입니다.
// @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
의 효과가 발생하게 되며 원하는 시점에 nowFetching
을 true
로 세팅하기만 하면 현재 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인 노가리 하우스에서 새로운 형태의 데이터를 다루게 되었을 때 확장이 매우 쉬워질 것을 기대할 수 있습니다!
// @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 컴포넌트 입니다.
가운데 나타내는 아이템 목록을 무한 스크롤로 제공하기 위한 코드를 hooks 로 모두 감출 수 있게 되어 view 컴포넌트의 역할에 조금 더 충실한 코드를 만들 수 있게 되었습니다!
노가리의 모든 view에서 위에 소개된 hook을 이용해주세요!
개선 방향이나 질문은 언제든지 환영입니다.