[노마드코더]★리덕스 입문자 추천★초보자를 위한 리덕스 101 정리 + 리덕스 툴킷

최예린·2022년 11월 12일
1

React

목록 보기
18/19
post-thumbnail

공부순서) 바닐라 JS -> JS에 리덕스 사용 -> 리액트에 리덕스사용 -> 리덕스 툴킷

리액트는 자바스크립트 프레임 워크이며 리덕스는 리액트와 자주 함께 묶어서 공부하게되지만 리액트와는 별개로 구분됩니다. 리액트뿐만 아니라 바닐라JS, Vue 등 다양한 곳에서도 리덕스를 사용할 수 있습니다. 그렇기때문에 위 강의에서는 바로 리액트에서 리덕스를 사용하는 걸 가르치는게 아니라 위의 강의 순서대로 진행하는 것 같습니다.

리덕스를 사용하면 어떤 이점이 있는지 공부해보겠습니다.


✔ 예제 1 Counter


1. Vanilla JS_Counter

const add = document.getElementById("add");
const minus = document.getElementById("minus");
const number = document.querySelector("span");

let count = 0;
number.innerText = count;

const updateText = () => {
  number.innerText = count;
};

const handleAdd = () => {
  count = count + 1;
  updateText();
};

const handleMinus = () => {
  count = count - 1;
  updateText();
};

add.addEventListener("click", handleAdd);
minus.addEventListener("click", handleMinus);

2. Pure Redux_Counter

  • Store
    store는 state(데이터)를 저장하는 곳입니다.
    createStore() 를 통해 store를 만들 수 있습니다. -> 업데이트 후 createStore()보다는 configureStore()를 사용하는 것을 권장합니다.
    createStore는 항상 인자로 reducer를 받아야합니다.

  • reducer는 데이터를 찾고 수정하는 함수입니다.
    reducer가 hello를 반환하면 이게 우리 앱에 데이터로 저장됩니다.
    store.getState() 으로 저장한 데이터를 가져올수있습니다.

👀 store에 내장된 함수들

import { createStore } from "redux";

const add = document.getElementById("add");
const minus = document.getElementById("minus");
const number = document.querySelector("span");

// reducer
const countModifier = (count = 0) => { // (1)
  return count; // (2)
};
// store
const countStore = createStore(countModifier);
// store에 저장된 값 가져오기
console.log(countStore.getState()); // (3)

(1) count가 undefined면 0으로 initial state 초기화하고 끝남
(2) store에 count state 저장
(3) 콘솔에 count값이 찍힘. 이 예제에서는 0

  • Actions
    reducer를 제외하고는 아무도 store의 count state 값을 변경할 수 없습니다.
    count ++ 혹은 count --를 해서 값을 변화시켜야하는데 그러기위해서는 reducer와 소통을 해야하고 그러한 역할을 하는게 action입니다.

reducer에게 action을 전달하는 방법 = store.dispatch({type: value})
전달하고싶은 메시지를 action에 넣고 reducer에서 action을 체크해서 그에 따라 데이터를 수정해서 반환하여 store에 저장하는 방식입니다. 액션을 보낼때는 반드시 type을 정의해야합니다. 그렇지않으면 에러가 발생합니다.

  • Subscriptions
    store 안에 있는 데이터의 변화를 알 수 있게 해줍니다.
const add = document.getElementById("add");
const minus = document.getElementById("minus");
const number = document.querySelector("span");

number.innerText = 0;

const countModifier = (count = 0, action) => {
  if (action.type === "ADD") {
    return count + 1;
  } else if (action.type === "MINUS") {
    return count;
  }
};

const countStore = createStore(countModifier);

const onChange = () => {
  number.innerText = countStore.getState();
};

countStore.subscribe(onChange); // store 변화 감지

// const handleAdd = () => {
//   countStore.dispatch({ type: "ADD" });
// };
// 
// const handleMinus = () => {
//   countStore.dispatch({ type: "MINUS" });
// };

// 익명함수로 버튼을 클릭하면 액션 전달
add.addEventListener("click", () => countStore.dispatch({ type: "ADD" }));
minus.addEventListener("click", () => countStore.dispatch({ type: "MINUS" }));

3. Refactor

const ADD = "ADD";
const MINUS = "MINUS";
const add = document.getElementById("add");
const minus = document.getElementById("minus");
const number = document.querySelector("span");

number.innerText = 0;

const countModifier = (count = 0, action) => {
  switch (action.type) {
    case ADD:
      return count + 1;
    case MINUS:
      return count - 1;
    default:
      return count;
  }
};

const countStore = createStore(countModifier);

const onChange = () => {
  number.innerText = countStore.getState();
};

countStore.subscribe(onChange); // store 변화 감지


//  버튼을 클릭하면 익명함수로 액션 전달
add.addEventListener("click", () => countStore.dispatch({ type: ADD }));
minus.addEventListener("click", () => countStore.dispatch({ type: MINUS }));
  1. if else 문을 switch문으로 변경
  2. String 대신 Constant 사용하기

👀 constant 사용이유 : 그냥 string으로 쓰면 오타났을때 알아채기어렵지만, constant 형태를 사용할 때 오타가 발생하면 선언되지않았다는 에러가 발생하면서 오타를 알아차리게됩니다.


✔예제 2 To Do List


1. Vanilla JS_ToDoList

노마드코더JS강의 투두리스트 velog포스팅
저번에 들은 JS 강의에서는 Todolist를 저장할때 로컬스토리지를 이용했습니다.
그러기위해서는 입력받은 값을 todo라는 배열에 데이터로 저장해서 추가하기, 빼기, 그리고 다시 그리기 등의 작업을 직접 관리해야했습니다.

2. Pure Redux_ToDoList

import { createStore } from "redux";
const form = document.querySelector("form");
const input = document.querySelector("input");
const ul = document.querySelector("ul");

const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";

const addToDo = text => {
  return {
    type: ADD_TODO,
    text
  };
};

const deleteToDo = id => {
  return {
    type: DELETE_TODO,
    id
  };
};

const reducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      const newToDoObj = { text: action.text, id: Date.now() };
      return [newToDoObj, ...state];
    case DELETE_TODO:
      const cleaned = state.filter(toDo => toDo.id !== action.id);
      return cleaned;
    default:
      return state;
  }
};

const store = createStore(reducer);

store.subscribe(() => console.log(store.getState()));

const dispatchAddToDo = text => {
  store.dispatch(addToDo(text));
};

const dispatchDeleteToDo = e => {
  const id = parseInt(e.target.parentNode.id); // HTML에서 받아오는 id는 String Type
  store.dispatch(deleteToDo(id));
};

const paintToDos = () => {
  const toDos = store.getState();
  ul.innerHTML = "";
  toDos.forEach(toDo => {
    const li = document.createElement("li");
    const btn = document.createElement("button");
    btn.innerText = "DEL";
    btn.addEventListener("click", dispatchDeleteToDo);
    li.id = toDo.id;
    li.innerText = toDo.text;
    li.appendChild(btn);
    ul.appendChild(li);
  });
};

store.subscribe(paintToDos);

const onSubmit = e => {
  e.preventDefault();
  const toDo = input.value;
  input.value = "";
  dispatchAddToDo(toDo);
};

form.addEventListener("submit", onSubmit);

action의 type으로 ADD_TODO와 DELETE_TODO를 줘서 어떤 일을해야하는지 리듀서와 소통할수있습니다. 하지만 중요한것은 Todolist에 뭘 저장해야하는지 어떻게 전달하느냐입니다.

우리는 action을 통해 원하는 걸 다 전달해줄수있습니다. type은 필수입력조건이지만 그 외에는 어떤 것이든 추가해서 보낼 수 있습니다. 여기에서는 text:로 저장해야하는 값을 전달해줍니다.

절대 MUTATE STATE하지말것

  • state는 read only, state를 바꾸는 유일한 방법은 action을 보내는 것뿐입니다.
    state를 mutate하는 것이아니라 new state object를 return해야합니다.
  • ...state, : ES6 spread
    새로운 배열을 만들어서 기존의 state내용을 모두가져오고 거기에 새로운 내용을 추가한 뒤 return합니다.

✔ Action Creators

오로지 액션 객체만 반환하는 함수를 따로 정의한 뒤에(보통 reducer 코드 윗쪽에 정의합니다) dispatch할때 그 함수를 불러옵니다.

배열에서 요소를 삭제하기위해서는 splice와 filter 등의 메서드를 사용하는 방법 존재합니다.
splice는 mutate하기때문에 지금 우리에게는 적합하지않습니다.

filter() 메서드는 주어진 배열의 일부분의 얕은 복사본을 생성하여 제공된 함수에 의해 구현된 테스트를 통과한 주어진 배열의 요소만 필터링합니다.


React Redux_To Do List

  • Setup
    리액트 앱의 세팅을 간단하게 해주었습니다. 강의가 예전 강의라서 코드를 조금 바꾸었습니다. (Router나 createRoot()같은 부분들)

  • Connecting the Store
    리액트에서는 변경이 생기면 오로지 그부분만 다시 렌더링해줍니다. 하지만 우리는 변경이생기면 전체를 다시 렌더링하고싶습니다. 위에서 store가 바뀔때마다 알려주는 subscribe을 이전에 사용했고 이번에도 그게 필요합니다. 대신 리액트를 사용해서 만들면 더 멋지게 만들 수 있습니다.

index.js

import { Provider } from "react-redux";
import store from "./store";
...
    <Provider store={store}>
        <App />
    </Provider>

Provider를 통해 우리가만든 store를 index.js에 연결합니다.

Connect the store to Component

컴포넌트들을 store에 연결하기위해 사용하는 함수가있습니다.
connect()입니다. connect는 두개의 인자를 받는데 바로 mapStateToProps()와 mapDispatchToProps() 입니다.

mapStateToProps

https://react-redux.js.org/using-react-redux/connect-mapstate

  • 이름은 다르게설정할수있음
  • 최근 state를 가져와 컴포넌트에 props로 전달함)를 이용해 store으로부터 Home에 state를 가져올겁니다.
function mapStateToProps(state, ownProps) {
  return { sexy: true };
}

export default connect(mapStateToProps) (Home);

직접 console.log로 state와 props들을 확인해보겠습니다.

state를 확인하기위해서 reducer에서 초기값을 "hello"로 설정해주겠습니다.

  • 첫번째 콘솔로그

    mapStateToProps에 인자로 전달되는 store의 state와 Home Component 자신의 props

  • 두번째 콘솔로그

    mapStateToProps 함수 실행 뒤 sexy가 정상적으로 추가되는지 확인하는 Home component의 props

connect()는 Home으로 보내는 props에 추가될수있도록 허용해주기때문에
Home의 props를 console.log(props)해보면 거기에 react-router로부터 받은 Home컴포넌트의 props뿐만 아니라 sexy:true가 추가된걸 확인할 수 있습니다.

우리가 필요한건 store의 state이기때문에 ownProps 인자는 전달받을 필요가없습니다.
그러므로 코드를 다음과 같이 수정합니다.

function mapStateToProps(state) {
    return { toDos: state };
}

mapDispatchToProps

https://react-redux.js.org/using-react-redux/connect-mapdispatch

  • 컴포넌트에서 action을 dispatch할 수 있도록 컴포넌트의 props에 dispatch를 전달합니다.
  • mapDispatchToProps는 두번째 인자인데 만약 mapStateToProps가 필요없다면
    connect(null, mapDispatchToProps)(Home)로 사용하면됩니다.
function mapDispatchToProps(dispatch) {
    return { addToDo: text => dispatch(actionCreators.addToDo(text)) }
}


이렇게 Home Component의 props로 addToDo가 전달되고 Home Component 내 어디서든 addToDo() 로 dispatch 할수있습니다.

Deleting To Do

import React from "react";
import { connect } from 'react-redux';
import { actionCreators } from "../store";

function ToDo({text, onBtnClick}) {
    return (
        <li>
            {text}<button onClick={onBtnClick}>DEL</button>
        </li>
    );
}

function mapDispatchToProps(dispatch, ownProps) {
    return {
        onBtnClick: () => dispatch(actionCreators.deleteToDo(ownProps.id))
    };
}


export default connect(null, mapDispatchToProps) (ToDo);


ownProps에 id가 저장되어있으므로 onBtnClick에서 따로 id를 인자로 전달받지않아도 됩니다.

Detail Screen

import React from "react";
import { connect } from "react-redux";
import { useParams } from "react-router-dom";


function Detail({toDos}){
    const { id } = useParams();
    const toDo = toDos.find(toDo => toDo.id === parseInt(id));
        return (
            <>
                <h1>{toDo?.text}</h1>
                <h5>Created at: {toDo?.id}</h5>
            </>
        );

}

function mapStateToProps(state) {
    return { toDos: state };
}

export default connect(mapStateToProps) (Detail);
  • useParams() 훅을 사용해서 id를 얻을 수도있지만
    ~~mapStateToProps()를 사용해도 id를 얻을 수 있습니다.

  • find()는 필터와 비슷하지만 테스트에 만족하는 첫번째 요소를 반환합니다.~~

-react-router-dom 버전 6부터는 element로 컴포넌트를 만들고, route props(match, history, location)을 받지 않는다. 따라서, useParams, useLocation, useHistory를 사용하여 route context에 접근한다
https://velog.io/@kcdoggo/Cannot-read-property-params-of-undefined-%EC%97%90%EB%9F%AC

  • 새로고침을 하면 state가 사라지기때문에 에러가 발생합니다. toDo?.text로 코드를 작성해서 에러를 없앨수있습니다.

Redux Toolkit(RTK)_To Do List

리덕스를 사용하려면 Action Constant와 ActionCreators, switch문 등등 작성해야하는 상용구 코드(boilerplate code), 즉 반복적으로 작성해야하는 코드가 많다는 단점이있습니다.

리덕스 툴킷은 적은량의 코드로 같은 기능을 하도록 도와줍니다.

❌리덕스툴킷부터 배우면 안됩니다. 리덕스가 어떻게 동작하는 지 알아야 확실하게 이해할 수 있습니다.

install

npm install @reduxjs/toolkit

createAction

const addToDo = createAction("ADD");
const deleteToDo = createAction("DELETE");

const reducer = (state = [], action) => {
  switch (action.type) {
    case addToDo:
      return [{ text: action.payload, id: Date.now() }, ...state];
    case deleteToDo:
      return state.filter(toDo => toDo.id !== action.payload);
    default:
      return state;
  }
};

이전에 리덕스에서는 우리가 action creators를 직접 만들어서 action 객체 안에 id나 text같은 것들을 이름 지어서 반환했습니다.
하지만 createAction() 을 사용하면 적은 코드로 쉽게 Action Creator을 만들수있습니다.

addToDo를 콘솔에 찍어보면 함수인 것을 알수있고, 이 함수를 실행시켜보면

  • type
  • payload

이 두가지값이 존재하는 걸 알수있습니다. 여기서 payload는 action을 통해 값을 reducer에 전달하기 위한 것으로 리덕스 툴킷에서 제공하는 것입니다.
앞으로는 이름을 직접 지어서 사용하지않고action.payload로 값을 받습니다.

createReducer

const reducer = createReducer([], {
  [addToDo] : (state, action) => {
    state.push({text: action.payload, id: Date.now() });
  },
  [deleteToDo] : (state, action) =>
    state.filter(toDo => toDo.id !== action.payload)
});

이전에 리덕스를 사용할때는 state를 mutate하면 안됐기때문에 새로운 state를 만들어서 return했습니다. 하지만 리덕스툴킷의 createReducer()를 사용할때는 state를 mutate해도 괜찮습니다. 그래서 addToDo안의 코드를 작성할때 return하지않고 state를 mutate해주었습니다.

  • why?
    리덕스 툴킷이 immer 아래에서 작동되기때문입니다. 사용자가 mutate하든 return을 하든 리덕스 툴킷이 알아서 해줍니다.

물론 return을 해줘야할때도 있습니다. deleteToDo에서 사용하는 filter는 새로운 state를 만들기때문에 return해주었습니다.
❌하지만 주의할점은 addToDo 부분을 바로 return해버리면 안됩니다. 왜냐하면 return을 하기위해서는 반드시 새로운 state를 만들어서 반환해야하는데 push는 반환하는게 없기때문입니다.

  • builder callback 사용 권장
    https://redux-toolkit.js.org/api/createReducer

    'createReducer'에 대한 개체 표기법은 더 이상 사용되지 않으며 RTK 2.0에서 제거됩니다. 대신 '빌더 콜백' 표기법을 사용하십시오.
createReducer(initialState, (builder) => {
  builder
    .addCase()

configureStore

const store = configureStore({ reducer });

Redux Developer Tools

모든 state들을 차트나 raw data로 확인할 수 있고 언제 어떤 action이 전달되어서 실행되었는지 확인할 수 있으며 실행전으로 되돌아가서 다시 실행해볼수있습니다.

  • 에어비앤비의 state chart

    redux로 만들어진 웹사이트에 들어가면 누구나 이 툴을 이용해 state들을 확인할 수 있습니다.

Redux Developer Tools을 사용하기위해서 redux toolkit이 꼭 필요한건 아니지만
configureStore()를 사용해서 store를 생성하면 다른 어떤 코드를 추가하지않아도 기본적으로 Redux Developer Tools이 활성화되어 사용할 수 있습니다.

createSlice

createSlice()는 이때까지 작성한 코드를 엄청나게 줄여줍니다.
reducer와 actions를 둘다 생성해주기때문입니다.

const toDos = createSlice({
  name: "toDosReducer",
  initialState: [],
  reducers: {
    add:(state, action) => {
      state.push({text: action.payload, id: Date.now() });
    },
    remove:(state, action) =>
      state.filter(toDo => toDo.id !== action.payload)
  }
});

export const { add, remove } = toDos.actions;
export default configureStore({ reducer: toDos.reducer });

이 짧은 코드만으로 아래의 동작을 수행할 수 있습니다.

  • reducer 생성
  • actions 생성
  • action creators를 생성할 필요없이 바로 toDos.actions에 들어있는 액션들을 export
  • createSlice가 자동으로 묶어준 toDos.reducer를 reducer로 넘겨주며 store 생성과 동시에 export

리덕스 툴킷 ToDoList 결과물


이렇게 add action을 통해 ToDo를 state에 추가할수있고


이는 Redux DevTools에서 쉽게 확인해볼수있습니다.


그리고 state뿐만아니라 add action이 실행된 기록도 확인할 수 있습니다.


하단에 있는 Go back 버튼을 누르거나 바를 움직여서 actions가 실행되기전으로 돌릴수도 있었습니다.


만들어진 ToDo를 클릭하면 라우터의 Link를 통해 /:id 로 URL이 변경됩니다.
그리고 그 URL에서 id를 받아와서 표시하거나, mapStateToProps를 사용해 state를 받아와 props로 보낸 뒤 그 값을 제목으로 보여주는 것도 가능했습니다.


이로써 바닐라JS, 리덕스, 리액트 리덕스, 리액트 리덕스 툴킷을 이용해 간단한 Counter와 ToDoList를 만들어보았습니다. 리덕스 툴킷이 널리 사용되다보니 리덕스 개념을 잡기도 전에 툴킷을 통해 공부를 하며 많은 어려움을 겪어왔지만 다시 리덕스를 처음부터 공부하게 되면서 자신감을 찾게되는 계기가 되었던 것 같습니다.

profile
경북대학교 글로벌소프트웨어융합전공/미디어아트연계전공

0개의 댓글