안녕 Redux-Saga! SWR로 무한 스크롤 쉽고 간결하게 구현하기
목차
저는 오랫동안 프로젝트에서 Redux와 Redux-Saga 조합을 사용해 왔는데요. 사실, 이 조합을 쓰다 보면 누구나 한 번쯤 느끼는 '그 고통'이 있습니다. 바로 어마어마한 양의 보일러플레이트 코드죠.
오늘은 제가 Redux-Saga의 반복 작업에 지쳐 SWR로 전환하게 된 계기와, 특히 페이징 처리를 위해 useSWRInfinite를 어떻게 활용했는지 이야기해 보려 해요.
😫 Redux-Saga, 왜 힘들었을까요?
예를 들어, 서버에서 '포스트 목록'을 받아오는 간단한 작업을 한다고 상상해 볼까요? Redux-Saga 환경에서는 이 작업 하나를 위해 3단 콤보를 준비해야 합니다.
1. Action 정의 가장 먼저 액션 타입을 정의하고 액션 생성 함수를 만듭니다.
// Actions.ts
export const getPosts = (page = 1): AppAction => ({
type: GET_POSTS.REQUEST,
payload: { page },
});2. Reducer 작성 스토어 상태 관리를 위해 리듀서를 연결해줍니다.
// Reducers.ts
export const postsSelector = createSelector<Post>('posts');
const postReducer = reduceReducers(
initialWrapData,
hydratedFetchReducer(postsSelector)(GET_POSTS)(),
clearListReducer(CLEAR_POSTS)
);3. Saga 비동기 로직 작성 마지막으로 실제 API를 호출하고 결과를 처리하는 사가(Saga) 함수까지 작성해야 하죠.
// Sagas.ts
function* getPosts(action: AppAction) {
const { result, error }: Api = yield api(GET_POSTS, 'get', '/api/posts', action.payload);
if (result) {
// 성공 로직...
}
if (error) {
// 에러 핸들링...
}
}생각만 해도 손가락이 아프지 않나요? 😅
단순히 목록 하나 불러오는데 이렇게 많은 파일과 코드를 오가야 합니다. 만약 '더 보기' 버튼이나 Intersection Observer를 활용한 무한 스크롤을 구현해야 한다면? 페이지별로 데이터를 합치는 로직까지 추가되어 코드는 더 복잡해질 거예요.
✨ SWR로의 전환, 그리고 useSWRInfinite
이런 반복적인 작업에 지쳐갈 때쯤, SWR을 공부하게 되었고 실무에 도입해 보았습니다. 그중에서도 페이징 처리에 특화된 **useSWRInfinite**라는 훅은 정말 신세계였어요!
Redux-Saga로 길게 작성했던 로직을 커스텀 훅 하나로 얼마나 깔끔하게 줄일 수 있는지 보여드릴게요.
커스텀 훅: useInfiniteFetch
// hooks/useInfiniteFetch.ts
import useSWRInfinite, { SWRInfiniteConfiguration } from 'swr/infinite';
const DEFAULT_PAGE_SIZE = 20;
export default function useInfiniteFetch<T>(
queryKey: string,
options?: SWRInfiniteConfiguration<Pagination<T>>
) {
const { data: list, error, size, setSize } = useSWRInfinite<Pagination<T>>(
(index: number, prev: Pagination<T>) => {
// 1. 마지막 페이지에 도달했다면 null을 반환하여 요청 중단
if (prev && (prev.page === prev.totalPages)) return null;
// 2. API 엔드포인트(Key) 반환
return `${queryKey}&page=${index + 1}&size=${DEFAULT_PAGE_SIZE}`;
},
fetcher,
options
);
const fetchNextPage = () => setSize(size + 1);
const isLoading = !list && !error;
// 마지막 페이지 여부 계산
const isReachingEnd = list && (list[list.length - 1].page === list[list.length - 1]?.totalPages);
return {
list,
error,
isLoading,
isReachingEnd,
fetchNextPage,
};
}이 코드가 핵심입니다! useSWRInfinite는 크게 3가지 파라미터를 받는데요, 여기서 가장 중요한 건 첫 번째 인자인 getKey 함수예요.
- getKey 함수: 현재 페이지 인덱스와 이전 페이지 데이터를 받아 다음 요청할 API 주소(Key)를 반환합니다.
- 자동 중단: 위 코드 주석 1번처럼
null을 반환하면 SWR이 알아서 API 호출을 멈춥니다. 정말 똑똑하죠?
컴포넌트에서 사용하기
이제 컴포넌트에서는 복잡한 로직 신경 쓸 필요 없이, 아래처럼 딱 한 줄로 불러다 쓰면 됩니다.
// Component.tsx
const { list, fetchNextPage } = useInfiniteFetch('/api/posts');
return (
<>
{/* 리스트 렌더링... */}
<button type="button" onClick={fetchNextPage}>
더 보기
</button>
</>
);Action, Reducer, Saga 파일 세 개를 왔다 갔다 하던 때와 비교하면 정말 놀라운 변화 아닌가요? 😍
🎁 마치며
새로운 기술을 도입한다는 건 언제나 공부가 필요하지만, 그만큼 개발 생산성을 높여주는 즐거움도 큰 것 같습니다.
Redux-Saga를 쓸 때 느꼈던 그 번거로움을 SWR 훅으로 바꾸니, 이제는 어떤 화면에서든 queryKey만 넘겨주면 아주 쉽게 무한 스크롤을 구현할 수 있게 되었습니다.
물론, SWR을 도입하면서 겪은 몇 가지 시행착오들도 있었는데요. (세상에 공짜는 없으니까요! 😂) 이 부분은 다음 포스트에서 자세히 정리해서 공유해 드리도록 할게요.
혹시 여러분도 반복되는 비동기 코드에 지치셨다면, SWR 도입을 한번 고민해 보시는 건 어떨까요?