Redux

Redux

[TOC]

๐ŸŽ‰What is Redux?

์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์ฃผ๋Š” API์ด๋ฉฐ, ์ตœ๊ทผ React์™€ ์ ‘๋ชฉํ•ด์„œ ๋œจ๊ณ  ์žˆ๋Š” ์ถ”์„ธ๋ผ Reduxํ•˜๋ฉด React๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์ง€๋งŒ, Redux๋Š” Node ๋ชจ๋“ˆ์ด๋ผ JS ๊ธฐ๋ฐ˜์œผ๋กœํ•˜๋Š” ๋ชจ๋“  ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ์‚ฌ์šฉ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•œ ํ”„๋กœ๊ทธ๋žจ์„ ์˜ˆ๋กœ ๋“ค๋ฉดโ€ฆ

์•„๋ž˜์˜ ํ”„๋กœ๊ทธ๋žจ์€ ์‚ฌ์šฉ์ž์˜ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด์„œ add, minus์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ ์‹œ์ผœ count์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋Š” ์˜ˆ์ œ์ด๋‹ค.

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)
  <body>
    <button id="add">Add</button>
    <span>0</span>
    <button id="minus">Minus</button>
  </body>

๊ฐ„๋‹จํ•˜๊ฒŒ ์ฝ”๋“œ์— ๋Œ€ํ•ด ์„ค๋ช…ํ•˜๋ฉด, html์— ์žˆ๋Š” add, minus๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๊ฐ๊ฐ ์š”์ฒญ์— ๋งž๊ฒŒ count๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ • ํ• ๋•Œ๋งˆ๋‹ค ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜๊ณ , ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์งˆ์ˆ˜๋ก ๊ด€๋ฆฌํ•˜๊ธฐ๋„ ํž˜๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

Redux์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์„ ์˜ˆ๋กœ ๋“ค๋ฉด MVC ํŒจํ„ด์˜ ๋‹จ์ ์ธ ์–‘๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์œผ๋กœ ๊ด€๋ฆฌ๊ฐ€ ๋ณต์žกํ•œ ๋ถ€๋ถ„์„ store์—์„œ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— state๊ด€๋ฆฌ๋ฅผ ์ˆ˜์›”ํ•˜๊ฒŒ ๋„์™€ ์ค๋‹ˆ๋‹ค.


๐ŸŽ‰How to use Redux?

์œ„ Vanilla JS์— Redux๋ฅผ ํ†ตํ•ด์•ผ๋งŒ state๋ณ€๊ฒฝ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค.

import { createStore } from 'redux'
const add = document.getElementById('add')
const minus = document.getElementById('minus')
const number = document.querySelector('span')


const countModifier = (count = 0, action) => {
  if (action.type === 'ADD')
    return count + 1
  else if (action.type === 'MINUS')
    return count - 1
  else
    return count
}
const countStore = createStore(countModifier)
const onChange = () => {
  number.innerText = countStore.getState()
}

countStore.subscribe(onChange)


const addHandler = () => {
  countStore.dispatch({ type: 'ADD' })
}

const minusHandler = () => {
 countStore.dispatch({ type: 'MINUS' })
}

add.addEventListener('click', addHandler)
minus.addEventListener('click', minusHandler)

createStore์— ์ธ์ž ๊ฐ’์œผ๋กœ Reducer๋ฅผ ์ค˜์•ผํ•˜๋ฉฐ, Reducer๋ฅผ ํ†ตํ•ด์„œ์•ผ๋งŒ state๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

Reduer๋Š” ์‚ฌ์šฉ์ž์˜ ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด, ๋ณ€๊ฒฝ๋  state๋ฅผ dispatch์— ๊ฐ์ฒด๋กœ ๋ฐ›์•„์„œ Reducer์— ๋ฏธ๋ฆฌ ์ž‘์„ฑ๋œ ํ”„๋กœ์„ธ์Šค ๋ฐ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

์ด ๋•Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฌด์กฐ๊ฑด Object ์•ˆ์— type์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.(์•„๋‹ˆ๋ฉด error ๋ฐœ์ƒ)

event listener => dispatch(with type object) => onChange => Reducer

๐Ÿ“ŒReducer๋Š” mutate ์ฆ‰ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ์ง€์–‘ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ mutate๋ž€ array.push(blabla)๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ˜•ํ•˜๋Š” ๊ฑด๋ฐ, JS๋Š” ์›์‹œ์  ํƒ€์ž…(Number, String, Object ๋“ฑ)์„ ์ œ์™ธํ•œ ๋ชจ๋“  ๊ฒƒ์ด ๊ฐ์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋Š”์ง€ ํ™•์ธ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” ์ฃผ์†Œ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•ด Re-Rendering ํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽ‰What is Redux Toolkit?

state๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ํŽธํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” Redux๊ฐ€ ๋ง‰์ƒ ์‚ฌ์šฉํ•ด๋ณด๋‹ˆ ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š” ์ฝ”๋“œ๋“ค ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•˜๊ธฐ ๋” ๋ณต์žกํ•˜๋‹ค๋Š” ์˜๊ฒฌ๋“ค์ด ์ƒ๊ฒจ๋‚ฌ์Šต๋‹ˆ๋‹ค.

yarn add @reduxjs/toolkit
npm install @reduxjs/toolkit

Toolkit์€ ๊ฐ€๋™์„ฑ ๋ฐ ์ฝ”๋“œ์ž‘์„ฑ์— ๋งŽ์€ ๋„์›€์„ ์ค๋‹ˆ๋‹ค.

// Original Redux
const ADD = 'ADD'
const DEL = 'DEL'
const addToDo = (text) => {
  return {
    type: ADD,
    text
  }
}

const deleteToDo = (id) => {
  return {
    type: DEL,
    id: parseInt(id)
  }
}

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

๊ธฐ์กด์˜ Redux์—์„œ๋Š” type์˜ ํ†ต์ผ๋„ ์—†๊ณ  ๊ฐ„๋‹จํ•œ ํ”„๋กœ๊ทธ๋žจ์„ ๋งŒ๋“œ๋Š”๋ฐ๋„ ์ค‘๋ณต์ธ ์ฝ”๋“œ๊ฐ€ ๋งŽ์•˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํŽธ๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์—ญ์„ค์ ์ด๊ฒŒ๋„ ๋ถˆํŽธํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ Toolkit์„ ์ด์šฉํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด

import { createAction, createReducer } from '@reduxjs/toolkit'

// ToolKit
const addToDo = createAction('ADD')
const deleteToDo = createAction('DEL')

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

๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋กœ ๋ฐ”๋€Œ์—ˆ์Šต๋‹ˆ๋‹ค.

state๋ฅผ returnํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์ง€ ์•Š์•„๋„ createAction()์„ ํ• ๋‹นํ›„ ํ˜ธ์ถœํ•˜๋ฉด

{type: 'ADD', payload: 'blabla'}

JSONํ˜•์‹์œผ๋กœ state๋ฅผ ์ถœ๋ ฅํ•ด ์ค๋‹ˆ๋‹ค! ์—ฌ๊ธฐ์„œ ๊ธฐ์กด์˜ Redux์™€ ๋‹ค๋ฅธ ์ ์€ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ์ด๋ฆ„์˜ state๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  payload๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ state๋ฅผ ์ „๋‹ฌํ•ด ์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค

๋˜ switch-case๋ฌธ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ์žฅ์ ‘์ด ์žˆ์–ด์„œ ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•ด์ง€๊ณ ,

๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ธ๋ฐ state.push๋ฅผ ํ•˜๋ฉด mutate๊ฐ€ ๋ผ์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•˜์ง€๋งŒ node package์ค‘์— immer๋ผ๋Š” ์—„์ฒญ๋‚œ ํŒจํ‚ค์ง€๊ฐ€ ์žˆ๋Š”๋ฐ ๊นŠ์€ ๋ณต์‚ฌ๋ฅผ ์ œ๊ณตํ•ด์ฃผ์–ด์„œ spread์—ฐ์‚ฐ์ž๋‚˜, ๊ฐœ๋ฐœ์ž๊ฐ€ ์ผ์ผํžˆ ๋ณต์‚ฌํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๋ฒˆ๊ฑฐ๋Ÿฌ์›€์„ ๋Œ€์‹  ํ•ด์ค๋‹ˆ๋‹ค.

๋˜ ๊ธฐ์กด์˜ return์œผ๋กœ ๊ฐ’์„ ๋„˜๊ฒจ์ฃผ๋Š” ๋ฐฉ์‹๋„ ๊ทธ๋Œ€๋กœ ์ œ๊ณตํ•˜๋‹ˆ ์›ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด๋„ ๋˜์ง€๋งŒ! return์„ ํ• ๊ฒฝ์šฐ ๊ธฐ์กด๊ณผ ๋™์ผํ•˜๊ฒŒ non-mutate๋ฅผ ์ง€ํ–ฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— new mutate๋ฅผ returnํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

ํ•˜.์ง€.๋งŒ ์—ฌ๊ธฐ์„œ ๋” ํŽธํ•œ ๊ธฐ๋Šฅ์„ toolkit์€ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

const addToDo = createAction('ADD')
const deleteToDo = createAction('DEL')

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

export const actionCreator = {
  addToDo,
  deleteToDo
}

๊ธฐ์กด์—๋Š” createAction์„ ํ†ตํ•ด์„œ action์„ ๋งŒ๋“ค๊ณ , reducer์— state๋งˆ๋‹ค ์ผ์ผํžˆ ์ž…๋ ฅํ•ด ์ค˜์•ผ ํ–ˆ๋Š”๋ฐ

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 !== parseInt(action.payload))
  }
})

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

createSlice๋ฅผ ์ด์šฉํ•˜๋ฉด reducer, action์„ ํ•œ๋ฒˆ์— ์„ ์–ธ, ํ• ๋‹น์ด ๊ฐ€๋Šฅํ•˜๋‹ค

toolkit ์‚ฌ์šฉ ์ „

import { createStore } from 'redux'

const ADD = 'ADD'
const DEL = 'DEL'

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

const deleteTodo = (id) => {
  return {
    type: DEL,
    id: parseInt(id)
  }
}

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

export const actionCreator = {
  addTodo,
  deleteTodo
}

const store = createStore(reducer)

export default store 

toolkit ์‚ฌ์šฉ ํ›„

import { configureStore, 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 !== parseInt(action.payload))
  }
})
export const { add, remove } = toDos.actions
export default configureStore({ reducer: toDos.reducer })

Reference

  • ๋…ธ๋งˆ๋“œ ์ฝ”๋” Redux ๋ฌด๋ฃŒ๊ฐ•์˜

    https://nomadcoders.co/redux-for-beginners/lobby