React의 러닝커브를 높이는 주범 ‘리덕스 사가’에 대해 간단한 개념과 예시를 정리하려한다. 까먹지말자 😮
Redux-saga란?
Redux-saga는 리덕스의 액션을 dispatch하기 전에 해야할 작업들을 처리하는 미들웨어이다. Redux-thunk라는 미들웨어도 있지만, thunk는 여러개의 액션을 디스패치하게 해주지만, 그 외의 기능들은 모두 직접 구현해야 하기 한다.
그러나 Saga는 모두 effect로 구현해놔서 대부분 미들웨어로 saga를 사용한다고 한다.
(Redux-saga는 Generator 문법을 기반으로 구현하기 때문에 generator에 대한 기본적인 이해가 요구된다.
)
Redux-saga 예시 (로그인)
로그인 과정은 서버와 클라이언트의 통신이 필요한 작업이기 때문에 비동기적인 작업을 해야한다. 리덕스에서 비동기작업은 미들웨어가 꼭 필요하다!
- initial state (로그인 중, 성공, 에러 과정을 나눠서 클라이언트에게 과정을 보여줄 수 있도록 한다 / user에는 로그인 정보를 담아 다른 컴포넌트에서 모두 공유할 수 있다.)
export const initialState = {
user: null,
loginLoading: false,
loginSuccced: false,
loginError: null
}
2. root saga : fork함수를 이용해 비동기로 generator함수를 호출한다. (여러개의 함수를 계속해서 listening)
export default function* rootSaga() {
yield all([
fork(watchLogin)
//다른 작업이 있을 경우, 추가할 수 있음
])
}
3. watchLogin : action이 dispatch 되기 전에 처리해야 할 generator 함수를 호출한다.
여기서는 LOGIN_REQUEST를 dispatch하면 login 함수도 같이 호출한다.
function* watchLogin(){
yield takeLatest(LOGIN_REQUEST, login)
}
추가 설명: take는 일회성 함수로 한 번 함수를 호출하면 다시 호출할 수가 없다. 그렇기 때문에 while(true)문으로 감싸서 실행하는데, 이렇게 하면 가독성이 떨어지기 때문에 takeEvery로 대체할 수 있다. 해당 예시에서는 takeLatest를 사용했는데, 동일한 action이 여러번 연속적으로 호출될 경우, 가장 마지막의 호출만 응답을 받게 하는 함수이다.
4. login : 실질적으로 동기작업을 하는 generator함수로 api를 호출한 후, 분기점을 나눠서 다음으로 실행할 action을 put한다(put은 dispatch와 같은 역할)
function* login(action){
try{
const res = yield call(loginAPI, action.data)
yield put({
type: LOGIN_SUCCEED,
data: res.data
})
} catch (err) {
yield put({
type: LOGIN_FAILED,
error: err.response.data
})
}
}
추가 설명: call은 동기작업을 호출하고, promise를 return 받으면 결과에 따라(try, catch) resolved일 경우, 성공 액션을 put하고 rejected일 경우, 실패 액션을 put한다.
5. action과 reducer : middleware와 action으로 변화된 state를 적용시킨다
//action type
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCEED = 'LOGIN_SUCCEED'
export const LOGIN_FAILED = 'LOGIN_FAILED'
//action creator (시작점인 request만 필요, 나머지는 saga에서 생성하기 때문)
export const loginRequest = LOGIN_REQUEST
//reducer
export default const reducer = (state = initialState, action) => {
switch(action.type){
case LOGIN_REQUEST:
return {
...state,
loginLoading: true,
loginSucceed: false,
loginError: null
}
case LOGIN_SUCCEED:
return {
...state,
loginLoading: false,
loginSucceed: true,
loginError: null,
user: action.data
}
case LOGIN_FAILED:
return {
...state,
loginLoading: false,
loginSucceed: false,
loginError: action.error
}
default: return state
}
}
사실 MobX를 어느정도 배우고 나서 redux-saga를 봤을 땐 보일러플레이트가 너무 많아서 굳이 사용해야 하나 싶었다. 그런데 로그를 찍어가면서 개발하고, 직관적인 에러 핸들링을 하다보니 오히려 MobX보다 개발이 쉽게 느껴졌다.
Redux-saga를 계속해서 공부해봐야겠다.