[JavaScript] Redux

Redux

image

Redux는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너로 일관적으로 동작하고 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동하고 테스트하기 쉬운 앱을 작성하게 도와줌. 여기에 더해서 시간여행형 디버거와 결합된 실시간 코드 수정과 같은 훌륭한 개발자 경험을 제공. React나 다른 뷰 라이브러리와 함께 사용할 수 있으며, 매우 작지만(의존 라이브러리 포함 2kB), 사용 가능한 애드온은 매우 많음

Project 생성

  • 기본적으로 JS application들의 state(상태)를 관리하는 방법. React, Vue.js, Vanilla JS(framework가 없는 pure JS)등에서 모두 사용 가능
  • Google chrome(browser), github(version contron), node.js, VSC(text editor) 준비!
$ npx create-react-app vanilla redux
$ npm install redux

store

  • Redux의 함수 createStore를 통해 state(application에서 바뀌는 data)를 넣을 장소를 생성
  • reducer(함수)에 의해서만 state의 값이 변경
  • store수정을 위해 state를 바꿀 수 있는 방법은 새로운 object를 return 하는 것 뿐. (Mutating 절대 금지)

store 메서드

  • getState() : 애플리케이션의 현재 상태 트리를 반환. 스토어의 리듀서가 마지막으로 반환한 값과 동일
  • dispatch(action) : action을 stroe에 전달하는 것을 의미
  • subscribe(listener) : store안에 있는 변화들을 알 수 있게 해줌
  • replaceReducer(nextReducer) : 현재 스토어에서 상태를 계산하기 위해 사용중인 리듀서를 교체

reducer

  • 상태를 변화시키는 로직이 있는 함수(Reducer에서 state의 변화가 일어남)
  • Reducer함수는 순수함수여야 함. 결과 값을 출력할 때는 파라미터 값에만 의존해야 하며 언제나 같은 결과를 출력해야함
  • Reducer에서 state를 사용한다면 반드시 state를 초기화(값의 갱신은 반드시 reducer에서 함)

action

  • 상태 변화를 일으킬 때 참조하는 객체(user가 reducer와 소통하기 위한 방법)
  • Redux에서 function을 부를 때 쓰는 두번 째 parameter(object여야 함. string X)
  • Event와 같다고 생각…dispatch 인수에서 Reduce로 넘길 객체(type)를 정의
  • Action이 실행되고 끝나면 type을 반환하는데 type은 Reducer로 전달

참조

  • if/else 보단 switch-case 활용
  • dispatch구문 action type을 string보단 constant활용(휴먼에러 감소를 위해)
// countStore.dispatch({type: "MINUS"})

const MINUS = "MINUS"
countStore.dispatch({type : MINUS})

import

redux에서 create store import

import {createStore} from "redux";

const reducer = (state = 0) => {                //reducer 생성
  return state;
}

const countStore = createStore(reducer);   //store생성
console.log(countStore.getState())      //0
  • countStore 내부 4개의 메서드

image


Source

JS Redux : 버튼을 이용한 간단한 예제

import {createStore} from "redux";

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

// let count = 0;
// number.innerText = count;
// const updateTxt = () => {
//   number.innerText = count;

// }

// const handleAdd = () => { 
//   count += 1;
// updateTxt();
// }
// const handleMinus = () => { 
//   count -= 1;
// updateTxt();
// }

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

const ADD = "ADD";
const MINUS = "MINUS";

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

const countStore = createStore(countModifier);   //store생성
// countStore.dispatch({type : "ADD"})
// countStore.dispatch({type : "MINUS"})
// console.log(countStore.getState());

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

countStore.subscribe(onChange);

add.addEventListener("click", () => {countStore.dispatch({type : ADD})})
minus.addEventListener("click", () => {countStore.dispatch({type : MINUS})})

Pure Redux - To Do List

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 DEL_TODO = "DEL_TODO";

const addTodo = text => {
  return {
    type : ADD_TODO, text
  }
}

const delTodo = id => {
  return{
    type : DEL_TODO, id
  }
}

const reducer = (state=[], action) => {
  switch(action.type){
    case ADD_TODO:
      const newTodo = {text:action.text, id:Date.now()}
      return[newTodo, ...state]
    case DEL_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 onSubmit = event => {
  event.preventDefault();
  const toDo = input.value;
  input.value = "";
  dispatchAddTodo(toDo)
}

const dispatchAddTodo = text => {
  store.dispatch(addTodo(text))
}

const disdpatchDelTodo = e => {
  const id = parseInt(e.target.parentNode.id)
  store.dispatch(delTodo(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", disdpatchDelTodo)
    li.id = toDo.id
    li.innerText = toDo.text
    li.appendChild(btn)
    ul.appendChild(li)
  }
  )
}

store.subscribe(paintTodos)
form.addEventListener("submit",onSubmit)

React-Redux - To Do List

$ npm install react-redux
$ npm install react-router-dom
function getCurrentState(state, ownProps){
  console.log(state, ownProps)
  return {state}
}

image

function Home(props){
  console.log(props);
  }

image

  • Index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import store from "./store";

ReactDOM.render(
  <Provider store = {store}>
    <App/>
  </Provider>
  ,document.getElementById("root")
  )
  • App.js
import React from "react";
import {HashRouter as Router, Route} from "react-router-dom";
import Home from "../routes/Home";
import Detail from "../routes/Detail";

function App(){
  return(
  <Router>
    <Route path = "/" exact component={Home}></Route>
    <Route path = "/:id" exact component={Detail}></Route>
  </Router>
  )
}

export default App;
  • ToDo.js
import React from "react";
import {connect} from "react-redux";
import {actionCreators} from "../store"
import {Link} from "react-router-dom";

function ToDo({text,onBtnClick, id}) {
  return(
  <li>
    <Link to={`/${id}`}>
      {text}
    </Link>
     <button onClick={onBtnClick}>Del</button>
    </li>
  )
}

function mapDispatchToProps(dispatch, ownProps) {
  // console.log(ownProps)
  return{
    onBtnClick: () => dispatch(actionCreators.delTodo(ownProps.id))
}
}

export default connect(null, mapDispatchToProps) (ToDo);
  • Home.js
import React, {useState} from "react";
import {connect} from "react-redux";
import {actionCreators} from "../store"
import ToDo from "../components/ToDo"

function Home({toDos,addTodo}){
  const [text, setText] = useState("")
  function onChange(e){
    setText(e.target.value)
  }

  function onSubmit(e){
    e.preventDefault();
    addTodo(text);
    setText("")
  }

  return (
  <>
    <h1>To Do</h1>
    <form onSubmit={onSubmit}>
      <input type="text" value={text} onChange={onChange}></input>
      <button>Add</button>
    </form> 
    <ul>
      {toDos.map(toDo =>(
      <ToDo {...toDo} key={toDo.id}/>
      ))}
      </ul>
  </>
  )
}

//mapStateToProps(다른이름 사용 가능은 하나 보통 mapStateToProps로 사용)
//mapStateToProps(Redux state에서 온 state, component의 props(지금은 사용X))
function mapStateToProps(state){
  // console.log(state)
  return {toDos : state}
}

function mapDispatchToProps(dispatch){
  return{
    addTodo : (text) => dispatch(actionCreators.addTodo(text))
  }
}

export default connect(mapStateToProps,mapDispatchToProps) (Home);
  • Detail.js
import React from "react";
// import {useParams} from "react-router-dom"
import {connect} from "react-redux"

function Detail( {toDo} ) {
  // const id = useParams();
  // console.log(id);
  return(
    <>
    <h1>{toDo?.text}</h1>
    <h5>Create at : {toDo?.id}</h5>
  </>
  );
}

function mapStateToProps(state, ownProps) {
  // console.log(ownProps);
  const {match: { 
    params: {
      id}
    }} = ownProps;
    // console.log(id)
  return { toDo : state.find(toDo => toDo.id === parseInt(id))}
}

export default  connect (mapStateToProps) (Detail);
  • store.js
import {createStore} from "redux";

const ADD = "ADD"
const DELETE = "DELETE"

const addTodo = (text) => {
  return{
    type : ADD,
    text
  }
}

const delTodo = id =>{
  return{
    type : DELETE,
    id
  }
}

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

const store = createStore(reducer)

export const actionCreators = {
  addTodo,
  delTodo
}

export default store

Redux toolkit

https://ko.redux.js.org/redux-toolkit/overview/

  • React-Redux를 사용할 때의 주로 불편한 점들로 Action의 이름, Action Creator, Switch-Case, Return, Defaust등 많은 양의 코드가 필요할 때 마다 Boilerplate Code(상용구 코드)를 써야 함
  • 적은 양의 코드로 같은 기능을 하도록 도와주는 package
$ npm install @reduxjs/toolkit react-redux

createAction

주어진 액션 타입 문자열을 이용해 액션 생산자 함수를 만들어줍니다. 함수 자체에 toString() 정의가 포함되어 있어서, 타입 상수가 필요한 곳에 사용할 수 있습니다.

function createAction(type, prepareAction?)
const INCREMENT = 'counter/increment'

function increment(amount) {
  return {
    type: INCREMENT,
    payload: amount,
  }
}

const action = increment(3)
// { type: 'counter/increment', payload: 3 }
  • 기존 위와같은 문법을 아래처럼 간단히 교체
import { createAction } from '@reduxjs/toolkit'

const increment = createAction('counter/increment')

let action = increment()
// { type: 'counter/increment' }

action = increment(3)
// returns { type: 'counter/increment', payload: 3 }

console.log(increment.toString())
// 'counter/increment'

console.log(`The action type is: ${increment}`)
// 'The action type is: counter/increment'
  • store.js

    • 기존 action.text대신 함수 내부 payload사용(redux toolkit이 제공하기에 관용처럼 사용)
    • action에 보내고 싶어 하는 정보가 무엇이던지 payload롸 함께 보내짐

image

import {createStore} from "redux";
import {createAction} from "@reduxjs/toolkit";

// const ADD = "ADD"
// const DELETE = "DELETE"
// const addTodo = (text) => {
//   return{
//     type : ADD,
//     text
//   }
// }

// const delTodo = id =>{
//   return{
//     type : DELETE,
//     id : parseInt(id)
//   }
// }

const addTodo = createAction("ADD") //Action의 이름
const delTodo = createAction("DELETE") //Action의 이름


const reducer = (state = [], action) => {
  switch(action.type) {
    //ADD와 DELETE가 이미 지워졌으므로 각 함수의 type으로 설정
    case addTodo.type:
      // return[{text:action.text, id:Date.now()},...state]
      return[{text:action.payload, id:Date.now()},...state]
    case delTodo.type:
      // return state.filter(toDo => toDo.id !== action.id)
      return state.filter(toDo => toDo.id !== action.payload)
    default:
      return state;
  }
}

const store = createStore(reducer)

export const actionCreators = {
  addTodo,
  delTodo
}

export default store

createReducer

switch 문을 작성하는 대신, 액션 타입과 리듀서 함수를 연결해주는 목록을 작성하도록 합니다. 여기에 더해 immer 라이브러리를 자동으로 사용해서, state.todos[3].completed = true와 같은 변이 코드를 통해 간편하게 불변 업데이트를 할 수 있도록 합니다.

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, value: state.value + 1 }
    case 'decrement':
      return { ...state, value: state.value - 1 }
    case 'incrementByAmount':
      return { ...state, value: state.value + action.payload }
    default:
      return state
  }
}
import { createAction, createReducer } from '@reduxjs/toolkit'

const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction('counter/incrementByAmount')

const initialState = { value: 0 }

const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state, action) => {
      state.value++
    })
    .addCase(decrement, (state, action) => {
      state.value--
    })
    .addCase(incrementByAmount, (state, action) => {
      state.value += action.payload
    })
})
import {createStore} from "redux";
import {createAction, createReducer} from "@reduxjs/toolkit";

// const ADD = "ADD"
// const DELETE = "DELETE"
// const addTodo = (text) => {
//   return{
//     type : ADD,
//     text
//   }
// }

// const delTodo = id =>{
//   return{
//     type : DELETE,
//     id : parseInt(id)
//   }
// }

const addTodo = createAction("ADD") //Action의 이름
const delTodo = createAction("DELETE") //Action의 이름


// const reducer = (state = [], action) => {
//   switch(action.type) {
//     //ADD와 DELETE가 이미 지워졌으므로 각 함수의 type으로 설정
//     case addTodo.type:
//       // return[{text:action.text, id:Date.now()},...state]
//       return[{text:action.payload, id:Date.now()},...state]
//     case delTodo.type:
//       // return state.filter(toDo => toDo.id !== action.payload)
//       return state.filter(toDo => toDo.id !== action.payload)
//     default:
//       return state;
//   }
// }

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

// redux toolkit은 immer아래에서 작동하기에 mutate와 새로운 state return모두
// 사용 가능(return값은 무조건 새로운 state)

const store = createStore(reducer)

export const actionCreators = {
  addTodo,
  delTodo
}

export default store

configureStore

createStore 를 감싸서 쓸만한 기본값들과 단순화된 설정을 제공합니다. 여러분의 리듀서 조각들을 자동으로 합쳐주고, 기본 제공되는 redux-thunk를 포함해서 여러분이 지정한 미들웨어들을 더해주고, Redux DevTools 확장을 사용할 수 있게 합니다.

  • chrome-browser 확장 프로그램에서 Redux-DevTools 설치

image

image

createSlice

조각 이름과 상태 초기값, 리듀서 함수들로 이루어진 객체를 받아 그에 맞는 액션 생산자와 액션 타입을 포함하는 리듀서 조각을 자동으로 만들어줍니다.

  • store.js
import {createStore} from "redux";
import {configureStore, createAction, createReducer, createSlice} from "@reduxjs/toolkit";


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)
  }
})

const store = configureStore({reducer : toDos.reducer})

export const{ add, remove} = toDos.actions

export default store
  • Home.js / ToDo.js
import {add} from "../store"
// import {actionCreator} from "../store"
...

addTodo : (text) => dispatch(add(text))
// import {actionCreators} from "../store"
import {remove} from "../store"
...
onBtnClick: () => dispatch(remove(ownProps.id))

태그: , ,

카테고리:

업데이트:

댓글남기기