Redux Saga의 Effects 와 generator function에 대해 알아보자
Introduction
Redux-Saga
는 Redux 애플리케이션에서 비동기 작업을 더 쉽게 관리할 수 있도록 도와주는 라이브러리다. Redux-Saga
는 Saga
라는 개념을 사용하여 비동기 흐름을 관리하며, 이를 통해 복잡한 비동기 작업의 흐름을 단순화하고 제어할 수 있다. Redux-Saga
는 제네레이터 함수 (generator function)를 사용하여 작성되며, 비동기 작업을 동기적 코드처럼 관리할 수 있게 한다.
Saga
Saga
는 애플리케이션에서 발생하는 Redux
액션을 가로채고, 그에 따라 비동기 작업을 수행하는 역할을 한다. Saga
는 call
, put
, take
, fork
, all
등 다양한 effect
를 사용해 작업을 수행한다.
Effect
Effect
는 Redux-Saga
에서 제네레이터 함수가 실행되는 비동기 작업의 일종이다. call
, put
, take
, fork
, all
등 다양한 종류의 effect
가 있다. Effect는 yield
키워드와 함께 사용되며, 이를 통해 비동기 작업을 순차적으로 실행하거나 병렬로 실행할 수 있다.
Middleware
Redux-Saga
는 Redux
의 미들웨어로 동작한다. Redux
의 미들웨어로 설정된 Saga
는 Redux
스토어에서 발생하는 액션을 가로채고, 필요한 비동기 작업을 수행하거나 특정 작업을 관리합니다.
오늘은 Saga
의 effect
함수들과 제네레이터 함수에 대해서 실제 내가 사용한 코드를 통해 전체적으로 알아보겠다.
function* userSaga()
나는 유저에 대한 요청과 응답을 처리하는 미들웨어로 userSaga라는 Saga
를 만들었다. 아래의 그림은 회원가입 요청의 플로우를 간략하게 설명해준다.
회원가입 요청 밖에도 로그인 요청, 로그아웃 요청들이 있을 수 있다. 이 요청들을 처리하는 Saga
는 각각 만들어지고, 이 모든 Saga
들을 한꺼번에 처리하는 최상위 Saga
가 userSaga()
가 될 것이다.
export default function* userSaga() {
yield all([fork(watchRegister), fork(watchLogin), fork(watchLogout)])
}
위의 그림을 보면 클라이언트의 요청은 Saga
가 받지만 (요청 액션을 “가로채지만”) 그 안에서도 watcher saga
라는 Saga
가 감지를 한다. 그래서 함수 이름도 watcherRegister
, watcherLogin
, watcherLogout
이라고 지었다.
userSaga()
함수에서 알아볼 수 있는 건 세가지로 추려질 수 있다.
function*
: 제네레이터 함수all
fork
이제 각각에 대해 알아보겠다.
function*
, 제네레이터 함수
제네레이터 함수는 function*
문법을 사용하여 정의되며, yield
키워드를 사용해 함수 실행을 일시 중지하고 필요한 값들을 반환할 수 있다. 제네레이터 함수는 일반 함수와 다르게, 실행 중간에 일시 정지를 하고 다시 재개할 수 있는 특징을 가지고 있다.
다음은 아주 간단한 제네레이터 함수의 예시이다.
이 함수에는 yield
키워드가 세번이 사용되는데, 이렇게 이해하면 된다.
제네레이터 함수가 호출이 되면 맨 처음에 Iterator
객체가 반환이 된다. Iterator
객체는 다음과 같은 구조를 갖고 있다.
{
next: function() {} // 제네레이터 함수의 실행을 다음 yield 지점까지 진행
return: function() {} // 제네레이터 함수를 종료시키는 메서드
throw: function() {} // 제네레이터 함수에 예외를 전달하는 메서드
}
Iterator
객체가 반환되면 Iterator
객체의 next()
메서드가 자동으로 호출되어 제네레이터 함수를 한 단계 실행한다. 따라서 위의 예시 그림에서는 yield 1
이 실행이 되는 것이다. 이것이 완료될 때까지 함수가 중단이 되었다가, 결과가 나오면 next()
가 다시 호출되고 다음 단계인 yield 2
가 실행이 되는 것이다.
Saga가 제네레이터 함수를 사용하는 이유
- 제네레이터 함수는
yield
를 통해 비동기 작업을async/await
문법 없이도 수행할 수 있다. 특히 여러개의 비동기 작업을 처리할 때 더 장점이 부각되는데, 작업중인 비동기 작업이 끝이 날 때까지 함수가 자동적으로 일시 중지가 되었다가 완료가 되면 제네레이터 함수 내Iterator
의next()
함수가 자동으로 다음 단계를 실행해주기 때문에 - 코드의 가독성도 높아지고 간결해진다.
- 또한 동시에 여러 작업 관리을 가능하게 해준다.
yield all([...])
같은effect
를 사용하면 여러 작업을 동시에 실행하고 모든 작업이 완료될 때까지 기다린다. - 마지막으로
Saga
는 리덕스의 미들웨어로 동작한다. 액션이 디스패치될 때 중간에서 이를 가로채어 비동기 작업을 처리하게 되는데, 이때 제네레이터 함수는effect
를 사용하여 비동기 작업의 흐름을 제어하고yield
를 통해 미들웨어가 액션을 처리하는 순서를 세밀하게 조정할 수 있다.
all
all
배열로 주어진 여러 effect
들을 병렬로 실행하는 effect
이다.
fork
fork
는 비동기적으로 새로운 Saga를 실행하는 effect
이다. fork
된 Saga는 부모 Saga와 병렬로 실행, 부모 Saga의 실행은 fork
된 Saga의 완료를 기다리지 않는다.
이해하기 어려운 문장이 나왔다.
fork
는 Saga
의 effect
이기 때문에 Saga
없이는 쓸 수가 없다. 다시 말하자면 fork
를 사용하는 Saga
는 분명 존재한다는 것이다. 그리고 이 Saga
를 부모 Saga
라고 정의내린 것이다. 그리고 fork
는 새로운 Saga
를 실행한다고 했다. 따라서 fork
된 Saga
는 자식 Saga
라고 정의가 내려질 수 있을 것이다.
부모 Saga
가 fork
를 통해 다른 Saga
를 실행시켰다면, 그 부모 Saga
는 fork
로 실행된 자식 Saga
의 완료를 기다려주지 않는다. 따라서 위의 예시를 다시 보자면
export default function* userSaga() {
yield all([fork(watchRegister), fork(watchLogin), fork(watchLogout)])
}
userSaga()
는 부모 Saga
로서 그 밑의 자식 Saga
인, watchRegister
, watchRegister
, watchRegister
를 fork
로 실행을 하는데, 이때 첫번째 자식 Saga
인 watchRegister
의 완료를 기다렸다가 그 다음 Saga
를 실행하는게 아니라, 세가지 Saga
가 모두 병렬로 같이 실행된다는 뜻이다.
watchRegister()
이제 userSaga()
가 실행하는 워쳐 Saga
들 중 watchRegister
에 대해서 알아보겠다.
watchRegister
는 다음과 같이 생겼다.
function* watchRegister() {
yield takeLatest(REGISTER_REQUEST, register);
}
watchRegister
도 제네레이터 함수로서 하나의 함수를 yield
통해 실행시킨다. 이때 takeLatest
라는 effect
가 사용되었다.
takeLatest
특정 액션이 디스패치 될 때 가장 마지막으로 디스패치된 액션만 처리하는 effect
이다. 위의 코드를 보면 REGISTER_REQUEST
가 회원가입 요청 액션인데, 이 요청 액션의 가장 최신 액션만 처리하는 effect
라고 이해하면 된다. 그리고 takeLatest
의 두번째 인자인 register
가 실제로 요청을 처리하는 Worker Saga
(워커 Saga
) 이다.
register
워커 Saga
인 register
는 실제로 API요청을 처리한다.
// Register API
function registerAPI(data: RegisterRequestAction['data']) {
return axios.post('/user', data);
}
// Register saga
function* register(action: RegisterRequestAction): SagaIterator {
try {
const response: any = yield call(registerAPI, action.data);
yield put({
type: REGISTER_SUCCESS,
payload: response.data.message,
});
} catch (err: any) {
yield put({
type: REGISTER_FAILURE,
error: err.response.data.message,
});
}
}
register
가 실행되었을때 어떤 일들이 일어나는지 순서대로 정리를 해보겠다.
Iterator
객체가 반환된다 (SagaIterator
라고 반환 타입이 지정된것도 이 때문이다.).Iterator
객체가 반환되면next()
메서드가 자동으로 호출되어 다음yield
까지 함수를 진행시킨다.yield call(registerAPI, action.data)
가 실행이 된다. 여기서registerAPI
는 제네레이터 함수가 아닌 일반 함수로서 실제axios
call 이 실행된다. 그리고 이 함수가 완료될 때까지register
는 함수 실행을 일시 중지 시킨다.response
가 나오게 되면 다시next()
가 실행되고 그 다음 코드인yield put()
이 실행된다.
call
2번에서 call
effect
가 실행되었다. call
은 특정 함수를 호출하는 effect 이다. 해당 함수가 반환하는 Promise가 해결되기 전까지 Saga의 실행을 중단시킴. 주로 비동기 함수의 호출에 사용된다.
put
3번에서 put
effect
가 실행되었다. put
은 Redux
액션을 dispatch
하는 effect
이다. 일반적으로 성공 또는 실패 상태를 디스패치 함.
위의 코드를 보면 API 호출이 성공일 때, GET_ALL_BOOKS_SUCCESS
액션을 디스패치하여 받은 데이터를 payload
로 전달하게 된다.
Conclusion
오늘은 Saga
의 effect
들과 Saga
를 정의하는 함수인 제네레이터 함수에 대해서 알아보았다. 오늘 다룬 내용을 정리해보겠다.
- 제네레이터 함수는
yield
라는 키워드를 사용해 함수 실행을 일시 중지하고 필요한 값을 반환 한 다음 다시 재개할 수 있는 특징을 갖고 있다. 제네레이터 함수는 호출이 되면Iterator
라는 객체를 반환하는데, 이 객체 안에next()
라는 메서드가 있다. 이 메서드는 함수를 다음yield
까지 실행시켜준다. Saga
는 제네레이터 함수를 사용한다. 제네레이터 함수의 특징은Saga
가 여러가지 비동기 작업을 동시에 처리/관리할 수 있게 해주고, 액션을 처리하는 순서를 세밀하게 조정할 수 있게 해준다.Saga
는fork
를 통해 다른 새로운Saga
를 실행할 수 있다. 이때fork
를 통해 실행되는Saga
를 편의상 자식Saga
라고 부르고 이 자식Saga
를 실행하는Saga
를 부모Saga
라고 부른다. 부모Saga
는fork
를 통해 실행된Saga
의 완료를 기다리지 않는다. 이것이call
과의 차이점이라고 할 수 있다.
댓글남기기