๋ฆฌ์กํธ ์ ํ๋ฆฌ์ผ์ด์
์ด ์ ์ ์ปค์ง๋ฉด์ ์ํ ๊ด๋ฆฌ(State Management)๋ ํ์๊ฐ ๋์๋ค.
๋ํ์ ์ธ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ Zustand๊ณผ Redux๋ ๊ฐ์ ๋ชฉ์ ์ ์ง๋์ง๋ง ์ฌ์ฉ์ฑ, ์ฒ ํ, ๊ท๋ชจ ๋ฉด์์ ๋งค์ฐ ๋ค๋ฅด๋ค.
์ด ๊ธ์์๋ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊น์ด ์๊ฒ ๋น๊ตํ๊ณ , ๊ฐ๊ฐ์ ์ฅ๋จ์ ๊ณผ ์ฌ์ฉ ์ ํฉ ์ํฉ์ ์๊ฐํ ๊ฒ์ด๋ค.
Zustand๋ ๋ ์ผ์ด๋ก ์ํ(state)๋ผ๋ ๋ป์ผ๋ก, ๊ฐ๋ณ๊ณ ์ง๊ด์ ์ธ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
ํน์ง
์์
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๋ ํ์ด์ค๋ถ์ Flux ์ํคํ ์ฒ์์ ์๊ฐ์ ๋ฐ์ ๋ง๋ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ์์ธก ๊ฐ๋ฅํ ์ํ ๊ด๋ฆฌ(Predictable State Container)๋ฅผ ๋ชจํ ๋ก ํ๋ค.
ํน์ง
์์
actions.tsexport const increase = () => ({ type: 'INCREASE' })
- ์ํ๋ฅผ ๋ณ๊ฒฝํ๊ธฐ ์ํ ์ก์ ์์ฑ ํจ์(Action Creator)
- ์ก์ ๊ฐ์ฒด
{ type: 'INCREASE' }๋ฅผ ๋ฐํํจ- ์ปดํฌ๋ํธ์์
dispatch(increase())๋ก ํธ์ถํจ
reducer.tsconst 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.tsimport { createStore } from 'redux'
export const store = createStore(counterReducer);
- Redux์ ์ ์ญ ์ํ ์ ์ฅ์(Store)๋ฅผ ์์ฑํจ
createStore์ ๋ฆฌ๋์๋ฅผ ๋๊ฒจ store๋ฅผ ์์ฑํจ- React ์ฑ ์ต์๋จ์
Provider๋ก ์ด store๋ฅผ ์ฃผ์ ํด์ผ ํจ
Component.tsxconst 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 | Redux |
|---|---|---|
| ์ํ ์ ์ ์์น | store.ts ํ ๊ณณ์์ ๋ชจ๋ ์ ์ | actions, reducers, store๋ก ๋ถ๋ฆฌ |
| ์ํ ์ ๋ฐ์ดํธ | set() ํจ์ ์ฌ์ฉ | dispatch() + reducer |
| ๊ตฌ์กฐ | ๋จ์ํ๊ณ ์ง๊ด์ | ๋ช ํํ์ง๋ง ๊ตฌ์กฐ๊ฐ ๋ณต์กํจ |
| ๋ฌ๋ ์ปค๋ธ | ๋ฎ์ | ์ค๊ฐ~๋์ |
| ์ปดํฌ๋ํธ ์ฝ๋ | ๊ฐ๊ฒฐํจ | ์ฝ๊ฐ ์ฅํฉํจ(์ก์ , dispatch ํ์) |
| ๋๋ฒ๊น ์ง์ | ๊ธฐ๋ณธ ์ง์ + Devtools (middleware) | ๋งค์ฐ ๊ฐ๋ ฅํ Redux DevTools ์ง์ |
| ๋ฏธ๋ค์จ์ด | ๊ฐ๋จํ ์ถ๊ฐ ๊ฐ๋ฅ (persist ๋ฑ) | ํ๋ถํ๊ณ ์ ์ฐ (thunk, saga ๋ฑ) |
| ํ์ ์คํฌ๋ฆฝํธ ์ง์ | ๋งค์ฐ ์ฐ์ | ๊ธฐ๋ณธ์ ์๋ ์ค์ ํ์, RTK ์ฌ์ฉ ์ ๊ฐ์ |
| ์ฑ๋ฅ ์ต์ ํ | ์์ฒด subscribe๋ก ์๋ ์ต์ ํ | useSelector ์ต์ ํ ํ์ |
| ๋ง์ดํฌ๋ก ์ํ ๊ตฌ๋ | ์ง์ (ํ์ํ ๊ฐ๋ง ๊ตฌ๋ ) | ๊ฐ๋ฅํ๋ ๋ณต์ก (memoization ํ์) |
| SSR ์ง์ | ๊ธฐ๋ณธ ์ง์ | ๋ณ๋ ์ฒ๋ฆฌ ํ์ |
๋น ๋ฅด๊ฒ ๊ฐ๋ฐํด์ผ ํ ํ๋ก์ ํธ
๋น๊ต์ ๊ท๋ชจ๊ฐ ์์ ์ ํ๋ฆฌ์ผ์ด์
์ํ ๋ก์ง์ด ๋ณต์กํ์ง ์๊ฑฐ๋ ๋จ์ 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๋ฅผ ๋๋๋ฉด ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ๊ฐ ์ ๋๊ณ , ์ ์ง๋ณด์๋ ์ฌ์์ง๊ณ , ๋ฆฌ๋ ๋๋ง๋ ์ต์ํ๋จ
์์
ํ๋ก์ ํธ๊ฐ ์ค~๋๊ท๋ชจ์ด๋ฉฐ ํ ์์ ์ด ํ์ํ ๊ฒฝ์ฐ
๋ช ํํ ์ํ ํ๋ฆ ๊ด๋ฆฌ๊ฐ ์ค์ํ ๊ฒฝ์ฐ
๋ณต์กํ ๋น๋๊ธฐ ๋ก์ง์ด ๋ง๊ฑฐ๋ ์ํ ๊ฐ ์์กด์ฑ์ด ํฐ ๊ฒฝ์ฐ
๋๋ฒ๊น , ์ก์ ์ถ์ ์ด ์ค์ํ ์ํฐํ๋ผ์ด์ฆ ํ๊ฒฝ
์ํ ๋ณ๊ฒฝ์ ์์ธก ๊ฐ๋ฅํ๊ฒ ์ถ์ ํ๊ณ ์ถ์ ๊ฒฝ์ฐ
์์
์ผํ๋ชฐ ์ฃผ๋ฌธ/๊ฒฐ์ ์ํ ํ๋ฆ
โ ์ํฉ ์ค๋ช
์ผํ๋ชฐ์์๋ ์ํ์ ์ฅ๋ฐ๊ตฌ๋์ ๋ด๊ณ โ ์ฃผ๋ฌธ์๋ฅผ ์์ฑํ๊ณ โ ๊ฒฐ์ ๋ฅผ ์งํํ๊ณ โ ๊ฒฐ์ ์๋ฃ๊น์ง ์ฌ๋ฌ ๋จ๊ณ๋ฅผ ๊ฑฐ์นจ
โ ์ํ ์์
{ 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๋ Redux Toolkit์ ํตํด ๋ง์ ๋จ์ ์ ๊ฐ์ ํ๊ณ ์์
createSlice, configureStore๋ก ๋ณด์ผ๋ฌํ๋ ์ดํธ ๊ฐ์createAsyncThunk๋ก ๋น๋๊ธฐ ๋ก์ง ๋จ์ํconst counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => { state.count += 1 },
}
})
Redux๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ๋ฌด์กฐ๊ฑด RTK๋ก ์์ํ๋ ๊ฒ์ด ๊ถ์ฅ๋จ
Redux Toolkit์ Redux์ ๊ณต์ ํดํท์ผ๋ก, Redux๋ฅผ ๋ ๊ฐ๋จํ๊ณ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
Redux์ ๋ถํธํ ์ ๋ค์ ๋ํญ ๊ฐ์ ํด์ฃผ๋ฉฐ, ์ฌ์ค์ ํ๋ Redux์ ํ์ค ๋ฐฉ์์ผ๋ก ์๋ฆฌ์ก๊ณ ์๋ค.
ํต์ฌ ๊ธฐ๋ฅ ์์ฝ
| ๊ธฐ๋ฅ | ์ค๋ช |
|---|---|
configureStore | store๋ฅผ ์ฝ๊ฒ ์ค์ + ๋ฏธ๋ค์จ์ด ์๋ ํฌํจ |
createSlice | ๋ฆฌ๋์ + ์ก์ ์์ฑ์ ์๋ ์์ฑ |
createAsyncThunk | ๋น๋๊ธฐ ๋ก์ง ์์ฑ ๊ฐํธํ |
createReducer | switch๋ฌธ ์์ด ๋ฆฌ๋์ ์์ฑ ๊ฐ๋ฅ |
createAction | ์ก์ ์์ฑ์ ๋จ๋ ์์ฑ (ํ์ํ ๊ฒฝ์ฐ) |
RTK Query | API ์ํ ๊ด๋ฆฌ๊น์ง ์๋ ์ฒ๋ฆฌํ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ |
๊ธฐ๋ณธ ๊ตฌ์กฐ ์์
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์์ ํ์ํ ์ก์ /๋ฆฌ๋์๋ฅผ ๋ณ๋ ์์ฑํ ํ์ ์์
- ์ํ ๊ด๋ฆฌ ๋ก์ง์ด ํ๋์ ๊ธฐ๋ฅ ๋จ์๋ก ์ ๋ฌถ์
| ๊ตฌ์ฑ ์์ | ์ค๋ช |
|---|---|
createSlice | state, reducer, action์ ํ ๋ฒ์ ์ ์ํ๋ Redux Toolkit์ ํต์ฌ ๊ธฐ๋ฅ |
name | slice์ ์ด๋ฆ. ์ก์ ํ์ (counter/increment) ๋ฑ์ ์ฌ์ฉ๋จ |
initialState | ์ด๊ธฐ ์ํ๊ฐ |
reducers | ์ก์ ์ด ๋ฐ์ํ์ ๋ ์ํ๋ฅผ ์ด๋ป๊ฒ ์ ๋ฐ์ดํธํ ์ง๋ฅผ ์ ์ํจ |
state.count += 1 | ์๋ Redux์์๋ ๋ถ๋ณ์ฑ ๋๋ฌธ์ ์คํ๋ ๋ ์ฐ์ฐ์๋ฅผ ์จ์ผ ํ์ง๋ง, RTK๋ Immer๋ฅผ ๋ด์ฅํด ์์ด ์ง์ ์์ ๊ฐ๋ฅ |
counterSlice.actions | ์๋ ์์ฑ๋ ์ก์
์์ฑ์๋ค(increment, decrement) |
counterSlice.reducer | slice์์ ์ ์๋ ๋ฆฌ๋์ ํจ์๋ง 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 ๋ฑ) ์๋ ์ ์ฉ๋จ
| ๊ตฌ์ฑ ์์ | ์ค๋ช |
|---|---|
configureStore | Redux 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.pendingfetchUser.fulfilledfetchUser.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
| ๊ตฌ์ฑ ์์ | ์ค๋ช |
|---|---|
initialState | user: ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅstatus: ์์ฒญ ์ํ ๊ด๋ฆฌ(idle, loading, succeeded, failed) |
reducers: {} | ๋๊ธฐ ์ก์ ์ ์์ผ๋ฏ๋ก ๋น์๋ |
extraReducers | ๋น๋๊ธฐ ์ก์
์ ๋ํ ์ํ ๋ณ๊ฒฝ ๋ก์ง ์ ์ builder๋ฅผ ํตํด ๊ฐ๊ฐ์ ์ก์ ์ํ ์ฒ๋ฆฌ |
๐ extraReducers ๋ด๋ถ ํ๋ฆ
์ก์ ํ์ ์ค๋ช ์ํ ๋ณํ fetchUser.pendingAPI ์์ฒญ ์์ ์ status = 'loading'fetchUser.fulfilled์์ฒญ ์ฑ๊ณต, ์๋ต ๋์ฐฉ status = 'succeeded', 'user = ์๋ต๊ฐ'fetchUser.rejected์์ฒญ ์คํจ status = 'failed'
๋ถ๋ณ์ฑ ์๋ ์ฒ๋ฆฌ (Immer ๋ด์ฅ)
state.count += 1์ฒ๋ผ ์์ฑํด๋ ๋ด๋ถ์ ์ผ๋ก๋ ...spread๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌ๋จ์ฝ๋ ์ ์ต์ํ
๊ธฐ๋ณธ ๋ฏธ๋ค์จ์ด ์๋ ํฌํจ
๋น๋๊ธฐ ์์ฒญ ๊ฐํธํ (createAsyncThunk)
| ํญ๋ชฉ | ๊ธฐ์กด Redux | Redux Toolkit |
|---|---|---|
| ์ฝ๋ ๊ธธ์ด | ๊ธธ๊ณ ๋ณต์ก | ์งง๊ณ ๊ฐ๊ฒฐ |
| ๋ถ๋ณ์ฑ ์ ์ง | ์ง์ ์ฒ๋ฆฌ | Immer๋ก ์๋ |
| ๋น๋๊ธฐ ์ฒ๋ฆฌ | Thunk/Saga ์ค์ ํ์ | createAsyncThunk ์ ๊ณต |
| DevTools ์ค์ | ์๋ | ์๋ ํฌํจ |
| ๊ตฌ์กฐ ์ค๊ณ | ์์์ ์ผ๋ก ๊ตฌ์ฑ | createSlice๋ก ์๋ํ ๊ฐ๋ฅ |
์ฌ๋ฌ ์ํ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํ ์ค~๋๊ท๋ชจ ํ๋ก์ ํธ
๋ณต์กํ ๋น๋๊ธฐ ์ฒ๋ฆฌ(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 |