프로그래밍/ReactJS

React 개발의 핵심: Redux와 Redux Saga로 상태 관리를 마스터하다

shimdh 2025. 10. 18. 09:59
728x90

현대 웹 애플리케이션 개발에서 상태 관리는 복잡성을 줄이고 효율성을 높이는 데 필수적인 요소입니다. 특히 React와 같은 컴포넌트 기반 UI 라이브러리를 사용할 때, 애플리케이션의 규모가 커질수록 상태 관리의 중요성은 더욱 부각됩니다. 컴포넌트 간 데이터 공유, 비동기 API 호출, 사용자 입력 처리 등 다양한 시나리오에서 상태가 엉키면 유지보수 지옥이 될 수 있죠. 이러한 요구에 발맞춰 등장한 것이 바로 ReduxRedux Saga입니다.

이 두 도구는 React 앱의 상태를 예측 가능하고 중앙화된 방식으로 관리하며, 비동기 로직을 깔끔하게 분리합니다. 오늘은 Redux의 기본 철학과 핵심 개념부터 Redux Saga의 비동기 마법, 그리고 이 둘을 React에 통합하는 실전 팁까지 깊이 탐구해보겠습니다. 초보자도 쉽게 따라할 수 있도록 코드 예시를 듬뿍 넣었으니, 함께 상태 관리의 마스터가 되어 보세요!

Redux: 예측 가능한 상태의 중앙 관리자

Redux는 JavaScript 애플리케이션을 위한 예측 가능한 상태 컨테이너입니다. "단일 진실의 원천(Single Source of Truth)"이라는 철학을 바탕으로, 애플리케이션의 전역 상태를 한 곳에서 관리할 수 있게 해줍니다. 이는 복잡한 앱에서 데이터 흐름을 명확하게 파악하고, 버그를 디버그하는 과정을 훨씬 수월하게 만듭니다. Redux를 도입하면 상태 변화가 어디서 발생했는지 추적하기 쉽고, 시간 여행 디버깅(Time Travel Debugging) 같은 강력한 기능으로 개발 생산성을 폭발적으로 높일 수 있어요.

Redux의 핵심 개념 파헤치기

Redux를 이해하는 데 있어 가장 중요한 네 가지 핵심 개념이 있습니다. 이 개념들을 마스터하면 Redux의 세계가 훨씬 직관적으로 느껴질 거예요.

  1. 스토어(Store)
    애플리케이션의 모든 상태 트리를 담고 있는 중앙 저장소입니다. 스토어는 상태를 읽고(getState()), 액션을 디스패치하며(dispatch()), 상태 업데이트 시 구독자들에게 알리는(subscribe()) 역할을 합니다. 마치 애플리케이션의 '뇌'와 같은 역할을 수행하죠. 하나의 앱에 하나의 스토어만 존재한다는 점이 핵심입니다.
  2. 액션(Actions)
    애플리케이션에서 '무슨 일이 일어났는지'를 설명하는 일반 JavaScript 객체입니다. 모든 액션은 반드시 type 속성을 가지며, 이는 수행될 작업의 종류를 나타냅니다. 예를 들어, ADD_TODOUSER_LOGGED_IN과 같이 상태 변화의 의도를 명확히 전달합니다. 액션은 "무엇을 할지"만 정의하고, 실제 상태 변경은 리듀서가 담당합니다.
  3. 리듀서(Reducers)
    현재 상태와 액션을 인수로 받아 새로운 상태를 반환하는 순수 함수입니다. 리듀서는 상태를 직접 변경하지 않고, 항상 새로운 상태 객체를 반환함으로써 Redux의 불변성 원칙을 따릅니다. 이는 예측 가능한 상태 관리를 가능하게 하며, 여러 리듀서를 결합하여 전체 애플리케이션 상태를 효율적으로 관리할 수 있습니다. combineReducers를 사용하면 모듈화가 쉬워집니다.
  4. 미들웨어(Middleware)
    스토어에 디스패치된 액션을 가로챌 수 있는 함수입니다. 이를 통해 API 호출, 로깅과 같은 부수 효과(side effects)를 수행할 수 있게 해줍니다. 미들웨어는 액션이 리듀서에 도달하기 전에 추가적인 로직을 실행할 수 있는 강력한 도구로, 비동기 작업이나 복잡한 로직을 깔끔하게 처리하여 애플리케이션의 확장성과 유지 보수성을 높여줍니다. 대표적인 미들웨어로 Redux Thunk나 Redux Saga가 있습니다.

간단한 할 일 앱 예시로 Redux의 동작을 보죠:

// 액션 정의
const ADD_TODO = 'ADD_TODO';

// 액션 생성자 함수
const addTodo = (todo) => ({
  type: ADD_TODO,
  payload: todo, // 액션과 함께 전달될 데이터
});

// 리듀서 함수
const todosReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload]; // 새로운 할 일 항목을 추가하여 새로운 상태 배열 반환
    default:
      return state; // 액션이 일치하지 않으면 현재 상태 유지
  }
};

// 스토어 생성
import { createStore } from 'redux';
const store = createStore(todosReducer); // 리듀서를 사용하여 스토어 생성

// 사용 예시
store.dispatch(addTodo({ id: 1, text: '공부하기' }));
console.log(store.getState()); // [{ id: 1, text: '공부하기' }]

이 코드처럼 액션을 디스패치하면 리듀서가 상태를 업데이트하고, 스토어가 이를 관리합니다. 간단하지만 강력하죠!

Redux Saga: 비동기 로직의 오케스트레이터

Redux 자체는 동기적인 상태 관리를 중점으로 하다 보니, 비동기 작업(예: API 호출)은 미들웨어가 필요합니다. 여기서 Redux Saga가 등장합니다. Redux Saga는 Redux 애플리케이션의 부수 효과, 특히 비동기 작업을 보다 효율적으로 처리하기 위해 설계된 미들웨어 라이브러리입니다. 제너레이터 함수를 사용하여 복잡한 비동기 흐름을 관리하면서도 UI 컴포넌트를 깔끔하게 유지할 수 있는 방법을 제공합니다.

Redux Saga는 네트워크 요청, 타이머, 로컬 스토리지 접근 등 순수하지 않은 비동기 로직을 전용 Saga 파일에서 분리하여 관리함으로써 UI 로직과 비즈니스 로직의 분리를 명확하게 합니다. 결과적으로 코드가 더 읽기 쉽고, 오류가 적어집니다.

728x90

Redux Saga의 주요 이점

  • 관심사의 분리(Separation of Concerns) 향상: UI 로직과 부수 효과를 명확하게 분리하여 코드의 가독성을 높이고, 특정 기능 변경이 다른 기능에 미치는 영향을 최소화합니다.
  • 쉬운 테스트: 제너레이터 함수의 특성을 활용하여 비동기 시나리오를 동기 코드처럼 테스트할 수 있어 테스트 코드 작성이 훨씬 간결해집니다.
  • 강력한 흐름 제어: 취소 가능성(cancellation), 병렬 실행, 에러 핸들링 등 복잡한 비동기 패턴을 ES6 제너레이터로 표현할 수 있습니다.

Redux Saga의 핵심 개념

  1. Sagas
    액션 디스패치 또는 특정 조건 대기 등 작업을 설명하는 객체를 yield하는 제너레이터 함수입니다. 사가는 특정 액션이 디스패치될 때 실행되며, 비동기 작업을 순차적으로 정의할 수 있게 해줍니다. 마치 애플리케이션의 '트랜잭션'을 관리하는 오케스트레이터와 같습니다. 워커 사가(실제 작업 수행)와 워처 사가(액션 감지)가 주요 유형입니다.
  2. 이펙트(Effects)
    사가에 의해 반환되는 일반 객체로, 미들웨어에 무엇을 해야 하는지 지시하는 명령형 설명입니다. call, put, takeEvery 등은 모두 이펙트의 예시이며, 이들은 Saga 미들웨어가 특정 작업을 수행하도록 지시하는 역할을 합니다. 이펙트는 사가의 선언적 스타일을 유지하면서도 실제 실행을 미들웨어에 위임합니다.
  3. TakeEvery / TakeLatest / Call / Put
    • takeEvery: 디스패치된 모든 액션 인스턴스를 수신하고 사가를 동시에 실행합니다. 특정 액션이 발생할 때마다 항상 사가를 실행해야 할 때 유용합니다.
    • takeLatest: 여러 액션이 빠르게 트리거될 때 가장 최근 인스턴스만 실행합니다. 이전 실행 중인 사가는 취소되고 새로운 사가가 시작됩니다. 자동 완성 입력이나 검색 요청과 같이 최신 결과만 필요한 경우에 적합합니다.
    • call: 사가 내에서 비동기 함수를 호출하는 데 사용됩니다. Promise를 반환하는 함수(예: fetch API)를 안전하고 테스트 가능하게 호출할 수 있도록 해줍니다.
    • put: Redux 흐름으로 액션을 다시 디스패치합니다. 비동기 작업이 완료된 후 상태를 업데이트하기 위해 새로운 액션을 트리거하는 데 사용됩니다.

할 일 목록을 비동기적으로 가져오는 예시입니다:

import { call, put, takeEvery, all } from 'redux-saga/effects';

// 워커 사가: 할 일 목록을 비동기적으로 가져오는 역할
function* fetchTodos() {
  try {
    const response = yield call(fetch, '/api/todos'); // `call` 이펙트를 사용하여 비동기 fetch 호출
    const data = yield response.json(); // 응답을 JSON으로 파싱
    yield put({ type: 'FETCH_TODOS_SUCCESS', payload: data }); // 성공 시 'FETCH_TODOS_SUCCESS' 액션 디스패치
  } catch (error) {
    yield put({ type: 'FETCH_TODOS_FAILURE', error }); // 실패 시 'FETCH_TODOS_FAILURE' 액션 디스패치
  }
}

// 워처 사가: 'FETCH_TODOS_REQUEST' 액션을 감지하고 fetchTodos 워커 사가를 실행
function* watchFetchTodos() {
  yield takeEvery('FETCH_TODOS_REQUEST', fetchTodos); // 'FETCH_TODOS_REQUEST' 액션이 디스패치될 때마다 fetchTodos 실행
}

// 루트 사가: 모든 사가를 결합하는 최상위 사가
export default function* rootSaga() {
  yield all([
    watchFetchTodos(), // watchFetchTodos 사가 실행
    // 다른 사가들도 여기에 추가할 수 있습니다.
  ]);
}

이처럼 Saga는 비동기 코드를 선언적으로 작성할 수 있게 해줍니다.

Redux와 React 통합하기

Redux와 Redux Saga를 React 앱에 통합하는 과정은 애플리케이션의 상태 관리와 비동기 로직을 React 컴포넌트로부터 분리하여 코드베이스를 더욱 체계적이고 관리하기 쉽게 만듭니다. react-redux 라이브러리를 사용하면 useSelectoruseDispatch 훅으로 쉽게 연결할 수 있어요.

1. 미들웨어를 적용한 스토어 생성

applyMiddleware를 사용하여 Redux Saga 미들웨어가 Redux 스토어에 연결되도록 합니다. 이는 사가가 액션을 가로채고 부수 효과를 처리할 수 있도록 하는 필수적인 단계입니다.

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers'; // 모든 리듀서 결합
import rootSaga from './sagas'; // 루트 사가

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  applyMiddleware(sagaMiddleware) // Saga 미들웨어 적용
);

sagaMiddleware.run(rootSaga); // 사가 실행

export default store;

2. 리듀서와 함께 사가 설정 및 Provider 연결

스토어를 생성한 후, React 앱의 최상위 컴포넌트에서 <Provider>로 감싸줍니다. 이렇게 하면 모든 자식 컴포넌트가 스토어에 접근할 수 있습니다.

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

컴포넌트에서 상태 사용 예시:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

const TodoList = () => {
  const todos = useSelector((state) => state.todos); // 상태 선택
  const dispatch = useDispatch(); // 액션 디스패치

  const handleFetch = () => {
    dispatch({ type: 'FETCH_TODOS_REQUEST' }); // 사가 트리거
  };

  return (
    <div>
      <button onClick={handleFetch}>할 일 가져오기</button>
      <ul>{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>
    </div>
  );
};

이 통합으로 컴포넌트는 UI에만 집중하고, 상태와 비동기는 Redux/Saga가 처리합니다.

728x90