← 홈으로

redux-saga 빠르게 시작하기

새로 온 회사에서는 상태 관리를 위해 reduxredux-saga를 쓰고 있다. 나는 redux 안쓴지가 너무 오래되어서 다 까먹어버렸다. 학습의지를 끌어올리기 위해 오랜만에 블로그를 적으러 들어왔다.

redux-saga 전에 알아야 하는 제너레이터 함수와 yield

redux-sagaredux와 함께 사용하는 비동기 처리를 위한 미들웨어 중 하나다. 튜토리얼도 잘 되어 있는 것 같은데, 예제를 이해하려면 먼저 제너레이터와 yield 같은 개념부터 알아야 한다.

  • 제너레이터 함수 function*는 중간에 멈췄다가 다시 실행할 수 있는 함수이고,
  • yield는 멈추는 포인트를 정하는 키워드다.

일반 함수는 처음부터 끝까지 쭉 실행되지만, 제너레이터 함수는 바로 실행되지 않으며 next()를 호출할 때마다 앞서 실행을 멈춘 지점에서부터 그 다음 yield까지 실행된다.

redux는 동기적이다

redux의 리듀서는 반드시 순수 함수여야 한다. 순수 함수란 주어진 입력값에 대해 항상 동일한 출력을 반환하고, 외부 상태에 의존하지 않고, 외부 상태를 변경하지도 않는 함수를 말한다. 순수 함수인 리듀서는 비동기 로직을 가질 수 없고, 항상 동기적으로 결과를 반환해야 한다.

redux에서 API를 어떻게 호출하면 좋을까?

redux의 리듀서에는 API 호출과 같은 비동기 로직을 작성할 수 없다. 그래서 다른 방법을 써야 하는데, 가장 대표적인 방법이 미들웨어를 사용하는 것이다.

오로지 redux로만 비동기 처리할 수 없을까?

미들웨어 없이 오로지 redux만 이용한다면,

  1. 컴포넌트에서 직접 API 요청을 하고,
  2. 결과값을 dispatch해서 상태를 업데이트하면 된다.

이렇게 했을 때의 단점은,

  • 책임 (관심사) 분리가 안되고,
  • 매번 API 요청 및 에러 처리를 위한 로직을 반복 작성해야 한다는 것이다.

비동기 처리를 위한 redux의 미들웨어들

비동기 처리를 위한 redux의 미들웨어에는 유명한 두 가지가 있다: redux-thunkredux-saga

redux-thunk는 비교적 쉽지만, 비동기 로직이 액션 생성과 뒤섞여서 가독성이 별로다. redux-saga를 쓰면 코드 양은 더 늘어나지만, 대신 관심사 분리가 정확히 되고, 요청, 취소, 대기 상태 등에 대한 처리가 쉽다는 이점이 있다.

간단한 예시

redux-saga를 이용해서 사용자를 조회한 API 결과를 사용자 데이터 및 로딩, 에러 상태에 업데이트하는 코드다:

// actions.js
export 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.js
const 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.js
import { 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 takeEveryselect는 왜 쓰이는지 알겠는데, 나머지는 회사 코드 보면서 실제 쓰임을 봐야겠다.