SWR

안녕 Redux-Saga! SWR로 무한 스크롤 쉽고 간결하게 구현하기

최정은
2022/04/14

저는 오랫동안 프로젝트에서 ReduxRedux-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 도입을 한번 고민해 보시는 건 어떨까요?