솔로 프로젝트 리팩토링 중, 리듀서 함수는 순수해야하기 때문에 useEffect에서 fetch를 통해 처음에 데이터를 받아오고 성공 했을 시, 리듀서함수에 액션에 데이터를 담아 dispatch 해주었습니다.
export default function useFetch(initialUrl: string){
const dispatch = useDispatch();
const [url, setUrl] = useState<string>(initialUrl);
const [data, setData] = useState<ApiDataInterFace[]>([]);
const fetchData = async () => {
fetch(url)//
.then(res => res.json())
.then(data => {
const newData = getLocalStorage(data.products, 'id');
dispatch(setProducts(newData)); // 리듀서함수로 액션 dispatch해준부분
setData(newData);
})
.catch(error => console.log(error))
}
useEffect(()=>{
fetchData();
}, [url])
return [data, setUrl];
}
하지만 리팩토링 과정 중에 리덕스에서 비동기 로직을 처리할 수는 없을까라는 생각과 함께 thunk를 적용해보기로 하였습니다.
React Query, RTK-Query도 있지만, 제가 redux Thunk 미들웨어를 선택한 이유는 .. 가장 많이 쓰고 있는 미들웨어였기 때문에 saga를 조금 잊었더라도, thunk는 알아두어야하는게 좋을 것같아 택하게 되었습니다. + RTK-Query공부하기에도 좋을 것 같아서
근 2년간에도 Thunk가 압도적인 1위 ..네염..
그래 오히려 잘됐다..!
● Thunk
리듀서 함수는 순수함수여야하는데 그럼 비동기 로직을 어디에 넣어야할까? 라는 의문점이 들었습니다.
thunk는 미들웨어인데요, Thunk는 action의 dispatch를 지연시키는데 사용될 수 있으며, 특정 조건이 충족되는 경우에만
dispatch할 수 있다. 내부 함수는 dispatch와 getState를 매개변수로 둔다.
즉, 바로 action이 dispatch되지 않는다는 것입니다. 일련의 과정을 거쳐서 조건이 충족되면 action이 dispatch되게 할 수도 있고 안되게 할수도 있습니다.
그럼 이런 일련의 과정들은 어디서 이뤄지는 걸까요?
RTK를 쓰지 않았을 때 정식적으로 Redux의 구조는
- 액션 생성자 함수 > 액션생성 > 리듀서로 dispatch 됨 > 리듀서 함수를 거쳐 store 변형
이런 구조였습니다. 따라서 리듀서 함수 자체에서는 비동기 로직을 포함할 수 없으니 이런 과정을 액션 생성자 함수에서 비동기 로직을 넣게 됩니다. 즉, 액션 생성자 함수에서 비동기 로직이 or 어떠한 조건을 거친 뒤 dispatch가 되는 것이죠. 즉 이것을 미들웨어라고 부르게 된 것입니다.
정말 아주 단순하게 그림을 그리면 위와 같고,
로직을 구체화한다면 위와 같겠습니다.
좀 그림이 크게 차이가 있어보이지만, 둘다 dispatch를 하면 바로 reducer에 곧장 전달되지 않고 미들웨어에 전달된다는 점입니다.
미들웨어에서 API를 불러온다는 등 비동기 로직이나 일련의 과정을 거친 후 액션이 reducer에 전달되고, reducer는 받은 액션을 기반으로 state를 변경하게 됩니다.
이전에 미들웨어를 공부했던게 큰 도움이 됐습니다.. 기억이 잘안나서 쓱 훑으니 기억이 많이 낫습니다..
[18장 리덕스 미들웨어를 통한 비동기 작업 관리 (1)]
● 실습 source src > index.js index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import { createStore} from 'redux'; import rootReducer from './modules'; import { Provider } from
ddaeunbb.tistory.com
미들웨어를 apply(적용)하는 방법과 미들웨어는 어떤 인수를 받는지 공식문서에 자세히 나와있습니다. (갓문서)
applyMiddleware | Redux
API > applyMiddleware: extending the Redux store
ko.redux.js.org
applyMiddleware말고도 아래 공식문서에서 createAsyncThunk를 발견할 수 있었는데요, 공식문서를 살펴보니 createAsyncThunk와 createSlice만 있으면 바로 비동기 로직을 포함한 리듀서를 만들 수 있다고 해서 살펴보았습니다.
createAsyncThunk | Redux Toolkit
redux-toolkit.js.org
공식문서에서는 RTK-Query를 쓰면 thunk나 데이터 페칭에 필요한 리듀서들을 사용할 필요가 없다고 자세히 바로 나와있네요.
RTK-Query의 사용을 권장하는 듯 싶었?습니다.
공식문서에서 createAsyncThunk의 사용법을 살펴보니.. 경악을 금치 못햇읍니다..(너무 편해서) typeScript를 활용한 코드인데
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
interface UsersState {
entities: []
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
const initialState = {
entities: [],
loading: 'idle',
} as UsersState
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
- 대충 코드를 살펴보면 userAPI는 객체같고, 그안에 API를 불러오는 로직들이 모여있는 것 같습니다.
- createAsyncThunk를 만들고, 안에 비동기 로직을 넣어준 것 같았습니다. (매개변수는 몇개, 어떤 걸 넣어야하는지 모르겠음)
- 매개변수가 뭔지 정확히는 모르겠지만 두번째 인수로 비동기 로직인 콜백함수를 넣어주는 것 같습니다. 그다음 promise를 리턴해줍니다.
- 그다음 slice에 어떻게 적용을하지? reducers에 바로 포함시켜주나 싶었는데
- extraReducers라는 새로운 하나의 프로퍼티를 넣어서 builder라는 인수를 받아서 로직이 fulfiled 됐을 때, pending, rejected인지에 따라 결과를 처리를 해주는 것 같아보였습니다. (상태가 pending, rejected, fulfiled) 3가지라고 공식문서 어디서 본 것 같앗읍니다.
아니.. 슬라이스마다 비동기 로직을 이렇게 추가하면 끝이라고? 겁나 편리하다고 생각들엇으빈다. . RTK만만세입니다 ㄹㅇ
createAsyncThunk와 createSlice를 사용하면 Redux Toolkit만으로 비동기 처리를 쉽게 할 수 있으며, redux-saga에서만 사용할 수 있던 기능(이미 호출한 API 요청 취소하기 등)까지 사용할 수 있다고 합니다.. ㅎㄷㄷ ㄹㅇ RTK 만만세..
● createAsyncThunk
- 액션 타입 문자열, 프로미스를 반환하는 비동기 함수, 추가 옵션 순서대로 인자를 받는 함수입니다.
- 입력받은 액션 타입 문자열을 기반으로 프로미스 라이프사이클 액션 타입을 생성하고, thunk action creator를 반환합니다.
- thunk action creator: 프로미스 콜백을 실행하고 프로미스를 기반으로 라이프사이클 액션을 디스패치합니다.
- 리듀서를 생성해주는 기능은 없기 때문에 액션들을 처리할 로직을 직접 작성해야 합니다.
먼저 위쪽에서 createAsyncThunk 부분만 잘라오면
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
const fetchUserById = createAsyncThunk(
// string action type value: 이 값에 따라 pending, fulfilled, rejected가 붙은 액션 타입이 생성된다.
'users/fetchByIdStatus',
// payloadCreator callback: 비동기 로직의 결과를 포함하고 있는 프로미스를 반환하는 비동기 함수
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId);
return response.data;
},
// 세 번째 파라미터로 추가 옵션을 설정할 수 있다.
// condition(arg, { getState, extra } ): boolean (비동기 로직 실행 전에 취소하거나, 실행 도중에 취소할 수 있다.)
// dispatchConditionRejection: boolean (true면, condition()이 false를 반환할 때 액션 자체를 디스패치하지 않도록 한다.)
// idGenerator(): string (requestId를 만들어준다. 같은 requestId일 경우 요청하지 않는 등의 기능을 사용할 수 있게 된다.)
);
- createAsyncThunk는 thunk action creator를 반환한다.
- 위의 경우를 예로 들면, 다음 세 가지 thunk action creator가 반환된다.
- fetchUserById.pending: 'users/fetchByIdStatus/pending' 액션을 디스패치하는 thunk action creator
- fetchUserById.fulfilled: 'users/fetchByIdStatus/fulfilled' 액션을 디스패치하는 thunk action creator
- fetchUserById.rejected: 'users/fetchByIdStatus/rejected' 액션을 디스패치하는 thunk action creator
- 이 액션들이 디스패치되면, thunk는 아래 과정을 실행한다.
- pending 액션을 디스패치한다.
- payloadCreator 콜백을 호출하고 프로미스가 반환되기를 기다린다.
- 프로미스가 반환되면, 프로미스의 상태에 따라 다음 행동을 실행한다.
- 프로미스가 이행된 상태라면, action.payload를 fulfilled 액션에 담아 디스패치한다.
- 프로미스가 거부된 상태라면, rejected 액션을 디스패치하되 rejectedValue(value) 함수의 반환값에 따라 액션에 어떤 값이 넘어올지 결정된다.
- rejectedValue가 값을 반환하면, action.payload를 reject 액션에 담는다.
- rejectedValue가 없거나 값을 반환하지 않았다면, action.error 값처럼 오류의 직렬화된 버전을 reject 액션에 담는다.
- 디스패치된 액션이 어떤 액션인지에 상관없이, 항상 최종적으로 디스패치된 액션을 담고 있는 이행된 프로미스를 반환한다.
- thunk가 항상 이행된 프로미스를 반환하는 이유
- Redux Toolkit은 처리된 오류가 그렇지 않은 경우보다 많다고 생각한다.
- 디스패치 결과를 사용하지 않는 경우에도 프로미스가 거부되는 상황을 방지하고자 하기 때문이다.
이 분의 블로그를 보고 참고를 많이 했다. 이해가 아주 많이 된다.. 굿 굿..
Redux Toolkit의 createAsyncThunk로 비동기 처리하기
Redux Toolkit에는 내부적으로 thunk를 내장하고 있어서, 다른 미들웨어를 사용하지 않고도 비동기 처리를 할 수 있다.
velog.io
이를 기반으로 제 코드를 리팩토링 해보았습니다.
원래는 App.tsx에서 useFetch라는 커스텀 훅으로 데이터를 받아오고 있었습니다.
App.tsx
const App : FC = () => {
useFetch('https://dummyjson.com/products?limit=100'); // 데이터를 받아오는 로직 부분
const isHamburgerOpen = useSelector((state: RootState) => state.hamburgerModal.isOpen);
const isDetailOpen = useSelector((state: RootState) => state.detailModal.isOpen);
const toastList = useSelector((state: RootState)=> state.toastAlram.toastList);
return (
<BrowserRouter>
<Header />
....
);
}
export default App;
useFetch.tsx
import { useDispatch } from "react-redux";
import { useState, useEffect } from "react";
import getLocalStorage from "../utils/getLocalStorage";
import { setProducts } from "../modules/productSlice";
import ApiDataInterFace from "../modules/apidata.interface";
export default function useFetch(initialUrl: string){
const dispatch = useDispatch();
const [url, setUrl] = useState<string>(initialUrl);
const [data, setData] = useState<ApiDataInterFace[]>([]);
const fetchData = async () => {
fetch(url)//
.then(res => res.json())
.then(data => {
const newData = getLocalStorage(data.products, 'id');
dispatch(setProducts(newData));
setData(newData);
})
.catch(error => console.log(error))
}
useEffect(()=>{
fetchData();
}, [url])
return [data, setUrl];
}
setProducts라는 리듀서 함수에 액션을 dispatch해주고 있었는데요, createAsyncThunk를 사용하니.. 이 로직이 모두 쓸모가 업어졋읍니다•••
productSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import ApiDataInterFace from "./apidata.interface";
interface ProductType {
products: ApiDataInterFace[];
}
const initialState: ProductType = {
products: [],
};
// create thunk
export const setData = createAsyncThunk(
"get/products",
async (url: any)=> {
const response = await fetch(url);
const parseData = await response.json();
return parseData.products;
}
)
const productSlice = createSlice({
name: "productSlice",
initialState,
reducers: {
//// 기존 로직
setProducts: (state, action: PayloadAction<ApiDataInterFace[]>) => {
state.products = action.payload;
},
setBookmark: (state, action: PayloadAction<number>)=> {
state.products = state.products.map(product => {
if(product.id === action.payload){
if (product.bookmark === undefined) product.bookmark = true;
else product.bookmark = !product.bookmark;
}
return product
})
}
},
// thunk 로직
extraReducers: (builder) => {
builder.addCase(setData.fulfilled, (state, action) => {
state.products = action.payload;
});
}
});
export const { setProducts, setBookmark } = productSlice.actions;
export default productSlice;
타입부분에 에러가 생겨서 any로 만들고 일단 해보았는데 정상 작동되었습니다.
App.tsx
const App : FC = () => {
const dispatch = useDispatch<AppDispatch>();
useEffect(()=>{
dispatch(setData("https://dummyjson.com/products?limit=100"));
}, [])
const isHamburgerOpen = useSelector((state: RootState) => state.hamburgerModal.isOpen);
const isDetailOpen = useSelector((state: RootState) => state.detailModal.isOpen);
const toastList = useSelector((state: RootState)=> state.toastAlram.toastList);
return (
<BrowserRouter>
<Header />
...
);
}
export default App;
허허.. useFetch이놈을 쓸 일이 없게 됐군요..
또 새롭게 알게 된 점은, dispatch할때 기존에 보이지 않았던 에러가 자꾸 떠서 살펴보니, 공식문서에서 useDispatch 와 useSelector를 customHook을 사용해서 사용하길 권장하고 있었습니다. 저는 useSelector를 사용할 때는 state에 Rootstate 타입을 지정하면서 값을 가져왔었는데, 그렇게 하지 않고 useAppDispatch 를 사용하길 권장하고 있엇읍니다..
Usage with TypeScript | React Redux
Usage > TypeScript: how to correctly type React Redux APIs
react-redux.js.org
Redux toolkit - React (hook)+ Typescript 적용
react 에서 redux-toolkit 을 사용하기 위해서는 '@reduxjs/toolkit' 과 'react-redux' package를 설치해줘야한다. npm install @reduxjs/toolkit react-redux 그리고 이전에 state를 호출하기 위한 getState와 action을 호출하기위
zakelstorm.tistory.com
이것도 너무 좋았읍ㄴ디ㅏ••• 타입을 알아서 추론 해주는거 아입니까..
코드 수정 바로 갈기겟읍니다.. 프로젝트에 반영하겟읍니다.. 굿 굿 다음은 RTK-Query로 수정해봐야겠네요.