리액트와 리덕스 그리고 리덕스 사가, 타입스크립트 - Redux series(3)

React, Redux, Middleware Redux-Saga, Typescript

Sat, 04 Jan 2020

redux

저번 포스팅에선 리액트와 타입스크립트 환경에서 리덕스를 실제로 사용하는 방법을 알아보았습니다. 이번 포스팅에선 리덕스 미들웨어에 대해서 알아보겠습니다.

리덕스 미들웨어란? 🤔

const middleware = (store) => {
  return (next) => {
    return (action) => {
      if (action.type === 'ACTION_TYPE') {
        // 미들웨어가 액션을 판단해서 실행된다
        // ...
      }
    }
  }
}

미들웨어의 형태는 이렇게 생겼습니다. 미들웨어 내에 사용되는 파라미터는 클로저로 넣어줍니다.
그렇다면 도대체 미들웨어를 왜 사용할까요?

미들웨어의 등장

기본적으로 리덕스의 규칙중 리덕스의 리듀서는 순수 함수이어야 합니다. 이는 즉 부수적인 효과 작업이 불가능하다 입니다. 따라서 비동기 작업 등 이러한 부수적인 작업을 처리하기 위해서는 미들웨어가 필요하게 되었습니다.
리덕스의 미들웨어 종류에는 대표적으로 Redux-Thunk와 Redux-Saga 두가지가 있습니다. 이번 포스팅에선 제가 주로 사용하는 타입스크립트 환경에서의 리덕스 사가를 알아보도록 하겠습니다.

리덕스 사가 🛰

리덕스 사가는 리액트 리덕스의 사이드이펙트만을 담당하는 미들웨어중 하나입니다. 리덕스 사가는 우리의 앱에서 액션들을 받아 처리하고, 멈추고, 취소할 수 있게 만들어주고 리덕스 상태에 접근하여 액션을 디스패치 할 수 있습니다.

리덕스 사가는 비동기흐름을 쉽게 읽고 쓰고 테스트 할 수있는 ES6의 문법인 Generator를 이용합니다. 리덕스 사가를 알아보기전에 Generator 함수에 대해 잘 모르신다면 이 곳에서 먼저 확인해보세요.

키워드

리덕스 사가를 사용하기 전에 알아두면 좋은 헬퍼함수를 알아보겠습니다. 이 헬퍼함수들은 redux-saga/effects 패키지에 포함되어있습니다.

  • put
    리듀서에게 액션을 디스패치합니다.

    // put example
    put(ActionCreateFunction)
  • take
    특정 문자열이 들어오기를 기다립니다.

    // take example
    yield take('String!')
  • delay
    실행중이던 함수를 잠깐 멈춥니다. 함수를 동기적으로 사용할 수 있습니다.

    // delay example
    delay(1000)
  • call
    비동기적인 처리를 할 때 사용하며 특정함수를 호출하고 결과물이 반환될 때 까지 기다립니다.

    // call exmaple
    call(api.getSomething)

우선은 이정도만 알아보고 추가로 필요한 키워드가 있으면 그때그때 알아보도록 하겠습니다.

리덕스 사가 사용해보기 🎩

먼저 기본적인 사가를 만들어 보겠습니다.

카운터사가 프로젝트 생성

npx create-react-app counter-saga --typescript
cd counter-saga
yarn add redux react-redux @types/react-redux react-saga @types/redux-saga

먼저 늘 하던 새로운 타입스크립트 기반의 CRA 프로젝트를 생성해주고, 사용할 패키지들을 설치합니다.

카운터 리덕스 모듈 작성

이번에는 전 포스팅과 조금 다른 Ducks 패턴이 아닌 모듈별로 새로운 폴더를 만들고 그 안에 index.ts, actions.ts, reducer.ts, sagas.ts 등으로 분리하는 패턴으로 작성을 해보겠습니다.

// src/modules/counter/actions.ts

// 액션
export const INCREASE = 'counter/INCREASE' as const;
export const DECREASE = 'counter/DECREASE' as const;

// 액션 생성함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// 액션객체 타입
export type CounterActionType =
  | ReturnType<typeof increase>
  | ReturnType<typeof decrease>;

먼저 src/modules/counter 경로대로 새로운 디렉토리를 생성하고 actions.ts 파일을 만들어 보겠습니다. actions.ts 파일에는 액션, 액션생성함수, 액션객체 타입등을 작성하겠습니다. 액션들은 이전 포스팅과 마찬가지로 나중에 작성할 리듀서에서 에러를 방지하기 위해 타입단언을 이용합니다.

// src/modules/counter/reducer.ts

import { INCREASE, DECREASE, CounterActionType } from './actions';

const initialState = 0;

const counter = (
  state: number = initialState,
  action: CounterActionType // (1)
): number => { // (2)
  switch (action.type) {
    case INCREASE:
      return state + 1;
    case DECREASE:
      return state - 1;
    default:
      return state;
  }
};

export default counter;

그리고 리듀서를 만들어줍니다. (1) 라인의 아까 타입단언을 이용하여 지정해둔 액션타입을 가져와 지정해줌으로써 리듀서 내부의 액션타입 추론이 정확하게 이루어집니다. 이 내용이 헷갈리신다면 이전 포스팅을 참고해보세요.

(2) 라인의 리듀서함수의 리턴타입을 미리 number로 지정함으로써 추후에 발생할 에러나 실수를 방지합니다.

// src/modules/counter/sagas.ts

import { put } from 'redux-saga/effects';
import { INCREASE, DECREASE, increase, decrease } from './actions';

export function* increaseSaga() {
  yield put(increase());
}

export function* decreaseSaga() {
  yield put(decrease());
}

다음의 작성 파일은 sagas.ts 입니다. 사실 지금 하고있는 카운터 예제는 사이드이펙트가 없어 사가를 이용하는것이 무의미 하다고 할 수 있지만 사가를 이용한 카운터를 만들기 위한 예제이니 만들어 주어야겠죠.

먼저 put 키워드를 보시면 위에서 설명한것과 같이 리듀서에게 액션을 디스패치한다는 의미입니다.

yield put(increase()); 코드를 좀 더 자세히 보면 increase()actions.ts 에서 작성한 액션생성 함수이며 { type: ‘counter/INCREASE’ } 를 반환하게됩니다. 그래서 저 코드라인은 put({ type: ‘counter/INCREASE’ }) 과 같은 맥락이며 리듀서에게 이러한 타입의 액션을 발생시킴을 의미합니다.

// src/modules/counter/index.ts

export { default } from './reducer';
export * from './actions';
export * from './sagas';

그리고 카운터모듈 디렉토리의 index.ts 파일을 생성하여 다른파일에서 사용할수 있도록 export 해줍니다. 혹시 추후에 파일에서 너무 많은 타입들이 이용된다면 types.ts 같은 파일을 만들거나 API를 이용한다면 api.ts 같은 파일을 생성하여 이용할 수 있습니다. 이러한 디렉토리 구조는 개발자의 자유입니다.

// src/modules/index.ts

import { combineReducers } from 'redux';
import counter, { increaseSaga, decreaseSaga } from './counter';
import { all } from 'redux-saga/effects';

const rootReducer = combineReducers({
  counter
});

export function* rootSaga() {
  yield all([increaseSaga(), decreaseSaga()]);
}

export type RootReducerType = ReturnType<typeof rootReducer>; // (1)

export default rootReducer;

우리가 만들었던 모듈들 지금은 하나의 카운터 모듈만 가지고 있지만, 이 모듈들을 하나의 파일에서 묶어줍니다.

all 키워드는 배열안의 사가들을 동시에 실행시킵니다. 이 헬퍼함수를 이용하여 여러개의 사가를 동시에 이용할 수 있게 됩니다.

(1) 라인의 루트리듀서의 타입을 미리 선언하는 이유는 추후에 작성할 컴포넌트에서 우리가 가진 리듀서모듈을 쉽게 조회하고 타입을 추론하기 위해서입니다.

루트리듀서 적용하기

// index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(rootSaga);

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

우리가 만든 리듀서모듈을 index.tsxProvider를 이용하여 적용합니다. 리덕스에 미들웨어를 적용하기 위해서 applyMiddleware 를 이용합니다. 렌더부분 위쪽에선 run을 이용하여 루트사가를 실행시킵니다.

카운터 컴포넌트 작성

// src/components/Counter.tsx

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootReducerType } from '../modules';
import { increase, decrease } from '../modules/counter';

const Counter = () => {
  const state = useSelector((state: RootReducerType) => state.counter); // (1)
  const dispatch = useDispatch();

  const handleIncrease = () => {
    dispatch(increase());
  }

  const handleDecrease = () => {
    dispatch(decrease());
  }

  return (
    <>
      <div>{state}</div>
      <button onClick={handleIncrease}>up</button>
      <button onClick={handleDecrease}>down</button>
    </>
  );
};

export default Counter;

이제 우리가 만든 리듀서모듈을 이용할 컴포넌트인 Counter 컴포넌트를 만들고 적용시켜보겠습니다.

(1) 라인의 state 에 아까 작성해두었던 루트리듀서 타입을 미리 지정해두면 쉽게 타입추론이 가능합니다. 그리고 각 액션생성 함수를 dispatch 안에서 실행시키면 우리가 만들어두었던 사가를 통해 리듀서에 도착합니다. 이렇게 중간에 사가를 거치면서 순수함수인 리듀서에서 컨트롤할수 없는 사이드이펙트(예를 들어 비동기처리)들을 처리할 수 있게 됩니다.

이제 up, down 버튼을 눌러 확인해보세요! 카운터가 제대로 작동하나요?

마무리 🎓

사실 이번 포스팅에서 사용한 예제는 사이드이펙트가 없기때문에 사가를 이용하지 않아도 충분합니다. 이 포스팅에서 사용된 튜토리얼은 미들웨어와 사가의 전체적인 동작을 알아보기 위함이고 다음 포스팅에서 사이드이펙트를 컨트롤하는 튜토리얼을 이용해 보겠습니다.

Reference

Loading...
byseop

BYSEOP 안녕하세요. BYSEOP입니다.
제 글을 읽어주셔서 감사합니다. 도움이 되셨다면 위쪽에 SHARE를 이용해주세요!
궁금한점은 댓글로 남겨주세요. 감사합니다!

  • this is a personal blog built by byseop
  • GatsbyJS, ReactJs, CSS in JS
  • deliverd by Netlify