IntroRedux-toolkitRedux 와 Redux ToolKit사용해보자 🫡1. store 만들기2. slice들 만들기3. slice 상태 저장하기, 불러오기thunk로 비동기 작업하기 😲createAsyncThunkextraReducers
Intro
“상태관리? 왜 할까?”
- props drilling을 막자!
- 상태의 일관성을 유지하자!
“어떤 상태관리 라이브러리가 있을까?”
- recoil
- zustand
- redux-toolkit
- connect, useDispatch, useSelector 등을 사용하면서 리덕스 사용 가능
- configureStore, createSlice , createAsyncThunk 등을 지원하며 더 간편하게 사용할 수 있고, 리덕스의 설정, 미들 웨어, 반복되는 코드 등의 이슈를 해결
“왜 redux-toolkit을 선택했나?”
- 상태관리의 필요성
- 중앙 상태 관리와 FLUX 패턴을 학습하자
Redux-toolkit
Redux 와 Redux ToolKit
// Redux const ADD_TODO = 'ADD_TODO' const TODO_TOGGLED = 'TODO_TOGGLED' export const addTodo = (text) => ({ type: ADD_TODO, payload: { text, id: nanoid() }, }) export const todoToggled = (id) => ({ type: TODO_TOGGLED, payload: { id }, }) export const todosReducer = (state = [], action) => { switch (action.type) { case ADD_TODO: return state.concat({ id: action.payload.id, text: action.payload.text, completed: false, }) case TODO_TOGGLED: return state.map((todo) => { if (todo.id !== action.payload.id) return todo return { ...todo, completed: !todo.completed, } }) default: return state } } // 사용할때 <button onClick={() => { dispatch({type='ADD_TODO', paylod: data }) }
- 각각의 액션 리듀서를 만들어줘야했음
- 하나의 store에 모든 상태 저장
// Redux ToolKit import { createSlice } from '@reduxjs/toolkit' const todosSlice = createSlice({ name: 'todos', initialState: [], reducers: { todoAdded(state, action) { state.push({ id: action.payload.id, text: action.payload.text, completed: false, }) }, todoToggled(state, action) { const todo = state.find((todo) => todo.id === action.payload) todo.completed = !todo.completed ///#3 }, }, }) export const { todoAdded, todoToggled } = todosSlice.actions export default todosSlice.reducer
- createSlice를 사용하여 작게 쪼갠 슬라이스를 만들고 configureStore를 통해 스토어에 저장할 수 있음
- action.type 없이 바로 리듀서 등록 가능
- 불변성을 위해 … 등을 사용하지 않고 바로 할당 가능 (#3)
사용해보자 🫡
1. store 만들기
중앙에서 관리하는 store 안에 작은 하나하나인 slice를 가진다. 즉, store는 작은 slice들의 모임이 된다.
// _redux/store.ts import heartsSlice from './slices/heartsSlice'; import { configureStore } from '@reduxjs/toolkit'; export const store = configureStore({ reducer: { hearts: heartsSlice, }, }); // Infer the `RootState` and `AppDispatch` types from the store itself export type RootStateType = ReturnType<typeof store.getState>; //-> useSelector 사용시 state의 타입으로 사용됨 !!! export type AppDispatchType = typeof store.dispatch; // useDispatch를 좀 더 명확하게 사용하기 위함
2. slice들 만들기
테스트 용으로, 버튼을 눌러서 특정 메세지를 hearts라는 slice에 저장해보자!
// _redux/slices/heartsSlice.ts import { createSlice } from '@reduxjs/toolkit'; interface IHeartsSlice { hearts: string[]; } const initialState: IHeartsSlice = { hearts: [], }; const heartsSlice = createSlice({ name: 'heartsSlice', initialState, reducers: { getHearts: (state, action) => { if (typeof action.payload === 'string') { state.hearts = [...state.hearts, action.payload]; } }, }, }); export const { getHearts } = heartsSlice.actions; //action을 export export default heartsSlice.reducer; //리듀서를 export
3. slice 상태 저장하기, 불러오기
slice의 reducer의 action을 실행시키기 위해선 dispatch !
store의 slice들, 즉 각 상태를 사용하기 위해서 useSelector !
→ state.슬라이스명.이름 으로 접근
// src/Test.tsx import { useDispatch, useSelector } from 'react-redux'; import { getHearts } from './_redux/slices/heartsSlice'; import { AppDispatchType, RootStateType } from './_redux/stores'; export const Test = () => { const useAppDispatch: () => AppDispatchType = useDispatch; const dispatch = useAppDispatch(); const hearts = useSelector((state: RootStateType) => state.hearts.hearts); // console.log('hearts state', hearts) return ( <> <button onClick={() => { dispatch(getHearts('hi')); }}> hi 보내기 </button> </> ); };
—> dispatch할때 리덕스에서처럼 type을 보내지 않아도 됨! heartsSlice.ts 에서
export const { getHearts } = heartsSlice.actions;
를 했기 때문에 바로 action에 접근할 수 있음// App.tsx import { ThemeProvider } from '@emotion/react'; import { Provider } from 'react-redux'; import { Outlet } from 'react-router-dom'; import { store } from './_redux/stores'; import { GlobalReset } from './style/GlobalReset'; import { theme } from './style/theme'; export const App = () => { return ( <Provider store={store}> // 이 React에 store를 등록해주어야한다. <ThemeProvider theme={theme}> <GlobalReset /> <Outlet /> </ThemeProvider> </Provider> ); };
버튼 클릭시 결과 →

thunk로 비동기 작업하기 😲
데이터 패칭 등 비동기 처리 작업을 여러번 발생시킬 때, 매번 패칭하고 그 결과를 dispatch 하는 액션 로직을 만드는 것은 불필요하다!
이 때, 패칭하고 dispatch하는 액션 로직을 따로 함수로 만들고 이를 슬라이스에 반영시킬 수 있다!
createAsyncThunk
→ action creator 이다! 액션을 만든다!
createAsyncThunk(type, payloadCreator callback, [options])
type
은 액션타입을 적어준다. 이후에 .pending, .fulfilled, .rejected를 생성해준다.
payloadCreator
는 Promise를 반환하는 콜백함수이다. 리듀서와 동일한 형태 (state,action) ⇒ { }
Promise 반환의 상태(pending, fullfilled, rejeected) 각각에 따라 리듀서가 필요하다.
extraReducers
reducers는 액션 creator를 만들어주는데, 비동기작업에 대해서는 만들어주지 못하기 때문에 extraReducers에 정의한다.
//_redux/slices/channelsSlice.ts import { IChannel } from '@/api/_types/apiModels'; import { getApi } from '@/api/apis'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; interface IChannelData { channels: IChannel[]; isLoading: boolean; } const initialState: IChannelData = { channels: [], iLoading: false, }; // 채널데이터를 받아오는 패칭 액션로직을 thunk로 만든다. export const getChannelsData = createAsyncThunk('getChannels', async () => { const response = await getApi('/channels'); return response?.data as IChannel[]; }); const channelsSlice = createSlice({ name: 'channelsSlice', initialState, reducers: {}, extraReducers: (builder) => { builder.addCase(getChannelsData.pending, (state, action) => { state.isLoading = true; }); builder.addCase(getChannelsData.fulfilled, (state, action) => { state.isLoading = false; state.channels = action.payload; }); builder.addCase(getChannelsData.fulfilled, (state, action) => { state.isLoading = false; }); }, }); export default channelsSlice.reducer;
// src/Test.tsx import { useDispatch, useSelector } from 'react-redux'; import { AppDispatchType, RootStateType } from './_redux/stores'; import { getChannelsData } from './_redux/slices/channelsSlice'; export const Test = () => { const useAppDispatch: () => AppDispatchType = useDispatch; const dispatch = useAppDispatch(); return ( <> <button onClick={() => { dispatch(getChannelsData()) .then((res) => console.log(res)) .catch((err) => console.error(err)); }}> GET CHANNEL </button> ); };
작동과정을 정리하자면,
button Onclick으로 thunk 액션 함수가 실행
→ 액션 로직내에서 비동기 처리
→ (성공시)fullfilled 상태의 리듀서에 그 반환값이 action.payload로 들어옴
→ 상태값에 반영
state.channelds = action.payload
참고자료