Redux-saga에 대하여

작성배경

탭조이(5Rocks)에서 소프트웨어 엔지니어로 첫걸음을 시작한지 어느 덧 한달이 다되어 간다. 리액트, 그래프큐엘 등 내가 가지고 있는 스택과 회사의 프로덕트 스택이 일치 하는게 많아 하루빨리 회사 코드를 이해하고 우선 첫번째 목표로 1.1인분을 하고 싶은 마음이 컸다. 하지만 현실은 내 생각대로 움직여주지 않았다. TypeScript 의 제네릭, 상속과 같은 개념 들과 Redux-saga는 설레는 마음으로 코드를 바라보는 나를 조금 주춤하게 했다. 그래도 한달이 지난 지금와서는 어느새 겨울이 성큼 내앞에 나타난 것 처럼, 어느새 눈에 익고, 그들과 조금 친해 진 것 같아 기쁘다. 그리고 나의 이런저런 질문으로 시간을 뺏기더라도 친절히 답변 해주신 팀원 분들께 진심으로 감사하다. 이번 글에선 나를 조금 주춤하게 만들었던 개념 중 Redux-saga 에 대해 복습할 겸 정리할 것이다.

리덕스 비동기 액션 처리 thunk 와 saga 비교

리덕스를 사용하면 데이터를 처리하는 비즈니스 로직을 컴포넌트로부터 분리할 수 있다. 리덕스에 대해 설명하자면 너무나 길어지기에 이부분에 대해 기본적인 지식이 있다고 가정 하에 따로 지면을 할애하지 않겠다.

thunk

거두절미하고 나는 회사에 들어오기전 모든 프로젝트에서 액션 생성자 함수에서 API 호출 같은 비동기 프로세스 구현을 위해 미들웨어로 thunk 를 사용해 왔었다. 이에 대해 간략히 설명하자면 아래와 같다.

Promise를 사용하면 resolve 시점에서 객체를 직접 리턴 할 수 없다. 그래서 비동기 처리를 할 때 액션 객체를 반환하는 대신 dispatch를 인자로 하는 함수를 리턴해 이 함수 안에서 데이터펫칭을 비롯한 네트워킹, 다수의 디스패치 등을 할 수 있게 해주는 미들웨어로 thunk를 사용하는 것이다.

thunk 를 사용해 작성한 예시 액션 함수를 주석과 함께 작성해 보았으니 참고하기 바란다.

export function fetchCars() {
// dispatch를 인자로 하는 함수를 리턴 한다.
return dispatch => {
// 요청이 시작됨을 알린다.
dispatch({ type: FETCH_CARS_BEGIN });
    // API 요청을 실행하며 완료 시 함수는 종결 된다
return axios
.get(`${API}/cars`)
      // 성공 시 성공했음을 알리고 받아온 자료를 payload 에 담아 리듀서로 보낸다
.then(res =>
dispatch({ type: FETCH_CARS_SUCCESS, payload: res.data })
)
// 실패 시 실패했음을 알리고 받은 에러를 payload 에 담아 리듀서로 보낸다
.catch(err =>
dispatch({ type: FETCH_CARS_FAILURE, payload: err }));
};
}

saga

thunk의 등장 이후 많은 사람들이 비동기 액션 처리의 대안으로 thunk 를 사용해왔고 지금도 많이 사랑받고 있다. 나도 스스로 개인 프로젝트를 진행할 때 프로젝트 볼륨이 크지 않아서 인지 치명적으로 이거 갈아 엎지 않고는 못견디겠다고 느껴본적은 없다. 그러나 마음 한켠에 클로저 패턴을 사용해야 하기에 다소 소스코드가 깔끔하지 못한 느낌은 어느정도 있었다.

내가 잘 구현되어 있는 saga 를 이해 한 순간 딱 들었던 생각은 ‘와 action이 순수한 객체(Pure Object)만을 반환하니 진짜 깔끔 하긴 하다’ 이었다. 즉, 비동기 처리 같은 단순하지 않은 작업들은 saga 에 만들어놓고 누군가 발생시킨 액션중 일치하는 saga와 연결된 액션타입이 있으면 해당 saga를 실행시켜 주는 것이다. 다시한번 이해를 위해 아주 비약하여 정리 하자면 아래와 같다.

* 액션엔 비동기 작업이 아닌 단순히 리듀서와만 통신하는 액션들만 있다.
* API 통신을 하는 비동기 작업 같은 것들은 saga 에 작성한다. 액션타입명-작성한 제너레이터 함수를 연결 해놓는데 이때 액션을 계속 리스닝하다가 일치하는 액션타입명이 발생할 때 잽싸게 해당 제너레이터 함수를 실행시킨다.
* 이 때 만약 data를 fetching 하는 비동기 액션이 였다면, 내부적으로 다시 단순히 리듀서에 받아온 data를 넣어주는 단순히 리듀서와만 통신하는 액션 이제너레이터 함수 안에 존재 할 것이다.

말로 하자니 너무 복잡하여 자동차 리스트를 API 통신을 통해 가져오는 것을 redux-saga 를 통해 구현한다고 가정해보자. 액션 — 리듀서 — 사가는 다음과 같을 것이다. (여기서 사가에서 쓰인 yield put 은 dispatch 와 같다.)

리듀서 쪽 작성 코드
사가 쪽 작성 코드

이제 이해가 되는가? 만약 리액트의 어떤 컴포넌트에서 액션에 있는 fetchCars 액션 함수를 호출하면 watchYoungJaeSaga 에서 액션을 계속 리스닝하고 있다가 캐치해 일치하는 액션타입(FETCH_CARS)과 연결되어 있는 fetchCarsSaga 제너레이터 함수를 실행 시키는 것이다! 그리고 그 안에서 yield put(actions.saveFetchedCars(data) 를 통해 리듀서에 저장 되는 흐름을 가지고 있다. (여기서는 오류 처리는 길이상 생략하였다.)

마치며

오랜만에 글을 작성하다보니 길기만 길고 영양가가 없을까봐 걱정이 된다. 물론 나는 글을 쓰며 다시금 정리하고 공부하는 기회가 되었지만 그래도 아주 소수의 독자들이 우연히 이글을 조우한다면 꼭 이해하는데 도움이 되길 바란다. 내가 많은 선배, 후배 개발자 분들의 글을 보고 많은 지식을 머리속에 넣을 수 있던 것처럼, 부족한 글이지만, 누군가에게 조금이나마 도움이 된다면 그걸로 정말 행복할 것 같다. 그래서 나는 개발직군이 너무 좋다. 지식을 공유하고 선순환 하는 구조…! 다음글은 타입스크립트로 찾아 뵙겠습니다. 부족한 글 읽어주셔서 감사드리며, 이해안되는 점이나 잘못 된 점 있다면 언제든 말씀 부탁 드립니다.