레시피

쓰로들링

throttle 헬퍼 함수를 사용해서 dispatch 된 액션들에 쓰로들링을 할 수 있습니다.

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

function* handleInput(input) {
  // ...
}

function* watchInput() {
  yield throttle(500, 'INPUT_CHANGED', handleInput)
}

throttle 헬퍼(helper) 함수를 사용하면 watchInput는 0.5초동안 handleInput 작업을 새로 수행하지 않습니다. 동시에 가장 최신의 INPUT_CHANGED 액션을 buffer에 넣습니다. 하여 0.5초의 지연 주기 사이에 발생하는 INPUT_CHANGED 액션들은 모두 놓치게 됩니다. Saga는 0.5초의 지연 시간 동안 최대 하나의 INPUT_CHANGED 액션을 수행하고 후행 액션을 처리 할 수 있도록 보장합니다.

디바운싱(Debouncing)

내장 헬퍼(helper) 함수인 delay를 fork된 작업(아래 예제에서는 handleInput)에 넣음으로써 디바운스(debounce)를 부여할 수 있습니다.


import { delay } from 'redux-saga'

function* handleInput(input) {
  // 500ms마다 지연
  yield call(delay, 500)
  ...
}

function* watchInput() {
  let task
  while (true) {
    const { input } = yield take('INPUT_CHANGED')
    if (task) {
      yield cancel(task)
    }
    task = yield fork(handleInput, input)
  }
}

delay 함수는 promise를 사용하여 간단한 디바운스(debounce)를 구현합니다.

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))

위 예제에서 handleInput은 로직을 수행하기 전에 0.5초를 기다립니다. 만약 유저가 그 0.5초 동안에 무언가를 타이핑한다면 더 많은 INPUT_CHANGED 액션을 얻게 됩니다. handleInput은 호출된 delay 함수에 의해 차단 될 것이기 때문에, 로직을 수행하기 전에watchInput에 의해 취소 될 것입니다.

위 예제는 또다른 헬퍼(helper) 함수인 takeLatest를 적용하여 다시 작성할 수 있습니다.


import { delay } from 'redux-saga'

function* handleInput({ input }) {
  // 500ms마다 
  yield call(delay, 500)
  ...
}

function* watchInput() {
  // 현재 실행중인 handleInput 작업을 취소합니다.
  yield takeLatest('INPUT_CHANGED', handleInput);
}

XHR호출 재시도(Retrying XHR calls)

특정 시간 동안 XHR 호출을 재시도하려면 지연(delay)이 있는 for 루프를 사용해야 합니다.


import { delay } from 'redux-saga'

function* updateApi(data) {
  for(let i = 0; i < 5; i++) {
    try {
      const apiResponse = yield call(apiRequest, { data });
      return apiResponse;
    } catch(err) {
      if(i < 5) {
        yield call(delay, 2000);
      }
    }
  }
  // 시도가 5x2초 후에 실패했습니다.
  throw new Error('API request failed');
}

export default function* updateResource() {
  while (true) {
    const { data } = yield take('UPDATE_START');
    try {
      const apiResponse = yield call(updateApi, data);
      yield put({
        type: 'UPDATE_SUCCESS',
        payload: apiResponse.body,
      });
    } catch (error) {
      yield put({
        type: 'UPDATE_ERROR',
        error
      });
    }
  }
}

위 예제에서 apiRequest는 각각 2초의 지연시간을 가지고 5번 다시 시도됩니다. 5번째 실패 후에 던져진(thrown) 예외는 부모 사가(parent saga)에 의해 catch되고, 부모 사가는 'UPDATE_ERROR` 액션을 전달(디스패치, dispatch)합니다.

만약 무제한으로 제시도하기를 원한다면, for 반복문을 while (true)로 대체하면 가능합니다. 또한 take대신에 takeLatest를 사용하면 마지막 요청만 재시도할 수 있습니다. 에러 핸들링에서 UPDATE_RETRY액션을 추가하면, 업데이트를 성공적으로 마치지 못했으며 다시 시도 할 것임을 유저에게 알릴 수 있습니다.

import { delay } from 'redux-saga'

function* updateApi(data) {
  while (true) {
    try {
      const apiResponse = yield call(apiRequest, { data });
      return apiResponse;
    } catch(error) {
      yield put({
        type: 'UPDATE_RETRY',
        error
      })
      yield call(delay, 2000);
    }
  }
}

function* updateResource({ data }) {
  const apiResponse = yield call(updateApi, data);
  yield put({
    type: 'UPDATE_SUCCESS',
    payload: apiResponse.body,
  });
}

export function* watchUpdateResource() {
  yield takeLatest('UPDATE_START', updateResource);
}

실행 취소(Undo)

실행 취소 기능은 '사용자가 자신이 무엇을 하고 있는지를 모르는 상황'을 가정합니다. 그 가정 아래 자연스럽게 이후 액션을 발생시켜 사용자의 선택을 존중합니다.(참고: GoodUI)

redux documentationpast, present, future 상태(state)를 담고 있는 리듀서(reducer)를 수정하는 것을 기본으로 실행 취소를 구현하는 강력한 방법을 기술합니다. 심지어 redux-undo라는 라이브러리도 제공합니다. 이 라이브러리는 개발자에게서 무거운 짐을 덜어주기 위해 고차원의 리듀서를 만들어줍니다. 하지만, 이 방법은 응용 프로그램의 이전 상태에 대한 '저장된' 레퍼런스(past)를 전달(overheard)받는 것입니다.

리덕스 사가의 delayrace를 사용하면, 리듀서를 고도화하거나 이전 상태(state)를 저장하지 않고도 한번의 실행 취소를 간단하게 구현할 수 있습니다.

import { take, put, call, spawn, race } from 'redux-saga/effects'
import { delay } from 'redux-saga'
import { updateThreadApi, actions } from 'somewhere'

function* onArchive(action) {

  const { threadId } = action
  const undoId = `UNDO_ARCHIVE_${threadId}`

  const thread = { id: threadId, archived: true }

  // 실행취소 UI 요소를 보여줍니다. 그리고 커뮤니케이션을 위한 키(key)를 제공합니다.
  yield put(actions.showUndo(undoId))

  // 낙관적으로, 쓰레드를 `archived`로 표시해둡니다.
  yield put(actions.updateThread(thread))

  // 사용자가 5초 동안 실행 취소를 수행할 수 있게 합니다.
  // 5초가 지나면, 'archive'가 race의 최종 승자가 됩니다.
  const { undo, archive } = yield race({
    undo: take(action => action.type === 'UNDO' && action.undoId === undoId),
    archive: call(delay, 5000)
  })

  // 실행 취소 UI 요소를 감춥니다. race의 최종 답안(answer)이 있을 것입니다. 
  yield put(actions.hideUndo(undoId))

  if (undo) {
    // 답안이 undo이면 쓰레드를 이전 상태로 되돌립니다.
    yield put(actions.updateThread({ id: threadId, archived: false }))
  } else if (archive) {
    // 답안이 archive이면, API를 호출하여 변경 사항을 원격으로 적용합니다.
    yield call(updateThreadApi, thread)
  }
}

function* main() {
  while (true) {
    // ARCHIVE_THREAD가 발생할때까지 기다립니다.
    const action = yield take('ARCHIVE_THREAD')
    // onArchive를 실행하기 위해 비차단(non-blocking) 방식으로 `spawn`을 사용합니다.
    // 이는 메인 사가가 취소되었을 때, onArchive도 함께 취소되는 것을 방지합니다.
    // 이는 서버와 클라이언트 간 상태(state)가 동일하게 유지되도록(동기화하도록) 돕습니다.
    yield spawn(onArchive, action)
  }
}

results matching ""

    No results matching ""