1. Redux-Saga 를 사용하는 이유
간단한 비동기 통신들 같은 경우에는 Redux-Thunk 정도로도 충분하지만 규모가 어느정도 커진 애플리케이션의 경우
- 테스트의 용이성
- 병렬 처리
- 유지보수성
등의 이유로 Redux-Saga를 로직을 처리하기 위한 미들웨어로 주로 사용
리덕스 사가는 제네레이터 함수를 기반으로 동작하는 라이브러리다. 또한 선언적인 방식으로 동작하기에 개발자가 코드를 읽고 이해하기 편하며, 테스트 코드를 작성하기 좋다.
2. 리덕스 사가의 활용법
1) 사가를 활용하기 위해서 미들웨어에 사가를 등록하기
사가를 이해하기 위해서는 리덕스에 별도의 스레드를 만든다는 개념으로 접근하는 것이 좋다.
스레드는 “특정한 일을 처리하는 하나의 공간”정도로 이해하자. 사가는 리덕스 내에 별도의 사가 스레드를 하나 생성한 후, 특정한 처리가 필요할 경우 이 사가 스레드 안에서 동작을 수행하는 개념으로 이해할 수 있다.
import createSagaMiddleware from 'redux-saga'
const sagaMiddleware = createSagaMiddleware() // 사가 스레드 생성
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// 사가함수를 run 메서드의 인자로 전달
sagaMiddleware.run() // 추후 전달된 인자가 사가 스레드 내에서 동작할 함수
2) run 메서드에 원하는 함수를 등록하면 사가에서 해당 함수를 실행
이때 run 메서드에 등록하는 함수는 반드시 제네레이터 함수여야한다.
function* helloSaga() {
console.log('Hello Sagas!')
}
// store/index.js
import { helloSaga } from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(helloSaga)
위와 같이 사가 함수를 run 메서드를 통해 등록하면 이제 사가 미들웨어 안에서 helloSaga 함수를 실행시켜준다.
3) 사가는 여러 이펙트를 이용해서 조작할 수 있다
이러한 이펙트들은 redux-saga/effects 패키지에서 import 해서 사용할 수 있다.
import { put, takeEvery } from 'redux-saga/effects'
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
export function* incrementAsync() {
yield delay(1000) // 2
yield put({ type: 'INCREMENT' }) // 3
}
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync) // 1
}
// 등록
sagaMiddleware.run(watchIncrementAsync);
위 코드에서는 1개의 유틸함수와, 두개의 이펙트가 사용되었다.
- delay 함수는 인자로 받은 ms만큼 시간이 지난후 resolve된 Promise를 반환하는 함수다.
- put 이펙트는 Redux에 Action을 Dispatch 하고자 할 때 사용할 수 있다.
- takeEvery 이펙트는 사가함수를 바로 실행하지 않고, 해당 액션이 디스패치될 때 까지 대기 후 액션이 디스패치되면 두번째 인자로 전달된 함수를 실행시켜주는 이펙트다. 위와 같이 코드를 작성했을 경우에는 INCREMENT_ASYNC 액션이 디스패치 될 때 마다 incrementAsync 함수를 실행하게 된다.
흔히 사가를 사용하게 되면 위와 같은 worker, watcher 두가지 종류의 함수를 만들게 된다.
- worker
수행해야 하는 동작을 이행하는 함수이며 여기서는 incrementAsync 함수를 의미 - watcher
worker 함수가 어떤 시점에서 실행되어야 할지를 결정짓는 함수이며 여기서는 watchIncrementAsync 함수를 의미
위의 코드의 흐름을 해석해보면
- INCREMENT_ASYNC 액션이 디스패치 될 때 마다 incrementAsync 함수가 실행
- yield delay(1000) 함수로 인해 1초간 실행이 지연
- yield put({ type: 'INCREMENT' }) 함수가 동작하며, INCREMENT 액션이 디스패치 된다.
이 코드는 비동기 통신을 흉내내기 위해서 delay를 사용했지만, 실제 비동기 통신 코드도 위와 같은 과정을 통해서 수행할 수 있다. 하지만 이렇게만 사용할거면 사실상 thunk와 큰 차이점이 없다. incrementAsync 내부에서는 delay(1000) 등 비동기 처리가 필요한 함수를 직접 호출하고 있다. 위와 같은 식으로 코드가 작성되면 비동기 호출의 결과값은 매번 달라질 수 있으며, Promise의 형태를 지니고 있기에 추후 이 함수의 결과를 테스트하기가 힘들어진다.
따라서 Redux-Saga는 개발자가 작성하는 사가 함수에서는 직접 비동기나 복잡한 코드를 호출하는 것이 아니라, 호출하라는 명령을 표현하는 객체만 만들어내며, 추후 사가 미들웨어단에서 해당 명령을 읽어서 수행하는 방식을 채택했다.
import { put, call, takeEvery } from 'redux-saga/effects'
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
export function* incrementAsync() {
yield call(delay, 1000) // 2
yield put({ type: 'INCREMENT' }) // 3
}
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync) // 1
}
// 등록
sagaMiddleware.run(watchIncrementAsync);
기존의 코드와 차이점을 분석해보자면 2번 과정에서 직접 delay 함수를 호출하는 것이 아니라, call이라는 이펙트를 통해 delay 함수의 호출을 표현했다.
call 이펙트를 호출하게 되면
{
fn: delay,
args: [1000]
}
위와 같이 어떤 함수를, 어떤 인자를 통해서 호출할지를 표현하는 객체를 리턴한다. 실제 리턴된 객체는 훨씬 더 많은 프로퍼티를 가지고 있지만, 간략하게 표현하자면 위와 같은 형태다.
그리고 사가 미들웨어단으로 이 객체가 전달되게 되며, 미들웨어 안에서는 이 객체를 해석해서 실제 함수를 호출하고 그 결과값을 다시 incrementAsync 함수로 돌려주게 된다.
핵심은 직접 동작을 수행하는 것이 아닌, 어떤 동작을 수행할지 의도를 표현하고 실제 수행은 사가 미들웨어에 위임하는 방식을 채택한 것이다. 이로 인해 개발자는 더 선언적인 방식으로 코딩을 할 수 있게 되었으며, 코드를 테스트하기가 굉장히 용이해졌다.
'Library & Framework > Redux' 카테고리의 다른 글
Redux MiddleWare 깊이 있게 살펴보자! (0) | 2022.10.31 |
---|---|
Redux Middleware / DevTools (0) | 2022.10.29 |
Redux (0) | 2022.10.29 |