리액트를 사용해서 무언가를 개발하다보면, 컴포넌트의 상태관리에 많은 신경을 써줘야한다. 작은 프로젝트의 경우 props를 이용해서 컴포넌트끼리 데이터를 교류해도 되지만, 프로젝트가 복잡해질수록 데이터 흐름을 추적하기 힘들어져서 많은 불편을 겪게된다.
그래서 나온것이 바로 리덕스이다. 리덕스를 사용하면 컴포넌트들의 상태 관련 로직들을 다른 파일들로 분리시켜서 더욱 효율적으로 관리 할 수 있으며, 데이터 교류 시 여러 컴포넌트를 거치지 않고도 손쉽게 상태 값을 전달 할 수 있다.
사실 리덕스 만으로는 특정 action에 대한 dispatch를 자바 스크립트의 Event listner 처럼 기다리거나 다른 action을 dispatch하는 동기적인 액션(순서대로 액션을 처리)처리 라던가 디스패치된 액션들을 모두 모아서 병렬적으로 수행하기 힘들다. 그러나 미들웨어를 사용하면 이러한 작업들을 보다 손쉽게 처리 할 수 있다. 즉, 액션과 리듀서 사이의 중개인이라 생각하면 될듯하다.
Redux-Saga는 Redux-Thunk 다음으로 많은 인기를 갖고있는 리덕스 미들웨어이다. Thunk에 비해 러닝커브가 높다고하는데, 그말이 맞는듯하다. 그러나 그만큼 다양한 작업들을 처리 할 수 있다. 특히 리덕스 사가에는 다양한 effect들이 존재하는데, 직접 사용해봤던 effect함수들만 기재했으며, 실제 예제를 진행하면서 좀더 디테일하게 설명하려한다.
Call()은 함수를 동기적으로 실행하며, Call에 넘겨진 함수가 Promise를 리턴 한다면 그 Promise가 resolved 될 때까지 call()을 호출한 부분에서 실행이 멈춘다.
가장 마지막에 실행된 액션에 대해서만 핸들러를 실행한다. 예를들어 실행 했을때 1초 뒤에 숫자가 1이 감소되는 액션이 있다면, 5번 연달아 실행했을 시 1초 뒤 숫자 1만 감소된다.
캐치된 모든 액션에 대해서 핸들러를 실행합니다. 예를들어 실행 했을때 1초 뒤에 숫자가 1이 증가하는 액션이 있다면, 5번 연달아 실행했을 시 1초마다 1씩 총 5까지 증가한다.
dispatch와 흡사하다. 보통 takeLatest, takeEvery로 액션을 캐치해서 api 호출을 call로 실행하고 성공/실패 여부에 따라 리덕스 스토어에 반영하기 위해서 호출하는 Effects입니다.
import * as postsAPI from "../api/posts";
import { call, put, takeEvery } from "redux-saga/effects";
const GET_TODOS = "GET_TODOS"; // 요청 시작
const GET_TODOS_SUCCESS = "GET_TODOS_SUCCESS"; // 요청 성공
const GET_TODOS_ERROR = "GET_TODOS_ERROR"; // 요청 실패
export const getPosts = () => ({ type: GET_TODOS });
export function* getPostsSaga() {
try {
const todos = yield call(postsAPI.getPosts);
yield put({
type: GET_TODOS_SUCCESS,
payload: todos,
});
} catch (e) {
yield put({
type: GET_TODOS_ERROR,
payload: e,
});
}
}
export function* todoSaga() {
yield takeEvery(GET_TODOS, getPostsSaga);
}
const initialState = {
todos: {
loading: false,
data: null,
error: null,
},
};
export default function todoReducer(state = initialState, action) {
switch (action.type) {
case GET_TODOS:
return {
...state,
todos: {
loading: true,
data: null,
error: null,
},
};
case GET_TODOS_SUCCESS:
return {
...state,
todos: {
loading: false,
data: action.payload,
error: null,
},
};
case GET_TODOS_ERROR:
return {
...state,
todos: {
loading: true,
data: null,
error: action.error,
},
};
default:
return state;
}
}
Axios를 이용하여 jsonplaceholder에서 제공하는 가상 rest api서버에서 투두 데이터가 담겨져있는 json을 받아오는 과정을 리덕스 사가를 통해 구현해 보았다. 먼저 Duck패턴을 이용해서 액션과 리듀서가 존재하는 todos.js파일을 만들어주었다. (Duck패턴이란 액션타입, 액션생성자함수, 리듀서를 하나의 파일로 작성하는 리덕스 디자인 패턴이다.)
먼저 데이터의 흐름순으로 정리해본다면 최상단 페이지(root page)접속 시 use-Effect 훅을 이용해서 getTodos라는 함수를 dispatch하는데, getTodos 함수는 'GET_TODOS'라는 타입을 반환하고, todoSaga라는 제네레이터 함수가 이를 감지하고 getPostSaga를 실행시킨다.
이 todoSaga는 감시자 (watcher)라 할수있는데, 위 예제로만 봤을땐 GET_TODOS라는 액션이 들어오는지 기다리고있다가 들어오면, getPostSaga를 실행시키는것이다. 자바스크립트의 Event listner과 비슷하다.
getPostsSaga의 역활은 getPosts라는 프로미스를 반환하는 함수를 실행한뒤 todos라는 변수에 할당한다. 여기서 saga의 effect함수인 call이 등장하는데, 이를 통해 Axios를 이용한 서버 통신을 하는 getPosts 함수가 처리 완료 될때까지 기다리게 된다.
다시 getPostsSaga로 돌아와서, try/catch 구문을 이용하여, 성공 시 GET_TODOS_SUCCESS를, 실패시, GET_TODOS_ERROR와 에러를 보내도록 분기 설정 해놓았다.
export default function todoReducer(state = initialState, action) {
switch (action.type) {
case GET_TODOS:
return {
...state,
todos: {
loading: true,
data: null,
error: null,
},
};
case GET_TODOS_SUCCESS:
return {
...state,
todos: {
loading: false,
data: action.payload,
error: null,
},
};
case GET_TODOS_ERROR:
return {
...state,
todos: {
loading: true,
data: null,
error: action.error,
},
};
default:
return state;
}
}