목록 보기
리덕스 사가(Redux Saga)란 무엇인가?
기타

리덕스 사가(Redux Saga)란 무엇인가?

트렌비
트렌비
2022년 5월 25일

리덕스(Redux) 리덕스(Redux)의 필요성 리액트(React)는 상태(State)를 가지고 어떻게 돔(DOM)으로 잘 변형할지에 대해 다루고 있습니다. 여기서 상태란 애플리케이션(Application)이 기본적으로 가지고 있는 아주 중요한 요소인데요. 애플리케이션의 규모가 커질수록 상태 즉 데이터를 관리하기가 매우 까다롭기 때문에 상태를 잘 다루고 실제 돔(DOM)으로 업데이트하기까지 이를 도와주는 라이브러리가 필요합니다. 리액트(React) 애플리케이션(Application)을 이루는 각각의 컴포넌트(Component) 요소들은 외부 상태를 공유받고 싶을 때 프롭스(Props)를 통해서만 전달받을 수 있습니다. 이러한 형태적인 특징 때문에 여러 개의 컴포넌트를 중첩시키고 조합시키면서 복잡한 계층 구조를 가지게 되는 상황에서는 상태(State)에 대한 흐름을 추적하기 어렵고 UI가 변경될 때마다 상태에 대한 의존성을 가진 컴포넌트들에 영향이 없는지 전달 구조를 일일이 확인해야 하는 등 관리하는 데 큰 어려움을 겪게 됩니다. 이러한 문제점을 극복하기 위해 상태를 전역적으로 관리하고 가공해 UI로 쉽게 변형할 수 있도록 도와주는 라이브러리인 리덕스(Redux)를 사용하게 되었습니다. 그렇다면 리덕스는 어떤 방식으로 상태를 관리하는지에 대해 간단하게 설명해드리겠습니다. 단방향으로 데이터가 흐르게 하는 Flux 아키텍처 아이디어를 잘 구현한 리덕스의 구조는 상태(State)를 저장하는 스토어(Store), 상태를 조작하는 리듀서(Reducer), 액션(Action)을 전달하는 디스패처(Dispatcher) 함수로 이뤄져 있습니다. *Flux에 대한 자세한 소개는 Flux 공식 사이트-개요 페이지를 참고해주세요 상태값을 저장하는 저장소 : 스토어(Store) 스토어(Store)는 모든 상태 값을 저장하며 상태 값을 조작할 리듀서(Reducer) 함수를 인자로 받습니다. 상태를 조작하는 함수 : 리듀서(Reducer) 리듀서(Reducer) 함수는 초기 상태 값과 액션(action)을 인자로 받아 액션에 조작할 상태(State)를 지정하는 역할을 합니다. 액션을 전달하는 함수 : 디스패처 함수(Dispatcher) 디스패처(Dispatcher) 함수는 액션 값과 상태에 관한 데이터를 리듀서(Reducer) 함수에 전달하는 역할을 합니다. 어떤 변화가 필요할 때 발생시키는 신호 객체 : 액션(Action) 어떤 데이터를 어떻게 바꿀 것이냐에 대한 일종의 신호를 의미합니다. 어떤 동작에 대해 선언된 액션(Action) 객체는 type이라는 필드를 변화가 필요한 상황을 인지하기위해 사용하며 이 객체에 변화할 때 필요한 데이터를 담을 수도 있습니다. 다음 이미지처럼 사용자에게 보이는 리액트(React) 컴포넌트(Component)에서 특정 이벤트 즉 액션(Action)이 발생하면 액션이 스토어(Store)에 디스패치(Dispatch) 됩니다. 스토어에서는 다시 리듀서(Reducer)에 액션을 전달하고 리듀서는 상태(State)를 가공해서 새로운 상태를 스토어로 전달하게 됩니다. 리덕스(Redux) 사용 시에는 따라야 할 세 가지 원칙이 있습니다. 리덕스를 사용하는 어플리케이션엔 하나의 스토어(Store)만이 존재할 것 상태(State)를 직접 변경할 수 없음, 상태를 변경하기 위해서는 액션(Action)이 디스패치(dispatch)되어야 할 것 리듀서(Reducer)는 순수 함수로 작성할 것 여기서 “스토어(Store)의 상태 변화를 다루는 리듀서(Reducer)는 순수 함수로 작성해야 한다”라는 원칙 때문에 리덕스 미들웨어(Redux Middleware)의 필요성이 생기게되었는데요. 리듀서에서 부수 효과(Side Effect)를 가진 함수를 실행할 경우 순수 함수 사용 원칙을 위배하게 됩니다. 그로 인한 문제를 사전에 방지하기 위해 리덕스는 리덕스 미들웨어를 통해 부수 효과를 처리하기를 원합니다. 리덕스 미들웨어(Redux Middleware) 미들웨어(Middleware)는 리덕스(Redux)의 기능을 확장하기 위한 수단으로, 디스패치(dispatch)함수를 감싸는 역할을 수행합니다. 여러 미들웨어는 서로 체이닝이 되고 최종적으로 체이닝된 함수가 스토어의 디스패치 함수로 주입됩니다. 이 디스패치 함수에 액션(Action) 객체를 담아 호출하게 되면 맨 마지막에 감싸진 미들웨어가 액션을 전달받아 작업을 수행하고 작업을 마치면 다음 감싸진 미들웨어를 실행하는 동작을 반복합니다. 이 모든 미들웨어를 지나면 리듀서(Reducer)로 액션이 전달되어 상태가 업데이트될 것입니다. 참고. 미들웨어를 리덕스 스토어에 연결하는 코드 function applyMiddleware() ( for (var _len = arguments.length, middlewares = new Array(_len), _key = 0 _key < _len _key++) ( middlewares[_key] = arguments[_key] )

return function (createStore) ( return function () ( var store = createStore.apply(void 0, arguments)

  var _dispatch = function dispatch() (
    throw new Error("Dispatching while constructing your middleware is not allowed. " + "Other middleware would not be applied to this dispatch.")
  )

  var middlewareAPI = (
    getState: store.getState,
    dispatch: function dispatch() (
      return _dispatch.apply(void 0, arguments)
    )
  )
  var chain = middlewares.map(function (middleware) (
    return middleware(middlewareAPI)
  ))
  _dispatch = compose.apply(void 0, chain)(store.dispatch)
  return _objectSpread((), store, (
    dispatch: _dispatch
  ))
)

) )

리덕스 미들웨어 (Redux Middleware) 소개 1.리덕스 프로미스 미들웨어(Redux Promise Middleware) $ yarn add redux-promise-middleware 이 미들웨어는 프로미스(Promise) 기반의 비동기 작업을 조금 더 편하게 해주는 미들웨어입니다. 리덕스 프로미스(Redux promise)를 사용하면 페이로드(Payload)로 전달된 객체가 만약 프로미스(Promise) 객체라면 통신에 대한 응답이 올 때까지 기다린 이후 결과값을 리듀서(Reducer)에게 전달합니다. 비교적 코드가 단순하기 때문에 기본적인 비동기 또는 조건부로 작업을 수행해야 하는 경우 유용합니다. 해당 미들웨어를 사용하지 않으면 액션(Action) 생성함수는 페이로드로 프로미스(Promise) 객체 자체를 실어 보내므로 리듀서에 상태(State)를 정상적으로 전달할 수 없습니다. 2.리덕스 썽크(Redux-Thunk) $ yarn add redux-thunk 리덕스 창시자인 Dan Abramov(댄 아브라모프)가 만든 가장 많이 사용되는 비동기 작업 미들웨어입니다. 리덕스 썽크(Redux Thunk)는 객체가 아닌, 동기 또는 비동기 작업을 수행할 수 있는 함수를 말합니다. 리덕스 썽크(Redux Thunk)를 사용하면 액션 생성자가 반환하는 객체로는 처리하지 못했던 작업을 함수를 반환할 수 있게 되면서 반환받은 함수를 통해 다양한 작업이 가능해졌다는 점이 장점입니다. 3.리덕스 로거(Redux logger) $ yarn add redux-logger 리덕스 로거(Redux Logger)를 사용하면 리덕스 미들웨어를 사용해 개발을 진행할 때, 리듀서(Reducer)가 실행되기 전과 실행된 후를 로그(log)로 편리하게 확인하여 비교할 수 있습니다. 리덕스 사가 리덕스 사가(Redux Saga)란? 리덕스 썽크(Redux Thunk) 다음으로 가장 많이 사용되고 있는 리덕스 사가(Redux Saga)는 리액트/리덕스 애플리케이션에서 비동기적으로 API를 호출하여 데이터를 가져오는 일과 같은 부수 효과(Side Effect)를 쉽게 처리하기 위해 사용하는 라이브러리입니다. 때에 따라 기존 요청을 취소 처리해야 한다거나 여러 개의 API를 순차적으로 호출해야 하는 등의 좀 더 까다로운 비동기 작업을 다루는 상황에 유용합니다. 리덕스 사가(Redux Saga)는 애플리케이션에서 전적으로 부수 효과(Side Effect)만을 담당하여 처리합니다. 비동기 함수 호출 결과 데이터를 통해 성공, 실패 여부를 판단하고 상태를 업데이트시키는 등의 작업(Task)을 제어할 수 있으며, 스토어에 접근하거나 특정 액션(Action)을 디스패치(Dispatch) 하여 다른 사가함수를 실행시킬 수 있습니다. 리덕스 사가(Redux Saga)는 부수 효과들을 처리하기 위해 제너레이터(Generator)라는 ES6 기능을 사용합니다. 제너레이터를 사용하게되면 비동기의 흐름을 표준 동기 코드처럼 보이게 하여 비동기 흐름을 쉽게 읽고 쓰고 테스트할 수 있어 복잡한 워크플로를 관리하는 데 매우 효과적입니다. 제너레이터(Generator)란? 제너레이터(Generator)에 대한 간단한 설명 function* 키워드를 사용하여 만든 제너레이터 함수를 호출했을 때는 한 객체가 반환되는데요, 이를 제너레이터(Generator) 또는 이터레이터(iterator) 객체라고도 부릅니다. 제너레이터의 내부 코드를 실행하기 위해서는 이 객체가 가지고 있는 next 메서드를 호출 해야만 하는데요. next 메서드의 호출 시마다 순차적으로 원소들을 탐색하고, yield키워드를 반환 포인트로 여기며 value 와 done 프로퍼티를 가진 새로운 객체를 반환합니다. 다시 말해서 제너레이터 안에서 yield 키워드를 사용하면 yield 키워드를 포인트로 코드의 흐름이 멈추게 되고 멈춘 코드의 흐름을 이어 나가기 위해서는 다시 next 메서드를 호출하는 방식으로 일시 정지와 재시작 기능을 사용할 수 있습니다. 제너레이터(Generator)의 이러한 흐름제어를 이용하여 리덕스 사가(Redux saga)에서는 액션(Action)을 모니터링하고 있다가 구독하고 있는 특정 액션이 발생했을 때 원하는 비동기 함수를 실행시킬 수 있습니다. 다음은 사가함수가 어떻게 액션(Action)의 발생여부를 알아내어 원하는 함수를 실행할 수 있는지에 대해 설명하기 위해 간단히 구현한 예시입니다. 간단히 설명하자면 watch라는 사가 함수는 watchSaga로부터 제너레이터를 전달받고 next 메서드를 통해 내부 코드를 실행시킵니다. watchSaga 내부에서는 while문을 통해 액션을 기다리고, next 메서드의 인자를 통해 전달받은 액션을 계속해서 모니터링하고 있다가 특정 액션이 디스패치된 경우 실행해야 하는 worker 함수를 호출할 수 있습니다. 리덕스 사가 API import ( createStore, applyMiddleware ) from 'redux' import createSagaMiddleware from 'redux-saga'

import reducer from './reducers' import mySaga from './sagas'

// saga 미들웨어를 생성합니다. const sagaMiddleware = createSagaMiddleware()

// 스토어에 적용시킵니다. const store = createStore( reducer, applyMiddleware(sagaMiddleware) )

// 그리고 saga를 실행합니다. sagaMiddleware.run(mySaga)

  1. createSagaMiddleware 리덕스 사가(Redux Saga)에서 제공하는 createSagaMiddleware API를 사용하면 사가 미들웨어(Saga Middleware)를 생성할 수 있습니다. 그리고 생성된 사가 미들웨어는 리덕스(Redux)에서 제공하는 applyMiddleware API를 호출할 때 인자로 넘겨 리덕스 미들웨어(Redux Middleware)로 추가할 수 있습니다. 2.middleware.run(saga, args) 사가 미들웨어(Saga Middleware)의 run 메서드를 통해 사가(Saga)를 실행할 수 있습니다. 사가가 여러 개가 존재하는 경우 진입점(Entry Point)에 해당하는 루트 사가(Root Saga)를 만들고 이 루트 사가를 실행시켜줄 수 있습니다. 사가(Saga)가 실행되면 해당 제너레이터(Generator) 함수를 호출하여 반복 가능한 제네레이터를 획득하게 되는데 해당 제네레이터의 next 메서드를 통해 이펙트(Effect) 타입을 확인하고 해당 이펙트에 대해 지시된 동작을 수행하는 작업을 반복하게 됩니다. 리덕스 사가 이펙트(Effect) 함수 앞서 언급한 이펙트(Effect)라고 하는 것은 어떤 기능을 수행하기 위해 주어진 함수와 인자들을 담은 명령 객체를 미들웨어에게 전달하는데 이러한 명령 객체를 말합니다. 이펙트를 전달받은 미들웨어는 yield 된 이펙트들을 확인하며 정확한 명령이 포함되었는지 검사하고, 이펙트 타입에 따라 어떻게 이펙트를 수행할지 결정합니다. 다음은 이펙트를 설명하기 위한 단순한 예제입니다. import ( put, takeEvery, delay ) from 'redux-saga/effects'

// Our worker Saga: will perform the async increment task export function* incrementAsync() ( yield delay(1000) yield put(( type: 'INCREMENT' )) )

// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC export function* watchIncrementAsync() ( yield takeEvery('INCREMENT_ASYNC', incrementAsync) ) 다음은 위 코드의 흐름을 이미지화 한것입니다. 사가(Saga)는 INCREMENT_ASYNC 액션(Action)을 모니터링하고 있다가 delay와 put이라는 이펙트(Effect)를 yield하고 자바스크립트 객체를 반환합니다. 따라서 INCREMENT_ASYNC 액션이 디스패치 되면 처음 1초를 기다렸다가 1초가 지나면 ( type: INCREMENT ) 액션을 디스패치(Dispatch) 하라는 각각의 이펙트 객체가 yield 되고 미들웨어에서는 해당 명령에 대한 수행이 이루어지게 됩니다. 이러한 이펙트를 생성할 때는 redux-saga/effects 패키지에 있는 라이브러리들이 제공하는 함수들을 사용하며 주요하게 사용하는 이펙트 함수 타입은 다음과 같습니다. 이펙트(Effect) 함수 타입 1.fork(fn, args) 매개변수로 전달된 함수를 비동기적으로 실행합니다. 비동기적으로 실행되기 때문에 블로킹(Blocking)이 발생하지 않는 새로운 맥락의 사가(Saga) 작업(Task) 생성하게 됩니다. 2.call(fn, args) 매개변수로 전달된 동기 혹은 비동기 함수를 실행합니다. 전달받은 함수가 비동기 함수인 경우 해당 함수가 수행(resolve)될 때까지 기다렸다가 결과값을 반환하므로 블로킹(Blocking)이 발생하게 됩니다. 3.put(action) 액션(Action)을 디스패치(dispatch) 합니다. 일반적으로 워커 사가(Worker Saga)에서 API 성공/실패 여부에 따라 상태를 반영하기 위해 리듀서에 액션을 디스패치 하는 용도로 많이 사용합니다. 4.takeEvery(pattern, saga, args) 액션(Action)이 디스패치(dispatch) 될 때마다 새로운 작업(Task) 분기(fork)합니다. 작업이 동시에 중복으로 발생해도 문제가 없는 경우에 사용해야 합니다. 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 ) 5.takeLatest(pattern, saga, args) 액션(Action)이 디스패치(을)될 때 이전에 실행 중인 작업(Task)이 있다면 취소하고 새로운 작업을 분기(fork)합니다. 이전 API 요청를 무시하고 최신데이터를 받아올 수 있도록 합니다. 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 ) 6.select(selector, args) 리듀서(Reducer)에 있는 특정 상태를 리덕스 사가(Redux)로 가져와서 사용할 수 있습니다. 블로킹이 발생하여 셀렉터(Selector) 함수가 정보를 가져온 이후에 다음 작업(Task)을 수행할 수 있습니다. 7.cancel(task) yield fork()는 실행되는 작업에 대한 오브젝트를 반환합니다. 이 오브젝트를 cancel 메서드의 인자로 넘기면 이미 비동기로 실행 중인 작업을 취소할 수 있습니다. 또한 작업을 취소한 경우라도 해당 워커 사가(worker Saga) 내의 finally 구간 안에서는 취소된 작업에 대해 처리할 수 있습니다. 8.추가 설명 모든 이펙트(Effect)는 반드시 yield 키워드와 함께 사용해야 합니다. 리덕스 사가의 이펙트는 Blocking effect와 Non-Blocking effect로 구분됩니다. Blocking Effect는 처리가 완료될 때까지 기다리며 Non-Blocking Effect는 완료를 기다리지 않고 진행합니다. 대표적인 Blocking Effect로는 call이 있고, Non-blocking Effect에는 fork가 있습니다. 웹앱에서 사용하는 리덕스 사가 패턴 예시 ├── container │ ├── action.js │ ├── constants.js │ ├── index.js │ ├── reducer.js │ ├── selectors.js └── saga.js 출처:https://github.com/react-boilerplate/react-boilerplate 1. useInjectReducer, useInjectSaga 그리고 key 현재 트렌비 프론트에서는 보일러 플레이트(Boilerplate)를 사용하여 컨테이너 컴포넌트에서 고유한 키(key)를 생성하여 useInjectSaga와 useInjectReducer 유틸함수를 통해 스토어(Store)에 사가(Saga)와 리듀서(Reducer)를 주입하고 있습니다. 이러한 구조를 사용하여 루트 사가(Root Saga)를 별도로 만들지 않고 useInjectReducer 내부에서 모니터링용 사가(Watcher Saga)를 실행시켜주며 특정 액션(Action)이 발생하면 상태(State)를 갱신하여 스토어(Store)에 전달할 수 있고 고유한 키(key)를 통해 특정한 상태를 가져올 수 있습니다. import Signup from './Signup'

댓글 0

댓글을 작성하려면 로그인이 필요합니다.

댓글을 불러오는 중...