Redux-saga를 알아보자
Front-End/React

Redux-saga를 알아보자

 

💡 이 글을 읽기 전에 읽어보면 좋은 글

1️⃣ Symbol을 알아보자

2️⃣ for-of와 Iterator

3️⃣ Generator란 무엇일까?

 

 

 

 

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))

미들웨어가 Promiseresolve를 기다리게 하기 때문에 동기함수(주로 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/#위-코드의-전체적인-실행-과정