잡썰..

어떤 데이터를 가져오기 위해서 외부 API를 호출하는 경우, 일단 화면에 렌더링할 수 있는 것들을 먼저 렌더링하고 실제 데이터는 비동기로 가져오는 것을 권장한다는 글을 종종 보게 되었다.


그 이유는?

요청 즉시 1차 렌더링을 함으로써 연동하는 API가 응답이 늦어지거나 응답이 없을 경우에도 영향을 최소화 시킬 수 있어서 사용자 경험 측면에서 유리하기 때문이다.


본론

리듀서 함수는 순수함수이여야 하며, 사이드 이팩트가 없어야 한다.

그렇기 때문에 api 요청과 같은 비동기 요청 관련 로직을 리듀서에 작성할 수 없고, 적는다면 컴포넌트에서 useEffect에 로직을 작성할 수 있다.

(아래의 로직은 취업을 준비를 모기 업 과제로 얼마 준비하지 않았을 때의 로직이다. 상태관리로 리코일을 이용)

// UserList.js
const UserList = () => {
  const catchUsers = useSetRecoilState(userState);

  useEffect(() => {
    async function getUsersData() {
      try {
        const response = await fetch('./data/data.json');
        const data = await response.json();
      } catch (error) {
        console.error(error);
      }
    }

    getUsersData();
  }, [catchUsers]);

  return <>...</>;
};

export default UserList;

위와 같이 작성할 경우 시간이 얼마 되지 않았음에도 불구하고 어디서 데이터가 호출하는지 까먹었었다. 그렇다. 외부 API 정보를 컴포넌트에서 호출하게 되면 어디서 호출하는 지 알아봐야하고 다른사람이 이어서 작업하게 되는 경우 흐름을 읽기도 불편할 것이다.

(컴포넌트에 외부 API를 통해 데이터를 호출하는 코드가 작성된다면 유지보수 및 관리가 힘들어질 것이다.)

위와 같은 이유로 Thunk를 이용하게 되었는데 Thunk를 이용하면 비동기 관련 로직을 중앙 집중화하여 재사용이 가능하게 만들고, 비동기식 API 요청을 성공, 오류, 로딩에 대해 처리할 수 있고 유지보수 관리를 조금 더 효율성 있게 바꿀 수 있다.

또 다른 말로는 데이터를 비동기적으로 받았을때 객체가 찍혀서 엑션 함수처리를 해야되는데, Thunk나 saga, query 같은 것을 쓰면 데이터 받음과 동시에 액션함수 처리를 할 수 있어서 사용한다.

사용방법은 thunk 코드를 작성해주고 이용하는 전역상태 관리 라이브러리에 사용 로직을 작성해주면 된다. 이렇게 되면 아까 말한 데로 외부에서 API를 이용하여 데이터를 사용하는 경우 전역에서 깔끔하게 이용할 수 있는 것 같다.

// productThunk.ts
import { createAsyncThunk } from '@reduxjs/toolkit';

import Instance from 'api/Instance';
import { Products } from 'types/product.types';

export interface FetchProduct {
  productList: Products[];
}

const fetchProduct = createAsyncThunk<FetchProduct>('products/fetchProduct', async () => {
  const { data } = await Instance.get<Products[]>('templates/ePNAVU1sgGtQ/data');

  const result: FetchProduct = {
    productList: [],
  };

  data.forEach(({ club, price, leaders, partners, createdAt }) => {
    result.productList.push({ club, price, leaders, partners, createdAt });
  });

  return result;
});

export default fetchProduct;

나는 리덕스 툴킷 안에 있는 내장형 thunk를 사용하였기 떄문에 툴킷을 사용하였다.

// productSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import fetchProduct from './productThunk';
import { Products } from 'types/product.types';

export interface InitialState {
  allIds: string[];
  byId: {
    [key: string]: Products;
  };
  isLoading: boolean;
  filterList: string[];
  searchKeyword: string;
  listLength: number;
}

const initialState: InitialState = {
  allIds: [],
  byId: {},
  isLoading: false,
  filterList: [],
  searchKeyword: '',
  listLength: 0,
};

export const productSlice = createSlice({
  name: 'product',
  initialState,
  reducers: {
    // ...
  },
  extraReducers: (builder) => {
    builder.addCase(fetchProduct.pending, (state) => {
      state.isLoading = true;
    });
    builder.addCase(fetchProduct.fulfilled, (state, action) => {
      const { productList } = action.payload;

      productList.forEach((item) => {
        state.byId[item.club.id] = {
          club: item.club,
          price: item.price,
          leaders: item.leaders,
          partners: item.partners,
          createdAt: item.createdAt,
        };

        state.allIds.push(item.club.id);
      });

      state.isLoading = false;
    });
  },
});

export const {
  addFilter,
  removeFilter,
  setSearchKeyword,
  resetFilter,
  addListLength,
} = productSlice.actions;

export default productSlice.reducer;

결론

외부 API를 이용하는 경우, 경우에 따라 다를 수도 있겠지만,

비동기 관련 로직을 중앙 집중화하여 재사용이 가능하게 만들고, 유지보수 관리를 조금 더 효율성 있게 바꿀 수 있도록 thunk, saga, query 등과 같은 라이브 러리를 사용하여 처리하는 것이 좋을 것 같다.


용어사전

사이드 이팩트: React 컴포넌트가 화면에 렌더링된 후에 비동기로 처리되어야 하는 부수적인 효과들


참고링크

redux toolkit thunk 이용방법

Why use Redux Thunk?

[React] Side Effect란?