💻 Frontend

[FE] Redux 시작하기

date
Apr 16, 2024
slug
fe-redux
author
status
Public
tags
Frontend
summary
redux는 한물간 상태관리 라이브러리인가?
type
Post
thumbnail
DALL·E 2024-04-16 11.51.55 - A visually engaging thumbnail for a blog post introducing Redux. The design features a large, central logo of Redux, characterized by its typical thre.webp
category
💻 Frontend
updatedAt
Apr 23, 2024 05:55 AM

a. redux 시작하기

notion image
notion image
  • 프로젝트의 규모가 커지고, 이에 따라 컴포넌트 수가 많아지게 된다면 상위 컴포넌트의 데이터를 하단에 계속 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-reduxuseDispatch라는 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-sagartk-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.jsreselect를 함께 사용하게 된다면, 선택자 함수에서 불변 데이터의 일부분을 선택하여 성능을 더욱 최적화할 수 있다.