Redux Architecture

jtwjs·2021년 7월 18일
0

상태관리

목록 보기
2/4
post-thumbnail

Redux Architecture

Redux Concept

  1. 상태를 전역으로 관리하게 되면 동일한 상태를 여러 컴포넌트가 수정을 하게되고 잘못 수정됐을 경우 잘못 수정된 컴포넌트를 찾기 힘든 문제를 각각의 컴포넌트가 직접적으로 상태를 수정하지 못하게 수정 요청만 가능하게 하고 실질적으로 수정을 한곳에서만 관리하게 만들어 그 부분만 파악함으로써 문제를 해결할 수 있다.

  2. Redux는 앱의 상태의 디자인을 알수가 없다. (앱마다 상태의 디자인이 다 다르기 때문)
    상태를 디자인에 맞게 수정하는 함수(reducer)를 Store에 전달만 해주고 상태를 수정해야하는 시점에 수정 요청 함수(dispatch)를 호출해주면 된다.

  3. Redux가 어떤 상태를 바꿔야 하는지 힌트를 주기 위해 데이터 구조가 정형화된 포맷(type이란 속성을 가지고 있는 객체 === Action 객체)을 강제한다.

  4. 같은 상태를 여러 곳에서 사용한다면 상태를 수정 요청하는곳 외에서는 상태가 바뀌는 타이밍을 알수없다. 상태가 바뀌고나서 호출될 로직들을 함수(subscribe: 구독)로 감싸서 상태가 변경되는 시점(store에 dispatch가 되어 reducer가 호출된 다음)에 호출되게 설정하면 상태가 바뀌고나서 호출될 함수를 설정 가능

  5. reducer는 동기 프로세스 전제로 설계되었기 때문에 비동기 작업을 처리 불가
    (비동기 처리작업이 완료되기 전에 이미 reducer는 state를 return함)
    redux는 middleware라는 형태로 비동기 처리를 제공한다.

  6. 순수하지 않은 side-effect가 있는 작업(ex: 비동기처리)들을 미들웨어 구조에서 작업한다.
    흘러가는 액션 중 비동기 액션만 가로채어 비동기 작업을 처리후에 완료나 실패한 결과물을 dispatch에 전달

export function createStore(reducer, middleware = []) {
  let state;
  let handler = [];
  
  // 수정 요청 함수
  function dispatch(action) {
    middleware(dispatch, action);
    // 리듀서에서 반환된 상태로 기존 상태를 변경
    state = reducer(state, action);
    // 디스패치(상태가 변경)될 때마다 구독 리스너 호출
    handler.forEach((listener) => {
      listener();
    })
  }
  
  function getState() {
    return state;
  }
  
  function subscribe(listener) {
    handler.push(listener);
  }
  
  /*
  * middleware가 있다면 middleware 순서대로 실행
  * middleware가 없으면 그 때 reducer가 호출
  */
  
  const store = {
    getState,
    subscribe
  }
  
  // 기존 미들웨어 순서를 뒤집는다.
  middleware = Array.from(middleware).reverse();
  
  // 마지막 리듀서를 저장할 변수
  const lastDispatch = dispatch;
  
  middleware.forEach(m => {
   lastDispatch =  m(store)(lastDispatch);
  })
  
  // store가 될 객체를 리턴
  return { ...store, dispatch: lastDispatch }
}

Store

Redux 스토어(store)는 애플리케이션의 상태를 관리하고, .getState(), .dispatch() .subscribe() 같은 메서드를 제공

import { createStore } from 'redux';

// 스토어는 하나만 존재한다.
const store = createStore(reducer);

Redux가 제공하는 모든 것들은 Store에 담겨 있다.
Store에 전달하면 Redux가 제공하는 도구들을 사용 할 수 있다.

State

Redux 스토어에서 관리하는 상태(데이터)

  • 일반적으로 state 또는 initState 이름으로 설정
  • 상태는 리듀서(함수)의 첫번째 인자로 전달 됨
const state = 0;
const initState = [];

스토어에 등록된 상태 정보는 .getState() 메서드를 사용해 가져올 수 있다.

store.getState();

Action

액션은 애플리케이션에서 "상태 변경을 설명하는 정보"를 스토어로 보내는 Js 객체로 Redux에 알려 (dispatch) 변화를 이끌어 낸다.

  • 상태 값을 변경할 경우, 변경할 상태 값(payload)을 리듀서(함수)에 보낼 수 있다.
// 액션 객체를 생성하는 액션 생성 함수
const increaseCountAction = (payload) => {
  
  // type과 변경할 상태 값을 갖는 액션 객체를 반환
  return {
    type: INCRASE_COUNT,
    payload
  }
}

애플리케이션 상태 트리를 변경하는 유일한 방법은 "액션을 보내는 것"

store.dispatch(action);

액션 타입을 별도 관리하는 파일을 만들어 상수(constant)로 관리하는 것이 유지 보수하기 좋다.

// store/actions/actionTypes.js
export const INCREASE_COUNT = 'INCREASE_COUNT';
export const DECREASE_COUNT = 'DECREASE_COUNT';

Reducer

애플리케이션 상태를 교체하는 함수
이전 상태(prevState)를 새로운 상태(state)로 교체한다.

const reducer = (state, action) {
  switch(action.type) {
    case INCREASE_COUNT:
      return state + 1;
      
    case DECREASE_COUNT:
      return state - 1;
      
    default: 
      return state;
  }
}

리듀서는 주어진 상태를 수정하는 것이 아니라, 새로운 상태로 교체하는 것이 중요하다. 즉, 리듀서는 '순수함수' 여야 한다.

리듀서 = (상태, 액션) {
  액션 타입 분석
  이전 상태 -> 다음 상태로 교체
  다음 상태 반환
}

리듀서(함수)는 순수해야 한다.

순수함을 잃어버리게 되면 사이드 이펙트(부작용)을 발생시킨다.

  • 전달 받은 매개변수 state, action에 변형을 가하면 안된다.
  • 네트워킹(API 호출 <- 비동기 통신)또는 라우팅을 변경하면 안된다.
  • 반드시 반환 값은 새로운 상태(state)이다.

Subscription

애플리케이션 상태 변경을 구독(subscribe, 감지)하여 상태가 업데이트 되면 등록된 리스너(listenr)를 실행시킨다.

store.subscribe(render);

.subscribe() 메서드는 구독을 취소할 수 있는 unscribe 함수를 반환한다.

const unsubscribe = store.subscribe(() => console.log(`상태 변경감지: ${store.getState()}`));

unsubscribe 함수를 실행하면 상태가 업데이트 되어도 UI를 업데이트 하지 않는다.

unsubscribe(); // 구독 취소

React Redux 라이브러리를 추가해 사용하면 서브스크립션은 자동으로 처리된다.

Pub-Sub 패턴
디자인 패턴 중 하나로, Observer 패턴의 반대
객체와 객체의 상태를 궁금해하는 Subscriber들이 있을때, 객체의 상태가 궁금해하는 Subscriber들만 상태변화를 인지한다.
이 때 상태를 가지고 있는 객체는 Publisher라고 하고 상태를 궁금해하는 객체를 Subscribe라고 한다. 이를 줄여서 Pub-Sub 패턴이라 한다.

Middleware

redux에서 발생하는 action들이 중간에 거쳐서 나가는 곳을 middleware라 함
(즉, 데이터처리기 안에 흘러들어와 프로세싱을 거쳐 나가는 구조)

  • 함수형프로그래밍의 커링을 활용한 구조
  • 비동기 작업은 3가지 정도의 action이 필요하다
    • requset(요청)
    • response(응답)
    • rejection(실패)
  • middleware는 여러개가 있을 수 있다.
    • 몽키패칭과 커링을 이용하여 미들웨어를 역순으로 처리 후 마지막에 reducer를 호출하는 구조
/* 리덕스가 제공하는 기능들을 미들웨어도 할수있게 만들어야 한다.
* 리덕스가 제공하는 모든 기능들은 Store가 가지고 있기 때문에
* Store에 넘겨 주기만 하면 된다.
*/

/* store를 인자로 제공 받는 이유 
 * middleware 안에서도 getState()를 사용할수 있기 때문
*/
const middleware = store => dispatch => action {
  if (action.type === '비동기 작업') {
    setTimeout(() => {
      dispatch({
        type: 'fetch-response',
        payload: [1,2,3]
      })
    }, 2000);
  } else {
      dispatch(action);
  }
}

// middleware는 여러개가 있을 수 있으니 배열로 받는다.
const store = createStore(reducer, [middleware]);

커링(Curring) (함수형 프로그래밍)
커링은 인자를 여러개 받는 함수를 분리하여, 인자를 하나씩만 받는 함수의 체인으로 만드는 방법이다.
인자를 넘겨주고 그 다음 인자를 넘겨주는 타이밍을 코드 바깥에서 제어 가능 (함수를 분해)
redux 안쪽에서 middleware 타이밍을 잡을 수 있다.

function middleware(dispatch) {
  return function(action) {
  }
}
↓↓↓
const middleware = (dispatch) => (action)  =>{
  dispatch(action);
 }
// 리턴 값이라 담을 수 있다.
const fn = middleware(dispatch);
// 필요한 시점에 액션을 넘겨줄 수 있다.
fn(action)

몽키패치란?
함수도 값이기 때문에 다른 객체의 메서드를 가져와 자신의 것처럼 사용할 수 있다.
객체의 메서드를 선언시점이 아닌 사용시점에 확장하는 것을 몽키패치라 한다.
즉, 미들웨어가 여러개 있을때 리덕스 안쪽에서 미들웨어 순서들을 재조립할수있는 가능성을 바깥쪽에서 할수있게하는 테크닉

profile
Front-End 😲

0개의 댓글