[TIL-0512] Zustand vs. Redux

jinyยท2025๋…„ 5์›” 26์ผ

์บก์Šคํ†ค2

๋ชฉ๋ก ๋ณด๊ธฐ
10/22

๐ŸŒŸ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋น„๊ต : Zustand vs. Redux

๋ฆฌ์•กํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ ์  ์ปค์ง€๋ฉด์„œ ์ƒํƒœ ๊ด€๋ฆฌ(State Management)๋Š” ํ•„์ˆ˜๊ฐ€ ๋˜์—ˆ๋‹ค.
๋Œ€ํ‘œ์ ์ธ ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ Zustand๊ณผ Redux๋Š” ๊ฐ™์€ ๋ชฉ์ ์„ ์ง€๋‹ˆ์ง€๋งŒ ์‚ฌ์šฉ์„ฑ, ์ฒ ํ•™, ๊ทœ๋ชจ ๋ฉด์—์„œ ๋งค์šฐ ๋‹ค๋ฅด๋‹ค.
์ด ๊ธ€์—์„œ๋Š” ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊นŠ์ด ์žˆ๊ฒŒ ๋น„๊ตํ•˜๊ณ , ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ๊ณผ ์‚ฌ์šฉ ์ ํ•ฉ ์ƒํ™ฉ์„ ์†Œ๊ฐœํ•  ๊ฒƒ์ด๋‹ค.


๐ŸŒŸ Zustand๋ž€?

Zustand๋Š” ๋…์ผ์–ด๋กœ ์ƒํƒœ(state)๋ผ๋Š” ๋œป์œผ๋กœ, ๊ฐ€๋ณ๊ณ  ์ง๊ด€์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

  • ํŠน์ง•

    • ๊ฐ„๋‹จํ•œ API : ๋ช‡ ์ค„์˜ ์ฝ”๋“œ๋กœ store๋ฅผ ๋งŒ๋“ค๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
    • Boilerplate ์—†์Œ : ์•ก์…˜ ํƒ€์ž…, ๋ฆฌ๋“€์„œ ๋“ฑ ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ๊ฐ€ ์—†์Œ
    • React Context ๋ฏธ์‚ฌ์šฉ : ์ž์ฒด ๊ตฌ๋… ์‹œ์Šคํ…œ์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
    • ์„œ๋ธŒ์Šคํฌ๋ฆฝ์…˜ ๊ธฐ๋ฐ˜ : ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋งŒ ๋ฆฌ๋ Œ๋”๋ง๋จ
    • Immer, persist ๋“ฑ ๋ฏธ๋“ค์›จ์–ด ์ œ๊ณต
    • ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ ๋›ฐ์–ด๋‚œ ๊ถํ•ฉ
  • ์˜ˆ์ œ

    • store.ts

      import { create } from 'zustand';
      
      const useCounterStore = create((set) => ({
        count: 0,
        increase: () => set((state) => ({ count: state.count + 1 })),
      }));
      • ์ „์—ญ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ƒํƒœ ์กฐ์ž‘ ํ•จ์ˆ˜(increase)๋ฅผ ์ •์˜ํ•จ
      • Zustand์˜ create ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ„๋‹จํžˆ ์ „์—ญ store๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ
      • count : ํ˜„์žฌ ์นด์šดํ„ฐ ์ƒํƒœ
      • increase : ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•จ์ˆ˜ (set ์‚ฌ์šฉ)
      • useCounterStore : ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ปค์Šคํ…€ ํ›…
    • Component.tsx

      const Counter = () => {
        const { count, increase } = useCounterStore();
        return (
          <div>
            <p>{count}</p>
            <button onClick={increase}>+1</button>
          </div>
        );
      };
      • useCounterStore๋ฅผ ์‚ฌ์šฉํ•ด count ๊ฐ’์„ ์ฝ๊ณ  increase()๋กœ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•จ
      • Zustand๋Š” use___Store() ํ›…์„ ํ†ตํ•ด ์ง์ ‘ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
      • count : ์ƒํƒœ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ
      • increase : ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ƒํƒœ ๋ณ€๊ฒฝ

๐ŸŒŸ Redux๋ž€?

Redux๋Š” ํŽ˜์ด์Šค๋ถ์˜ Flux ์•„ํ‚คํ…์ฒ˜์—์„œ ์˜๊ฐ์„ ๋ฐ›์•„ ๋งŒ๋“  ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ๊ด€๋ฆฌ(Predictable State Container)๋ฅผ ๋ชจํ† ๋กœ ํ•œ๋‹ค.

  • ํŠน์ง•

    • ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ธฐ๋ฐ˜
    • ์•ก์…˜ โ†’ ๋ฆฌ๋“€์„œ โ†’ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํŒจํ„ด
    • ๋ฏธ๋“ค์›จ์–ด ์ค‘์‹ฌ ํ™•์žฅ์„ฑ (e.g. redux-thunk, redux-saga)
    • Redux DevTools ์ง€์›: ๋””๋ฒ„๊น… ํŽธ๋ฆฌ
    • Context API๋ฅผ ํ™œ์šฉํ•œ ์ƒํƒœ ์ „๋‹ฌ
    • ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉ
  • ์˜ˆ์ œ

    • actions.ts
      export const increase = () => ({ type: 'INCREASE' })
      • ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•œ ์•ก์…˜ ์ƒ์„ฑ ํ•จ์ˆ˜(Action Creator)
      • ์•ก์…˜ ๊ฐ์ฒด { type: 'INCREASE' }๋ฅผ ๋ฐ˜ํ™˜ํ•จ
      • ์ปดํฌ๋„ŒํŠธ์—์„œ dispatch(increase())๋กœ ํ˜ธ์ถœํ•จ
    • reducer.ts
      const initialState = { count: 0 };
      export const counterReducer = (state = initialState, action) => {
        switch (action.type) {
          case 'INCREASE':
            return { ...state, count: state.count + 1 }
          default:
            return state
        }
      };
      • ์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง์„ ๋‹ด๋‹นํ•˜๋Š” ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜
      • ์ดˆ๊ธฐ ์ƒํƒœ: count = 0
      • ์•ก์…˜์˜ type์— ๋”ฐ๋ผ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•จ
      • ์ƒˆ๋กœ์šด ์ƒํƒœ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๋ถˆ๋ณ€์„ฑ ์œ ์ง€ ํ•„์ˆ˜
    • store.ts
      import { createStore } from 'redux'
      export const store = createStore(counterReducer);
      • Redux์˜ ์ „์—ญ ์ƒํƒœ ์ €์žฅ์†Œ(Store)๋ฅผ ์ƒ์„ฑํ•จ
      • createStore์— ๋ฆฌ๋“€์„œ๋ฅผ ๋„˜๊ฒจ store๋ฅผ ์ƒ์„ฑํ•จ
      • React ์•ฑ ์ตœ์ƒ๋‹จ์— Provider๋กœ ์ด store๋ฅผ ์ฃผ์ž…ํ•ด์•ผ ํ•จ
    • Component.tsx
      const Counter = () => {
        const count = useSelector((state) => state.count);
        const dispatch = useDispatch();
        
        return (
          <div>
            <p>{count}</p>
            <button onClick={() => dispatch(increase())}>+1</button>
          </div>
        )
      }
      • Redux์˜ ์ƒํƒœ๋ฅผ ์ฝ๊ณ (useSelector) ๋ณ€๊ฒฝ(dispatch)ํ•˜๋Š” UI ์ปดํฌ๋„ŒํŠธ
      • useSelector : store์—์„œ count ๊ฐ’์„ ๊ฐ€์ ธ์˜ด
      • useDispatch : ์•ก์…˜์„ ๋””์ŠคํŒจ์น˜ํ•ด์„œ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•จ

๐ŸŒŸ ํ‘œ๋กœ ๋น„๊ตํ•˜๊ธฐ - Zustand vs. Redux

ํ•ญ๋ชฉZustandRedux
์ƒํƒœ ์ •์˜ ์œ„์น˜store.ts ํ•œ ๊ณณ์—์„œ ๋ชจ๋‘ ์ •์˜actions, reducers, store๋กœ ๋ถ„๋ฆฌ
์ƒํƒœ ์—…๋ฐ์ดํŠธset() ํ•จ์ˆ˜ ์‚ฌ์šฉdispatch() + reducer
๊ตฌ์กฐ๋‹จ์ˆœํ•˜๊ณ  ์ง๊ด€์ ๋ช…ํ™•ํ•˜์ง€๋งŒ ๊ตฌ์กฐ๊ฐ€ ๋ณต์žกํ•จ
๋Ÿฌ๋‹ ์ปค๋ธŒ๋‚ฎ์Œ์ค‘๊ฐ„~๋†’์Œ
์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๊ฐ„๊ฒฐํ•จ์•ฝ๊ฐ„ ์žฅํ™ฉํ•จ(์•ก์…˜, dispatch ํ•„์š”)
๋””๋ฒ„๊น… ์ง€์›๊ธฐ๋ณธ ์ง€์› + Devtools (middleware)๋งค์šฐ ๊ฐ•๋ ฅํ•œ Redux DevTools ์ง€์›
๋ฏธ๋“ค์›จ์–ด๊ฐ„๋‹จํžˆ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ (persist ๋“ฑ)ํ’๋ถ€ํ•˜๊ณ  ์œ ์—ฐ (thunk, saga ๋“ฑ)
ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ง€์›๋งค์šฐ ์šฐ์ˆ˜๊ธฐ๋ณธ์€ ์ˆ˜๋™ ์„ค์ • ํ•„์š”, RTK ์‚ฌ์šฉ ์‹œ ๊ฐœ์„ 
์„ฑ๋Šฅ ์ตœ์ ํ™”์ž์ฒด subscribe๋กœ ์ž๋™ ์ตœ์ ํ™”useSelector ์ตœ์ ํ™” ํ•„์š”
๋งˆ์ดํฌ๋กœ ์ƒํƒœ ๊ตฌ๋…์ง€์› (ํ•„์š”ํ•œ ๊ฐ’๋งŒ ๊ตฌ๋…)๊ฐ€๋Šฅํ•˜๋‚˜ ๋ณต์žก (memoization ํ•„์š”)
SSR ์ง€์›๊ธฐ๋ณธ ์ง€์›๋ณ„๋„ ์ฒ˜๋ฆฌ ํ•„์š”

๐ŸŒŸ Zustand๊ฐ€ ์ ํ•ฉํ•œ ์ƒํ™ฉ

  • ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœํ•ด์•ผ ํ•  ํ”„๋กœ์ ํŠธ

  • ๋น„๊ต์  ๊ทœ๋ชจ๊ฐ€ ์ž‘์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

  • ์ƒํƒœ ๋กœ์ง์ด ๋ณต์žกํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋‹จ์ˆœ UI ์ œ์–ด

  • ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์—†์ด ์ง๊ด€์ ์ธ ์ฝ”๋“œ ์ง€ํ–ฅ

  • ๋ชจ๋“ˆ ๋‹จ์œ„ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ

    ๐Ÿ” ๋ชจ๋“ˆ ๋‹จ์œ„ ์ƒํƒœ ๊ด€๋ฆฌ๋ž€?

    • ๊ธฐ๋Šฅ๋ณ„๋กœ ์ƒํƒœ(store)๋ฅผ ์ชผ๊ฐœ์„œ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹
    • ๊ฐ ๋…๋ฆฝ์ ์ธ ๊ธฐ๋Šฅ์ด๋‚˜ UI ์˜์—ญ๋งˆ๋‹ค ์ž๊ธฐ๋งŒ์˜ ์ƒํƒœ ์ €์žฅ์†Œ๋ฅผ ๊ฐ€์ง€๋„๋ก ๋ชจ๋“ˆํ™”ํ•˜๋Š” ๊ฒƒ

    ๐Ÿ” ์˜ˆ์‹œ๋กœ ์ดํ•ดํ•ด๋ณด๊ธฐ

    • ์ „์—ญ ์ƒํƒœ๋ฅผ ํ•˜๋‚˜์˜ store์— ๋‹ค ๋„ฃ์€ ๊ฒฝ์šฐ (๋น„์ถ”์ฒœ)
      const useGlobalStore = create((set) => ({
        modalOpen: false,
        loginUser: null,
        cartItems: [],
        theme: 'light',
        // ...
      }));
      • ์ƒํƒœ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ๊ด€๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์ง
      • ํ•˜๋‚˜๋งŒ ๋ฐ”๋€Œ์–ด๋„ ๊ด€๋ จ ์—†๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋  ์œ„ํ—˜ ์žˆ์Œ
    • ๊ธฐ๋Šฅ๋ณ„๋กœ store๋ฅผ ๋‚˜๋ˆˆ ๊ฒฝ์šฐ (๋ชจ๋“ˆ ๋‹จ์œ„ ๊ด€๋ฆฌ)
      // modal.store.ts
      export const useModalStore = create((set) => ({
        isOpen: false,
        open: () => set({ isOpen: true }),
        close: () => set({ isOpen: false }),
      }));
      // auth.store.ts
      export const useAuthStore = create((set) => ({
        user: null,
        login: (user) => set({ user }),
        logout: () => set({ user: null }),
      }));
      // cart.store.ts
      export const userCartStore = create((set) => ({
        items: [],
        addItem: (item) => set((state) => ({ items: [...state.items, item] })),
      }));
      • ๋ชจ๋‹ฌ ๊ธฐ๋Šฅ์€ modal store, ๋กœ๊ทธ์ธ์€ auth store, ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋Š” cart store
      • ๊ฐ ๊ธฐ๋Šฅ๋ณ„๋กœ store๋ฅผ ๋‚˜๋ˆ„๋ฉด ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ๊ฐ€ ์ž˜ ๋˜๊ณ , ์œ ์ง€๋ณด์ˆ˜๋„ ์‰ฌ์›Œ์ง€๊ณ , ๋ฆฌ๋ Œ๋”๋ง๋„ ์ตœ์†Œํ™”๋จ
  • ์˜ˆ์‹œ

    • ์‚ฌ์ด๋“œ๋ฐ” ์—ด๊ธฐ/๋‹ซ๊ธฐ
    • ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ƒํƒœ
    • ๋ชจ๋‹ฌ ์—ด๊ธฐ/๋‹ซ๊ธฐ
    • ํ•„ํ„ฐ๋ง ์กฐ๊ฑด ๊ด€๋ฆฌ

๐ŸŒŸ Redux๊ฐ€ ์ ํ•ฉํ•œ ์ƒํ™ฉ

  • ํ”„๋กœ์ ํŠธ๊ฐ€ ์ค‘~๋Œ€๊ทœ๋ชจ์ด๋ฉฐ ํŒ€ ์ž‘์—…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

  • ๋ช…ํ™•ํ•œ ์ƒํƒœ ํ๋ฆ„ ๊ด€๋ฆฌ๊ฐ€ ์ค‘์š”ํ•œ ๊ฒฝ์šฐ

  • ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ๋กœ์ง์ด ๋งŽ๊ฑฐ๋‚˜ ์ƒํƒœ ๊ฐ„ ์˜์กด์„ฑ์ด ํฐ ๊ฒฝ์šฐ

  • ๋””๋ฒ„๊น…, ์•ก์…˜ ์ถ”์ ์ด ์ค‘์š”ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ™˜๊ฒฝ

  • ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ฒŒ ์ถ”์ ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ

  • ์˜ˆ์‹œ

    • ์‡ผํ•‘๋ชฐ ์ฃผ๋ฌธ/๊ฒฐ์ œ ์ƒํƒœ ํ๋ฆ„

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      ์‡ผํ•‘๋ชฐ์—์„œ๋Š” ์ƒํ’ˆ์„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ณ  โ†’ ์ฃผ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  โ†’ ๊ฒฐ์ œ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  โ†’ ๊ฒฐ์ œ ์™„๋ฃŒ๊นŒ์ง€ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์นจ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      {
        cart: [...],
        order: {
          deliveryInfo: { name, address, phone },
          paymentMethod: '์นด๋“œ',
          orderStatus: '๊ฒฐ์ œ ๋Œ€๊ธฐ' | '๊ฒฐ์ œ ์™„๋ฃŒ' | '๋ฐฐ์†ก ์ค‘' | '๋ฐฐ์†ก ์™„๋ฃŒ',
        },
        ui: {
          step: 2, // ์ฃผ๋ฌธ์„œ ์ž‘์„ฑ ๋‹จ๊ณ„
        }
      }

      โœ… ์ด์œ 

      • ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๊ฐ€ ์ง๋ ฌํ™”๋œ ์ƒํƒœ ํ๋ฆ„์„ ๊ฐ€์ง€๋ฏ€๋กœ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ถ”์ ํ•˜๊ณ  ๋กค๋ฐฑํ•˜๊ธฐ์— ์ข‹์Œ
      • ๊ฒฐ์ œ ์‹คํŒจ ์‹œ ์ด์ „ ์ƒํƒœ๋กœ ๋ณต๊ตฌ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ
      • Redux DevTools๋กœ ์ƒํƒœ ์ถ”์  ๋ฐ ๋””๋ฒ„๊น… ์šฉ์ด
      • Redux Saga ๋“ฑ์œผ๋กœ ๋น„๋™๊ธฐ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ๋กœ์ง ์‰ฝ๊ฒŒ ๊ตฌ์„ฑ ๊ฐ€๋Šฅ
    • ์ธ์ฆ ํ† ํฐ + ๋ฆฌํ”„๋ ˆ์‹œ ๋กœ์ง

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      ๋กœ๊ทธ์ธ ํ›„์—๋Š” access token๊ณผ refresh token์„ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋ฉฐ, access token์ด ๋งŒ๋ฃŒ๋˜๋ฉด refresh token์œผ๋กœ ์ž๋™ ์žฌ๋ฐœ๊ธ‰ํ•ด์•ผ ํ•จ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      {
        auth: {
          accessToken: '...',
          refreshToken: '...',
          isAuthenticated: true,
          user: { id, email, role },
        }
      }

      โœ… ์ด์œ 

      • ํ† ํฐ ๊ฐฑ์‹ ์€ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ ํƒ€์ด๋ฐ ๊ด€๋ฆฌ๊ฐ€ ์ค‘์š”ํ•จ
      • axios interceptors๋‚˜ Redux middleware๋ฅผ ํ†ตํ•ด ์š”์ฒญ ์ „ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
      • isAuthenticated ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋ผ์šฐํŒ… ์ œ์–ด
      • Redux๋Š” ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ํ๋ฆ„๊ณผ ์ƒํƒœ ๊ณต์œ ์— ๊ฐ•ํ•จ
    • ์ฑ—๋ด‡ ๋Œ€ํ™” ์ƒํƒœ ์ถ”์ 

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      ์‚ฌ์šฉ์ž์™€ ์ฑ—๋ด‡์˜ ๋Œ€ํ™” ๋‚ด์—ญ์„ ์ €์žฅํ•˜๊ณ , ํ˜„์žฌ ๋Œ€ํ™” ์ƒํƒœ(๋‹ต๋ณ€ ์ค‘, ์™„๋ฃŒ ๋“ฑ)๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•จ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      {
        chat: {
          messages: [
            { id: 1, sender: 'user', text: '์•ˆ๋…•' },
            { id: 2, sender: 'bot', text: '์•ˆ๋…•ํ•˜์„ธ์š”!' },
          ],
          status: '๋‹ต๋ณ€ ์ค‘' | '์™„๋ฃŒ'
        }
      }

      โœ… ์ด์œ 

      • ์ƒํƒœ๊ฐ€ ์‹œ๊ฐ„ ์ˆœ์„œ๋Œ€๋กœ ์Œ“์ด๋ฉฐ, ๋ถˆ๋ณ€์„ฑ ์œ ์ง€๊ฐ€ ์ค‘์š”
      • ๋‹ค์–‘ํ•œ ๋ชจ๋“ˆ(์ž…๋ ฅ์ฐฝ, ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ, ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜)์ด ์ƒํƒœ์— ๋ฐ˜์‘ํ•ด์•ผ ํ•จ
      • ๋น„๋™๊ธฐ ์‘๋‹ต ๋Œ€๊ธฐ ์‹œ ๋กœ๋”ฉ ์ฒ˜๋ฆฌ ๋“ฑ ๋ณต์žกํ•œ ๋กœ์ง ํ•„์š”
      • ์˜ˆ: Redux Thunk๋กœ async ๋ฉ”์‹œ์ง€ ์ „์†ก ์ฒ˜๋ฆฌ โ†’ ๋ฉ”์‹œ์ง€ ๋„์ฐฉ ์‹œ ์ƒํƒœ ๊ฐฑ์‹ 
    • ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด ๋ช…ํ™•ํžˆ ์ •์˜๋ผ์•ผ ํ•˜๋Š” SaaS ์‹œ์Šคํ…œ

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      SaaS(Software as a Service)์˜ ์˜ˆ๋กœ๋Š” ๊ณ ๊ฐ ๊ด€๋ฆฌ CRM, ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌํˆด ๋“ฑ์ด ์žˆ์œผ๋ฉฐ, ์ด๋Ÿฌํ•œ ์‹œ์Šคํ…œ์—์„œ๋Š” ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž, ํŒ€, ๊ถŒํ•œ, ๋ฐ์ดํ„ฐ๊ฐ€ ์„œ๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Œ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      {
        user: { id, role, permissions },
        teams: [{ id, name, members: [...] }],
        currentProject: { id, tasks: [...], status: '์ง„ํ–‰ ์ค‘' },
        ui: { selectedTab: 'dashboard' }
      }

      โœ… ์ด์œ 

      • ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ๋ณต์žกํ•˜๊ณ  ๊ณ„์ธต์ ์ž„
      • ์—ญํ• /๊ถŒํ•œ๋ณ„ ๊ธฐ๋Šฅ์ด ๋‹ฌ๋ผ์ง€๋ฏ€๋กœ ์ƒํƒœ์— ๋”ฐ๋ผ UI ๋ณ€ํ™” ๋ฐœ์ƒํ•จ
      • ๋™๊ธฐํ™” ์ด์Šˆ ๋ฐœ์ƒ ์‹œ ์ •ํ™•ํ•œ ์ƒํƒœ ์Šค๋ƒ…์ƒท ์ถ”์ ์ด ํ•„์š”ํ•จ
      • ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ์ด ํ•˜๋‚˜์˜ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๋ฏ€๋กœ Redux์˜ ์ „์—ญ ์ƒํƒœ ํ†ตํ•ฉ ๊ด€๋ฆฌ๊ฐ€ ํšจ์œจ์ ์ž„

๐ŸŒŸ Redux Toolkit(RTK) ๋„์ž… ์‹œ์˜ ๋ณ€ํ™”

Redux๋Š” Redux Toolkit์„ ํ†ตํ•ด ๋งŽ์€ ๋‹จ์ ์„ ๊ฐœ์„ ํ•˜๊ณ  ์žˆ์Œ

  • createSlice, configureStore๋กœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ๊ฐ์†Œ
  • Immer ๊ธฐ๋ฐ˜ ๋ถˆ๋ณ€์„ฑ ์ž๋™ ์ฒ˜๋ฆฌ
  • TypeScript ์ง€์› ๊ฐ•ํ™”
  • createAsyncThunk๋กœ ๋น„๋™๊ธฐ ๋กœ์ง ๋‹จ์ˆœํ™”
const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => { state.count += 1 },
  }
})

Redux๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ๋ฌด์กฐ๊ฑด RTK๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋จ


๐ŸŒŸ Redux Toolkit์ด๋ž€?

Redux Toolkit์€ Redux์˜ ๊ณต์‹ ํˆดํ‚ท์œผ๋กœ, Redux๋ฅผ ๋” ๊ฐ„๋‹จํ•˜๊ณ  ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.
Redux์˜ ๋ถˆํŽธํ•œ ์ ๋“ค์„ ๋Œ€ํญ ๊ฐœ์„ ํ•ด์ฃผ๋ฉฐ, ์‚ฌ์‹ค์ƒ ํ˜„๋Œ€ Redux์˜ ํ‘œ์ค€ ๋ฐฉ์‹์œผ๋กœ ์ž๋ฆฌ์žก๊ณ  ์žˆ๋‹ค.

  • ๊ธฐ์กด Redux์˜ ๋‹จ์ 
    • ์•ก์…˜ ํƒ€์ž… ์ •์˜, ์•ก์…˜ ์ƒ์„ฑ์ž, ๋ฆฌ๋“€์„œ ๋”ฐ๋กœ ์ž‘์„ฑ โ†’ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ๋งŽ์Œ
    • ๋ถˆ๋ณ€์„ฑ ์œ ์ง€ ์ง์ ‘ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ
    • ๋ณต์žกํ•œ ์„ค์ • โ†’ ์ง„์ž… ์žฅ๋ฒฝ ๋†’์Œ
    โžก๏ธ ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋“ฑ์žฅํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ RTK
  • ํ•ต์‹ฌ ๊ธฐ๋Šฅ ์š”์•ฝ

    ๊ธฐ๋Šฅ์„ค๋ช…
    configureStorestore๋ฅผ ์‰ฝ๊ฒŒ ์„ค์ • + ๋ฏธ๋“ค์›จ์–ด ์ž๋™ ํฌํ•จ
    createSlice๋ฆฌ๋“€์„œ + ์•ก์…˜ ์ƒ์„ฑ์ž ์ž๋™ ์ƒ์„ฑ
    createAsyncThunk๋น„๋™๊ธฐ ๋กœ์ง ์ž‘์„ฑ ๊ฐ„ํŽธํ™”
    createReducerswitch๋ฌธ ์—†์ด ๋ฆฌ๋“€์„œ ์ž‘์„ฑ ๊ฐ€๋Šฅ
    createAction์•ก์…˜ ์ƒ์„ฑ์ž ๋‹จ๋… ์ƒ์„ฑ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
    RTK QueryAPI ์ƒํƒœ ๊ด€๋ฆฌ๊นŒ์ง€ ์ž๋™ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

๐ŸŒŸ Redux Toolkit ์˜ˆ์ œ

  • ๊ธฐ๋ณธ ๊ตฌ์กฐ ์˜ˆ์ œ

    • counterSlice.ts : Redux state + reducer + action ์ƒ์„ฑ์ž๋ฅผ ํ•œ ํŒŒ์ผ์—์„œ ํ†ตํ•ฉ ์ •์˜ํ•˜๋Š” ๊ณณ

      import { createSlice } from '@reduxjs/toolkit';
      
      const counterSlice = createSlice({
        name: 'counter',
        initialState: { count: 0 },
        reducers: {
          increment: (state) => {
            state.count += 1; // Immer ์‚ฌ์šฉ โ†’ ๋ถˆ๋ณ€์„ฑ ๊ฑฑ์ • X
          },
          decrement: (state) => {
            state.count -= 1;
          },
        },
      });
      
      export const { increment, decrement } = counterSlice.actions;
      export default counterSlice.reducer;
      • Redux์—์„œ ํ•„์š”ํ•œ ์•ก์…˜/๋ฆฌ๋“€์„œ๋ฅผ ๋ณ„๋„ ์ž‘์„ฑํ•  ํ•„์š” ์—†์Œ
      • ์ƒํƒœ ๊ด€๋ฆฌ ๋กœ์ง์ด ํ•˜๋‚˜์˜ ๊ธฐ๋Šฅ ๋‹จ์œ„๋กœ ์ž˜ ๋ฌถ์ž„
      ๊ตฌ์„ฑ ์š”์†Œ์„ค๋ช…
      createSlicestate, reducer, action์„ ํ•œ ๋ฒˆ์— ์ •์˜ํ•˜๋Š” Redux Toolkit์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ
      nameslice์˜ ์ด๋ฆ„. ์•ก์…˜ ํƒ€์ž…(counter/increment) ๋“ฑ์— ์‚ฌ์šฉ๋จ
      initialState์ดˆ๊ธฐ ์ƒํƒœ๊ฐ’
      reducers์•ก์…˜์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ƒํƒœ๋ฅผ ์–ด๋–ป๊ฒŒ ์—…๋ฐ์ดํŠธํ• ์ง€๋ฅผ ์ •์˜ํ•จ
      state.count += 1์›๋ž˜ Redux์—์„œ๋Š” ๋ถˆ๋ณ€์„ฑ ๋•Œ๋ฌธ์— ์Šคํ”„๋ ˆ๋“œ ์—ฐ์‚ฐ์ž๋ฅผ ์จ์•ผ ํ–ˆ์ง€๋งŒ, RTK๋Š” Immer๋ฅผ ๋‚ด์žฅํ•ด ์žˆ์–ด ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅ
      counterSlice.actions์ž๋™ ์ƒ์„ฑ๋œ ์•ก์…˜ ์ƒ์„ฑ์ž๋“ค(increment, decrement)
      counterSlice.reducerslice์—์„œ ์ •์˜๋œ ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋งŒ export (store์—์„œ ์‚ฌ์šฉ)
    • store.ts : Redux ์ค‘์•™ ์ €์žฅ์†Œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ตฌ์„ฑํ•˜๋Š” ๊ณณ

      import { configureStore } from '@reduxjs/toolkit';
      import counterReducer from './counterSlice';
      
      const store = configureStore({
        reducer: {
          counter: counterReducer,
        },
      });
      
      export default store;
      • ๋ชจ๋“  slice๋“ค์ด ํ•˜๋‚˜๋กœ ํ†ตํ•ฉ๋˜์–ด ์•ฑ ์ „์ฒด์—์„œ ์ƒํƒœ ๊ณต์œ  ๊ฐ€๋Šฅ
      • DevTools์™€ ๋ฏธ๋“ค์›จ์–ด(thunk ๋“ฑ) ์ž๋™ ์ ์šฉ๋จ
      ๊ตฌ์„ฑ ์š”์†Œ์„ค๋ช…
      configureStoreRedux Toolkit์—์„œ store๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜. createStore๋ณด๋‹ค ์„ค์ •์ด ๊ฐ„ํŽธํ•จ
      reducer: { counter: counterReducer }์—ฌ๋Ÿฌ slice ๋ฆฌ๋“€์„œ๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•˜๋Š” ์—ญํ•  (์—ฌ๊ธฐ์„  ํ•˜๋‚˜๋ฟ์ด์ง€๋งŒ ํ™•์žฅ ๊ฐ€๋Šฅ)
      counter ํ‚ค ์ด๋ฆ„state.counter๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋˜๋Š” ๋ถ€๋ถ„
      ๋ฏธ๋“ค์›จ์–ด ์ž๋™ ํฌํ•จRedux DevTools, thunk ๋“ฑ ๊ธฐ๋ณธ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์ž๋™์œผ๋กœ ์„ค์ •๋จ
    • Counter.tsx : ์ƒํƒœ๋ฅผ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๊ณ  ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๋Š” UI ์ปดํฌ๋„ŒํŠธ

      import { useSelector, useDispatch } from 'react-redux';
      import { increment } from './counterSlice';
      
      const Counter = () => {
        const count = useSelector((state: any) => state.counter.count);
        const dispatch = useDispatch();
        
        return (
          <div>
            <p>{count}</p>
            <button onClick={() => dispatch(increment())}>+1</button>
          </div>
        );
      };
      • ์‚ฌ์šฉ์ž๋Š” ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ count ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•จ
      • ์ƒํƒœ์— ๋”ฐ๋ผ UI๊ฐ€ ์ž๋™์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง๋จ
      ๊ตฌ์„ฑ ์š”์†Œ์„ค๋ช…
      useSelector()Redux์˜ store ์ƒํƒœ๋ฅผ ์กฐํšŒํ•จ. state.counter.count์—์„œ counter๋Š” store์—์„œ ๋“ฑ๋กํ•œ slice ์ด๋ฆ„
      useDispatch()์•ก์…˜์„ ๋ฐœ์ƒ์‹œ์ผœ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝ
      dispatch(increment())counterSlice์—์„œ ์ž๋™ ์ƒ์„ฑ๋œ ์•ก์…˜์„ ์‹คํ–‰ํ•จ โ†’ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋จ
      <p>{count}</p>store์˜ ์ƒํƒœ๋ฅผ UI์— ๋ฐ˜์˜

      ๐Ÿ’ก count์™€ increment()๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐฉ์‹์ด ์„œ๋กœ ๋‹ค๋ฅธ ์ด์œ 

      • count๋Š” ์ƒํƒœ ๊ฐ’(์ƒํƒœ)
        counterSlice.ts์—์„œ count๋ผ๋Š” ์ƒํƒœ๊ฐ’์„ ์ •์˜ํ•œ ๊ฒƒ์ž„
        // counterSlice.ts
        initialState: { count: 0 },
        ๊ทธ๋ฆฌ๊ณ  ์ด ๋ฆฌ๋“€์„œ๋ฅผ store.ts์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋“ฑ๋กํ•จ
        const store = configureStore({
          reducer: {
            counter: counterReducer,
          },
        });
        ์—ฌ๊ธฐ์„œ counter๋ผ๋Š” ํ‚ค๋กœ store์— ๋“ฑ๋กํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ „์ฒด store์˜ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋จ
        {
          counter: {
            count: 0
          }
        }
        ๐Ÿ‘‰ ๋”ฐ๋ผ์„œ useSelector((state) => state.counter.count)๋ผ๊ณ  ์จ์•ผ state.counter(ํ•ด๋‹น slice) ์•ˆ์˜ count ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ!
      • increment()๋Š” ์•ก์…˜ ์ƒ์„ฑ์ž(์•ก์…˜)
        ๊ทธ๋ž˜์„œ useSelector๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ๊ทธ๋ƒฅ import ํ•ด์„œ dispatch()๋กœ ์‹คํ–‰์‹œํ‚ค๋Š” ๊ฒƒ
        import { increment } from './counterSlice' // ์•ก์…˜ ์ƒ์„ฑ์ž
        ์ด๊ฑธ dispatch(increment())์ฒ˜๋Ÿผ ์“ฐ๋Š” ๊ฒŒ Redux์˜ ๊ธฐ๋ณธ ๋ฐฉ์‹์ž„
  • ๋น„๋™๊ธฐ ๋กœ์ง (createAsyncThunk)

    • userThunks.ts : ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์—์„œ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง์„ ์ •์˜ํ•˜๋Š” ํŒŒ์ผ

      import { createAsyncThunk } from '@reduxjs/toolkit'
      
      export const fetchUser = createAsyncThunk(
        'user/fetch',
        async (userId: string) => {
          const res = await fetch(`/api/users/${userId}`);
          return await res.json();
        };
      );
      ๊ตฌ์„ฑ ์š”์†Œ์„ค๋ช…
      'user/fetch'์•ก์…˜ ํƒ€์ž… ์ ‘๋‘์‚ฌ. ๋‚ด๋ถ€์ ์œผ๋กœ user/fetch/pending, user/fetch/fulfilled, user/fetch/rejected๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋จ
      async function์‹ค์ œ API ์š”์ฒญ ๋กœ์ง. userId๋ฅผ ๋ฐ›์•„์„œ fetch ์š”์ฒญํ•˜๊ณ , JSON์œผ๋กœ ์‘๋‹ต ๋ฐ˜ํ™˜
      ๋ฐ˜ํ™˜๊ฐ’fulfilled ์ƒํƒœ์—์„œ action.payload๋กœ ์ „๋‹ฌ๋จ

      createAsyncThunk๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ 3๊ฐ€์ง€ ์•ก์…˜์„ ์ž๋™ ์ƒ์„ฑํ•จ

      • fetchUser.pending
      • fetchUser.fulfilled
      • fetchUser.rejected
    • userSlice.ts : ์‚ฌ์šฉ์ž ๊ด€๋ จ ์ƒํƒœ์™€ ์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง(Reducer)์„ ์ •์˜ํ•˜๋Š” Redux slice ํŒŒ์ผ

      import { createSlice } from '@reduxjs/toolkit'
      import { fetchUser } from './userThunks'
      
      const userSlice = createSlice({
        name: 'user',
        initialState: { user: null, status: 'idle' },
        reducers: {},
        extraReducers: (builder) => {
          .addCase(fetchUser.pending, (state) => {
            state.status = 'loading';
          })
          .addCase(fetchUser.fulfilled, (state, action) => {
            state.user = action.payload;
            state.status = 'succeeded';
          })
          .addCase(fetchUser.rejected, (state) => {
            state.status = 'failed';
          })
        },
      });
      
      export default userSlice.reducer
      ๊ตฌ์„ฑ ์š”์†Œ์„ค๋ช…
      initialStateuser: ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
      status: ์š”์ฒญ ์ƒํƒœ ๊ด€๋ฆฌ(idle, loading, succeeded, failed)
      reducers: {}๋™๊ธฐ ์•ก์…˜์€ ์—†์œผ๋ฏ€๋กœ ๋น„์›Œ๋‘ 
      extraReducers๋น„๋™๊ธฐ ์•ก์…˜์— ๋Œ€ํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง ์ •์˜
      builder๋ฅผ ํ†ตํ•ด ๊ฐ๊ฐ์˜ ์•ก์…˜ ์ƒํƒœ ์ฒ˜๋ฆฌ

      ๐Ÿ” extraReducers ๋‚ด๋ถ€ ํ๋ฆ„

      ์•ก์…˜ ํƒ€์ž…์„ค๋ช…์ƒํƒœ ๋ณ€ํ™”
      fetchUser.pendingAPI ์š”์ฒญ ์‹œ์ž‘ ์ „status = 'loading'
      fetchUser.fulfilled์š”์ฒญ ์„ฑ๊ณต, ์‘๋‹ต ๋„์ฐฉstatus = 'succeeded', 'user = ์‘๋‹ต๊ฐ’'
      fetchUser.rejected์š”์ฒญ ์‹คํŒจstatus = 'failed'

๐ŸŒŸ RTK๊ฐ€ ๊ฐ€์ ธ๋‹ค์ฃผ๋Š” ์žฅ์ 

  1. ๋ถˆ๋ณ€์„ฑ ์ž๋™ ์ฒ˜๋ฆฌ (Immer ๋‚ด์žฅ)

    • state.count += 1์ฒ˜๋Ÿผ ์ž‘์„ฑํ•ด๋„ ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ...spread๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋จ
  2. ์ฝ”๋“œ ์–‘ ์ตœ์†Œํ™”

    • ์•ก์…˜/ํƒ€์ž…/๋ฆฌ๋“€์„œ๊ฐ€ ํ•˜๋‚˜์˜ slice๋กœ ํ†ตํ•ฉ
  3. ๊ธฐ๋ณธ ๋ฏธ๋“ค์›จ์–ด ์ž๋™ ํฌํ•จ

    • Redux DevTools
    • serializableCheck
    • thunk
  4. ๋น„๋™๊ธฐ ์š”์ฒญ ๊ฐ„ํŽธํ™” (createAsyncThunk)

    • ์„œ๋ฒ„ API ํ˜ธ์ถœ ํ๋ฆ„์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์Œ

๐ŸŒŸ Redux Toolkit vs. ๊ธฐ์กด Redux

ํ•ญ๋ชฉ๊ธฐ์กด ReduxRedux Toolkit
์ฝ”๋“œ ๊ธธ์ด๊ธธ๊ณ  ๋ณต์žก์งง๊ณ  ๊ฐ„๊ฒฐ
๋ถˆ๋ณ€์„ฑ ์œ ์ง€์ง์ ‘ ์ฒ˜๋ฆฌImmer๋กœ ์ž๋™
๋น„๋™๊ธฐ ์ฒ˜๋ฆฌThunk/Saga ์„ค์ • ํ•„์š”createAsyncThunk ์ œ๊ณต
DevTools ์„ค์ •์ˆ˜๋™์ž๋™ ํฌํ•จ
๊ตฌ์กฐ ์„ค๊ณ„์ˆ˜์ž‘์—…์œผ๋กœ ๊ตฌ์„ฑcreateSlice๋กœ ์ž๋™ํ™” ๊ฐ€๋Šฅ

๐ŸŒŸ Redux Toolkit์ด ์ ํ•ฉํ•œ ์ƒํ™ฉ

  • ์—ฌ๋Ÿฌ ์ƒํƒœ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ค‘~๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ

  • ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ(API ์š”์ฒญ, ๋กœ๋”ฉ ๋“ฑ)

  • ํŒ€ ๋‹จ์œ„ ํ˜‘์—… ํ”„๋กœ์ ํŠธ

  • Redux์˜ ๊ตฌ์กฐํ™”๋œ ํŒจํ„ด์€ ์œ ์ง€ํ•˜๋˜, ์ƒ์‚ฐ์„ฑ์„ ๋†’์ด๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ

  • ์˜ˆ์‹œ

    • ์‡ผํ•‘๋ชฐ ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ๋‹จ๊ณ„ (์žฅ๋ฐ”๊ตฌ๋‹ˆ โ†’ ๊ฒฐ์ œ โ†’ ์™„๋ฃŒ)

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ์„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ณ , ๊ฒฐ์ œ ํŽ˜์ด์ง€๋ฅผ ๊ฑฐ์ณ ์ฃผ๋ฌธ ์™„๋ฃŒ๊นŒ์ง€ ์ง„ํ–‰ํ•˜๋Š” ๋‹ค๋‹จ๊ณ„ ํ”„๋กœ์„ธ์Šค์—์„œ๋Š” ๊ฐ ๋‹จ๊ณ„๋ณ„ ์ƒํƒœ(ํ˜„์žฌ ๋‹จ๊ณ„, ์ฃผ๋ฌธ ํ•ญ๋ชฉ ๋“ฑ)๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•ด์•ผ ํ•จ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      import { createSlice } from '@reduxjs/toolkit'
       const orderSlice = createSlice({
         name: 'order',
         initialState: {
           step: 1,
           status: 'idle',
           items: [],
         },
         reducers: {
           nextStep: (state) => { state.step += 1 },
           addItem: (state, action) => { state.items.push(action.payload) },
           resetOrer: () => ({ step: 1, status: 'idle', items: [] }),
         },
       });
       export const { nextStep, addItem, resetOrder } = orderSlice.actions
       export default orderSlice.reducer

      โœ… ์ด์œ 

      • ์ƒํƒœ๊ฐ€ ์ ์ง„์ ์œผ๋กœ ์ „ํ™˜๋˜๋ฉฐ, ๊ฐ ๋‹จ๊ณ„๋งˆ๋‹ค ์ƒํƒœ ๊ฐ’์ด ๋ณ€๊ฒฝ๋จ
      • ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ณ , RTK์˜ createSlice๋กœ ํ•œ ํŒŒ์ผ์—์„œ ์ƒํƒœ/์•ก์…˜/๋กœ์ง์„ ํ†ตํ•ฉํ•ด์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด ํŽธ๋ฆฌํ•จ
      • Immer๊ฐ€ ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด์„œ ๋ถˆ๋ณ€์„ฑ ์œ ์ง€ ๊ฑฑ์ • ์—†์ด ์ƒํƒœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ
    • ์œ ์ € ์ •๋ณด API ํ˜ธ์ถœ (loading/success/fail ์ƒํƒœ ์ถ”์ )

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋น„๋™๊ธฐ API ์š”์ฒญ์€ ์š”์ฒญ ์ค‘(loading), ์„ฑ๊ณต(fulfilled), ์‹คํŒจ(rejected)์˜ ์—ฌ๋Ÿฌ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•จ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
       export const fetchUser = createAsyncThunk(
        'user/fetch',
        async (id) => {
          const res = await fetch(`/api/users/${id}`);
          return await res.json();
        }
      );
      const userSlice = createSlice({
        name: 'user',
        initialState: { user: null, status: 'idle' },
        reducers: {},
        extraReducers: (builder) => {
          builder
            .addCase(fetchUser.pending, (state) => { state.status = 'loading' })
            .addCase(fetchUser.fulfilled, (state, action) => {
            state.user = action.payload;
            state.status = 'succeeded';
          })
            .addCase(fetchUser.rejected, (state) => { state.status = 'failed' })
        },
      });
      export default userSlice.reducer

      โœ… ์ด์œ 

      • createAsyncThunk๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋น„๋™๊ธฐ ์š”์ฒญ์„ 3๋‹จ๊ณ„ ์ƒํƒœ๋กœ ์ž๋™ ๋ถ„๋ฆฌํ•ด์„œ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ
      • extraReducers์—์„œ ์š”์ฒญ ํ๋ฆ„์— ๋”ฐ๋ผ ์ƒํƒœ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ๋‚˜๋ˆ ์„œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
      • ๋น„๋™๊ธฐ ๋กœ์ง๊ณผ UI ์ƒํƒœ๋ฅผ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
    • ์‚ฌ์šฉ์ž ์ž…๋ ฅ ํผ ์ƒํƒœ ์ฒ˜๋ฆฌ + ์ œ์ถœ ์ฒ˜๋ฆฌ

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      • ์ž…๋ ฅ์ฐฝ์ด ์—ฌ๋Ÿฌ ๊ฐœ ์žˆ๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ํผ์„ ์ž…๋ ฅํ•˜๊ณ  ์ œ์ถœํ•˜๋Š” ๊ธฐ๋Šฅ์€ ์ƒํƒœ๋ฅผ ์ปดํฌ๋„ŒํŠธ์—๋งŒ ๋‘๊ธฐ์—” ๋„ˆ๋ฌด ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์Œ
      • ์ œ์ถœ ์—ฌ๋ถ€, ์—๋Ÿฌ ์ƒํƒœ ๋“ฑ๋„ ๊ด€๋ฆฌ ๋Œ€์ƒ์ด ๋จ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      import { createSlice } from '@reduxjs/toolkit'
       const formSlice = createSlice({
        name: 'form',
        initialState: {
          name: '',
          email: '',
          submitted: false,
        },
        reducers: {
          setName: (state, action) => { state.name = action.payload },
          setEmail: (state, action) => { state.email = action.payload },
          submitForm: (state) => { state.submitted = true }
        }
      })
      export const { setName, setEmail, submitForm } = formSlice.actions
      export default formSlice.reducer

      โœ… ์ด์œ 

      • ์ƒํƒœ๋ฅผ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•จ์œผ๋กœ์จ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ผ๊ด€๋˜๊ฒŒ ํผ ๊ฐ’์„ ๊ณต์œ  ๊ฐ€๋Šฅ
      • RTK์˜ createSlice๋ฅผ ํ†ตํ•ด ์ž…๋ ฅ๊ฐ’ ์„ค์ •(setName, setEmail)๊ณผ ์ œ์ถœ ์ƒํƒœ ์ฒ˜๋ฆฌ(submitForm)๋ฅผ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ
      • ์ƒํƒœ ๋ณ€๊ฒฝ์ด ๊ฐ„๋‹จํ•œ ๋กœ์ง์ผ ๋•Œ RTK์˜ ๊ฐ„๊ฒฐํ•œ ๋ฌธ๋ฒ•์ด ์œ ๋ฆฌ
    • UI ์ƒํƒœ, ์•Œ๋ฆผ ์ƒํƒœ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ๋ชจ๋“ˆ ๋‹จ์œ„๋กœ ๊ตฌ๋ถ„

      โœ… ์ƒํ™ฉ ์„ค๋ช…

      • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์œ„ํ•œ UI ์š”์†Œ(๋ชจ๋‹ฌ, ํ…Œ๋งˆ, ์•Œ๋ฆผ ๋“ฑ)๋Š” ์ „์—ญ ์ƒํƒœ๋กœ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ณต์œ ๋˜์–ด์•ผ ํ•จ
      • ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ชจ๋‹ฌ์€ ์–ด๋””์„œ๋“  ์—ด๊ณ  ๋‹ซ์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ

      โœ… ์ƒํƒœ ์˜ˆ์‹œ

      import { createSlice } from '@reduxjs/toolkit'
       const uiSlice = createSlice({
        name: 'ui',
        initialState: {
          isModalOpen: false,
          theme: 'light',
        },
        reducers: {
          toggleModal: (state) => { state.isModalOpen = !state.isModalOpen },
          setTheme: (state, action) => { state.theme = action.payload },
        },
      });
      export const { toggleModal, setTheme } = uiSlice.actions
      export default uiSlice.reducer

      โœ… ์ด์œ 

      • ๋‹จ์ˆœํ•˜๊ณ  ๋ฐ˜๋ณต์ ์ธ ์ƒํƒœ ๋กœ์ง์„ createSlice๋กœ ๊ฐ„๋‹จํžˆ ์ž‘์„ฑ ๊ฐ€๋Šฅ
      • RTK๋ฅผ ํ†ตํ•ด ์ „์—ญ UI ์ƒํƒœ๋„ ๋ถˆ๋ณ€์„ฑ ๊ฑฑ์ • ์—†์ด ๊ด€๋ฆฌ
      • UI ์ƒํƒœ๊ฐ€ ๋‹ค์–‘ํ•œ ์ปดํฌ๋„ŒํŠธ์™€ ์—ฐ๊ฒฐ๋  ์ˆ˜ ์žˆ์–ด ์ค‘์•™ ์ง‘์ค‘์‹ ๊ด€๋ฆฌ๊ฐ€ ํŽธ๋ฆฌ

๐ŸŒŸ ๊ฒฐ๋ก : ์–ด๋–ค ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ• ๊นŒ?

์ƒํ™ฉ์ถ”์ฒœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
๋น ๋ฅธ ๊ฐœ๋ฐœ, ๋‹จ์ˆœ ์ƒํƒœ, ์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธZustand
ํŒ€ ํ”„๋กœ์ ํŠธ, ๊ตฌ์กฐํ™”๋œ ๊ด€๋ฆฌ, ๋””๋ฒ„๊น… ์ค‘์š”Redux (RTK)
๋ณต์žกํ•œ ๋น„๋™๊ธฐ ํ๋ฆ„๊ณผ ๊ณ ๊ธ‰ ๋ฏธ๋“ค์›จ์–ด ํ•„์š”Redux + Thunk/Saga
์ƒํƒœ ๊ตฌ๋… ์ตœ์ ํ™”, ๋ชจ๋“ˆ ๋‹จ์œ„ ์ƒํƒœ ๋ถ„๋ฆฌ ํ•„์š”Zustand

0๊ฐœ์˜ ๋Œ“๊ธ€