💻 Frontend
[FE] Redux 시작하기
a. redux 시작하기
- 프로젝트의 규모가 커지고, 이에 따라 컴포넌트 수가 많아지게 된다면 상위 컴포넌트의 데이터를 하단에 계속 props를 전달하게 되는 Props Drilling 문제점이 생긴다.
- 이에 따라 데이터의 관리가 파편화되며, 컴포넌트 간의 의존성이 커진다는 단점이 발생한다.
- FLUX 패턴에서는 계층에 관련 없이 각각의 컴포넌트를 View의 역할로만 생각하고, 데이터 관리(비즈니스 로직)에 대한 책임을 별도로 관리한다.
- Redux는 이 분리된 비즈니스 로직를 Action, Store, Reducer를 통해 관리한다.
- Action: 상태에 어떤 변화가 필요하게 될 때 발송되는 객체
- Reducer: Action이 발송되면, Reducer는 이전 상태와 Action을 받아 새로운 상태를 반환
- 기능에 따라 데이터 관리 로직을 분리하기 위해 여러 개의 reducer를 사용한다.
- Store: 애플리케이션의 상태를 전역적으로 저장하는 객체
- 기능에 따라 분리된 reducer를 하나로 combine해 전역적으로 관리하기 위해 configureStore를 활용한다.
export const store = configureStore({ reducer: { filter: filterReducer, reviewList: reviewsReducer, [reviewApi.reducerPath]: reviewApi.reducer, [assistantSocket.reducerPath]: assistantSocket.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware(), devTools: true, });
- Redux Flow 이해하기
- 1. 컴포넌트의 상태 변화 요청(dispatching an action)
- React 프로젝트에서는
react-redux
의useDispatch
라는 Hooks API를 사용하여 이를 수행한다. - 이 훅을 통해 반환된
dispatch
함수에 Action 객체를 전달하여 Dispatch 한다. - 이때, Action 객체는 주로 Action 생성자 함수를 호출하여 생성한다.
- 2. 상태 변경 로직 처리
- Reducer는 Action의
type
과 필요시payload
를 전달 받는다. - 그에 맞는 상태 변화 로직을 실행하여 새로운 상태 객체를 반환한다.
- 이때, 해당 로직은 같은 인자에 대해 언제나 같은 결과를 반환해야 한다는 원칙을 가져야 한다.
- 이 순수하다는 원칙이 어긋나게 되면 실행할 때마다 다른 결과가 나올 수 있다는 side-effect가 발생한다.
- 3. 변경된 상태 저장: Reducer에서 반환된 새로운 상태는 configureStore를 통해 단일 Store에 저장된다.
- 이 Store는 전역적으로 관리되며, Provider에 의해 컴포넌트 트리와 연결된다.
- 4. 컴포넌트의 변경된 상태 감지 및 반영:
react-redux
라이브러리의useSelector
훅을 사용하면, Redux Store의 상태를 컴포넌트에서 직접 선택하여 사용할 수 있다.- 지정한 상태가 변경될 때마다 해당 컴포넌트가 자동으로 리렌더링되어 최신 상태를 반영하게 된다.
b. redux-saga, rtk-query
상태 변경 로직 처리:
reducer
는 순수 함수를 통해 새로운 상태를 반환해야 한다.- 앞서 side-effect를 방지하기 위해 reducer는 순수 함수로 비즈니스 로직이 구성되어야 한다.
- 그러나 실제 애플리케이션은 API를 통해 상태가 변경되는 경우가 많고, 이러한 비동기 작업은 외부 상태(서버 데이터나 네트워크 상태)에 의존하게 되어 순수성이 어긋나게 된다.
- 따라서, 비동기 작업은 reducer 레이어를 통해 수행되지 않고, dispatch 과정에서 middleware 계층을 통해 수행되게 된다. middleware 계층을 구성할 수 있는 라이브러리로는
redux-saga
와rtk-query
가 있다.
특징 | Redux Saga | RTK Query |
주 사용 사례 | 복잡한 비동기 작업 및 사이드 이펙트 관리 | 데이터 패칭 및 캐싱, 상태 동기화 |
학습 곡선 | 높음 (Generator 함수 사용) | 낮음 |
설정 복잡성 | 높음 | 낮음 (Redux Toolkit과 긴밀히 통합) |
사용 용이성 | 비동기 작업 관리에 유연함 | 데이터 패칭과 캐싱을 자동화 |
API 연동 | 모든 종류의 비동기 작업에 사용 가능 | 주로 RESTful or GraphQL API 패칭에 최적화 |
- Use-Case
- 복잡한 비동기 작업 흐름 관리가 필요한 경우: 여러 단계의 API 호출과 그 결과에 따른 조건부 로직이 필요한 경우 Redux Saga가 더 적합하다.
- ex) 사용자 인증 흐름에서 여러 단계의 인증 절차와 실패 시 재시도 로직이 필요한 경우
- 서버 데이터 패칭 및 캐싱에 초점을 둔 경우: 웹 애플리케이션에서 포스트 목록을 불러오고, 이를 캐싱하여 다른 페이지에서 빠르게 재사용할 수 있도록 하고 싶은 경우 RTK Query가 더 적합하다.
- 실시간 데이터 업데이트가 필요한 경우: 웹소켓을 통한 실시간 데이터 업데이트와 같이 복잡한 실시간 상호작용이 필요한 경우, Redux Saga를 통해 이러한 비동기 이벤트를 효과적으로 관리할 수 있다.
// react-saga function* fetchLoanCategories() { const fetch = async () => await API.GET({ url: API_CATEGORY_LIST }); try { // `call` 이펙트를 사용하여 `fetch` 함수를 호출// `yield` 키워드는 해당 Promise가 resolve되기를 기다린다. const loanCategories = yield call(fetch); // `put` 이펙트는 특정 액션을 Dispatch하는 데 사용 (dispatch 단계) yield put(setLoanCategories(loanCategories)); } catch (error) { // 필요에 따라 에러에 따른 액션을 put할 수 있음 console.log(error); } } // `watchFetchLoanCategories`는 액션이 Dispatch될 때마다 사가 함수를 실행하는 Watcher export function* watchFetchLoanCategories() { // takeLatest: 가장 최근의 액션만을 처리 yield takeLatest(FilterActionTypes.FETCH_LOAN_CATEGORIES, fetchLoanCategories); }
// rtk-query export const reviewApi = createApi({ reducerPath: 'reviewApi', tagTypes: ['Review'], // fetch 함수 설정 baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8081' }), // 주로 기능별 CRUD에 따른 엔드포인트들 선언 endpoints: (builder) => ({ getReviews: builder.query<{ data: Review[]; totalCount: number; page: number }, ReviewsFilter>({ // 요청 함수 정의 query: (filter) => ({ url: API_REVIEW_LIST, params: filter }), // 캐시 태그 이름 지정 providesTags: () => [{ type: 'Review', id: 'LIST' }], }), postReview: builder.mutation<boolean, any>({ query: (body) => ({ url: API_REVIEW, method: 'POST', body }), // 응답값을 컴포넌트 필요에 따라 변형 transformResponse: (res: { isSuccess: boolean }) => res.isSuccess, // 해당 태그에 해당하는 엔드포인트를 invaild 상태로 만들어 다시 fetching하도록 동작 invalidatesTags: [{ type: 'Review', id: 'LIST' }], }), }), }); export const { useGetReviewQuery, usePostReviewMutation } = reviewApi;
c. redux/toolkit
Redux는 상태 관리 비즈니스 로직를 Action, Store, Reducer를 통해 관리
- 기존 Redux는 상태 관리를 위해 action과 reducer, selector 등을 별도로 관리하게 되어 보일러플레이트 코드가 많이 필요하게 되며, 이는 신규 개발자의 러닝 커브를 증가시킨다는 단점이 있다.
- 이에 따라 redux/toolkit 에서는 redux 개발 경험을 간소화하기 위해 등장했다.
const initialState = { reviewList: [], }; const reviewSlice = createSlice({ name: 'reviews', initialState, reducers: {// 리듀서 및 액션 생성자 appendReviews: (state, action) => { state.reviewList = [...state.reviewList, ...action.payload]; }, clearReviews: (state) => { state.reviewList = []; }, }, }); export const { appendReviews, clearReviews } = reviewSlice.actions; export default reviewSlice.reducer;
- 특히, createSlice 기능은 상태의 초기값, reducer, action을 한 객체 내에서 정의할 수 있다.
기능/특성 | 기존 Redux 방식 | Redux Toolkit 방식 |
설정 및 구성 | 복잡하고 보일러플레이트 코드가 많음 | 간결하고 간소화된 설정 |
Action 생성 | 별도의 Action 파일과 함수 필요 | createSlice 를 통해 Action과 Reducer를 한 곳에서 정의 |
Reducer 정의 | 각 Action 타입별로 처리 로직을 작성해야 함 | createSlice 에서 Action 처리 로직을 직접 정의 |
불변성 관리 | 직접적인 불변성 유지 코드 작성 필요 | 내부적으로 Immer 라이브러리를 사용하여 불변성 자동 관리 |
개발자 도구 | 수동으로 Redux DevTools 설정 필요 | 자동으로 Redux DevTools 통합 |
d. reselect
컴포넌트의 상태 감지 및 반영:
useSelector
훅을 통해 store의 상태를 컴포넌트에서 직접 선택하여 사용Redux Flow의 4번 과정에서, 만약 상태가 자주 업데이트되거나 계산 로직이 복잡하면 성능 저하가 발생한다. reselect 라이브러리는 Memoization 기법을 통해 선택자 함수의 결과를 캐싱해 이러한 문제를 해결한다.
기능 | useSelector 사용 | reselect 추가 사용 |
계산 최적화 | ❌ 계산 결과가 캐싱되지 않음 | ✅ 메모이제이션을 통한 계산 결과 캐싱 |
ㅤ | reselect 는 동일한 인자로 호출될 때 이전 결과를 재사용한다. | ㅤ |
재사용성 | ❌ 컴포넌트 간 재사용 어려움 | ✅ 선택자를 여러 컴포넌트에서 재사용 가능 |
ㅤ | 선택자 로직을 공유하고 싶을 때 reselect 가 유용하다. | ㅤ |
성능 | 🚫 복잡한 계산에 비효율적 | ✅ 복잡한 선택 로직에 효율적 |
ㅤ | reselect 는 복잡한 계산을 최적화한다. | ㅤ |
유지 보수성 | 🚫 상태 변경 시 다수의 컴포넌트 영향 | ✅ 선택자 함수 수정으로 관련 컴포넌트 일관성 유지 |
ㅤ | reselect 를 사용하면 선택자 로직의 중앙 집중화가 가능하다. | ㅤ |
const filterSelector = (state: RootState) => state.filter; export const getCategories = () => createSelector([filterSelector], (filter) => filter.categories?.map((category) => ({ ...category, isSelect: category.id === filter.loanCategoryId, })) );
- reselect 선택자에서는 컴포넌트의 필요에 따라 redux의 상태값을 변형 후 캐싱하여 제공할 수 있다.
- 데이터를 불변 상태로 관리할 수 있게 하는
Immutable.js
와reselect
를 함께 사용하게 된다면, 선택자 함수에서 불변 데이터의 일부분을 선택하여 성능을 더욱 최적화할 수 있다.