시작하기에 앞서

Redux와 Rematch를 소개하기에 앞서
먼저 간략하게 redux에 대한 설명을 하고 넘어가겠습니다.

Redux란?

redux에 대한 한글 소개문서에서는 자바스크립트앱을 위한 예측가능한 상태 컨테이너라고 소개합니다.

좀더 설명하자면 flux 아키텍쳐 구조를 가진 상태관리 라이브러리로 모든상태저장소를 중앙집중형식으로 관리하는 라이브러리라고 생각하면 됩니다.

그리고 해당 상태를 변경하기 위해서는 직접 해당상태를 조회하고 변경하는것이 아니라 action이라는 객체를 store에 dispatch시켜야하고 특정상태를 변경처리할 함수 reducer를 정의해야 가능합니다.

reducer라는 개념은 저도 처음에는 혼란스러웠는데요.

간단하게 event와 eventHandler라고 생각하시면 편할듯싶습니다.

reducer의 구성 조건은 순수 함수형이어야 하고 새로운 상태로 변경이되면 이전상태를 그대로 반환하는 대신에 상태를 복제하여 변경된 상태를 반환해야 합니다.

즉 이전 상태를 변경하는 행위는 하지말아야합니다.

function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

counter리듀서가 처리할수있는 액션이 없다면 현재상태는 변경되지않고 그대로 반환되지만 변경이 된다면 변경된 상태를 반환합니다.

현재상태에서 새로운상태복제를 하지않는건 js number는 값객체이기 때문입니다.

redux의 난해함

redux는 이런식으로 상태관리를 하고있지만 초보자가 보기에는 난해합니다.
비슷한 객체지향 패턴인 command pattern도 잘사용하지않는사람이 많기떄문이죠

간단하게 counter앱을 정의해봅시다.

import { createStore } from "redux";

// 액션타입을 정의하고

const INCREMENT = "INCREMENT";
const DECEREMENT = "DECREMENT";

///액션생성자를 만들어주고
const createIncrementAction = () => {
  return {
    type: INCREMENT
  };
};
const createDecrementAction = () => {
  return {
    type: DECREMENT
  };
};

// 이것을 처리할 reducer를 만듭시다.
function counter(state = 0, action) {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
}

const store = createStore(counter);

sotre.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(createIncrementAction()); //혹은
store.dispatch({ type: "INCREMENT" });
store.dispatch(createDecrementAction()); //혹은
store.dispatch({ type: "DECREMENT" });

상태를 변경하는 조건은 액션이라고 하는 객체를 store에 dispatch해야합니다.
고작 counter 상태하나 변경하는데 거추장스러운 코드가 굉장히 많아지게됩니다.
이렇게 하는데에는 강력한 장점이 있기때문에 그렇습니다.

  1. 확장성
  2. 모든 액션을 기록
  3. 시간여행디버깅가능

그렇다고해도 함수형의 장점인 소프트웨어의 결합도가 낮은건 좋지만
응집도가 낮은것은 난해함을 극복하는데 있어서 큰 도움이 되지 않습니다.

redux커뮤니티중에서는 이런점을 극복하기 위한 방법으로 duck패턴을 사용하기를 권하고 있습니다.

https://github.com/JisuPark/ducks-modular-redux

하지만 이런 패턴으로 구현하는 대신에 개인적으로 rematch라고하는 framework를 사용하기를 권장합니다.

Rematch는 또 뭔데?

Rematch

소개문서를 설명하면

rematch는 redux의 보일러플레이트없는 best pratice입니다.

redux의 action type, action creator, switch문과 비동기처리를 하기위한 별도의 라이브러리인 thunk가 더이상 필요하지 않습니다.

간단하게 rematch소개페이지의 counter를 보면

index.js

import { init } from "@rematch/core";
import * as models from "./models";

const store = init({
  models
});

export default store;

store.js

export const count = {
  state: 0,
  reducers: {
    increment(state, payload) {
      return state + payload;
    }
  },
  effects: dispatch => ({
    async incrementAsync(payload, rootState) {
      await new Promise(resolve => setTimeout(resolve, 1000));
      dispatch.count.increment(payload);
    }
  })
};

dispatch

// reducers
store.dispatch({ type: "count/increment", payload: 1 }); // state = { count: 1 }
store.dispatch.count.increment(1); // state = { count: 2 }

// effects
store.dispatch({ type: "count/incrementAsync", payload: 1 }); // state = { count: 3 } after delay
store.dispatch.count.incrementAsync(1);

보시다시피 직접 액션타입문자열을 정의할필요없이 모델의 이름과 리듀서 함수의 이름으로 actionType까지 프레임워크가 정의해주고 그것을 생성하기 위한 actionCreator까지 정의할필요가 사라졌습니다.
redux-thunk같은 비동기처리를 위해 붙이던 라이브러리도 필요없이 effects에서 정의하는것만으로 손쉽게 비동기액션도 붙일수있게 되었습니다.

게다가 typescript에서도 mapState, mapDispatch에 사용할 dispatch와 rootState의 타입을 정의하기 굉장히 간편합니다.
더군다나 dispatch에 정의되는 메소드에서도 엄격하지는 않지만 정의한 reducer의 정보를 토대로 타입정보를 알려줄수있습니다.

import { init, RematchRootState } from "@rematch/core";
import * as models from "./models";

export const store = init({
  models,
});

export type Store = typeof store;
export type Dispatch = typeof store.dispatch;
export type iRootState = RematchRootState<typeof models>;

Conclusion

이제 매번 ducks패턴에 맞춰서 구현하는 대신에 프레임워크가 제공해주는 틀대로 개발해보는건 어떨까요?

보일러플레이트코드를 매번 작성하는 피곤함에서 벗어나실수 있을겁니다.

redux & rematch comparison
why we created rematch

본포스트는 저의 블로그에서 쓴 글을 재구성한 글입니다.