Skip to content

Latest commit

ย 

History

History
488 lines (342 loc) ยท 16.7 KB

redux.md

File metadata and controls

488 lines (342 loc) ยท 16.7 KB

Redux (part 1)

Redux๋Š” "action"์ด๋ผ๋Š” ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•ด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ state๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ ํŒจํ„ด์ด๋‹ค. Redux๋Š” state๋“ค์˜ ์ค‘์•™ ์ €์žฅ์†Œ(centralized store)์˜ ์—ญํ• ์„ ํ•˜๋ฉฐ state๊ฐ€ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋ฐฉ์‹(predictable)์œผ๋กœ๋งŒ ์—…๋ฐ์ดํŠธ ๋˜๋„๋ก ํ•˜๋Š” ๊ทœ์น™์„ ์ œ๊ณตํ•œ๋‹ค.

Redux์—์„œ ์ œ๊ณต๋˜๋Š” ํŒจํ„ด๋“ค๊ณผ ๋„๊ตฌ๋“ค์„ ์‚ฌ์šฉํ•˜๋ฉด ์–ธ์ œ/์–ด๋””์„œ/์™œ/์–ด๋–ป๊ฒŒ state๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๊ณ , state๊ฐ€ ์—…๋ฐ์ดํŠธ ๋  ๋•Œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋กœ์ง์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.

Background Concepts

Redux๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„  ๋ช‡ ๊ฐ€์ง€ ๊ฐœ๋…๊ณผ ์šฉ์–ด์— ๋Œ€ํ•ด ์•Œ์•„์•ผํ•œ๋‹ค.

State Management

function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0);

  // Action: code that causes an update to the state when something happens
  const increment = () => {
    setCounter((prevCounter) => prevCounter + 1);
  };

  // View: the UI definition
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  );
}

์œ„ ์˜ˆ์ œ๋Š” ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด counter state๊ฐ€ ์ฆ๊ฐ€ํ•˜๋Š” Counter ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์„ธ ๊ฐ€์ง€ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋œ ๋…๋ฆฝํ˜• ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜(self-contained app)์ด๋‹ค.

  • state : ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ๋™์˜ ๋ฐ”ํƒ•์ด ๋˜๋Š” ์ƒํƒœ(the source of truth that drives our app)
  • view : UI ์ •์˜
  • actions : ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ์ด๋ฉฐ state๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ํŠธ๋ฆฌ๊ฑฐ

์ด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์€ state์— ๋”ฐ๋ผ view๊ฐ€ ๋ Œ๋”๋ง๋˜๋ฉฐ action์— ์˜ํ•ด state๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด ๋‹ค์‹œ view๊ฐ€ ๋ Œ๋”๋ง๋˜๋Š” ํ๋ฆ„์ธ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ(one-way data flow)์˜ ์˜ˆ์ด๋‹ค.

๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ๋Š” ๋™์ผํ•œ state๋ฅผ ๊ณต์œ ํ•˜๊ณ  ์‚ฌ์šฉํ•˜๋Š” ๋‹ค์ˆ˜์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๋น„ํšจ์œจ์ ์ด๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์€ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ณต์œ ๋˜๋Š” state๋ฅผ ์ถ”์ถœํ•ด ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ ์™ธ๋ถ€ ์ค‘์•™์— ์œ„์น˜์‹œ์ผœ์„œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํŠธ๋ฆฌ์˜ ์–ด๋Š ๋ถ€๋ถ„์— ์œ„์น˜ํ•˜๋“  state๋ฅผ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

์ด๊ฒƒ์ด Redux์˜ ๊ธฐ๋ณธ ์•„์ด๋””์–ด์ด๋‹ค.

Immutability

Redux์—์„œ state์˜ ์—…๋ฐ์ดํŠธ์‹œ, ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•ด์•ผํ•˜๋Š” ์›์น™์ด ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ธฐ์กด์˜ state๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ ๊ธฐ์กด state๋ฅผ ํฌํ•จํ•œ ์ƒˆ๋กœ์šด state๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ state๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

Redux Terminology

Actions

action์€ type ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฐ์ฒด์ด๋ฉฐ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ฐœ์ƒํ•œ ์–ด๋–ค ๋™์ž‘์„ ์„ค๋ช…ํ•˜๋Š” ์ด๋ฒคํŠธ์ด๋‹ค.

action์€ ๋ฐœ์ƒํ•œ ๋™์ž‘์— ๋Œ€ํ•œ ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” payload ํ•„๋“œ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk',
};

Reducers

reducer๋Š” ํ˜„์žฌ์˜ state์™€ action์„ ์ „๋‹ฌ ๋ฐ›์œผ๋ฉฐ state๋ฅผ ์–ด๋–ป๊ฒŒ ์—…๋ฐ์ดํŠธ ํ•  ๊ฒƒ์ธ๊ฐ€๋ฅผ ๊ฒฐ์ •ํ•˜๊ณ  ์ƒˆ๋กœ์šด state๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (state,action) => newState

์ „๋‹ฌ๋ฐ›์€ action(์ด๋ฒคํŠธ) type์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๊ฐœ๋…์œผ๋กœ ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

reducer๋Š” ํ•ญ์ƒ ์•„๋ž˜์™€ ๊ฐ™์€ ๊ทœ์น™์„ ์ง€์ผœ์•ผํ•œ๋‹ค.

  • state์™€ action ์ธ์ž๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด state ๊ฐ’ ๋งŒ ๊ณ„์‚ฐํ•ด์•ผํ•œ๋‹ค.
  • ๊ธฐ์กด state๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†๋‹ค. ๊ธฐ์กด state๋ฅผ ๋ณต์‚ฌํ•˜๊ณ  ๋ณต์‚ฌ๋œ ๊ฐ’์„ ๋ณ€๊ฒฝํ•ด ๋ถˆ๋ณ€์„ฑ์„ ์ง€์ผœ์•ผํ•œ๋‹ค.
  • ๋น„๋™๊ธฐ ๋กœ์ง, ๋žœ๋ค ๊ฐ’ ๊ณ„์‚ฐ ๋“ฑ side effect๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ์„  ์•ˆ๋œ๋‹ค.

reducer๋Š” ์ฒ˜๋ฆฌํ•  action์ธ์ง€ ํ™•์ธ ํ›„, ์ฒ˜๋ฆฌํ•  action์ด ๋งž์„ ๊ฒฝ์šฐ state์˜ ๋ณต์‚ฌ๋ณธ์„ ๋งŒ๋“ค์–ด ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ๋ณต์‚ฌ๋ณธ์„ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์„ ๊ฒฝ์šฐ์—๋Š” ๊ธฐ์กด state๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

const initialState = { value: 0 };

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === 'counter/incremented') {
    // If so, make a copy of `state`
    return {
      ...state,
      // and update the copy with the new value
      value: state.value + 1,
    };
  }
  // otherwise return the existing state unchanged
  return state;
}

Store

store๋Š” reducer๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์ƒ์„ฑ๋˜๋ฉฐ ํ˜„์žฌ state ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” getState ๋ฉ”์†Œ๋“œ๊ฐ€ ์žˆ๋‹ค.

import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({ reducer: counterReducer });

console.log(store.getState());
// {value: 0}

Dispatch

Redux store๋Š” dispatch๋ผ๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. state๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€ store.dispatch()๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  action ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค. store๋Š” reducer ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด ์ƒˆ๋กœ์šด state ๊ฐ’์„ ๋‚ด๋ถ€์— ์ €์žฅํ•œ๋‹ค. ๋˜ํ•œ getState()๋ฅผ ํ†ตํ•ด ์—…๋ฐ์ดํŠธ๋œ ๊ฐ’์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

store.dispatch({ type: 'counter/incremented' });

console.log(store.getState());
// {value: 1}

dispatch๋Š” action์„ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ ๊ฐœ๋…์œผ๋กœ ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

Action Creator

action creator๋Š” action์„ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ํ•จ์ˆ˜์ด๋‹ค. store.dispatch({ type: '...'})์„ ํ†ตํ•ด action์„ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์ง€๋งŒ ๋™์ผํ•œ ์ž‘์—…์ด ๋ฐ˜๋ณต๋  ๊ฒฝ์šฐ ๋น„ํšจ์œจ์ ์ด๋ฏ€๋กœ action creator๋ฅผ ์‚ฌ์šฉํ•ด store์— action์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

const doAddToDoItem = (payload) => ({ type: 'TODO_ADDED', payload });

store.dispatch(doAddToDoItem('be awesome today'));

store.dispatch() ๋ถ€๋ถ„์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์ƒˆ๋กœ์šด "wrapper" ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•จ์œผ๋กœ์จ dispatch ๊ณผ์ •์„ ์†์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

// assume we've created a store here
const store = createStore(someReducer);

// and we have our plain action creator function from above
const doAddToDoItem = (payload) => ({ type: 'TODO_ADDED', payload });

// we'd simply need to make a new function that did both things:
const boundActionCreator = (text) => store.dispatch(doAddToDoItem(text));

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๋‚ด์žฅ๋œ bindActionCreators() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ์œ„ "wrapper" ํ•จ์ˆ˜๋ฅผ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋‹ค. bindActionCreators() ํ•จ์ˆ˜๋Š” action creator๋ฅผ ๊ฐ€์ ธ์™€ ์ž๋™์œผ๋กœ ๋ฐ”์ธ๋”ฉ ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— action์˜ ์ˆ˜๊ฐ€ ๋งŽ์„๋•Œ ์œ ์šฉํ•˜๋‹ค.

Selector

selelctor๋Š” store state ๊ฐ’์—์„œ ํŠน์ • ๋ถ€๋ถ„์˜ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค.

const selectCounterValue = (state) => state.value;

const currentValue = selectCounterValue(store.getState());
console.log(currentValue);
// 2

Redux Application Data Flow

dataflow

  • Redux cycle

    • Action Creator โ†’ Action โ†’ Dispatch โ†’ Reducer โ†’ State
  • ์ดˆ๊ธฐ์„ค์ •

    • root reducer ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด Redux store ์ƒ์„ฑ
    • store๋Š” root reducer๋ฅผ ํ•œ๋ฒˆ ํ˜ธ์ถœํ•˜๊ณ  ๋ฐ˜ํ™˜ ๊ฐ’์„ ์ดˆ๊ธฐ state๋กœ ์ €์žฅ
    • UI๊ฐ€ ์ฒ˜์Œ ๋ Œ๋”๋ง ๋  ๋•Œ, UI ์ปดํฌ๋„ŒํŠธ๋Š” Redux store์˜ ํ˜„์žฌ state์— ์•ก์„ธ์Šคํ•˜๊ณ  ๋ Œ๋”๋ง ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ๊ฒฐ์ •
  • ์—…๋ฐ์ดํŠธ

    • ๋ฒ„ํŠผ ํด๋ฆญ ๋“ฑ์˜ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
    • store์— dispatch({type: 'counter/incremented'}) ์ž‘์—… ์ „๋‹ฌ
    • store๋Š” ์ด์ „ state์™€ ํ˜„์žฌ action์œผ๋กœ reducer ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ณ  ๋ฐ˜ํ™˜ ๊ฐ’์„ ์ƒˆ๋กœ์šด state๋กœ ์ €์žฅ
    • store์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ UI ์ปดํฌ๋„ŒํŠธ๋“ค์€ ํ•„์š”ํ•œ state๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    • ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋กœ ๋ฆฌ๋ Œ๋”๋ง๋œ UI ์ปดํฌ๋„ŒํŠธ๋“ค์ด ํ™”๋ฉด์— ์ถœ๋ ฅ

Redux (part 2)

๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด action์ด dispatch๋œ ๋‹ค์Œ reducer์— ์ „๋‹ฌ๋˜๊ธฐ ์ „์— ์ถ”๊ฐ€์ ์ธ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฃผ๋กœ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์œ„ํ•ด ์ž์ฃผ ์‚ฌ์šฉ๋œ๋‹ค.

Middleware

๋ฏธ๋“ค์›จ์–ด๋Š” Redux์—์„œ ์ œ๊ณต๋˜๋Š” createStore์™€ applyMiddleware๋ผ๋Š” ํŠน๋ณ„ํ•œ ์Šคํ† ์–ด ์ธํ•ธ์„œ(store inhancer)๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducer';
import { print1, print2, print3 } from './exampleAddons/middleware';

const middlewareEnhancer = applyMiddleware(print1, print2, print3);

// Pass enhancer as the second arg, since there's no preloadedState
const store = createStore(rootReducer, middlewareEnhancer);

export default store;

๊ฐ ๋ฏธ๋“ค์›จ์–ด๋Š” action์ด dispatch ๋  ๋•Œ ๋ฒˆํ˜ธ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.

import store from './store';

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' });
// log: '1'
// log: '2'
// log: '3'

๋ฏธ๋“ค์›จ์–ด๋Š” store์˜ dispatch ๋ฉ”์†Œ๋“œ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ํŒŒ์ดํ”„๋ผ์ธ์„ ํ˜•์„ฑํ•œ๋‹ค. store.dispatch(action)์„ ํ˜ธ์ถœํ•˜๋ฉด ํŒŒ์ดํ”„๋ผ์ธ์˜ ์ฒซ๋ฒˆ์งธ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ํ˜ธ์ถœ๋˜๊ณ  ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฏธ๋“ค์›จ์–ด๋Š” reducer์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์‹คํ–‰ํ•ด์•ผํ•  action์ธ๊ฐ€๋ฅผ ํ™•์ธํ•œ ํ›„ ์˜ฌ๋ฐ”๋ฅธ action์ผ ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ •ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์„ ๊ฒฝ์šฐ ๋‹ค์Œ ๋ฏธ๋“ค์›จ์–ด๋กœ ์ž‘์—…์„ ์ „๋‹ฌํ•œ๋‹ค.

Writing Custom Middleware

๋ฏธ๋“ค์›จ์–ด๋Š” ์„ธ ๊ฐ€์ง€์˜ ์ค‘์ฒฉ๋œ ํ•จ์ˆ˜๋กœ ์ž‘์„ฑ๋œ๋‹ค.

// Middleware written as ES5 functions

// Outer function:
function exampleMiddleware(storeAPI) {
  return function wrapDispatch(next) {
    return function handleAction(action) {
      // Do anything here: pass the action onwards with next(action),
      // or restart the pipeline with storeAPI.dispatch(action)
      // Can also use storeAPI.getState() here

      return next(action);
    };
  };
}

// ES6 arrow function
const anotherExampleMiddleware = (storeAPI) => (next) => (action) => {
  // Do something in here, when each action is dispatched

  return next(action);
};
  • exampleMiddleware : applyMiddleware์— ์˜ํ•ด ํ˜ธ์ถœ๋˜์–ด์ง€๋Š” "๋ฏธ๋“ค์›จ์–ด" ํ•จ์ˆ˜์ด๋ฉฐ ์Šคํ† ์–ด์˜ {dispatch, getState}๋ฅผ ํฌํ•จํ•œ storeAPI ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ ๋ฐ›๋Š”๋‹ค. dispatch ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋ฏธ๋“ค์›จ์–ด ํŒŒ์ดํ”„๋ผ์ธ์˜ ์‹œ์ž‘์œผ๋กœ action์„ ๋ณด๋‚ธ๋‹ค.
  • wrapDispatch : next ์ธ์ž๋กœ ํ˜ธ์ถœ๋œ ํ•จ์ˆ˜๋ฅผ ๋ฐ›๋Š”๋‹ค. ์ด ํ•จ์ˆ˜๊ฐ€ ์‹ค์ œ ํŒŒ์ดํ”„๋ผ์ธ์˜ ์‹œ์ž‘์ด๋˜๋Š” ๋ฏธ๋“ค์›จ์–ด์ด๋‹ค. next(action)์„ ํ˜ธ์ถœํ•ด ๋‹ค์Œ ๋ฏธ๋“ค์›จ์–ด๋กœ ์ „๋‹ฌํ•œ๋‹ค.
  • handleAction : action์„ ์ธ์ž๋กœ ์ „๋‹ฌ๋ฐ›์•„ action์ด ์ „๋‹ฌ ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋œ๋‹ค.
+ ์˜ˆ์ œ ์ถ”๊ฐ€ํ•˜๊ธฐ
+ redux-thunk
+ redux-saga
...

Redux Toolkit

Redux Toolkit์€ ๋ณต์žกํ•œ Redux ์Šคํ† ์–ด ๊ตฌ์„ฑ, Redux ์‚ฌ์šฉ์„ ์œ„ํ•œ ๋‹ค์ˆ˜์˜ ํŒจํ‚ค์ง€ ํ•„์š”์„ฑ๊ณผ ๋งŽ์€ ์ฝ”๋“œ๋Ÿ‰์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ ๊ฐœ๋ฐœ ๋„๊ตฌ์ด๋‹ค.

Redux Toolkit์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์ง„๋‹ค.

  • simple : ์Šคํ† ์–ด ์„ค์ •, ๋ฆฌ๋“€์„œ, ๋ถˆ๋ณ€์„ฑ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๋“ฑ์˜ ๊ธฐ์กด Redux์˜ ์‚ฌ์šฉ๋ฒ•์„ ๊ฐ„ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค.
  • opinionated : ์Šคํ† ์–ด ์„ค์ •์— ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ๋ณธ ์„ค์ •์ด ์ œ๊ณต๋˜๋ฉฐ ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” Redux addon์ด ๋‚ด์žฅ๋˜์–ด ์žˆ๋‹ค.
  • powerful : ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๋กœ์ง์œผ๋กœ ๋ถˆ๋ณ€์„ฑ ๋กœ์ง์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. state์˜ ์ „์ฒด slice๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • effective : ์ ์€ ์ฝ”๋“œ๋กœ ๋งŽ์€ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

Installation

Using Create React App

Redux Toolkit ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์ƒˆ๋กœ์šด ์•ฑ ์‹œ์ž‘์‹œ ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์€ CRA์šฉ Redux + TypeScript ํ…œํ”Œ๋ฆฟ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

// Redux + JS
npx create-react-app app-name --template redux
// Redux + TS
npx create-react-app app-name --template redux-typescript

Package

๋ชจ๋“ˆ ๋ฒˆ๋“ค๋Ÿฌ ๋˜๋Š” ๋…ธ๋“œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” npm ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด์•ผํ•œ๋‹ค.

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

What's Included

Redux Toolkit์— ํฌํ•จ๋œ api๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

configureStore()

import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
  reducer: {
    ex1: ex1Reducer,
    ex2: ex2Reducer,
  },
});

configureStore()๋Š” Redux์˜ createStore๋ฅผ ์‚ฌ์šฉํ•ด ์ž๋™์œผ๋กœ ์Šฌ๋ผ์ด์Šค ๋ฆฌ๋“€์„œ๋ฅผ ๊ฒฐํ•ฉํ•œ๋‹ค. Redux ๋ฏธ๋“ค์›จ์–ด์™€ Redux DevTools์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

createReducer()

const exReducer = createReducer(0, {
  increment: (state, action) => state + action.payload,
});

createReducer()๋Š” switch๋ฌธ ์ž‘์„ฑ์—†์ด reducer ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์†Œ๋“œ์ด๋‹ค. Immer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ์‚ฌ์šฉํ•ด ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ state๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ ์ธ์ž๋Š” ์ดˆ๊ธฐ ๊ฐ’, ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” reducer๋ฅผ ๊ฐ€์ง„๋‹ค.

createAction()

const increment = createAction('increment');

createAction()์€ action type์— ๋”ฐ๋ผ action create function์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

createSlice()

const exSlice = createSlice({
  name: 'ex',
  initialState: [],
  reducers: {
    action1(state, action) {
      state.ex = action.payload;
    },
  },
});

createSlice()๋Š” action๊ณผ reducer๋ฅผ ๋ชจ๋‘ ๊ฐ€์ง„ ๋ฉ”์†Œ๋“œ์ด๋‹ค. action์˜ ์ด๋ฆ„์„ ๋‚˜ํƒ€๋‚ด๋Š” name ํ”„๋กœํผํ‹ฐ์™€ ์ดˆ๊ธฐ ๊ฐ’์„ ๋ฐ›์•„ slice reducer์™€ action creator, action type์„ ์ƒ์„ฑํ•œ๋‹ค.

Example

๋‹ค์Œ์€ React, TypeScript, Redux Toolkit์„ ์‚ฌ์šฉํ•œ ์นด์šดํ„ฐ ์˜ˆ์ œ์ด๋‹ค.

Store

configureStore()๋ฅผ ํ†ตํ•ด ๋ฉ”์†Œ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  index.ts์—์„œ Provider๋ฅผ ์‚ฌ์šฉํ•ด Redux ์Šคํ† ์–ด๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค.

// app/store.ts
import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// index.ts
import ReactDOM from 'react-dom';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';

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

Create Redux State Slice

๋‹ค์Œ์€ slice๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค. slice ์ƒ์„ฑ์„ ์œ„ํ•ด์„œ๋Š” slice ์ด๋ฆ„, ์ดˆ๊ธฐ ๊ฐ’, state ์—…๋ฐ์ดํŠธ ๋ฐฉ๋ฒ•์„ ์ •์˜ํ•˜๋Š” ํ•˜๋‚˜ ์ด์ƒ์˜ reducer ํ•จ์ˆ˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์ƒ์„ฑ๋œ ์Šฌ๋ผ์ด์Šค๋Š” action creator์™€ ์ „์ฒด slice์— ๋Œ€ํ•œ reducer ๊ธฐ๋Šฅ์„ export ํ•  ์ˆ˜ ์žˆ๋‹ค.

Redux Toolkit์˜ createSlice์™€ createReducer๋Š”ย Immer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ์ ์šฉํ•ด ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๋กœ์ง์œผ๋กœ ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉฐ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

// app/features/counter/counterSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

export interface State {
  value: number;
}

const initialState: State = {
  value: 0,
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;

Add Slice Reducers to the Store

๋‹ค์Œ์€ slice reducer๋ฅผ ์Šคํ† ์–ด์— ์ถ”๊ฐ€ํ•˜๋Š” ๋‹จ๊ณ„์ด๋‹ค.

configureStore() ๋ฉ”์†Œ๋“œ์˜ reducer ํ”„๋กœํผํ‹ฐ์— ์ด์ „ ๋‹จ๊ณ„์—์„œ ์ •์˜ํ•˜๊ณ  exportํ•œ reducer importํ•ด ์ถ”๊ฐ€ํ•œ๋‹ค.

// app/store.ts

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export default configureStore({
  reducer: {
    counter: counterReducer,
  },
});

Use Redux State and Actions in React Components

์ด์ œย hooks๋ฅผ ์‚ฌ์šฉํ•ด ๋ฆฌ์•กํŠธ ๊ตฌ์„ฑ์š”์†Œ์™€ Redux ์Šคํ† ์–ด์™€ ์ธํ„ฐ๋ž™์…˜ํ•  ์ˆ˜ ์žˆ๋‹ค. useSelector์™€ย useDispatch๋ฅผย ์‚ฌ์šฉํ•ด Redux ์Šคํ† ์–ด์˜ state๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

import React from 'react';
import { RootState } from '../../app/store';
import { useSelector, useDispatch } from 'react-redux';
import { decrement, increment } from './counterSlice';

export function Counter() {
  const count = useSelector((state: RootState) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <div>
        <button
          aria-label='Increment value'
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label='Decrement value'
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  );
}
Note: Redux Toolkit with middlewares

Reference