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를 도입하는 이유
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값으로 저장하는 것을 권장하지 않는다.
이때 redux-saga를 사용하면, callback값을 redux-saga에서 처리해주기 때문에 action payload로 callback을 넘길 수 있게 된다.
Redux-Saga 동작 원리
Redux-saga는 제너레이터 함수 문법을 기반으로 비동기 작업을 관리한다.
Redux-saga는 우리가 dispatch하는 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을 어떻게 캐치하고 구분하는지를 비슷하게 흉내낸 코드다.
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과 비슷하다.)
'🍞 Front-End > React' 카테고리의 다른 글
[React] React-query에 대해서 알아보자 (0) | 2023.01.05 |
---|---|
[React] React-slick 캐러셀 구현 모듈 (0) | 2023.01.04 |
[React] 구독하기(팔로워, 팔로우) 기능 (0) | 2022.12.29 |
[React] 비디오 업로드(multipart/form-data) 기능 (0) | 2022.12.29 |
[React] Expected an assignment or function call and instead saw an expression no-unused-expressions 오류 (0) | 2022.12.28 |