[NEXT] 5. Redux-Toolkit으로 비동기 api 불러오기, 상태관리하기
🚀 Redux-Toolkit 내부 thunk로 비동기 처리
- Redux-Toolkit에는 내부적으로 thunk를 내장하고 있어서, 다른 미들웨어를 사용하지 않고도 비동기 처리를 할 수 있음
- createAsyncThunk와 createSlice를 사용하여 Redux Toolkit만으로 비동기 처리를 쉽게 할 수 있으며, redux-saga에서만 사용할 수 있었던 기능까지 사용할 수 있음
https://redux-toolkit.js.org/usage/usage-with-typescript
Usage With TypeScript | Redux Toolkit
redux-toolkit.js.org
난 타입스크립트로 하고 있기 때문에 이 공식문서를 참고하면서 만들었다!
1️⃣ thunk 만들기 - createAsyncThunk
인자: 액션 타입 문자열, 프로미스를 반환하는 비동기 함수, 추가 옵션
입력받은 액션 타입 문자열을 기반으로 프로미스 라이프사이클 액션 타입을 생성하고, thunk action creator를 반환
thunk action creator: 프로미스 콜백을 실행하고 프로미스를 기반으로 라이프사이클 액션을 디스패치
import {
createSlice,
createAsyncThunk,
ActionReducerMapBuilder,
AsyncThunk,
} from "@reduxjs/toolkit";
interface CocktailsState {
loading: boolean;
cocktails: any[];
error: any;
cocktail: any[];
}
// createAsyncThunk를 사용해 비동기로 칵테일 데이터 가져오기
export const fetchCocktails = createAsyncThunk(
"cocktails/fetchCocktails",
async () => {
const res = await fetch(
"https://www.thecocktaildb.com/api/json/v1/1/search.php?s="
);
return await res.json();
}
);
- createAsyncThunk는 세 가지 thunk action creator 반환
- fetchCocktails.pending
- fetchCocktails.fulfilled
- fetchCocktails.rejected
- 이 액션들이 디스패치 되면, thunk는 아래 과정들을 실행
- pending 액션 디스패치
- payloadCreator 콜백 호출하고 프로미스가 반환되기를 기다림
- 프로미스가 반환되면, 프로미스의 상태에 따라 다음 행동을 실행
- 프로미스가 이행된 상태라면, action.payload를 fulfilled 액션에 담아 디스패치
- 프로미스가 거부된 상태라면, rejected 액션을 디스패치하되 rejectedValue(value) 함수의 반환값에 따라 액션에 어떤 값이 넘어올지 결정됨
- rejectedValue가 값을 반환하면, action.payload를 reject 액션에 담음
- rejectedValue가 없거나 값을 반환하지 않았다면, action.error 값처럼 오류의 직렬화된 버전을 reject 액션에 담음
- 디스패치된 액션이 어떤 액션인지에 상관없이, 항상 최종적으로 디스패치된 액션을 담고 있는 이행된 프로미스를 반환
2️⃣ slice 만들기
- thunk를 만들었다면, slice 만들기
import {
createSlice,
createAsyncThunk,
ActionReducerMapBuilder,
AsyncThunk,
} from "@reduxjs/toolkit";
interface CocktailsState {
loading: boolean;
cocktails: any[];
error: any;
cocktail: any[];
}
// createAsyncThunk를 사용해 비동기로 칵테일 데이터 가져오기
export const fetchCocktails = createAsyncThunk(
...
);
const cocktailSlice = createSlice({
// 리듀서 이름
name: "cocktails",
// 초기 상태
initialState: {
loading: false,
cocktails: [],
error: null,
cocktail: [],
} as CocktailsState,
reducers: {},
// 추가 리듀서
extraReducers: (builder: ActionReducerMapBuilder<CocktailsState>) => {
builder
// 데이터 가져오는 중
.addCase(fetchCocktails.pending, (state) => {
state.loading = true;
})
// 데이터 가져오기 성공
.addCase(fetchCocktails.fulfilled, (state, action) => {
state.loading = false;
state.cocktails = action.payload.drinks;
})
.addCase(fetchCocktails.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});
export default cocktailSlice.reducer;
3️⃣ store에 저장
import { configureStore } from "@reduxjs/toolkit";
import cocktailSlice from "./features/cocktailSlice";
import { useDispatch } from "react-redux";
// 스토어 생성
const store = configureStore({
reducer: {
app: cocktailSlice,
},
});
// useSelector 사용 시 타입으로 사용하기 위함
export type RootState = ReturnType<typeof store.getState>;
// useDispatch를 좀 더 명확하게 사용하기 위함
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export default store;
- store에 저장하지 않는 다면 애써 만든 리듀서를 사용할 수 없게 됨
4️⃣ 컴포넌트에 적용
- app/page.tsx
"use client";
export default function Home() {
const [modifiedCocktails, setModifiedCocktails] = useState([]);
const { loading, cocktails, error } = useSelector((state: any) => ({
...state.app,
}));
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCocktails());
}, []);
useEffect(() => {
if (cocktails) {
const newCocktails = cocktails.map((item: any) => {
const { idDrink, strAlcoholic, strDrinkThumb, strGlass, strDrink } =
item;
return {
id: idDrink,
name: strDrink,
img: strDrinkThumb,
info: strAlcoholic,
glass: strGlass,
};
});
setModifiedCocktails(newCocktails);
} else {
setModifiedCocktails([]);
}
}, [cocktails]);
if (loading) {
return <Loading />;
}
if (error) {
return <p>{error.message}</p>;
}
return (
...
);
}
📔 useSelector
- 스토어의 상태값을 반환해주는 역할
- 리덕스 스토어의 상태값이 바뀐 경우 바뀐 스토어의 상태값을 다시 가져와 컴포넌트 렌더링 시킴
- state => state.모듈명 형식으로 상태값을 반환 -> state.app
📔 useDispatch
- useDispatch 객체를 dispatch로 재선언한 후, dispatch 변수를 활용하여 액션 호출
- 실행할 액션함수명을 적은 후, 해당 액션함수의 파라미터에 변경할 상태값을 추가하고 dispatch로 감싸면 해당 액션을 호출하는 dispatch 함수 완성.
- 파라미터에는 문자열뿐만 아니라 true/false, 정수, json 타입 등 다양한 타입의 내용을 넣을 수 있음
- 크롬 Redux DevTools에서 상태값 확인
- 처음에는 초깃값 형태로 들어오는 것을 확인
- 렌더링 되면 칵테일 api에 담겨있는 정보들이 들어옴
5️⃣ 로딩페이지 만들기
- 위에서 useSelector로 loading, cocktails, error를 받아왔었다
- 나의 경우 칵테일 아이콘을 다운받아 로딩이 될 때 이 화면이 보이게끔 설정하였다
6️⃣ 실제 데이터 붙이기
- app/recipe/page.tsx
"use client";
const RecipeListPage = () => {
const [modifiedCocktails, setModifiedCocktails] = useState([]);
const { loading, cocktails, error } = useSelector((state: any) => ({
...state.app,
}));
const dispatch = useDispatch<AppDispatch>();
useEffect(() => {
dispatch(fetchCocktails());
}, []);
useEffect(() => {
if (cocktails) {
const newCocktails = cocktails.map((item: any) => {
const { idDrink, strAlcoholic, strDrinkThumb, strGlass, strDrink } =
item;
return {
id: idDrink,
name: strDrink,
img: strDrinkThumb,
info: strAlcoholic,
glass: strGlass,
};
});
setModifiedCocktails(newCocktails);
} else {
setModifiedCocktails([]);
}
}, [cocktails]);
if (loading) {
return <Loading />;
}
if (error) {
return <p>{error.message}</p>;
}
return (
<div className={styles.root}>
<div className={styles.top}>
<span>총 {modifiedCocktails.length}개</span>
<div>
<select>
<option>오름차순</option>
<option>내림차순</option>
<option>등록순</option>
</select>
</div>
</div>
<div className={styles.bottom}>
{modifiedCocktails.map((item) => {
return <CardItem item={item} />;
})}
</div>
</div>
);
};
export default RecipeListPage;
- 우선, 나는 전체 칵테일 레시피를 recipe 페이지에서 보여줄 것이기 때문에 메인페이지에 썼던 코드를 recipe 파일로 옮겼다
- 그리고 cardItem에 item을 props로 넘겨주었다
- components/Items/CardItem/index.tsx
"use client";
interface CocktailProps {
item: {
id: string;
glass: string;
img: string;
info: string;
name: string;
};
}
const CardItem = ({ item }: CocktailProps) => {
const router = useRouter();
return (
<div
className={styles.box}
onClick={() => router.push(`recipe/${item.id}`)}
>
<div className={styles.top}>
<img src={item.img} alt={item.name} className={styles.thumbnail} />
</div>
<div className={styles.bottom}>
<p className={styles.name}>{item.name}</p>
<p className={styles.explain}>
{item.info}
{item.glass}
</p>
</div>
</div>
);
};
export default CardItem;
- 받아온 Item 타입도 정해주고
- 아이템에 대한 정보를 넣어주었다.
🔥 [오류 해결] 'AsyncThunkAction<any, void, AsyncThunkConfig>' 형식의 인수는 'UnknownAction' 형식의 매개 변수에 할당될 수 없습니다.
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCocktails());
}, []);
- 자꾸 이부분에서 오류가 났는데 생각보다 쉽게 해결하였다.. 다행!!
const dispatch = useDispatch<AppDispatch>();
useEffect(() => {
dispatch(fetchCocktails());
}, []);
- 이렇게 useDispatch옆에 <AppDispatch>만 붙여주면 해결~~ 역시 스택오버플로우 짱,,
Argument of type 'AsyncThunkAction<any, void, {}>' is not assignable to parameter of type 'AnyAction'
store.ts export const store = configureStore({ reducer: { auth: authReducer }, middleware: [], }); export type AppDispatch = typeof store.dispatch; export type RootState = Retu...
stackoverflow.com