Redux 기초, 상태를 전역으로 관리해야 할 때

라용·2022년 10월 9일
0

위코드 - 스터디로그

목록 보기
80/100

위코드에서 공부하며 정리한 내용입니다.

1. Redux

Redux(리덕스)는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너 입니다. 리덕트는 데이터가 단방향으로 흐르게 설계한 Flux 에서 영감을 받아 만들었습니다. Flux 는 개념적인 디자인 패턴이고, Redux 는 Flux 개념을 자바스크립트로 구현한 것입니다.

2. Redux 구조

2-1. 3가지 원칙

2-1-1. 진실은 하나의 소스로부터 Single source of truth

애플리케이션의 모든 상태(state)를 하나의 저장소(Storage)안에 하나의 객체 트리로 저장합니다.

2-1-2. 상태 읽기 전용이다. State is read only

store 에 직접 접근해 state 를 수정하지 않으므로 읽기 전용입니다. setState 처럼 Dispatch 함수에 액션객체를 담아서 보내면 state 를 수정할 수 있습니다. 이렇게 상태 업데이트 방식과 시점을 제한해서 동작을 예측할 수 있습니다.

// 무슨일이 벌어지는지 묘사하는 액션객체를 전달하는 dispatch 예시

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1,
});

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED',
});

2-1-3. 변화는 순수 함수로 작성 Changes are made with pure functions

순수함수는 input 받은대로 output 이 나오는 예측하기 위한 구조로,
리듀셔는 이전 상태값과 액션 객체를 입력 받아 다음 상태를 반환하는 순수 함수 입니다. 이는 테스트 코드를 작성하기도 쉽습니다.

2-2. Redux 주요 개념

View 에서 이벤트가 발생하면 그 이벤트의 eventHandler의 함수가 호출되면서 Dispatch 함수가 Action 객체를 Store 에 있는 Reduce로 보냅니다. Reduce 에서 Action 객체를 따라 State 를 업데이트하고 새로운 State 를 반환합니다. 반환한 State 는 UI 를 업데이트하면서 한 사이클을 종료합니다.

2-2-1. View

유저가 사용하는 화면, 버튼, 컴포넌트 등 UI 구성요소를 말합니다.

2-2-2. Action

Action 은 상태 변화에 대한 의도를 표현한 자바스크립트 객체입니다. Action 은 Store 에 담긴 데이터를 변형시키는 유일한 방법이지만 Action 스스로 Reducer로 이동할 수 없습니다. Action 은 store.dispatch 에 인자로 담겨 Reducer에 정보를 제공하는 역할을 합니다. 어떤 동작인지 알려주는 type 프로퍼티만 필수적으로 넣으면 나머지 요소는 사용자에 따라 추가할 수 있습니다.

// action 객체 예시

const ADD_TODO = {
  type: 'ADD_TODO',   //무슨일이 벌어질지 type으로 묘사
  payload: { content: '출근하기', priority: 1 },
};

2-2-3. Action Creator

Action Creator 는 Action 객체를 정해진 틀에 맞게 리턴하는 단순 함수입니다. 규모가 작다면 Action 을 손수 만들 수 있겠지만, 이는 규모가 커질수록 비효율적이고 반복적인 작업이 될 것이므로 Action Creator 를 이요해 반환된 Action 을 Dispatch 에 담아 보냅니다.

// action creator 함수 예시

export const addCart = (item) => {   // 액션 "생성 함수"
  return {
    type: 'ADD_ITEM', // 액션 "객체"
    payload: item,
  };
};

2-2-4. Dispatcher

Dispatcher 는 Action 객체를 Reducer 에 보내는 역할을 합니다. store.dispatch() 형태로 사용하고, Dispatch 메서드는 동기적으로 처리되게 설계되어 있습니다. 비동기 Action 이 필요하다면 event 함수의 dispatch 와 reducer 사이엥서 Middleware 로 비동기 처리합니다.

// Dispatcher 메서드 에시

store.dispatch(addCart(payload));

2-2-5. Reducer

Reducer 는 이전 상태와 액션을 받아서 새로운 상태를 반환하는 함수입니다. (예측 가능한 순수 함수 형태) Reduce 는 감지된 Action 타입에 따라 이벤트를 처리하는 이벤트 리스너와 비슷합니다.

// Array.prototye.reduce() 의 예시

const cart = [
  { id: 1, name: '청바지', price: 10000, quantity: 2 },
  { id: 2, name: '반바지', price: 10000, quantity: 1 },
  { id: 3, name: '반팔', price: 10000, quantity: 2 },
];

const totalPrice = cart.reduce((acc, cur) => {
  return acc + cur.price * cur.quantity;
}, 0);

// output 50000
// reducer 형태

(previousState, action) => newState;
// slice reducer 예시1
// cart.js
// reducer 형태

const INITIAL_STATE = [];

export default function cart(state = INITIAL_STATE, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return [...state, action.payload]; // 이전 상태에 새로운 item을 추가
    case 'DELETE_ITEM':
      return state.filter((product) => product.id !== payload.id);
    default:
      return state; // 해당 사항 없으면 이전 상태를 그대로 리턴
  }
}
// slice reducer 예시2
// count.js

const INITIAL_STATE = { number: 0 };
export default function count(state = INITIAL_STATE, action) {
  switch (action.type) {
    case 'INCREASE':
      return { ...state, number: state.number + 1 };
    case 'DECREASE':
      return { ...state, number: state.number - 1 };
    default:
      return state;
  }
}
// rootReducer
// redux/index.js

import { combineReducers } from 'redux';
import cart from './cart';
import count from './count';

const rootReducer = combineReducers({ cart, count });
export default rootReducer;

관심사에 따른 Reducer 로직들을 모두 하나의 Reducer 에 넣는건 유지보수하기 어렵기 때문에 관심사별로 slice Reducer 를 만들고 redux에서 제공하는 combineReducer 메서드로 slice Reducer 들을 root Reducer 로 만들어 관리합니다.

Slice Reducer - 관심사에 따라 분리된 Reducer. 여러 Slice Reducer 들이 결합되어 Root Reducer 를 구성
Root Reducer - createStore 의 첫 번째 인자로 전달되는 함수. 관심사에 따라 분리되어 있는 Reducer 들을 하나로 묶기

주의할 점
첫번째, Reducer는 불변성을 지켜 업데이트 해야 합니다. state 원본을 직접 수정하면 안됩니다. useState 를 쓰듯 기존 값을 복하사고 새롭게 복사된 값을 덮어쓰는 방식으로 업데이트 합니다.
두번째, Reducer 내부에서 비동기나 여타 순수하지 않은 Promise(), Math.random(), Date.now() 같은 로직을 처리하면 안됩니다.

2-2-6. Store

Store 은 Redux 앱 전체의 상태로 보통 깊게 중첩되어 있는 객체입니다. 모든 state 저장소로 store.getState() 로 접근할 수 있습니다. 항상 JSON 으로 변환할 수 있어야 하므로 JSON 으로 변환할 수 없는 함수나 Promise 들은 제외하는 게 좋습니다.

// store 예시
// store/index.js

import { createStore } from 'redux';
import rootReducer from 'redux/index.js';

const store = createStore(rootReducer);

2-2-7. Middleware

Middleware 는 액션을 처리하기 전에 실행되는 함수로 비동기 API 호출 등 순수하지 않은 요청을 처리하거나 Redux Store 로 전달되는 Action 등을 로깅하는 장소입니다. Middleware 를 설치하지 않으면 dispatch한 action 은 동기적으로 바로 리듀서로 보내집니다. 대표적인 Middleware 로 redux-logger, redux-thunk, redux-saga 등이 있습니다.

// redux-thunk 예시

export default function thunkMiddleware({ dispatch, getState }) {
  return (next) => (action) =>
    typeof action === 'function' ? action(dispatch, getState) : next(action);
}

3. Start from Scratch

Redux 주요 개념에 대한 예시 코드입니다.

3-1. redux 주요개념 예시 코드 (reduce, store, dispatch, action)

// reducers/index.js 

// reducer 예시  : (store, action) => newStore
const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;

    case 'DECREMENT':
      return state - 1;

    default:
      return state;
  }
};

// store생성 예시 - 애플리케이션 전체에서 하나만 존재
const store = createStore(reducer); // getState, subscribe, dispatch

// store조회 예시
const render = () => {
  document.querySelector('.app').innerText = store.getState();
};

// view에서 일어난 이벤트에 Action을 실어 dispatch
document.addEventListener('click', () => {
  store.dispatch({ type: 'INCREMENT' }); // action
});

// store에 업데이트가 일어났을 때 subscribe에 전달받은 함수를 실행
store.subscribe(render);

3-2. createStore 의 API 3가지 예시

createStore Store 의 API 는 getState, subscribe, dispatch 3가지 입니다. getState 는 state 를 조회하고, subscribe 는 변경사항을 구독하고 있다가 상태 일부가 변경될 수 있을 때 마다 호출되고, dispatch 는 Action 을 보내는 메서드이면서 상태 변경을 일으킵니다.(유일한 방법)

// createStore API 예시

// reducer를 받는 store
const createStoreFromScratch = (reducer) => {
  let state;
  let listeners = [];

  // store의 state 를 조회하는 getState
  const getState = () => state;

  // state의 변화를 구독(감지)하는 subscribe
  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  };

  // reducer에 action을 전달하는 dispatch
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  };

  dispatch({});

  return { getState, subscribe, dispatch }; // store
};

3-3. redux 예제코드

import { createStore } from "redux";
import "./styles.css";

document.getElementById("root").innerHTML = `
  <h1>Welcome Redux!</h1>
  <div class="app">
    store count: ${0}
  </div>
`;

const store = createStore(reducer);

// getState, dispatch, subscribe
store.subscribe(render);
document.addEventListener("click", handleClick);

// dispatcher
function handleClick() {
  store.dispatch({ type: "INCREMENT" }); // action
}

// view
function render() {
  const target = document.querySelector(".app");
  target.innerText = `store count: ${store.getState()}`;
}

// reducer : (store, action) => newStore 인 함수
function reducer(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}
profile
Today I Learned

0개의 댓글