💡 이 글을 읽기 전에 읽어보면 좋은 글
Redux-saga란?🤔
공식문서에서는 Redux-saga를 다음과 같이 소개하고 있다.
Redux-saga는 react/redux 애플리케이션의 사이드 이펙트, 예를 들면 데이터 fetching이나 브라우저 캐시에 접근하는 순수하지 않은 비동기 동작들을 더 쉽고 보기 좋게 만드는 것을 목적으로 하는 라이브러리이다.
redux에서는 action을 발생시키면 reducer를 통해 state를 변화시켜 store를 갱신했다.
Redux-saga는 action과 reducer사이에서 흐름을 제어하는 미들웨어이다. 이 중간에서 Redux-saga는 action이 발생하면 reducer가 액션을 처리하기 전에 다양한 작업을 할 수 있다.
다양한 작업들의 예시는 이렇다.
- 기존 요청을 취소 처리하거나 불필요한 중복 요청을 방지할 수 있다.
- 비동기 작업을 처리하는데 효과적이다.
- 특정 액션이 발생했을 때 다른 액션을 발생시키거나, 리덕스와 관계없는 코드 실행시 사용한다.
즉, Redux-saga를 이용하면 보다 간편하면서도 깊게 state의 흐름을 제어할 수 있다.
Redux-saga를 도입한 이유
우리 팀이 Redux-saga를 도입하기로 결정한 이유는 크게 두가지였다.
1️⃣ api 호출 로직을 효율적으로 관리할 수 있다.
오로지 비동기 작업만을 위해 redux-saga를 쓰는것은 개인적으로 크게 효율적이지 않다고 생각한다.async/await
라는 흘륭한 기능으로 비동기 동작들을 처리할 수 있는데 굳이 코드량을 늘려가며 redux-saga를 도입할 이유가 있을까?
하지만 redux-saga에서는 비동기 동작에 대한 더 세부적인 컨트롤이 가능했다. 예를 들면 사용자의 부주의로 동일한 api를 여러번 호출할 경우, 가장 마지막 호출의 response
만 받아오도록 제어할 수 있다.
무엇보다 api 호출 로직을 saga에서 관리하면서, Presentational 컴포넌트와 Container 컴포넌트의 명확한 분리가 가능해졌다. 또한 같은 api를 다른 페이지에서 호출시 같은 코드를 두번 적어줘야 했다면, api 호출 로직을 Redux-saga로 관리하면서 selector로 데이터 만을 간편하게 가져올 수 있게되었다.
2️⃣ callback 함수를 action payload로 넘길 수 있다.
공통으로 사용할 모달창을 구현했다. 이 모달창에서 확인 버튼을 누르면 모달창을 호출한 페이지에서 그 사실을 알아야 하는데 그 로직을 어떻게 구현할지에 대해 많은 고민을 했다.
모달창을 열 때 페이지의 callback 함수를 함께 넘겨주는 것이 가장 좋은 방법이지만, redux에서는 callback을 state
값으로 저장하는 것을 권장하지 않는다.(# Can I put functions, promises, or other non-serializable items in my store state? 참고)
이때 redux-saga를 사용하면 callback값을 redux-saga에서 처리해주기 때문에 action payload
로 callback을 넘길 수 있게되었다.
Redux-saga의 동작 원리
Redux-saga는 제너레이터 함수 문법을 기반으로 비동기 작업을 관리한다. (symbol을 알아보고, generator를 공부한 이유가 여기에 있다.) Redux-saga는 우리가 디스패치 하는 action을 모니터링해서 그에 따라 필요한 작업을 따로 수행할 수 있다.
function* watchGenerator(){
console.log("모니터링 중...");
let prevAction = null;
while(true){
const action = yield;
console.log("이전 액션: ", prevAction);
prevAction = action;
if(action.type === "HELLO"){
console.log("안녕");
}
}
}
const watch = watcjGenerator();
watch.next();
// 모니터링 중...
// { value: undefined, done: false}
watch.next({type: "TEST"});
// 이전 액션: null
// { value: undefined, done: false}
watch.next({ type: "HELLO"});
// 이전 액션: {type: "TEST"}
// 안녕
// {value: undefined, done: false}
위 코드는 Redux-saga가 실제로 action을 어떻게 캐치하고 구분하는지를 비슷하게 흉내낸 코드이다.
앞서 Generator란 무엇일까? 포스팅에서 Generator안에서 while(true)
를 사용하면 무한으로 사용가능한 로직을 만들 수 있다고 했다. Redux-saga에서는 실제로 while(true)
를 사용하여 지속적으로 action을 모니터링 하고 action이 발생하면 해당하는 로직을 수행한다.
redux-saga의 헬퍼 함수
delay
설정된 시간 이후에 resolve
를 하는 Promise
객체를 리턴한다.
put
특정 액션을 dispatch
한다. (ex: put({type: 'INCREMEMT})
)
call
주어진 함수를 실행한다. (ex: call(delay, 1000)
)
미들웨어가 Promise
의 resolve
를 기다리게 하기 때문에 동기함수(주로 api 호출)에 사용한다.
take
들어오는 특정 액션을 처리한다. 한번 실행되고 이벤트가 삭제된다.
takeEvery
모든 리퀘스트에 대해 task를 실행한다.
function* watchFetchData(){
yield takeEvery('FETCH_REQUESTED', fetchData)
}
만약 fetchData
task가 시작되었을때 이미 이전 task가 실행중이라면, 이전 task는 자동으로 취소된다.
fork
백그라운드에서 task가 실행된다.
function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if (token) {
yield call(Api.storeItem, {token})
yield take('LOGOUT')
yield call(Api.clearItem, 'token')
}
}
}
위와 같은 로그인 로직이 있다. 이 로직은 로그인을 하고('LOGIN_REQUEST'
), 사용자 인증을 거치면 'LOGOUT'
task를 기다린다.
만약 'LOGIN_REQUEST'
가 실행되고, token을 받아오는 중에 사용자가 'LOGOUT'
task를 실행한다면 어떻게 될까? 'LOGOUT'
task는 무시된다. call
은 봉쇄(blocking) effect라서 호출이 종료되기 전까지는 아무것도 수행할 수 없기 때문이다.
function* loginFlow() {
while (true) {
...
try {
// non-blocking call, what's the returned value here ?
const ?? = yield fork(authorize, user, password)
...
}
...
}
}
이럴때 fork
를 사용해주면, task는 백그라운드에서 시작되고, 호출자는 fork
된 task가 종료될 떄까지 기다리지 않고 플로우를 계속해서 진행한다.
❗ 단, fork
는 백그라운드에서 실행되기 때문에 token을 받아올 수 없다. 이럴 경우에는 token을 authorize
안에서 받아와야한다.
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
...
} catch(error) {
...
}
}
function* loginFlow() {
while (true) {
...
yield fork(authorize, user, password)
...
}
}
그러면 위와 같은 로직이 된다.
( 만약 finally
구간에서 제너레이터가 취소된건지 확인이 필요하다면 yield cancelled()
으로 확인가능하다. )
cancle
fork
된 task를 취소시킨다. (ex: yield cancel(task)
)
제너레이터를 finally
구간으로 가게한다. 이때 취소한 task 하위에 다른 task가 포함되어 있다면 모두 취소된다.
all
이 함수를 사용해서 제너레이터 함수를 배열의 형태로 넣어주면, 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 resolve
될때까지 기다린다. (Promise.all
과 비슷하다.)
📌
https://mskims.github.io/redux-saga-in-korean/
https://kyounghwan01.github.io/blog/React/redux/redux-saga/#위-코드의-전체적인-실행-과정