Redux-saga vs RTK Middleware

RTK middleware란?

RTK의 디폴트 비동기 처리 메서드로 들어가있는 thunk의 한계점

Despite its simplicity, thunks have limitations. One of the most cited limitations is the inability to run code in response to dispatched actions or state updates. Doing so requires writing custom middleware or using more powerful middleware libraries like Redux-Saga. Thunks are also relatively difficult to test.

: 사실 이건 RTK thunk만의 아쉬운점이 아니라 redux-thunk(사실상 똑같은)의 아쉬운점이기도 하다(지난 포스팅에서 다뤘다). 이러한 한계점

  • state update에 맞춰 코드를 실행하는 것
  • dispatch 된 action들에 맞춰 코드를 실행하는 것

에 따른 해결책으로 RTK v1.8.0 업데이트에서 새로운 미들웨어가 나오게 된다.

The new middleware’s primary functionality is let users respond to dispatched actions, or run code in response to state updates.

이 미들웨어는 우리가 흔히 아는 useEffect와 같이 작동한다. 즉, state or action이 dispatch 됐을 때 useEffect가 state 변경에 따라 작동하듯이, 이 미들웨어도 이에 반응하여 작동한다.

listenerMiddleware.startListening({
  actionCreator: addTodo,
  effect: async (action, listenerApi) => {
    console.log(listenerApi.getOriginalState());
    console.log(action);
    await listenerApi.delay(5000);
    console.log(listenerApi.getState());
  },
});

위에서 effect 콜백이 특정 actrion이 dispatch 됐을 때 발동되는 부분이다. 이 때, 매개변수 중 action에 response 할 action이 들어오고, listenerApi에는 getState() or getOriginalState() 를 통해 state를 받을 수 있고, dispatch도 할 수 있고, delay 등의 functions가 들어있고, 이를 로직에 쓸 수 있다.

추가로 위에서 effect, actionCreator 와 같은 property 종류에는 다음과 같은 것들이 있다.

  • Action type: the type property is the exact action type string that will trigger the effect callback
  • Action creator: the actionCreator property is the exact action creator that triggers the effect callback
  • Matcher: the matcher property matches one of many actions using RTK matcher and triggers the effect callback when there is a match
  • Predicate: the predicate property is a function that returns true or false to determine whether the effect callback should run or not. It has access to the dispatched action and current and previous states

실제로 미들웨어에 추가할 때는 다음과 같이 해주면 된다.

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleWare) => {
    return getDefaultMiddleWare({ thunk: false }).prepend(listenerMiddleware);
  }
});

redux-saga 사용법(with RTK)

지난 포스팅에서도 다뤘지만 redux-saga는 watcherSaga, workerSaga로 나뉜다.

const fetchTodo = (url) => fetch(url).then((res) => res.json());

function* workerSaga(action) {
  const { url } = action.payload;
  try {
    const todo = yield call(fetchTodo, url);
    yield put(addTodo(todo));
  } catch (error) {
    yield put(setError({ error }));
  }
};

function* watcherSaga() {
  yield takeEvery(fetchTodo.toString(), workerSaga);
};

그리고 redux-saga를 RTK와 같이 쓴다면 아래와 같이 세팅을 해주면 된다.

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleWare) => {
    return getDefaultMiddleWare({ thunk: false }).prepend(listenerMiddleware);
  }
});

Redux-saga의 기능과 RTK’s listenerMiddleware에서 이와 대응되는 메서드 비교

앞서 말한 Redux-saga에서 제공하는 편리한 effect들을 살펴볼겸 그리고 RTK가 업데이트한 listenerMiddleware를 이와 비교하면서 살펴볼겸 실제로 문법을 보면서 정리해보고자 한다.

: saga의 delay와 같은 기능을 하는 것이 RTK 미들웨어에도 delay라는 메서드로 존재한다. 말그대로 effect callback 내의 로직을 연기시키기 위한 것임. 아래 예시는 특정 api를 호출 및 response를 받고 500ms 후에(delay) 디스패치 하는 로직이다.

  • saga
function* fetchTodo(action){
  const { todoId } = action.payload;
  const todo = yield api.fetchTodo(todoId);
  yield delay(500);
  yield put(addTodo(todo));
}
  • RTK
listenerMiddleware.startListening({
  actionCreator: fetchTodo,
  effect: async (action, listenerApi) => {
    const { todoId } = action.payload;
    const todo = await api.fetchTodo(todoId);
    await listenerApi.delay(500);
    listenerApi.dispatch(addTodo(todo));
  },
});

: debounce : delay처럼 빌트인 메서드는 없지만, 두가지를 이용해서 saga의 debounce와 유사한 기능을 RTK로도 구현 할 수 있다.

  • saga
function* watcherSaga() {
  yield debounce(500, fetchTodo.toString(), workerSaga);
}
// 아래의 takeLatest effect 또한 위의 debounce와 유사한 형태로 사용할 수 있다. 
// 단지 위의 debounce는 구체적으로 시간을 정해줄 수 있고, 아래의 takeLatest는 알아서 가장 마지막 액션만 처리한다. 
function* watchSaga() {
  yield takeLatest(fetchTodo.toString(), workerSaga); 

}
  • RTK
listenerMiddleware.startListening({
  actionCreator: fetchTodo,
  effect: async (action, listenerApi) => {
    listenerApi.cancelActiveListeners();
    await listenerApi.delay(500);
  },
});

: throttle 도 가능하다. debounce와 같이 빌트인은 없지만 커스텀해서 만들 수 있다. 아래와 같은 로직으로 처리를 해주면 1000ms(1s) 동안 새로운 action 을 받지 않으면서 동시에 가장 마지막에 dispatch된 액션을 buffer에 넣게된다.

  • saga
function* watcherSaga() {  
  yield throttle(1000, fetchTodo.toString(), workerSaga)  
}
  • RTK
listener.startListening({
  type: fetchTodo.toString(),
  effect: async (action, listenerApi) => {
    listenerApi.unsubscribe();
    console.log('Original state ', listenerApi.getOriginalState());
		// 이부분 안해주면 에러난다(getOriginalState synchronously otherwise it will throw an error.)
    await listenerApi.delay(1000);
    console.log('Current state ', listenerApi.getState());
    listenerApi.subscribe();
  }
});

: saga의 takeEvery(’*’, workerSaga) 와 같은 로직도 가능하다

  • RTK
listenerMiddleware.startListening({
  predicate: (action, currState, prevState) => true,
  // 위에 predicate 부분이 로직에 상관없이 모든 action을 받는(takeEvery와 같은) 설정을 해주는 부분이다.
  // 예를 들어, 특정 action이 들어오면 아래 effect callback 을 무조건 실행시킨다는 것인데,
  // debounce와 반대로 모든 요청에 대해 처리를 해야할 때 쓸 수 있다. 
  effect: async (action, listenerApi) => {
  },
});
  • saga
function* watchEveyDispatchAndLog(){
  yield takeEvery('*', logger);
}

: thunk 에서도 asnyc & await를 쓸 수 있기에 그렇게 대단한? 기능은 아니지만 saga의 yield처럼 rtk 미들웨어에서도 callback hell 이 일어나지 않도록 async & await를 사용할 수 있음(saga의 yield === await).

  • RTK
listenerMiddleware.startListening({
  actionCreator: fetchTodo,
  effect: async (action, listenerApi) => {
    const task = listenerApi.fork(async (forkApi) => {
    });
    const result = await task.result;
  },
});
  • saga
function* fetchTodos() {
  const todo1 = yield fork(fetchTodo, '1');
  const todo2 = yield fork(fetchTodo, '2');
}

: 위에서도 등장(?)했지만 redux-saga의 'takeLatest'와 같은 기능을 rtk middleware에서도 제공한다.

  • RTK
listenerMiddleware.startListening({
  actionCreator: fetchTodo,
  effect: async (action, listenerMiddlewareApi) => {
    listenerMiddlewareApi.cancelActiveListeners();
    // 이렇게 하면 특정 action이 연속으로 dispatch 될 경우 가장 마지막 dispatch에만 callback이 실행된다. 
     },
});
  • saga
function* watchFetchTodo() {
  yield takeLatest(addTodo.toString(), fetchTodo);
};

: 마지막으로 redux-saga의 'take'와 같은 역할을 하는 기능을 제공한다. redux-saga의 take는 '딱 한번의 디스패치만 허용'하기 위해 쓰인다. 예를 들어, 프로세스상 한번만 하는 로그인 디스패치가 이미 한번 acitvate 됐다면 그 이후에는 그 dispatch가 발생해도 callback을 실행하지 않게 하는 것이다.

  • RTK
listenerMiddleware.startListening({ 
  actionCreator: fetchTodo,
  effect: async (action, listenerApi) => {
    console.log(action);
    listenerApi.unsubscribe();
  },
});
  • saga
function* watchSomeDispatch(){
    yield take(activateSomething());
    yield api.activateSomething();
}

Redux-Saga vs RTK new middleware 비교

Package Minified size Minified + Gzipped size
Redux-Saga 14kB 5.3kB
Listener middleware 6.6kB 2.5kB
Redux Toolkit (RTK) 39.3kB 12.7kB
: 결론적으로 redux-saga가 2배정도 더 크다. 물론 커봤자 한자리 수의 KB 단위이기는 하지만

결론

: 결론적으로, RTK middleware에서도 redux-saga가 제공하는 편리한(?) effects 들을 대부분 유사하게 지원하고 있는 것을 알 수 있었다. 이러한 배경을 바탕으로 redux-saga의 러닝커브 그리고 3년동안 업데이트가 안됐다는 부분 등을 고려했을 때(사실 이부분이 제일 크다. 만든 사람이 3년간 손놓고 있었다는건,,?) 굳이 크기도 2배 큰 redux-saga를 배워서 사용할 필요가 있나 싶다. 물론 아직도 redux-saga 만의 매력(편리하고, 심플한 코드 및 로직 설계가 가능)이 있음은 분명하겠지만(이전에 사용했던 사람들에게는 익숙함 자체도 매력일것이고) 지금 시점에서 redux-saga냐 RTK를 그대로 쓰냐 했을 때는 후자가 좀더 맞는 것 같다. 이전에는 프로젝트에 따라서 redux-saga가 필요한 프로젝트도 있었을테지만 이렇게 둘다 비슷한 기능을 제공한다하면 얘기는 달라질 것이다.

Reference

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글