[Redux] Redux-Saga

mokyoungg·2020년 10월 10일
1

Redux

목록 보기
6/7

출처는 공식 페이지와 인프런 [리뉴얼] React로 NodeBird SNS 만들기, PoiemaWeb입니다.
공식 페이지 : https://mskims.github.io/redux-saga-in-korean/
인프런 : https://www.inflearn.com/
PoiemaWeb : https://poiemaweb.com/

redux-saga는 redux-thunk와 같은 middleware이다.
즉, redux의 기능을 확장(새로운 기능을 추가)하는 라이브러리이며
redux-thunk와 마찬가지로 네트워크 요청을 만드는데 도움이 되는 미들웨어이다.

그렇다면 redux-thunk와 redux-saga의 차이는 무엇인가?



1. Redux-saga의 특징

reux-saga는 리액트/리덕스 애플리케이션의 사이드 이펙트, 예를 들면 데이터 fetching이나 브라우저 캐시에 접근하는 순수하지 않은 비동기 동작들을, 더 쉽고 좋게 만드는 것을 목적으로 하는 라이브러리 입니다.(중략)
이 라이브러리는 비동기 흐름을 쉽게 읽고, 쓰고, 테스트 할 수 있게 도와주는 ES6의 피쳐인 Generator를 사용합니다. Generator를 사용함으로써, 비동기 흐름은 표준 동기식 자바스크립트 코드처럼 보이게 됩니다.

redux-thunk와는 대조적으로, 콜백 지옥에 빠지지 않으면서 비동기 흐름들을 쉽게 테스트할 수 있고 액션들을 순수하게 유지합니다.

출처: https://mskims.github.io/redux-saga-in-korean/

redux-saga의 특징은 다음과 같다.

  • 비동기 흐름을 쉽게 테스트할 수 있다.
  • Generator를 사용한다.
  • 콜백 지옥을 벗어나게 한다.
  • redux-saga의 다양한 effect의 도움을 받을 수 있다.

2. Generator란 무엇인가?

redux-saga는 Generator 함수의 특징을 사용한다.

Generator는 빠져나갔다가 나중에 다시 돌아올 수 있는 함수입니다.
이때 컨텍스트(변수 값)는 출입 과정에서 저장된 상태로 남아 있습니다.
Generator 함수는 호출되어도 즉시 실행되지 않고, 대신 함수를 위한 Iterator 객체가 반환됩니다.
Iterator의 next() 메서드를 호출하면 Generator 함수가 실행되어 yield 문을 만날 때까지 진행되고, 해당 표현식이 명시하는 Iterator로부터의 반환값을 반환합니다. (중략)
이후 next() 메서드가 호출되면 진행이 멈췄던 위치에서부터 재실행합니다.
next()가 반환하는 객체는 yield문이 반환할 값을 나타내는 value 속성과, Generator 함수의 모든 yield문의 실행 여부를 표시하는 boolean 타입의 done 속성을 갖습니다. next()를 인자값과 함께 호출할 경우, 진행을 멈췄던 위치의 yield문을 next() 메서드에서 받은 인자값으로 치환하고 그 위치에서 다시 실행하게 됩니다.

출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*

  • 제너레이터 함수는 일반 함수와 같이 함수의 코드 블록을 한 번에 실행하지 않는다.
  • 제너레이터 함수는 함수 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재시작할 수 있는 함수.
  • 함수 내부에서 중단점이 있으며 이 기능을 활용한 것이 redux-saga이다.
  • 중단점(yield)이 있어 비동기 처리를 동기처럼 구현할 수 있다.
  • 중단점(yield)이 있어 코딩 테스트에 적합하다.

2-1. generator 함수 예시 코드

예시 1)

//제너레이터 함수 선언. function* 을 사용
const 예시함수 = function* (){
  console.log('Ponint 1');
  yield 1;		  //첫번째 next 메서도 호출 시 여기까지 실행된다.
  console.log('Point 2');
  yield 2;		  //두번째 next 메서도 호출 시 여기까지 실행된다.
  console.log('Point 3');
  yield 3;		  //세번째 next 메서도 호출 시 여기까지 실행된다.
  console.log('Point 4'); //네번째 next 메서도 호출 시 여기까지 실행된다.
}

//첫번째 next 메소드 호출 : 첫번째 yield 문까지 실행되고 중단.
console.log(예시함수.next())
// Point 1
// {value: 1, done: false}

//두번째 next 메소드 호출 : 두번째 yield 문까지 실행되고 중단.
console.log(예시함수.next())
// Point 2
// {value: 2, done: false}

//세번째 next 메소드 호출 : 세번째 yield 문까지 실행되고 중단.
console.log(예시함수.next())
// Point 3
// {value: 3, done: false}

//네번째 next 메소드 호출 : 함수내 모든 yield 문이 실행되면 done 프로퍼티 값은 true가 도니다.
console.log(예시함수.next())
// Point 1
// {value: 4 done: true

출처 : https://poiemaweb.com/es6-generator

예시 2)

let i =0
//제너레이터 함수 선언, function*을 사용
const 예시함수 = function*() {
  //ture일 때 계속 작동하는 무한 함수
  while(true) {
  // yield에서 멈춤
  yield i++
  }
}

예시함수.next() // 1
예시함수.next() // 2
.... 
무한으로 작동하지 않고 next()를 호출할 때만 작동한다.

출처 : [리뉴얼] React로 NodeBird SNS 만들기

  • 제너레이터 함수는 function* 을 사용하여 선언한다.
  • next 메소드를 호출하면 yield문을 만날 때까지 실행되고 중단된다.

3. Redux-saga의 주요 effects

redux-saga를 쓰는 다양한 이유 중 하나는..
redux-thunk를 사용했을 때 추가적으로 코딩해야 할 기능들을 effect의 도움을 받아 쉽게 구현할 수 있다는 점이다. 예를 들어 몇초 뒤에 액션을 실행해야 할 경우, redux-thunk는 setTimeout을 사용하여 코딩해야하지만 redux-saga에선 같은 기능을 하는 delay를 사용하여 쉽게 구현할 수 있다.

redux-saga에서, Saga들은 제너레이터 함수들을 사용해서 구현되었습니다. Saga 로직을 표현하기 위해서 우리는 제너레이터로부터 온 순수 자바스크립트 객체를 yield합니다. 이런 오브젝트들을 이펙트라고 부릅니다. 이펙트는 미들웨어에 의해 해석되는 몇몇 정보들을 담고있는 간단한 객체입니다. 어떤 기능을 수행하기 위해 미들웨어에 전해지는 명령(스토어에 액션을 dispatch 하는 행위나 비동기 함수를 호출하는 등)이라고 볼수 있죠. 이펙트들을 만들기 위해서, redux-saga/effects 패키지에 있는 라이브러리들일 제공하는 함수들을 사용합니다.

출처 : https://mskims.github.io/redux-saga-in-korean/basics/DeclarativeEffects.html

3-1. takeEvery / takeLatest

  • takeEvery / takeLatest 는 액션의 반환을 보는 이벤트 리스너의 기능.
  • 따라서 redux-saga 작동에서 가장 먼저 일어남.
  • 요청 방식에 따라 takeEvery와 takeLatest로 나뉨.

takeEvery와 takeLatest 는 redux-thunk와 비슷한 기능을 제공한다고 한다.(공식페이지에서..)
redux-thunk는 함수인지 아닌지 판단하고 함수이면 순수한 객체가 나올 때까지 함수를 실행,
이후 객체가 반환되면 이를 dispatch 하는 것으로 알고 있다. 왜 비슷하다고 하는지 아직 모르겠다.
(네트워크 요청을 위해 사용하는 것이기 때문인가?)

아무튼 takeEvery와 takeLatest에 대해 내가 인프런을 통해 이해하는 것은
이 이펙트가 eventListener 와 같은 역할을 한다는 것이다.
store에 action이 반환이(event) 되면 이를 확인하고 동작한다.(listener)
따라서 saga가 작동할 때 가장 먼저 일어나는 부분이다.

takeEvery와 takeLatest의 차이는 action의 반환이 여러번 일어날 때
takeEvery는 그 모든 반환에 대해 기능하고 takeLatest는 제일 마지막의 반환에 대해서만 기능한다는 점이다.

(action의 반환이라는 것은 네트워크의 요청이 일어난 것.)

takeLatest는 마지막의 요청만 인정하나, 이는 프론트엔드측에서만 해당된다.
네트워크를 요청하는 이벤트를 100번 발생했을 때 마지막 100번째 요청만 인정하고 99번의 요청에 대한 답은 거부한다.
그러나 백엔드(서버)에는 100번의 요청을 받음. 그리고 99번의 요청을 거절하지만 이미 완료가 되었다면 실행된다.
예를 들어.. 100개의 데이터를 받는 1번의 요청을 100번 하였을 때, 99번 요청쯤에서 20개의 데이터를 받았다면 이는 남겨두고 요청을 한다는 것이다.

예시 코드(src/sagas/index.js)

1) redux-saga/effects 에서 가져온다.
import { takeEvery, takeLastest } from 'redux-saga/effects'

2) 제너레이터 함수 fetchData
function* fetchData() {
  yield 생략
}

3) 제너레이터 함수 watchFetchData
function* watchFecthData() {
  // 'FETCH_REQUESTED' 액션이 반환되면, fetchData 함수 실행
  yield takeEvery('FETCH_REQUESTED', fetchData) //takeLatest도 같은 방식으로 작성.
}

순서는 1 - 3 - 2

  • redux-saga/effects 에서 takeEvery(takeLatest)를 가져옴
  • watchFetchData 라는 제너레이터 함수를 작성한다.
  • 이 함수는 'FETCH_REQUESTED'라는 액션이 반환되었을 때
  • fetchData 라는 함수를 실행한다.
  • fetchData실행

3-2. call

제너레이터 안에서, fetch 요청과 같은 비동기 함수를 직접적으로 호출하지 않는다.
call 함수를 사용하며 이는 이펙트에 대한 설명을 생성한다. Redux에서와 마찬가지로, 스토어에 실행될 액션을 설명하는 순수 객체를 만들기 위해 액션 생성자들을 사용하고, call은 함수 호출을 설명하는 순수 객체를 생성한다. redux-saga 미들웨어는 함수 호출과 제너레이터를 resolve 된 응답과 함께 재가동 시킨다. call은 그저 순수 객체만 리턴하는 함수기 때문에 제너레이터를 Redux 환경 바깥에서 쉽게 테스트하게 만든다.

출처 : https://mskims.github.io/redux-saga-in-korean/basics/DeclarativeEffects.html

  • 비동기 함수에서 사용된다.
  • redux-saga 미들웨어의 기능으로, 응답과 함께 재가동 된다.
    .next()가 자동으로 된다는 말인 것 같다. 확실하지 않다.
  • 편한 테스트를 위해 사용된다.

예시 코드(src/sagas/index.js)

예시 1) call의 형태

// 이펙트 -> Api.fetch 함수를 './products' 인자와 함께 호출
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']
}

// 제너레이터 함수에선 다음과 같은 형태
call(fn(함수), ...args(인자))

예시 2) call의 사용

// 라이브러리에서 call을 가져옴
import { call } from 'redux'saga/effects'

// 제너레이터 함수 fetchData 작성
function* fetchData() {
  // call(함수, 인자)
  const response = yiled call(Api.fetch, '/products')
}

출처 : https://mskims.github.io/redux-saga-in-korean/basics/DeclarativeEffects.html

call과 fork(이 부분 잘 모름)

둘 다 함수를 호출하는 이펙트다. 그러나 fork는 비동기함수 호출이고 call은 동기함수 호출이라고 한다.
예를 들어 call을 하면 API가 리턴할 때까지 기다리는데(결과값을 기다림) fork 비동기라 요청을 보내고 결과 없이 다음 코드가 실행된다고 한다. 즉, call은 .then()과 같은 역할이고 yield는 await와 비슷하다.


3-3. put

스토어의 dispatch 함수를 제너레이터에게 넘기면, 제너레이터는 이 함수를 fetch 응답을 받을 후에 실행한다.
이는 제너레이터 내부에서 함수를 직접적으로 호출하는 것과 비슷한 단점이 있다.
미들웨어에게 어떤 액션을 distpatch 해야하는지 지시하는 객체를 만들고, 실제 dispatch는 미들웨어가 하도록 놔두자. 이렇게만 한다면 yield 된 이펙트를 검사하고 정확한 명령이 포함되어있는지 확인하는 것만으로 제너레이터의 dispatch를 테스트할 수 있다. 이런 목적들 때문에 dispatch 이펙트를 생성하는 put 함수를 제공한다.

출처 : https://mskims.github.io/redux-saga-in-korean/basics/DispatchingActions.html

  • dispatch의 역할을 한다.
  • call과 마찬가지로 테스트를 위해 사용된다.

예시 코드(src/sagas/index.js)

1
// redux-saga/effects에서 call 과 put을 가져온다.
import { call, put } from 'redux-saga/effects'

2
// 제너레이터 함수 선언
function* fetchProducts() {
  const product = yield call(Api.fetch, '/products')
 
  3
  //dispatch 이펙트를 생성하고 yield 한다.
  yield put({ type: 'PRODUCTS_RECEIVED', products })
  }

코드 순서 1 - 2 - 3

  • redux-saga/effets 에서 call와 put 이펙트를 가져온다.
  • 제너레이터 함수 fetchProducts를 선언한다.
  • call(함수, 인자)의 형태로
  • Api.fetch 함수를 실행하고 그 함수의 인자로 '/products'를 준다.
  • put을 통해
  • action 'PRODUCTS_REDCEIVED'와 call에서 받은 응답(product)를
  • dispatch 한다.

3-4. all

다양한 saga들을 모아 rootSaga를 만들 때 사용된다.
reducer의 combineReducers와 비슷한 역할을 한다.

예시 코드(src/sagas/index.js)

// redux-saga/effects 에서 all을 가져온다.
import { all ] from 'redux-saga/effects'

//모든 Saga들을 한번에 시작하기 위한 단일 entry point 입니다.
export default function* rootSaga() {
  yield all([
    Saga1(),
    Saga2()
  ])
}
  • all은 배열의 형태로 Saga들을 요소로 둔다.
  • all로 만들어진 rootSaga()는 미들웨어를 선언할 때 쓰인다.


Redux-Saga 에 대해 이해한 부분만 작성하였다.
redux-saga의 장점이라고 할 수 있는 코드 테스트 부분은 아직 잘 모르겠다.
yield와 이펙트들을 사용했기 때문에 코드 테스트에 강점이 있는데
이 부분을 모르니 완벽하게 redux-saga를 쓸 수 있다고는 할 수 없다.
그리고 redux-saga의 이펙트와 함수들을 구별하는 것도 아직 부족하고
작성한 이펙트외에 많은 이펙트들이 있는데 이것 역시 아직 공부하지 못 했다.

그냥 redux-saga가 어떻게 돌아가는지 정도만 이해한 정도이다.

profile
생경하다.

0개의 댓글