티스토리 뷰

책/리다기

리다기 정리8(리덕스)

안양사람 2021. 1. 25. 00:50
728x90
SMALL

리덕스 라이브러리 이해하기

액션

액션생성함수

리듀서

스토어

디스패치(액션발생)

구독(subscribe)(스토어 상태가 바뀔때마다 호출)

 

yarn global add parcel-bundler

parcel index.html

개발서버 실행

 

바닐라 자바스크립트 리덕스

import { createStore } from "redux";
const divToggle = document.querySelector(".toggle");
const counter = document.querySelector("h1");
const btnIncrease = document.querySelector("#increase");
const btnDecrease = document.querySelector("#decrease");

const TOGGLE_SWTICH = "TOGGLE_SWTICH";
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";

const toggleSwitch = () => ({ type: TOGGLE_SWTICH });
const increase = (difference) => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });

const initialState = {
  toggle: false,
  counter: 0,
};

// state가 undefined일 때는 initialState를 기본값으로 사용
function reducer(state = initialState, action) {
  // action.type에 따라 다른 작업을 처리함
  switch (action.type) {
    case TOGGLE_SWTICH:
      return {
        ...state,
        toggle: !state.toggle,
      };
    case INCREASE:
      return {
        ...state,
        counter: state.counter + action.difference,
      };
    case DECREASE:
      return {
        ...state,
        counter: state.counter - 1,
      };
    default:
      return state;
  }
}

const store = createStore(reducer);

const render = () => {
  const state = store.getState(); // 현재 상태를 불러옵니다.
  // 토글 처리
  if (state.toggle) {
    divToggle.classList.add("active");
  } else {
    divToggle.classList.remove("active");
  }
  //카운터 처리
  counter.innerText = state.counter;
};

// render();
store.subscribe(render);

divToggle.onclick = () => {
  store.dispatch(toggleSwitch());
};

btnIncrease.onclick = () => {
  store.dispatch(increase(1));
};

btnDecrease.onclick = () => {
  store.dispatch(decrease());
};

 

1. 리듀서 함수는 이전 상태와 액션 객체를 피라미터로 받습니다.

2. 피라미터 외의 값에는 의존하면 안 됩니다.

3. 이전 상태는 절대로 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환합니다.

4. 똑같은 피라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 합니다.

(랜덤값이나 Date 함수 사용은 리듀서 함수 바깥에서 처리해, 액션을 만드는 과정에서 처리해도 되고, 리덕스 미들웨어에서 처리해도 돼)

리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

보통 리덕스를 사용할 때는 actions, constants, reducers 폴더로 나눠서 관리하거나(액션타입, 액션생성함수, 리듀서코드)

modules 폴더 하나에 전부 관리한다.(Ducks 패턴)

 

ex) modules/counter.js

//액션 타입
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";

//액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

//초기 상태 및 리듀서 함수
const initialState = {
  number: 0,
};

function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return {
        number: state.number + 1,
      };
    case DECREASE:
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
}

export default counter;

 

루트 리듀서 만들기

modules/index.js

import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

const rootReducer = combineReducers({
  counter,
  todos,
});

export default rootReducer;

 

Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용.  redux devtools 이용하려면 

yarn add redux-devtools-extension 설치하고 composeWithDevTools()입력

const store = createStore(rootReducer, composeWithDevTools());

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

 

컨테이너 컴포넌트 만들기

connect(mapStateToProps,mapDispatchToProps)(연동할 컴포넌트)

전자 : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수

후자 : 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수

gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e

 

connect.js explained

connect.js explained. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

containers/CounterContainer.js

import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

// const mapStateToProps = (state) => ({
//   number: state.counter.number,
// });
// const mapDispatchToProps = (dispatch) => ({
//   increase: () => {
//     dispatch(increase());
//   },
//   decrease: () => {
//     dispatch(decrease());
//   },
// });
// export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);

// export default connect(
//   (state) => ({ number: state.counter.number }),
//   (dispatch) => ({
//     increase: () => dispatch(increase()),
//     decrease: () => dispatch(decrease()),
//   })
// )(CounterContainer);

// export default connect(
//   (state) => ({ number: state.counter.number }),
//   (dispatch) =>
//     bindActionCreators(
//       {
//         increase,
//         decrease,
//       },
//       dispatch
//     )
// )(CounterContainer);

export default connect((state) => ({ number: state.counter.number }), {
  increase,
  decrease,
})(CounterContainer);

 

액션 생성 함수를 더 짧은 코드로 작성하기

yarn add redux-actions yarn add immer

import { createAction, handleActions } from "redux-actions";

//액션 타입
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

//액션 생성 함수
// export const increase = () => ({ type: INCREASE });
// export const decrease = () => ({ type: DECREASE });
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

//초기 상태 및 리듀서 함수
const initialState = {
  number: 0,
};

// function counter(state = initialState, action) {
//   switch (action.type) {
//     case INCREASE:
//       return {
//         number: state.number + 1,
//       };
//     case DECREASE:
//       return {
//         number: state.number - 1,
//       };
//     default:
//       return state;
//   }
// }

const counter = handleActions(
  {
    [INCREASE]: (state, action) => ({ number: state.number + 1 }),
    [DECREASE]: (state, action) => ({ number: state.number - 1 }),
  },
  initialState
);

export default counter;

 

createAction으로 액션을 만들면 액션에 필요한 추가 데이터는 payload라는 이름을 사용한다.

헷갈리지 않게 객체 비구조화 할당 문법을 사용

immer는 필요한 부분만 사용하면 돼 TOGGLE제외한 함수는 immer 안쓰는 코드가 더 짧아 ㅇㅇ

import { createAction, handleActions } from "redux-actions";
import produce from "immer";

const CHANGE_INPUT = "todos/CHANGE_INPUT"; // 인풋 값을 변경함
const INSERT = "todos/INSERT"; // 새로운 TODO를 등록함
const TOGGLE = "todos/TOGGLE"; // todo를 체크/체크 해제함
const REMOVE = "todos/REMOVE"; // todo를 제거함

// export const changeInput = (input) => ({
//   type: CHANGE_INPUT,
//   input,
// });
export const changeInput = createAction(CHANGE_INPUT, (input) => input);
// 변형을 원하는 경우 위와 같이... 물론 위에는 저거 안해도 돼. 직관적으로 보이게 쓴거야

let id = 3; // insert가 호출될 때마다 1씩 더해집니다.

// export const insert = (text) => ({
//   type: INSERT,
//   todo: {
//     id: id++,
//     text,
//     done: false,
//   },
// });
export const insert = createAction(INSERT, (text) => ({
  id: id++,
  text,
  done: false,
}));

// export const toggle = (id) => ({
//   type: TOGGLE,
//   id,
// });
export const toggle = createAction(TOGGLE, (id) => id);

// export const remove = (id) => ({
//   type: REMOVE,
//   id,
// });
export const remove = createAction(REMOVE, (id) => id);

const initialState = {
  input: "",
  todos: [
    {
      id: 1,
      text: "리덕스 기초 배우기",
      done: true,
    },
    {
      id: 2,
      text: "리액트와 리덕스 사용하기",
      done: false,
    },
  ],
};

// function todos(state = initialState, action) {
//   switch (action.type) {
//     case CHANGE_INPUT:
//       return {
//         ...state,
//         input: action.input,
//       };
//     case INSERT:
//       return {
//         ...state,
//         todos: state.todos.concat(action.todo),
//       };
//     case TOGGLE:
//       return {
//         ...state,
//         todos: state.todos.map((todo) =>
//           todo.id === action.id ? { ...todo, done: !todo.done } : todo
//         ),
//       };
//     case REMOVE:
//       return {
//         ...state,
//         todos: state.todos.filter((todo) => todo.id !== action.id),
//       };
//     default:
//       return state;
//   }
// }
const todos = handleActions(
  {
    //[CHANGE_INPUT]: (state, action) => ({ ...state, input: action.payload }),
    [CHANGE_INPUT]: (state, { payload: input }) =>
      // ({ ...state, input }),
      produce(state, (draft) => {
        draft.input = input;
      }),
    [INSERT]: (state, { payload: todo }) =>
      // ({
      //   ...state,
      //   todos: state.todos.concat(todo),
      // }),
      produce(state, (draft) => {
        draft.todos.push(todo);
      }),
    [TOGGLE]: (state, { payload: id }) =>
      // ({
      //   ...state,
      //   todos: state.todos.map((todo) =>
      //     todo.id === id ? { ...todo, done: !todo.one } : todo
      //   ),
      // }) ,
      produce(state, (draft) => {
        const todo = draft.todos.find((todo) => todo.id === id);
        todo.done = !todo.done;
      }),
    [REMOVE]: (state, { payload: id }) =>
      // ({
      //   ...state,
      //   todos: state.todos.filter((todo) => todo.id !== id),
      // }),
      produce(state, (draft) => {
        const index = draft.todos.findIndex((todo) => todo.id === id);
        draft.todos.splice(index, 1);
      }),
  },
  initialState
);

export default todos;

 

리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때 connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks를 사용할 수도 있다.

useSelector로 상태 조회

useDispatch로 액션 디스패치

useDispatch 사용할 때는 useCallback으로 최적화 습관!!

// const CounterContainer = ({ number, increase, decrease }) => {
const CounterContainer = () => {
  const number = useSelector((state) => state.counter.number);
  const dispatch = useDispatch();
  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
  return (
    // <Counter number={number} onIncrease={increase} onDecrease={decrease} />
    <Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
  );
};

export default CounterContainer;

// export default connect((state) => ({ number: state.counter.number }), {
//   increase,
//   decrease,
// })(CounterContainer);

 

useStore를 사용하면 내부에서 리덕스 스토어 객체를 직접 사용할 수 있다.

이건 진짜 엄청 가끔 쓰는거야. 어쩌다가 스토어에 직접 접근해야 하는 상황에만

const store=useStore();
store.dispatch({type:'SAMPLE_ACTION'});
store.getState();

 

useActions는 원래 react-redux에 내장된 상태로 릴리즈될 계획이였으나 제외됨.

그 대신 공식문서에서 복붙하면 돼

react-redux.js.org/next/api/hooks#recipe-useactions

 

Hooks | React Redux

Hooks

react-redux.js.org

useActions Hooks은 액션 생성 함수를 액션을 디스패치하는 함수로 변환해 준다.

첫번째 피라미터는 액션 생성 함수로 이루어진 배열이고

두번째 피라미터는 deps 배열이며, 이 배열 안에 들어 있는 원소가 바뀌면 액션을 디스패치하는 함수를 새로 만들게 된다.

  const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
    [changeInput, insert, toggle, remove],
    []
  );
  // const dispatch = useDispatch();
  // const onChangeInput = useCallback((input) => dispatch(changeInput(input)), [
  //   dispatch,
  // ]);
  // const onInsert = useCallback((text) => dispatch(insert(text)), [dispatch]);
  // const onToggle = useCallback((id) => dispatch(toggle(id)), [dispatch]);
  // const onRemove = useCallback((id) => dispatch(remove(id)), [dispatch]);

 

앞으로 connect 함수를 사용해도 좋고, useSelector와 useDispatch를 사용해도 좋다.

하짐나 Hokks를 사용하여 컨테이너 컴포넌트를 만들 때는 성능 최적화를 위해 React.memo를 컨테이너 컴포넌트에 사용해 주어야 한다.

728x90
LIST

' > 리다기' 카테고리의 다른 글

리다기 정리10(코드 스플리팅)  (0) 2021.01.28
리다기 정리9(리덕스 미들웨어)  (0) 2021.01.27
리다기 정리7(context)  (0) 2021.01.24
리다기 정리6(뉴스뷰어 만들기)  (0) 2021.01.22
리다기 정리5(immer, SPA)  (0) 2021.01.22
댓글
공지사항