Redux Toolkit으로 유저 상태 관리하기 (persist, thunk type)

먼지·2023년 8월 25일
0
post-thumbnail

목표

  • 이메일로 로그인 후 유저 토큰을 관리하고 여러 컴포넌트에 공유해야 하는 상태를 Redux Toolkit을 사용해 관리
  • 데이터 영속성을 유지하기 위해 localStorage 대신 redux-persist를 도입

Redux Toolkit이란?

리덕스 툴킷은 리덕스를 더 편리하게 사용할 수 있도록 도와주는 도구입니다. 리덕스의 복잡함을 줄여주고 보일러 플레이트(불필요한 반복 코드)가 줄어들어 개발 시간을 단축시켜줍니다.

전역 상태 관리 라이브러리를 고민하던 중 Redux의 장점(action과 reducer를 통해 상태 변경 과정을 명확하게 관리해 디버깅과 테스팅이 용이)은 유지하면서 보일러플레이트나 초기 설정의 어려운 단점을 해결해주기에 사용하였습니다.

Redux Persist란?

Redux Persist는 웹 페이지 새로고침 시에도 리덕스 스토어의 상태 데이터가 사라지지 않도록 돕는 라이브러리입니다. Redux Persist는 앱의 상태 데이터를 브라우저의 로컬 스토리지 등에 저장하므로, 페이지 새로고침 후에도 데이터가 유지됩니다.

1. 상태 타입과 슬라이스 정의

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;

2. 비동기 액션 생성

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;
  }
});

3. extraReducers로 비동기 액션의 상태에 따른 처리 로직 정의

extraReducerscreateSlice 함수 내부에서 정의되는 특별한 필드로, 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;
      })
    },
)};

4. 루트 리듀서와 Persist 설정

  • combineReducers 각 슬라이스의 리듀서를 모아 하나의 루트 리듀서로 합침.
  • 웹 페이지 새로고침 시 상태 데이터 유실 문제 해결을 위해 리듀서에 Persist 설정을 적용.
// 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);

5. store와 persistor 구성 및 내보내기, Middleware 설정

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>
);

궁금했거나 배운 점

리덕스 툴킷의 장단점과 어느 경우에 사용하는지?

장점

  • Redux 코드를 간결하게 작성할 수 있고 액션 생성 함수, 리듀서, 미들웨어 등을 편리하게 관리할 수 있는 API를 제공함
  • Redux의 Best Practices를 따르도록 설계되어 있음
  • Immer 라이브러리를 내장하고 있어 불변성(immutability)을 유지하면서 데이터를 쉽게 업데이트할 수 있음
    단점
  • Redux 자체의 복잡성은 여전히 존재하므로 상태 관리 로직이 복잡해질수록 코드가 길어질 수 있음

Redux와 Redux Toolkit의 차이와 공통점

  • 둘 다 Flux 아키텍처 패턴을 따르며, 애플리케이션의 전역 상태 관리에 사용
  • Redux는 기본적인 Flux 패턴을 구현하는 가장 기본적인 도구로 보일러 플레이트 코드가 많음
  • Redux Toolkit은 보일러 플레이트 코드를 최소화하고, Immer와 같은 유용한 라이브러리를 내장해 편안한 개발 경험 제공

localStorage vs redux persist

  • 로컬스토리지 사용 시 추가적인 라이브러리 없이도 구현 가능하며 로직을 완전히 제어하고 세밀하게 조정할 수 있음. 하지만 많은 코드 작성과 모든 케이스를 고려해 직접 처리해야 하므로 버그 발생 가능성이 높아질 수 있고,,
  • redux-persist 사용 시 자동으로 스토어와 로컬 스토리지 사이의 동기화 처리가 가능하고 다양한 저장소를 지원함. but 라이브러리 자체가 가진 버그나 한계에 영향을 받으며 추가적인 종속성(dependency)가 필요함.
profile
꾸준히 자유롭게 즐겁게

0개의 댓글