리액트 학습 노트 (중급) 5일차 정리

아놀드·2021년 8월 11일
0
post-thumbnail

리덕스 사가

비동기 요청을 동기적인 코드로 작성하게끔 지원하는 리덕스 미들웨어이다.

제네레이터 함수를 활용하여 여러 기능을 제공한다.

ex) 액션 이벤트 리스너 등록, 디바운싱, 채널을 활용한 동일한 비동기 액션에 대해 로드 밸런싱 등등...

리덕스 사가 함수

리덕스 사가 함수를 작성할 땐 제네레이터 함수로 작성하는데

감시자(The watcher) 함수워커(The worker) 함수로 나누어 작성한다.

감시자 함수는 특정 액션이 dispatch되는 순간을 캐치해서 워커 함수를 실행하고

워커 함수는 액션에 대해 테스크를 처리하고 종료한다.

예시

// 감시자
function* watcher() {
  while (true) {
    const action = yield take(ACTION)
    yield fork(worker, action.payload)
  }
}

// 워커
function* worker(payload) {
  // ... do some stuff
}

리덕스 사가의 이펙트 함수

이펙트 함수들은 사가 함수 내부에서 호출하며

모두 순수한 객체를 리턴한다.

리턴된 객체를 받는 사가 미들웨어는 이펙트의 타입에 따라

명령을 실행하고 사가 함수를 다시 작동시킨다.

마치 리덕스가 action과 action에 대한 처리를 분리한 것처럼

리덕스 사가는 이펙트 생성과 이펙트 실행을 분리했다.

put

사가 미들웨어에게 디스패치를 하라는 이펙트를 보낸다.

예시)

put({type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }

사가 미들웨어는 이펙트 객체 안에 있는 액션을 받아
내부적으로 next함수를 호출해서 다음 미들웨어나 reducer에게 넘긴다.

call

사가 미들웨어에게 함수를 블로킹하게 실행하라는 이펙트를 보낸다.

예시)

function* fetchItemWorker(action) {
    const items = yield call(fetchItemApi, action.payload); // => { CALL: {fn: fetchItemApi, args: [action.payload]}}
    yield put({ type: 'ITEMS_RECEIVED', items });
}

사가 미들웨어는 이펙트 객체가 CALL 타입이기 때문에
이펙트 객체 안에 있는 함수와 인수들을 받아 실행시킨다음 결과값을 다시 사가에게 넘겨준다.

-사가 미들웨어 내부 구현 상상도-

const callEffect = yield fetchItemGen.next().value.CALL;
yield callEffect.fn(...callEffect.args).then(fetchItemGen.next);

fetchItemApi를 호출해 받아온 값을 fetchItemGen.next 함수에 넘겨준다.
그러면 fetchItemWorker 함수 내부에 items라는 변수에 담기게 된다.
마치 await같은 느낌을 준다.
사가 미들웨어도 CALL 이펙트 객체를 받으면 스스로 블로킹이 되어야하기 때문에
제네레이터 함수로 구현돼있지 않을까 추측한다.

fork

사가 미들웨어에게 함수를 논블로킹하게 실행하라는 이펙트를 보낸다.
그러면 사가 미들웨어는 받은 함수를 백그라운드에서 실행시킨다음 테스크 객체를

gen.next(task)

로 넘겨준다.

예시)

// 워커
function* logWorker() {
    yield call(delay, 3000);
    yield call(console.log, '3초 뒤에 콘솔 로그');
}

// 감시자
function* logWatcher() {
    while (yield take('LOG_EVENT')) {
    	const task1 = yield fork(logWorker);
    	const task2 = yield fork(logWorker);
    }
}

// 사가 미들웨어
const forkEffect = yield logGen.next().value.FORK;
yield logGen.next({ id: forkEffect.fn(...forkEffect.args) });

// ... 이하 생략

이 예시는 fork 이펙트를 보냈기 때문에
3초 뒤에 '3초 뒤 콘솔 로그' 메세지를 콘솔에 두 번 찍는다.

만약에 fork대신 call 이펙트를 보냈다면
사가 미들웨어는 블로킹하게 함수 처리를 해주기 때문에
3초 뒤에 '3초 뒤 콘솔 로그' 메세지를 콘솔에 찍고
6초 뒤에 '3초 뒤 콘솔 로그' 메세지를 콘솔에 찍는다.

사실 자바스크립트는 싱글 쓰레드 기반으로 돌아가기 때문에
백그라운드 실행은 불가능하다.
하지만 사가 미들웨어는 비동기적인 호출로 새로운 쓰레드를 만드는 효과를 일으킨다.
fork 이펙트를 쓸 땐 백그라운드에 새로운 쓰레드가 생성돼서 테스크가 진행된다고 상상하는 게
사가를 이해하는데 더 도움이 된다.

take

사가 미들웨어에게 특정 액션의 디스패치를 감지하라는 이펙트를 보낸다.

예시)

function* watchFirstThreeTodosCreation() {
    for (let i = 0; i < 3; i++) {
        const action = yield take('TODO_CREATED')
    }
    yield put({type: 'SHOW_CONGRATULATION'})
}

처음 세 번의 TODO_CREATED 액션 후에, SHOW_CONGRATULATION 액션을 디스패치하고 종료된다.

takeEvery

사가 미들웨어에게 특정 액션에 대해 worker 사가를 병렬로 모두 실행하라는 이펙트를 보낸다.

예시)

function* fetchData(action) {
   try {
      const data = yield call(Api.fetchUser, action.payload.url)
      yield put({type: "FETCH_SUCCEEDED", data})
   } catch (error) {
      yield put({type: "FETCH_FAILED", error})
   }
}

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

FETCH_REQUESTED 액션이 여러번 dispatch되어도 이전에 실행된 fetchData 테스크들이
종료되지 않았더라도 새로운 fetchData 테스크를 실행한다.

takeEvery 구현체

function* takeEvery(pattern, saga, ...args) {
  const task = yield fork(function* () {
    while (true) {
      const action = yield take(pattern)
      yield fork(saga, ...args.concat(action))
    }
  })
  return task
}

특정 액션에 대해 새로운 쓰레드를 생성해서 백그라운드에서 병렬로 계속 실행시키는 로직이다.

takeLatest

사가 미들웨어에게 특정 액션이 여러번 dispatch 됐을 때 마지막으로 dispatch 된
action에 대해서만 worker 사가를 실행시키라는 이펙트를 보낸다.

예시)

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

이젠 여러번 FETCH_REQUESTED 액션이 dispatch 되어도
마지막으로 시작된 테스크만 실행하고 이전 테스크는 취소시킨다.
디바운싱 비슷한 효과를 낸다.

takeLatest 구현체

function* takeLatest(pattern, saga, ...args) {
  const task = yield fork(function* () {
    let lastTask
    while (true) {
      const action = yield take(pattern)
      if (lastTask)
        yield cancel(lastTask) // cancel is no-op if the task has already terminated

      lastTask = yield fork(saga, ...args.concat(action))
    }
  })
  return task
}

특정 액션에 대한 테스크 객체가 있을 땐 이전 테스크를 취소하고
새로운 테스크를 실행한다.

cancel

사가 미들웨어에게 fork된 테스크를 취소하라는 이펙트를 보낸다.
그러면 사가 미들웨어는 백그라운드에서 돌고있는 task를 종료시킨다.

all

사가 미들웨어에게 나열한 테스크들을 병렬로 실행하라는 이펙트를 보낸다.

예시)

// correct, effects will get executed in parallel
const [users, repos]  = yield all([
  call(fetch, '/users'),
  call(fetch, '/repos')
]);

모든 이펙트들이 resolve 되거나, 어느 하나라도 reject될 때까지 봉쇄된다.
Promise.all의 효과와 비슷하다.

기타 이펙트

Promise.race 효과를 가진 race 이펙트와
특정 액션을 dispatch된 순서대로 실행하는 actionChannel 이펙트,
외부 이벤트와 통신하는 eventChannel 이펙트,
특정 액션을 로드밸런싱하게 실행하는 channel 이펙트가 있다.

쓰게 된다면 추후에 다시 정리하겠다.

root 사가를 작성하는 법

// 상품을 가져오는 워커
function* fetchItemWorker(action) {
    const items = yield call(API.fetchItem, action.payload);
    yield put({ type: 'ITEM_RECEIVED', items });
}

// 상품을 가져오라는 액션을 감지하는 감시자
function* watchFetchItem() {
    yield takeEvery('FETCH_ITEMS', fetchItemWorker);
}

// 영화를 가져오는 워커
function* fetchMovieWorker(action) {
    const movies = yield call(API.fetchMovie, action.payload);
    yield put({ type: 'MOVIES_RECEIVED', movies });
}

// 영화를 가져오라는 액션을 감지하는 감시자
function* watchFetchMovie() {
    yield takeEvery('FETCH_MOVIES', fetchItemWorker);
}

// all 이펙트와  fork 이펙트를 통해 
// 상품을 가져오는 이벤트와 영화를 가져오는 이벤트를 백그라운드에서 계속 실행시켜 대기시킨다.
export default function* rootSaga() {
    yield all([
        fork(watchFetchItem),
        fork(watchFetchMovie)
    ]);
}

사가 쪼개기

itemSaga.js

// 상품을 가져오는 워커
function* fetchItemWorker(action) {
    const items = yield call(API.fetchItem, action.payload);
    yield put({ type: 'ITEM_RECEIVED', items });
}

// 상품을 가져오라는 액션을 감지하는 감시자
function* watchFetchItem() {
    yield takeEvery('FETCH_ITEMS', fetchItemWorker);
}

// 상품을 삭제하는 워커
function* deleteItemWorker(action) {
    const status = yield call(API.deleteItem, action.payload);
    yield put({ type: 'DELETE_ITEM_SUCCESS', status });
}

// 상품을 삭제하라는 액션을 감시하는 감시자
function* watchDeleteItem() {
    yield takeEvery('DELETE_ITEM', deleteItemWorker);
}

// 병렬로 백그라운드에 모두 등록한다.
export default function* itemSaga() {
    yield all([
        fork(watchFetchItem),
        fork(watchDeleteItem),
    ]);
}

movieSaga.js

// 영화를 가져오는 워커
function* fetchMovieWorker(action) {
    const movies = yield call(API.fetchMovie, action.payload);
    yield put({ type: 'MOVIES_RECEIVED', movies });
}

// 영화를 가져오라는 액션을 감지하는 감시자
function* watchFetchMovie() {
    yield takeEvery('FETCH_MOVIES', fetchItemWorker);
}

// 영화를 삭제하는 워커
function* deleteMovieWorker(action) {
    const status = yield call(API.deleteMovie, action.payload);
    yield put({ type: 'DELETE_MOVIE_SUCCESS', status });
}

// 영화를 삭제하라는 액션을 감시하는 감시자
function* watchDeleteMovie() {
    yield takeEvery('DELETE_MOVIE', deleteMovieWorker);
}

// 병렬로 백그라운드에 모두 등록한다.
export default function* movieSaga() {
    yield all([
        fork(watchFetchMovie),
        fork(watchDeleteMovie),
    ]);
}

rootSaga.js

import itemSaga from './item';
import movieSaga from './movie';

// 병렬로 백그라운드에 모두 등록한다
export default function* rootSaga() {
    yield all([
        fork(itemSaga),
        fork(movieSaga),
    ]);
}

참고 자료 - https://mskims.github.io/redux-saga-in-korean/

profile
함수형 프로그래밍, 자바스크립트에 관심이 많습니다.

0개의 댓글