리덕스 툴킷은 리덕스를 더 편리하게 사용할 수 있도록 도와주는 도구입니다. 리덕스의 복잡함을 줄여주고 보일러 플레이트(불필요한 반복 코드)가 줄어들어 개발 시간을 단축시켜줍니다.
전역 상태 관리 라이브러리를 고민하던 중 Redux의 장점(action과 reducer를 통해 상태 변경 과정을 명확하게 관리해 디버깅과 테스팅이 용이)은 유지하면서 보일러플레이트나 초기 설정의 어려운 단점을 해결해주기에 사용하였습니다.
Redux Persist는 웹 페이지 새로고침 시에도 리덕스 스토어의 상태 데이터가 사라지지 않도록 돕는 라이브러리입니다. Redux Persist는 앱의 상태 데이터를 브라우저의 로컬 스토리지 등에 저장하므로, 페이지 새로고침 후에도 데이터가 유지됩니다.
createSlice 함수를 사용하여 유저 정보를 관리할 AuthState 슬라이스를 생성합니다. 이 과정에서 리듀서 함수들과 초기 상태값도 함께 설정됩니다.
// redux/slice/authSlice.ts
interface AuthState {
accessToken: string | null;
refreshToken: string | null;
status: 'idle' | 'loading' | 'successed' | 'failed';
message: string | null | undefined;
}
const initialState: AuthState = {
accessToken: null,
refreshToken: null,
status: 'idle',
message: null,
};
const authSlice = createSlice({
name: 'auth', // slice 이름
initialState, // 초기 상태값
reducers: {}, // 각 액션 타입에 대응하는 리듀서 함수들
)};
export default authSlice.reducer;
Redux Toolkit에서 제공하는 createAsyncThunk 함수를 사용하여 비동기 액션을 생성
// redux/thunks/AuthThunk
import { createAsyncThunk } from '@reduxjs/toolkit';
interface MyKnownError {
message: string;
}
export const emailSigninThunk = createAsyncThunk<
SigninResponseData, // 비동기 작업을 성공하면 Promise가 반환하는 값
{ email: string; password: string }, // 비동기 작업을 실행할 때 필요한 인자
{ rejectValue: MyKnownError } // thunk API와 관련된 설정들
>('auth/emailSignin', async (inputs, thunkAPI) => {
try {
const res = await signinAPI(inputs);
return res.data.data;
} catch (error) {
if (isAxiosError<ErrorResponse, any>(error)) {
return thunkAPI.rejectWithValue({ message: error.response?.data.status || 'UNKNOWN' });
}
throw error;
}
});
extraReducers
는 createSlice
함수 내부에서 정의되는 특별한 필드로, slice 외부에서 생성된 액션들에 대한 리듀서를 작성합니다.
reducers 필드 내부에선 슬라이스 내부에서만 정의된 동기적인 상태 업데이트에 적합하며, 비동기 작업(예: API요청) 같은 경우는 시작(pending), 성공(fullfilled), 실패(rejected) 단계 등의 서로 다른 상태에 따른 처리가 필요하므로 한 번에 여러 개의 액션이 발생하게 돼서 각각 상태별로 처리할 로직을 작성하기 위해 사용합니다.
Redux Toolkit 1.3 버전부터 builder.addCase()
메소드를 통해 extraReducer 를 추가할 수 있습니다.
// redux/slice/authSlice.ts
const authSlice = createSlice({
name: 'auth', // slice 이름
initialState, // 초기 상태값
reducers: {}, // 각 액션 타입에 대응하는 리듀서 함수들
extraReducers: (builder) => {
// 주로 비동기 작업 처리나 라이브러리에서 제공하는 특별한 액션들을 다루기 위해 사용
builder
.addCase(emailSigninThunk.pending, (state) => {
state.status = 'loading';
})
.addCase(emailSigninThunk.fulfilled, (state, action) => {
state.status = 'successed';
state.accessToken = action.payload.accessToken;
state.refreshToken = action.payload.refreshToken;
})
.addCase(emailSigninThunk.rejected, (state, action) => {
state.status = 'failed';
state.message = action.payload?.message;
})
},
)};
combineReducers
각 슬라이스의 리듀서를 모아 하나의 루트 리듀서로 합침.// redux/reducer.ts
import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';
import localStorage from 'redux-persist/es/storage';
import authReducer from '@redux/slice/authSlice';
const authPersistConfig = {
key: 'auth',
storage: localStorage,
whitelist: ['accessToken', 'refreshToken'],
};
const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer),
});
export const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
Redux Toolkit에서 제공하는 configureStore
함수로 Store를 구성하고, getDefaultMiddleware
로 필요한 Middleware 설정을 추가하고, persistStore
함수로 persistor 객체를 만든 후 store와 함께 내보냅니다
// redux/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { persistedReducer } from '@redux/reducer';
import { persistStore, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist';
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => {
const defaultMiddleware = getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
});
return [...defaultMiddleware];
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const persistor = persistStore(store);
export default store;
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store, persistor } from '@redux/store';
import router from '@/router';
import { PersistGate } from 'redux-persist/es/integration/react';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RouterProvider router={router} />
</PersistGate>
</Provider>
</React.StrictMode>
);
장점