Redux ? Redux Reducer?

soom·2021년 1월 21일
1
post-thumbnail

Flux?

Flux는 Facebook에서 만든 client-side web applications을 구축할 때 사용하는 application architecture(앱 구조), design pattern(디자인 패턴)이다.
MVC (Model–View–Controlle)구조 의 단점을 보완할 목적으로 개발된 Flux는 대규모 프로젝트에서 너무 복잡해지는 MVC구조의 단점을 보완하는 단방향 데이터 흐름(unidirectional data flow)의 구조이다.

action -> dispatcher -> store -> view. 위의 순서대로 작업이 일어나게된다.

action을 통해 이벤트가 일어나게 되면, dispatcher가 action을 통제하여 store에 업데이트를 한다. store에서 업데이트된 데이터를 토대로 현재 view가 수정되어야할경우 리렌더링이 일어난다.
dispatcher는 작업이 중첩되면 안된다.

Redux?

리덕스(Redux)는 Javascript app을 위한 예측가능한(predictable) state container이다. 리액트 뿐만 아니라 Augular, jQuery, vanilla JavaScript 등 다양한 framework와 작동되게 설계되었다. 즉, 리액트만을 위한 Library는 아니다.

3가지 원칙

  1. Single Source of Truth

    단 하나의 store를 사용한다. flux를 토대로 만들어진 라이브러리이지만, 여러개의 store를 사용하는 flux와 다르게 단 하나의 store만 사용하며, 이 안에 모든 state값들이 포함되어있다. 그래서 ‘전지전능한 진리’이다. store에는 데이터가 nested 된 구조로 이루어져있다.

    스토어는 언제나 단 한개이다. 스토어를 여러 개 생성해서 상태를 관리하면 안된다. 그 대신 리듀서를 여러 개를 만들어서 관리 할 수 있다.

  2. State is Read-only

    store안에 있는 모든 state값들은 ‘읽기’전용 데이터들이다. 컴포넌트에서 직접 state값을 수정할 수 없다. 이를 가능하게 하려면 action > dispatcher 구조를 통해야한다. 객체를 통해 ‘어떠한’ 변화를 ‘무슨’ 값들과 함께 일어날지 storedispatch해줌으로 써 state들을 변경하게된다.

    리덕스의 상태, state는 읽기 전용이다. 이 값은 절대로 직접 수정하면 안된다. 직접 수정을 하게 된다면 구독 함수를 제대로 실행하지 않거나 컴포넌트의 리렌더링이 되지 않을 수 있다.

    • 상태를 업데이트 할 때는 언제나 새 상태 객체를 만들어서 넣어 주어야 한다. 업테이트를 할 때마다 새 객체를 만든다면 메모리 누수가 일어나지 않을까? 아니다. 이전에 사용하던 객체들이 메모리에 누적되지 않는다. 상태 레퍼런스가 사라지면 자동으로 메모리 관리를 한다.
  3. Changes are made with pure functions

    위에서 설명했듯이 state를 변경하려면 action > dispatcher를 통해야만한다. 이때 받아온 action객체를 처리하는 것이 reducer입니다. action은 어떤 변화가 일어날지를 알려준다면 reducer는 이때 받은 값을 토대로 변화를 일으킵니다. reducer에 대해선 다음과 같습니다

    • 외부 네트워크 혹은 데이터베이스에 접근하지 않아야한다.
    • return 값은 오직 parameter 값에만 의존되어야한다.
    • 인수는 변경되지 않아야한다.
    • 같은 인수로 실행된 함수는 언제나 같은 결과를 반환해야한다.
    • 순수하지 않은 API 호출을 하지 말아야 한다. (DateMath 의 함수 등)
    • 모든 변화는 순수 함수로 구성해야 한다. 여기에서 함수는 리듀서 함수를 말한다. 순수 함수에서 결과값을 출력할 때는 파라미터 값에만 의존해야 하며, 같은 파라미터는 언제나 같은 결과를 출력해야 한다.
    • 예를 들어 리듀서 함수 내부에서 외부 네트워크와 데이터베이스에 직접 접근하면 안된다. 요청 실패 할 수도 있고, 외부 서버의 반환 값이 변할 수 있기 때문이다.

Terminology

  • 액션(Action) : 상태 변화를 일으킬 때 참조하는 객체이다.

    • const mapActionToProps = (dispatch) => { } 함수 사용
    • Action이라는 단어는 Event와 같아고 생각하면 된다.
    • dispatch 인수에서 Reducer로 넘길 객체(type)를 정의한다.
    • Action이 실행되고 끝나면 type을 반환하는데 type은 Reduce로 전달된다.
  • 스토어(Store) : 애플리케이션의 상태 값들을 내장하고 있다.

    • state 값을 가지고 있다.
    • 중앙에서 변수 관리 개념이라고 생각하면 된다.
    • 리듀스에 의해서만 state의 값이 변경된다.
  • 리듀서(Reducer): 상태를 변화시키는 로직이 있는 함수이다.

    • ex) export function reducer(state = {state : 10, age:100}, action)
    • Reducer 함수를 생성 할 때 살찐 에로우를 사용하지 않는다.
    • Reducer 함수는 순수함수여야 한다. 결과 값을 출력 할 때는 파라미터 값에만 의존해야 하며, 언제나 같은 결과를 출력해야 한다.
    • Reducer에서 state를 사용한다면 반드시 state를 초기화 해야 한다.
    • Reducer에서 state의 변화가 일어난다.
    • 값의 갱신은 반드시 reducer에서 해야 한다.
  • State : 컴포넌트에 최종 출력하기 전 거치는 중간과정이다.

    • mpaStateToProps(state) 함수 사용
    • statestore에서 가져왔다 라고 생각하면 된다.
    • Store에 저장되어 있는 변수를 가져와서 최종 가공을 위한 목적으로 사용된다.
    • 예를 들어, num:state.num*100 이라고 갱신을 하더라도 실제 num의 값은 갱신되지 않고 컴포넌트에 출력하는 값을 가공한 것이다.
    • 중간 과정을 거치게 되면 중간 수정이 가능하다. 원화를 달러로 바꿀 수 있듯이 가지고 있는 원화를 실제로 출력을 할 때는 달러로 출력을 하게 되는 것이며, 원화는 변화지 않는다.
  • 디스패치(dispatch) : 액션을 스토어에 전달하는 것을 의미한다.

  • 구독 : 스토어 값이 필요한 컴포넌트는 스토어를 구독한다.

    • 리액트 컴포넌트에서 리덕스 스토어를 구독하는 작업은 후에 react-reduxconnect 함수가 대신 한다.
    • 리덕스의 내장 함수를 사용하여 subscribe, unsubscribe 함수를 사용하여 구독 및 구독 취소를 할 수 있다.

Redux vs. Flux

리덕스는 Flux의 몇 가지 중요한 특성에 영감을 받아 개발되었다.
Flux와 달리 리덕스는 dispatcher라는 개념이 존재하지 않는다. 그리고 다수의 store도 존재하지 않는다. 대신 리덕스는 하나의 root에 하나의 store만이 존재하고, 순수함수(pure functions)에 의존하는데, 이 순수 함수는 이것들을 관리하는 추가적인 entity 없이도 조합하기 쉽다 .

물론 차이점이 있지만, 결론적으로 Flux, 리덕스 두 가지의 구조 모두 닮았으며 결국 리덕스는 Flux 패턴을 좀 더 쉽고 정돈된 형태로 쓸 수 있게 도와주는 라이브러리라고 볼 수 있다. 서로 영향을 많이 주고받았기 때문에 리덕스를 공부할 때 Flux구조를 공부하는 것은 많은 도움이 될 것이다.

Why Redux for React?

리액트로 프로젝트를 진행하게 되면, Component는 local state를 가지게 되고, 어플리케이션은 global state를 가지게 된다.

  • Local state: 각각의 Component가 가지는 state. 어플리케이션은 이 state를 기반으로 만들어진다.

  • Global state: 예를 들어, 유저의 로그인의 유무에 따라 어플리케이션의 state가 달리 보이는 것을 들 수 있다. 어플리케이션 전체에서 global state는 유지, 즉 local stateglobal state를 공유하게 되는 것이다.

리액트로만 프로젝트를 진행하게 될 경우 우리의 어플리케이션은 local state, 그리고 global state를 관리하기 어렵다. 이 때 생기는 몇가지 문제를 가지고 왜 리덕스가 필요한지 설명해보도록 하겠다.

local state의 전달이 어렵다

이전 프로젝트 에서는 최상단 ComponentApp.js에서 cart state를 만들고, 각각의 Component에 이를 props로 전달하여 프로젝트를 관리했었다.

프로젝트의 구조가 굉장히 단순하였기 때문에 단 한번의 data이동으로 cart state가 전달되었다. 하지만, 프로젝트의 규모가 커지고 Component의 수가 늘어나게 된다면 어떻게 될까?

단순히 cart stateCartItem.js로 전달하고 싶은데, 이를 위해 cart props를 사용하지 않는 Component에도 cart props를 전달하게 된다. 프로젝트의 규모가 커질수록, propsdata를 전달하기 위해 이렇게 필요없는 data의 흐름이 생기게 된다. 또한, 만약 CartItem.js에서 제대로 cart props가 전달이 안 될 경우, 중간에 끼인 모든 Component에서 일일이 문제점을 찾아봐야 한다.

global state의 유지가 어렵다

대부분 어플리케이션에서는 로그인 기능이 구현되어야 한다. 유저마다의 개인정보, 그리고 결제정보 등이 있어야 되기 때문이다. 하지만 리액트만으로 프로젝트를 진행하면, global state를 모든 Component에 유지하기 어렵다. 유저의 인증정보를 모든 Componentprops로 계속해서 전달해야 하는데, 하단의 diagram보다 훨씬 복잡한 절차를 거치게 될 것이다.

Reducer?

  • 리듀서는 이전 상태와 동작을 받아 새 상태를 리턴한다.
  • 리듀서는 반드시 순수 함수여야 한다. 이를테면 데이터베이스 호출이나 HTTP 호출 등 외부의 데이터 구조를 변형하는 호출은 허용되지 않는다.
  • 리듀서는 항상 현재 상태를 '읽기 전용'으로 다룬다. 기존 상태를 변경하지는 않지만 새 상태를 리턴은 할 수 있다.

3 Core things of Reducer

  • 할 일을 정의하는 Action(인수는 옵션)
  • 애플리케이션의 모든 데이터를 저장하는 state
  • stateAction을 받아 새 상태를 리턴하는 Reducer

Basic Reducer

  • 가장 단순한 리듀서는 상태 자체만을 리턴한다(identity reducer라고 한다).
  • payload는 종류와 상관 없이 객체가 될 수 있다.
  • Reducer에는 타입 TstateAction을 받아 새 state를 리턴하는 함수가 포함된다.
  • 의미는 없지만, 여기서 알 수 있는 것은 리듀서는 기본적으로 원래 상태를 리턴한다.

example 1.1

interface Action {
  type: string;
  payload?: any;
}

interface Reducer<T> {
  (state: T, action: Action): T;
}

let reducer: Reducer<number> = (state: number, action: Action) => {
  return state;
};

console.log(reducer(0, null)); //0

State change

리덕스에서는 상태를 변경할 수 없다는 사실을 잊지 말자.
switchdefault case에서는 원래 state를 리턴한다. 그래야 알 수 없는 동작이 전달되어도 오류가 출력되지 않고 원래 state가 변경되지 않는다.

let incrementAction: Action = { type: 'INCREMENT' };
let decrementAction: Action = { type: 'DECREMENT' };

let reducer: Reducer<number> = (state: number, action: Action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    case 'PLUS':
      return state + action.payload;
    default:
      return state;
  }
};

console.log(reducer(3, { type: 'PLUS', payload: 7 })); // 10
console.log(reducer(5, { type: 'PLUS', payload: -2 })); // 3

State update

리듀서는 순수한 함수이며, 외부 환경을 변경하지 않는다. 문제는 앱에서 모든 것이 변경된다는 점이다. 즉 상태는 변화하고 앱 어딘가에서는 새 상태를 유지하고 있어야 한다.
리덕스에서는 상태를 저장소(store)에 보관한다. 저장소는 리듀서를 실행하여 새 상태를 유지할 책임을 진다.

class Store<T> {
  private _state: T;

  constructor(private reducer: Reducer<T>, initialState: T) {
    this._state = initialState;
  }

  getState(): T {
    return this._state;
  }

  dispatch(action: Action): void {
    this._state = this.reducer(this._state, action);
  }
}

let store = new Store<number>(reducer, 0);
console.log(store.getState()); // 0

store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // 1

store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // 2

store.dispatch({ type: 'DECREMENT' });
console.log(store.getState()); // 1
  1. constructor에서는 _state를 초기 상태로 설정했다.

  2. getState()는 단순히 현재 _state를 리턴한다.

  3. dispatch는 동작을 받아 이를 리듀서로 보낸 뒤 _state의 값을 리턴값으로 업데이트한다.

  4. dispatch는 아무것도 리턴하지 않는다. 저장소의 상태를 '업데이트'할 뿐이다. (리덕스의 중요한 원칙)

다음의 글을 참고하였습니다.

profile
yeeaasss rules!!!!

2개의 댓글

comment-user-thumbnail
2021년 11월 16일

리덕스 공부중인데 큰 도움이 되었습니다.
한 가지 궁금한 질문이 있습니다.
action 설명에서 action > dispatcher 구조로 통해야만 reducer로 보내어 상태값을 바꿀 수 있다고 되어 있는데
flux 비교 설명에서는 리덕스는 flux랑 달리 dispathcer가 없다라고 되어 있어 헷갈려서요...
감사합니다 !!

답글 달기
comment-user-thumbnail
2024년 3월 8일

Redux works by maintaining the state of your entire application in a single immutable state tree (object), which can't be changed directly. fnf

답글 달기