글에 앞서 저는 리덕스 사가의 광팬임을 밝힙니다.
리덕스 없인 살 수 있어도 리덕스 사가 없이는 살 수 없습니다.
상태 관리를 구현하다 보면, 그 상태들과 관련된 비동기적인 작업들이 함께 필요하기 마련인데요. 데이터 페칭이라던지, 외부의 이벤트에 반응해야 한다던지, 작업의 지연이 필요하다던지 말예요. redux만으로는 그러한 복잡한 작업들을 해결할 수 없어 우리는 필연적으로 middleware를 사용하게 됩니다.
저는 Redux-thunk와 Redux-saga를 둘 다 사용해보았는데요. 먼저 thunk는 이해가 쉽고 매우 간단하게 비동기 작업을 구현 가능하다는 장점이 있습니다.
// action type
const INCREMENT_COUNTER = 'counter/INCREMENT_COUNTER'
// [Basic] action을 리턴하는 action creator
const increment = () => ({
type: INCREMENT_COUNTER
});
// [Thunk] function을 리턴하는 action creator
const incrementAsync = () => {
return (dispatch, getState) => {
const { counter } = getState();
setTimeout(() => {
if (counter === 0) dispatch(increment())
}, 1000)
}
}
// Counter 컴포넌트가 렌더링되고 1초 후 count 값이 1 증가
function Counter: React.FC () {
const dispatch = useDispatch();
const count = useSelector((store) => store.counter);
useEffect(() => {
dispatch(incrementAsync());
}, []);
...
}
위 코드와 같이 기존의 action creator를 dispatch
와 getState
를 매개변수로 받는 function을 리턴하는 함수로만 만들면 thunk가 완성됩니다. 해당 function 내에서 데이터 페칭, 지연 등의 비동기 작업을 구현할 수 있게 된 거죠👏
하지만 thunk를 사용하면서 아쉬운 점이 있었다면 다음과 같습니다
- 액션 객체가 너무나 복잡해지는 점 (redux의 디자인 패턴인 Flux 패턴에서는 액션을
type
과payload
만을 전달하는 역할로 두기를 제안합니다)- 제공되는 기능 자체가 너무 단순해 복잡한 비동기 작업을 깔끔하게 구현하는 데 제약이 있는 점 (데이터 패칭 실패 후 재시도, 외부 이벤트 채널 구독 등)
- 콜백 지옥이 아른아른🤦♂️
thunk의 아쉬운 점들을 살펴보았고 이제 saga로 자연스럽게 넘어갑니다. saga는 ES6에서 새로 등장한 generator 문법을 사용합니다.
// action creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const incrementAsync = () => ({ type: INCREMENT_ASYNC });
const decrementAsync = () => ({ type: DECREMENT_ASYNC });
// saga [PUSH Way]
function* incrementSaga() {
yield delay(1000); // 1초 지연
yield put(increment()); // "INCREMENT" 액션 dispatch
}
// saga [PULL way]
function* decrementSaga() {
while (true) {
yield take(DECREMENT_ASYNC); // "DECREMENT_ASYNC" 액션 wait
yield put(decrement()); // "DECREMENT" 액션 dispatch
}
}
function* counterSaga() {
// "INCREMENT_ASYNC" 액션을 감시하여 task로 push
yield takeEvery(INCREASE_ASYNC, increaseSaga);
// background task로 실행시켜 "DECREMENT_ASYNC" 액션을 pull
yield decrementSaga();
}
위 코드는 counter를 증가/감소시키는 기능을 각각 다른 방식으로 구현해본 것입니다. generator에 익숙하지 않다면 조금은 낯선 코드일 수 있지만 해당 문법에 익숙해지면 saga의 작동 방식이 조금 이해가 될 거에요. 이후 saga가 제공하는 여러 헬퍼 함수들의 기능을 익힌다면, 복잡하고 까다로운 비동기 작업을 깔끔하게/유지보수하기 쉽게/아름답게 구현이 가능합니다✌️
제공하는 헬퍼 함수들을 아무리 봐도 어떻게 사용할 수 있을지 막연한 분들을 위해, 실제 서비스 기능들을 개발하며 saga를 적용했던 경험을 공유합니다. (휴대폰 인증, 세션 유지, 메세지 구독, router 핸들링 등)
물론 추상화시켜서 대략적인 saga flow 만요😅
가장 기본적인 함수들인 call
, put
, takeEvery
, takeLatest
부터, 다소 까다롭지만 강력한 take
, fork
, race
, eventChannel
등의 함수들까지 왜 적용했고 어떤 점이 좋았는지 등을 공유할 예정이에요.
그럼 다음 포스트로 찾아뵙겠습니다👋