Redux

송나은·2021년 6월 12일
0

React

목록 보기
5/9

Redux를 시작해보자

npx create-react-app my-app --template redux

리덕스를 사용하는 이유

  1. 컴포넌트 코드로부터 상태관리 코드를 분리할 수 있다.
  2. 로컬 스토리지에 데이터를 저장하고 불러오는 코드를 쉽게 작성할 수 있다.
  3. 같은 상탯값을 다수의 컴포넌트에서 필요로 할 때 사용하기 좋다.
  4. 부모 컴포넌트에서 깊은 곳에 있는 자식 컴포넌트에 상탯값을 전달할 때 좋다.

리덕스 사용 시 따라야 할 세가지 원칙

  1. 전체 상탯값을 하나의 객체에 저장한다.
  2. 상탯값은 불변객체다.
  3. 상탯값은 순수함수에 의해서만 변경되어야 한다.
    reducer: 이전 상탯값과 액션 객체를 입력으로 받아 새로운 상탯값을 만드는 순수 함수. (state, action) => nextState

순수함수: 같은 인수에 대해 항상 같은 값을 반환하고, 부수 효과를 발생시키지 않는 함수.

리덕스에서 상탯값이 변경되는 과정

액션 -> 미들웨어 -> 리듀서 -> 스토어 -> 뷰(리액트) -> 액션

액션

type 속성값을 가진 자바스크립트 객체. dispatch 메서드에 넣어 호출하면 리덕스는 상태값을 변경한다.

export const ADD = 'product/ADD';
export const REMOVE = 'product/REMOVE';
export const REMOVE_ALL = 'product/REMOVE_ALL';

export const addProduct = ({ id }) = { type: ADD, id };
export const removeProduct = ({ id }) = { type: REMOVE, id };
export const removeAllProduct = () = { type: REMOVE_ALL };

type 속성값은 리듀서에서 액션 객체를 구분할 때도 사용되기 때문에 상수 변수로 만드는게 좋다.

미들웨어

리듀서가 액션을 처리하기 전에 실행되는 함수. ex) 상탯값 변경 시 로그 출력, 예외 처리
기본구조: const Middleware = store => next => action => next(action)

// 로그를 출력해 주는 미들웨어 (디버깅)
const pringLog = store => next => action => {
  console.log(`prev state = ${store.getState()}`);
  const result = next(action);
  console.log(`next state = ${store.getState()}`);
  return result;
}

// 에러 정보를 전송해 주는 미들웨어
const reportCrash = store => next => action => {
  try {
    return next(action);
  } catch (err) {
    console.log(err)
  }
};

// 로컬 스토리지에 값을 저장하는 미들웨어
const saveToLoaclStorage = store => next => action => {
  if (action.type === "SET_NAME") {
    localStorage.setItem('name', action.name);
  }
  return next(action);
};

리듀서

액션이 발생했을 때 새로운 상탯값을 만드는 함수.

const INITIAL_STATE = { cartList: [] }
}
// 매개변수의 기본값 설정
const reducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case "ADD":
      return {
        ...state,
        cartList: [...state.cartList, action.id],
      };
    case "REMOVE":
      return {
        ...state,
        cartList: state.cartList.filter(list => list.id !== action.id),
      };
    case "REMOVE_ALL":
      return {
        ...state,
        cartList: [],
      };
    defult:
      return state;
  }
}
  • 스토어를 생성할 때 상탯값이 없는 상태로 리듀서를 호출하므로 매개변수의 기본값을 사용해서 초기 상탯값을 정의한다.
  • 상탯값은 불변 객체로 관리해야 하므로 수정할 때마다 새로운 객체를 생성한다.

Tip) immer 이용해서 리듀서 작성하기

전개 연산자를 이용해서 깊은 곳의 값을 수정할 때 코드의 가독성을 높이기 위해 사용하는 패키지. 불변 객체를 관리한다.

import produce from 'immer';

const cat = { name: 'tomy', age: 10 };

// draft 객체를 수정하면 produce 함수가 새로운 객체를 반환한다.
const newCat = produce(cat, draft => {
  draft.age = 11;
});
const INITIAL_STATE = { cartList: [] }
// 매개변수의 기본값 설정
const reducer = (state = INITIAL_STATE, action) => {
  return produce(state, draft => {
    switch (action.type) {
      case "ADD":
        draft.cartList.push(action.id);
        break;
      case "REMOVE":
        draft.cartList.filter(list => list.id !== action.id);
        break;
      case "REMOVE_ALL":
        draft.cartList = [],
        break;
      defult:
        break;
    }
  }
}

Tip) createReducer 함수로 리듀서 작성하기

const reducer = createReducer(INITIAL_STATE, {
  [ADD]: (state, action) => state.cartList.push(action.id),
  [REMOVE]: (state, action) => (state.cartList = []),
  [REMOVE_ALL]: (state, action) => state.cartList.filter(list => list.id !== action.id),

스토어

리덕스의 상탯값을 가지는 객체. 액션의 발생은 스토어의 dispatch 메서드로 시작된다.

데이터 종류별로 상탯값 나누기

덕스패턴

  • 연관된 액션 타입, 액션 생성자 함수, 리듀서 함수를 하나의 파일로 작성한다.
  • 리듀서 함수는 export default 키워드로 내보낸다.
  • 액션 생성자 함수는 export 함수로 내보낸다.
  • 액션 타입은 접두사와 액션 이름을 조합해서 만든다.
// createReducer.js
import produce from "immer";

export default function createReducer(initialState, handelrMap) {
  return function(state = initialState, action) {
    return produce(state, draft => {
      const handler = handelrMap[action.type];
      if (handler) {
        handler(draft, action);
      }
    })
  }
}

// cartList/state.js
import createReducer from '../common/createReducer";

export const ADD = 'product/ADD';
export const REMOVE = 'product/REMOVE';
export const REMOVE_ALL = 'product/REMOVE_ALL';

export const addProduct = ({ id }) = { type: ADD, id };
export const removeProduct = ({ id }) = { type: REMOVE, id };
export const removeAllProduct = () = { type: REMOVE_ALL };

const INITIAL_STATE = { cartList: [] };

const reducer = createReducer(INITIAL_STATE, {
  [ADD]: (state, action) => state.cartList.push(action.id),
  [REMOVE]: (state, action) => (state.cartList = []),
  [REMOVE_ALL]: (state, action) => state.cartList.filter(list => list.id !== action.id)
});
export default reducer;

CombineReducers

여러 개의 리듀서를 하나로 합치는 함수.

import { createStore, combineReducers } from 'redux';
import cartListReducer, {addProduct, removeProduct, removeAllProduct} from './cartList/state';
import likeListReducer, {addLikeList, removeLikeList, removeAllLikeList} from './likeList/state';

const reducer = combineReducers({
  cartList: cartListReducer,
  likeList: likeListReducer
});

const store = createStore(reducer);
store.subscribe(() => {
  const state = store.getState();
  cosnole.log(state);
});

store.dispatch(addProduct({ id: 1}));
store.dispatch(removeAllProduct());
/*
state = {
  { cartList: [] },
  { likeList: [] },
}
*/

react-redux

Provider 컴포넌트

최상위 컴포넌트로 정의하여 리덕스의 상탯값이 변경되면 하위 컴포넌트에서 자동으로 컴포넌트 함수가 호출되도록 한다.
contextAPI를 사용해 하위 컴포넌트로 상탯값을 전달한다.

useSelector

리덕스의 상탯값이 변경되면 이전 반환값과 새로운 반환값을 비교한다.

두 번째 매개변수는 컴포넌트 렌더링 여부를 판단하는 역할을 한다.
const [cartItem, totalPrice] = useSelector(state => [state?.cart, state?.cart.totalPrice], shallowEqual);

useDispatch

액션을 발생시키기 위해 필요한 dispatch 함수를 반환한다.

Reference

profile
그때그때 공부한 내용과 생각을 기록하는 블로그입니다.

0개의 댓글