redux-saga 빠르게 시작하기
새로 온 회사에서는 상태 관리를 위해 redux
와 redux-saga
를 쓰고 있다.
나는 redux
안쓴지가 너무 오래되어서 다 까먹어버렸다. 학습의지를 끌어올리기 위해 오랜만에 블로그를 적으러 들어왔다.
redux-saga
전에 알아야 하는 제너레이터 함수와 yield
redux-saga
는 redux
와 함께 사용하는 비동기 처리를 위한 미들웨어 중 하나다.
튜토리얼도 잘 되어 있는 것 같은데, 예제를 이해하려면 먼저 제너레이터와 yield
같은 개념부터 알아야 한다.
- 제너레이터 함수
function*
는 중간에 멈췄다가 다시 실행할 수 있는 함수이고, yield
는 멈추는 포인트를 정하는 키워드다.
일반 함수는 처음부터 끝까지 쭉 실행되지만, 제너레이터 함수는 바로 실행되지 않으며 next()
를 호출할 때마다 앞서 실행을 멈춘 지점에서부터 그 다음 yield
까지 실행된다.
redux
는 동기적이다
redux
의 리듀서는 반드시 순수 함수여야 한다.
순수 함수란 주어진 입력값에 대해 항상 동일한 출력을 반환하고, 외부 상태에 의존하지 않고, 외부 상태를 변경하지도 않는 함수를 말한다.
순수 함수인 리듀서는 비동기 로직을 가질 수 없고, 항상 동기적으로 결과를 반환해야 한다.
redux
에서 API를 어떻게 호출하면 좋을까?
redux
의 리듀서에는 API 호출과 같은 비동기 로직을 작성할 수 없다.
그래서 다른 방법을 써야 하는데, 가장 대표적인 방법이 미들웨어를 사용하는 것이다.
오로지 redux
로만 비동기 처리할 수 없을까?
미들웨어 없이 오로지 redux
만 이용한다면,
- 컴포넌트에서 직접 API 요청을 하고,
- 결과값을
dispatch
해서 상태를 업데이트하면 된다.
이렇게 했을 때의 단점은,
- 책임 (관심사) 분리가 안되고,
- 매번 API 요청 및 에러 처리를 위한 로직을 반복 작성해야 한다는 것이다.
비동기 처리를 위한 redux
의 미들웨어들
비동기 처리를 위한 redux
의 미들웨어에는 유명한 두 가지가 있다: redux-thunk
와 redux-saga
redux-thunk
는 비교적 쉽지만, 비동기 로직이 액션 생성과 뒤섞여서 가독성이 별로다.
redux-saga
를 쓰면 코드 양은 더 늘어나지만, 대신 관심사 분리가 정확히 되고, 요청, 취소, 대기 상태 등에 대한 처리가 쉽다는 이점이 있다.
간단한 예시
redux-saga
를 이용해서 사용자를 조회한 API 결과를 사용자 데이터 및 로딩, 에러 상태에 업데이트하는 코드다:
// actions.jsexport const FETCH_USER = 'FETCH_USER';export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';export const FETCH_USER_ERROR = 'FETCH_USER_ERROR';export const fetchUser = () => ({ type: FETCH_USER });
// reducer.jsconst initialState = {loading: false,user: null,error: null,};function userReducer(state = initialState, action) {switch (action.type) {case 'FETCH_USER':return { ...state, loading: true, error: null };case 'FETCH_USER_SUCCESS':return { ...state, loading: false, user: action.payload };case 'FETCH_USER_ERROR':return { ...state, loading: false, error: action.error };default:return state;}}
// sagas.jsimport { call, put, takeEvery } from 'redux-saga/effects';import {FETCH_USER,FETCH_USER_SUCCESS,FETCH_USER_ERROR} from './actions';// API 호출 함수 (비동기)const fetchUserFromApi = () =>fetch('/api/user').then((res) => {if (!res.ok) throw new Error('서버 에러');return res.json();});// saga 제너레이터 함수function* fetchUserSaga() {try {// 1. call은 함수를 yield로 호출함 → 기다림const user = yield call(fetchUserFromApi);// 2. 성공 → 액션 디스패치yield put({ type: FETCH_USER_SUCCESS, payload: user });} catch (error) {// 3. 실패 → 에러 액션 디스패치yield put({ type: FETCH_USER_ERROR, error: error.message });}}// 루트 사가export default function* rootSaga() {// FETCH_USER 액션이 발생할 때마다 fetchUserSaga 실행yield takeEvery(FETCH_USER, fetchUserSaga);}
redux-saga
는 이펙트 Effect
로 비동기 흐름을 제어한다
Effect
함수는 실제로 무언가를 실행하는 대신 saga 미들웨어에 어떤 행동을 요청한다.
call(fn, ...args)
비동기 함수 호출put(action)
액션 디스패치select(selector)
현재 상태 가져오기takeEvery(type, saga)
모든 액션 감시 및 처리takeLatest(type, saga)
최신 액션 하나만 처리take(type)
특정 액션 하나 발생할 때까지 대기delay(ms)
시간 지연
gpt가 짜준 간단한 예시 보고 call
put
takeEvery
나 select
는 왜 쓰이는지 알겠는데, 나머지는 회사 코드 보면서 실제 쓰임을 봐야겠다.