Pandaman Blog

[Redux-saga] Helper 함수 본문

Front end/Redux-Saga

[Redux-saga] Helper 함수

oyg0420 2021. 2. 13. 12:10

1. Helper 함수란

redux-saga에서는 Task의 생성을 위해 내부 함수를 감싸는 헬퍼 이펙트를 제공한다. 이 헬퍼 함수는 Lower level API 기반으로 만들어졌다. Lower level API 기반으로 만들어졌다고 하는데, 아래에서 헬퍼 함수의 종류와 이 헬퍼 함수가 어떻게 동작하는지 알아보자.

2. Helper 함수의 종류

1) takeEvery

takeEvery 헬퍼 함수는 디스 패치된 각 액션에 대해 사가를 실행한다고 한다. 다시 말해서 액션이 디스 패치될 때마다 비동기적으로 사가를 실행할 수 있다.
아래의 예제를 살펴보자.

import { takeEvery } from 'redux-saga/effects'

function* watchFetchData() {
  yield takeEvery('FETCH_REQUESTED', fetchData)
}

FETCH_REQUESTED과 일치하는 액션이 발생하면 fetchData Task가 실행된다. 또다시 FETCH_REQUESTED 액션이 발생하면 fetchData 가 종료되지 않았더라도, 새로운 fetchData가 실행된다. 다시 말해서 한 개 혹은 여러 개의 아직 종료되지 않은 태스크들이 있더라도 새로운 태스크를 시작할 수 있다.

2) takeLatest

takeLatest는 각 액션에 대해 사가를 실행하는데, 실행 중인 사가 Task가 존재한다면 Task을 취소하고 새로운 사가 Task를 실행한다.

예제를 통해 알아보자.

import { takeLatest } from 'redux-saga/effects'

function* watchFetchData() {
  yield takeLatest('FETCH_REQUESTED', fetchData)
}

FETCH_REQUESTED과 일치하는 액션이 발생하면 fetchData Task가 실행된다. 또다시 FETCH_REQUESTED 액션이 발생하면 fetchData 가 실행 중이라면 현재 실행 중인 사가 Task를 종료한다. 그리고 새로운 fetchData Task를 실행한다.
takeLatest는 마지막에 요청된 데이터(최신의 데이터)를 보여줄 때 사용합니다.

3. Helper 함수의 동작

Helper 함수는 어떻게 동작이 되는 것일까?라는 생각으로 redux-saga 오픈 소스를 확인했다.

1) takeEvery

export function takeEvery(patternOrChannel, worker, ...args) {
  if (process.env.NODE_ENV !== 'production') {
    validateTakeEffect(takeEvery, patternOrChannel, worker)
  }

  return fork(takeEveryHelper, patternOrChannel, worker, ...args)
}

fork라는 함수가 실행되는데 이는 takeEveryHelper를 인자로 받는다. fork 함수의 역할이 무엇일까. API 문서를 확인해보자.
fork 함수는 첫 번째 인자(여기서는 takeEveryHelper)에 대해 non-blocking 호출을 수행하도록 미들웨어에게 지시하는 Effect Description을 반환한다고 한다. 이 말은 takeEveryHelper가 완료될 때까지 기다리지 않고 계속 수행하도록 미들웨어에게 지시, 즉 비동기적으로 실행하는 것을 지시하는 것이라고 볼 수 있다. 그리고 fork 함수의 결과를 Task 객체라고 한다.
자 이제, takeEveryHelper 함수를 살펴보자.

export default function takeEveryHelper(patternOrChannel, worker, ...args) {
  const yTake = { done: false, value: take(patternOrChannel) }
  const yFork = ac => ({ done: false, value: fork(worker, ...args, ac) })

  let action,
    setAction = ac => (action = ac)

  return fsmIterator(
    {
      q1() {
        return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
      },
      q2() {
        return { nextState: 'q1', effect: yFork(action) }
      },
    },
    'q1',
    `takeEvery(${safeName(patternOrChannel)}, ${worker.name})`,
  )
}

yTake, yFork에는 이터레이터의 Result 객체와 동일한 속성 done, value 포함한다. fsmIterator 함수의 next 메서드는 q1, q2 순서대로 q1, q2 함수에서 effect값을 갖는 이터레이터를 반환한다.

takeEveryHelper를 실행하자면 아래와 같이 실행할 수 있을 것 같다.

const gen = takeEveryHelper(...);
gen.next(); // { done: false, value: yTake };
gen.next(); // { done: false, value: yFork };

실제로 next메서드의 result 객체의 value를 통해서 미들웨어에게 지시하는 것을 알 수 있다.
아래의 코드는 interator의 next 메서드를 실행하는 코드이고, digestEffect는 Effect Description으로 middleware에 해당 Effect를 수행하도록 지시를 하는 함수이다.

const finalRunEffect = env.finalizeRunEffect(runEffect)
...

next()

...
function next(arg, isErr) {
    try {
       ...
      result = iterator.next(arg)
      if (!result.done) {
        digestEffect(result.value, parentEffectId, next)
      }
      ...

takeEveryHelper 함수에서는 take()가 실행되고 다음 fork()가 실행된다고 생각하면 된다.
아직 확인하지 못한 take에 대해 확인해보자. take는 미들웨어가 Store에서 지정된 작업을 기다리도록 지시하는 Effect Description을 만들고, Generator는 패턴과 일치하는 작업이 전달될 때까지 일시 중지된다고 한다.

우리는 위 내용을 토대로 takeEvery를 직접 구현해보자.

// takeEvery 사용
// function* watchFetchData() {
//  yield takeEvery('FETCH_REQUESTED', fetchData)
// }


// takeEvery 미사용
function* watchFetchDateHelper() {
  while(true) {
    yield take('FETCH_REQUESTED');
    yield fork(fetchData);
  }
}

function* watchFetchData() {
 yield fork(watchFetchDateHelper)
}
  1. fork 이팩트로 인해 watchFetchDateHelper Task가 시작된다.
  2. take 이팩트로 FETCH_REQUESTED Action이 Dispatch 되길 기다린다.
  3. FETCH_REQUESTED 액션을 dispatch 한다.
  4. fork 이팩트로 fetchData가 시작된다.
  5. take 이팩트로 FETCH_REQUESTED Action이 Dispatch 되길 기다린다.

2) takeLatest

takeLatest의 본래 코드를 살펴보자.


export function takeLatest(patternOrChannel, worker, ...args) {
  if (process.env.NODE_ENV !== 'production') {
    validateTakeEffect(takeLatest, patternOrChannel, worker)
  }

  return fork(takeLatestHelper, patternOrChannel, worker, ...args)
}

takeLatesttakeEvery와 마찬가지로 fork함수가 실행된다. takeLatestHelper함수를 살펴보자.

export default function takeLatestHelper(patternOrChannel, worker, ...args) {
  const yTake = { done: false, value: take(patternOrChannel) }
  const yFork = ac => ({ done: false, value: fork(worker, ...args, ac) })
  const yCancel = task => ({ done: false, value: cancel(task) })

  let task, action
  const setTask = t => (task = t)
  const setAction = ac => (action = ac)

  return fsmIterator(
    {
      q1() {
        return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
      },
      q2() {
        return task
          ? { nextState: 'q3', effect: yCancel(task) }
          : { nextState: 'q1', effect: yFork(action), stateUpdater: setTask }
      },
      q3() {
        return { nextState: 'q1', effect: yFork(action), stateUpdater: setTask }
      },
    },
    'q1',
    `takeLatest(${safeName(patternOrChannel)}, ${worker.name})`,
  )
}

takeEveryHelper함수와는 다른 점이 있다. 이미 눈치챘을 수 있겠지만, yCancel가 추가되었다. cancel(task)함수에 대해 API문서를 확인해보자. 이전에 fork 된 작업을 취소하도록 미들웨어에 지시하는 Effect description를 만든다고 한다.
fsmIterator의 첫 번째 인자 함수도 조금 다르다. task가 존재하는지에 따라 순서가 다르다.

  1. task가 없는 경우: q1 실행(take Effect) -> q2 실행(fork Effect) -> q1 실행(take Effect)
  2. task가 존재하는 경우: q1 실행(take Effect) -> q2 실행(cancel Effect) -> q3 실행(fork Effect) -> q1 실행(take Effect)

다시 말해서 실행 중인 task가 존재하면 이전 task를 취소하고 현재 task를 실행하는 것이다.

위의 예제에서 takeLatest를 직접 구현해보자.

// function* watchFetchData() {
//  yield takeLatest('FETCH_REQUESTED', fetchData)
// }


function* watchFetchDateHelper() {
  let latestTask;
  while(true) {
    yield take('FETCH_REQUESTED');
    if (latestTask) {
      yield cancel(latestTask);
    }
    latestTask = yield fork(fetchData);
  };
};

function* watchFetchData() {
  yield fork(watchFetchDateHelper);
}
  1. fork 이팩트로 인해 watchFetchDateHelper Task가 시작된다.
  2. take 이팩트로 FETCH_REQUESTED Action이 Dispatch 되길 기다린다.
  3. FETCH_REQUESTED 액션을 dispatch 한다.
  4. latestTask가 존재하지 않으면 바로 fork 이팩트로 fetchData가 시작된다. fork함수가 반환한 값을 latestTask에 할당한다.
  5. take 이팩트로 FETCH_REQUESTED Action이 Dispatch 되길 기다린다.
  6. FETCH_REQUESTED 액션을 dispatch 한다.
  7. latestTask가 존재하여, cancel 이팩트로 지난 Task인 latestTask를 취소한다.
  8. fork 이팩트로 fetchData가 시작된다. fork함수가 반환한 값을 latestTask에 할당한다.

3) takeLeading

takeLeading은 task가 완료될때까지 블록되고, task가 완료되면 action에 대해 수신한다.

takeLeadingtakeEvery, takeLatest와 동일하게 fork함수가 실행된다.
다음의 takeLeadingHelper 함수를 살펴보자.

export default function takeLeadingHelper(patternOrChannel, worker, ...args) {
  const yTake = { done: false, value: take(patternOrChannel) }
  const yCall = ac => ({ done: false, value: call(worker, ...args, ac) })

  let action
  const setAction = ac => (action = ac)

  return fsmIterator(
    {
      q1() {
        return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
      },
      q2() {
        return { nextState: 'q1', effect: yCall(action) }
      },
    },
    'q1',
    `takeLeading(${safeName(patternOrChannel)}, ${worker.name})`,
  )
}

takeEveryHelper와 다른점은 fork()call()로 변경되었다는 점이다.
API문서를 통해 call 이팩트가 무엇은 하는지 알아보자.
call(fn)은 fn이 이터레이터 객체인 경우 미들웨어는 Generator 함수를 실행한다. 상위 Generator는 하위 Generator가 종료될 때까지 일시 중지된다.
fn이 함수이고 프로미스를 반환한다면 promiseresolved되면 Generator가 재개된다.
fork와 다른점은 call이팩트는 task가 완료될때까지 기다렸다가 완료되면 generator가 재개된다는 점이 다르다.
따라서, takeLeading은 call 이팩트로 인해 task가 진행중이라면 다른 action에 대해 수신하지 않는다고 말하는 것이다.

지금까지 Helper 함수에 대해 알아보았습니다. 다음은 Effect에 대해 자세히 알아보겠습니다.

 

github.com/reactkr/learn-react-in-korean/blob/master/translated/deal-with-async-process-by-redux-saga.md

'Front end > Redux-Saga' 카테고리의 다른 글

[Redux-saga] pulling future actions  (0) 2021.02.18
[Redux-saga 배경지식]Saga란 무엇인가?  (0) 2021.01.28
Redux-Saga 란  (0) 2021.01.21
Comments