๐งฉ 23. useReducer & ์ํ ๋จธ์ : ๋ณต์กํ ์ํ ๋ก์ง์ ํด๋ฐฉ
๐ ๊ฐ์
isSaving๊ณผ isSaved๊ฐ ๋์์ true๊ฐ ๋๋ ๋ฒ๊ทธ๋ ์ ์๊ธฐ๋๊ฐ? useReducer์ ์ ํ ์ํ ๋จธ์ (FSM) ์ฌ๊ณ ๋ฐฉ์์ผ๋ก '๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ'์ ์ฝ๋ ๊ตฌ์กฐ ์์ฒด๋ก ์ฐจ๋จํ๋ ๋ฒ์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ์ ์์์ผ ํ๋๊ฐ: ์ฐ๊ด ์ํ์ ๋ถ์ด
- ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- useReducer ํต์ฌ ๊ตฌ์กฐ
- useState vs useReducer ์ ํ ๊ธฐ์ค
- ์ ํ ์ํ ๋จธ์ ์ฌ๊ณ ๋ฐฉ์
- TypeScript๋ก ํ์ ์์ ํ Reducer
- useContext + useReducer ์กฐํฉ
- ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[useState ํ๊ณ ์ธ์] โ [useReducer ๊ตฌ์กฐ ์ดํด] โ [FSM ์ฌ๊ณ ๋ฐฉ์] โ [TypeScript ์ ์ฉ] โ [useContext ์กฐํฉ]
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- "
isSaving && isSaved๊ฐ ๋์์ true๊ฐ ๋๋ ๋ฒ๊ทธ"๊ฐ ์ ์๊ธฐ๋์ง ์ค๋ช ํ ์ ์๋ค -
useReducer๋ก ๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ์ ์ฐจ๋จํ๋ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค -
useContext + useReducer์กฐํฉ์ผ๋ก ๊ฒฝ๋ ์ ์ญ ์ํ๋ฅผ ๋ง๋ค ์ ์๋ค
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์ ์ ): "๊ฒ์๊ธ ์ ์ฅ ๊ธฐ๋ฅ ๋ง๋ค์๋๋ฐ์... '์ ์ฅ ์ค' ํ์๊ฐ ๋ฌ ์ํ์์ ์๋ฌ๊ฐ ๋๋ฉด '์ ์ฅ ์ค'์ด ์์ํ ์ฌ๋ผ์ง์ง ์์์. ๊ทธ๋ฆฌ๊ณ ๊ฐ๋ '์ ์ฅ๋จ' ์ด๋ '์ค๋ฅ ๋ฐ์'์ด ๋์์ ํ๋ฉด์ ๋ ์ ใ ใ "
- ์ํธ(๋ฆฌ๋): "์์ฒ ๋,
isSaving / isSaved / isError์ธ ๊ฐ๋ฅผ ๋ฐ๋ก ๊ด๋ฆฌํ๋ ์๊ฐ ๋ถ๊ฐ๋ฅํ ์กฐํฉ์ด ํ์ํฉ๋๋ค. ์ํ๋ '๊ฐ๋ค์ ๋ฌถ์'์ด ์๋๋ผ 'ํ์ฌ ์ด๋ค ๋จ๊ณ์ธ๊ฐ'๋ก ์๊ฐํด์ผ ํด์. ์ค๋ ์ ํ ์ํ ๋จธ์ (FSM)์ ๋ฐฐ์๋ด ์๋ค."
๐ค ์ ์์์ผ ํ๋๊ฐ: ์ฐ๊ด ์ํ์ ๋ถ์ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ฌ๋ฌ ๊ฐ์
useState๊ฐ ์ ๋๊ธฐํ ๋ฒ๊ทธ๋ฅผ ๋ง๋๋์ง ์ค๋ช ํ ์ ์๋ค- "๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ"์ด ๋ฌด์์ธ์ง ์ดํดํ ์ ์๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
isSaving,isSaved,isError์ธ ๊ฐ๋ฅผ ๋ถ๋ฆฌ์ธ์ผ๋ก ๋ฐ๋ก ๊ด๋ฆฌํ๋ฉด ์ด ๋ช ๊ฐ์ง ์ํ ์กฐํฉ์ด ๊ฐ๋ฅํ ๊น?
๊ทธ ์ค ์ค์ ๋ก ์๋ฏธ ์๋ ์กฐํฉ์ ๋ช ๊ฐ์ผ๊น?
์์ฒ ์ด์ ๊ฒ์๊ธ ์ ์ฅ ์ปดํฌ๋ํธ (์ฌ์์ ์์):
// โ ์ฐ๊ด ์ํ๋ฅผ ๊ฐ๊ฐ useState๋ก ๊ด๋ฆฌ โ ๋๊ธฐํ ๋ฒ๊ทธ ์ํํญํ
function PostEditor() {
const [content, setContent] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [isSaved, setIsSaved] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const handleSave = async () => {
setIsSaving(true);
setIsSaved(false); // ๐ฃ ๊น๋นก ์์ผ๋ฉด ์ด์ "์ ์ฅ๋จ"์ด ๋จ์์์
setIsError(false); // ๐ฃ ๊น๋นก ์์ผ๋ฉด ์ด์ ์๋ฌ๊ฐ ํ๋ฉด์ ๋จ์
setErrorMessage(null);
try {
await savePost(content);
setIsSaved(true);
setIsSaving(false); // ๐ฃ ์ ์ค์์ ์์ธ ๋๋ฉด ์ด ์ค ์คํ ์ ๋จ โ isSaving ์์ํ true
} catch (e) {
setIsError(true);
setErrorMessage(e.message);
setIsSaving(false);
}
};
// ๐ฑ ๋ฐ์ ๊ฐ๋ฅํ ๋ถ๊ฐ๋ฅํ ์กฐํฉ๋ค:
// isSaving=true && isSaved=true โ ์ ์ฅ ์ค์ธ๋ฐ ๋์์ ์ ์ฅ๋จ? ๋
ผ๋ฆฌ ์ค๋ฅ
// isSaved=true && isError=true โ ์ ์ฅ๋๋๋ฐ ๋์์ ์ค๋ฅ? ๋
ผ๋ฆฌ ์ค๋ฅ
// isSaving=false && isSaved=false && isError=false โ ์๋ฌด๊ฒ๋ ์๋ ์ํ? (์ด๊ธฐ ์ํ์ ๊ตฌ๋ถ ๋ถ๊ฐ)
}3๊ฐ ๋ถ๋ฆฌ์ธ์ด ๋ง๋๋ 8๊ฐ์ง ์กฐํฉ ์ค ์๋ฏธ ์๋ ๊ฑด 4๊ฐ๋ฟ์ด์์:
isSaving | isSaved | isError | ์ค์ ์๋ฏธ |
|---|---|---|---|
| false | false | false | ๋๊ธฐ ์ค (idle) โ |
| true | false | false | ์ ์ฅ ์ค (saving) โ |
| false | true | false | ์ ์ฅ๋จ (saved) โ |
| false | false | true | ์ค๋ฅ (error) โ |
| true | true | false | ๋ถ๊ฐ๋ฅ! โ |
| true | false | true | ๋ถ๊ฐ๋ฅ! โ |
| false | true | true | ๋ถ๊ฐ๋ฅ! โ |
| true | true | true | ๋ถ๊ฐ๋ฅ! โ |
๋ถ๊ฐ๋ฅํ ์กฐํฉ 4๊ฐ๋ฅผ useState 3๊ฐ๋ก๋ ๋ง์ ์ ์์ด์. ์ฝ๋ ๊ตฌ์กฐ ์์ฒด๋ก ๋ถ๊ฐ๋ฅํ ์ํ๋ฅผ ์ฐจ๋จํ๋ ๊ฒ์ด useReducer + FSM์ ํต์ฌ์ด์์.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"N๊ฐ์ ๋ถ๋ฆฌ์ธuseState๋ 2^N ๊ฐ์ ์ํ ์กฐํฉ์ ํ์ฉํ๋ค. ๊ทธ ์ค ๋๋ถ๋ถ์ ๋ถ๊ฐ๋ฅํ ์กฐํฉ์ด๊ณ , ๋ฒ๊ทธ์ ์จ์์ด๋ค."
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
์ ํธ๋ฑ์ ์์ํด๋ด. ์ ํธ๋ฑ์ ๋์์ ๋นจ๊ฐ๋ถ์ด๋ฉด์ ์ด๋ก๋ถ์ผ ์ ์์ด.
ํ์ง๋ง ์์ฒ ์ด ๋ฐฉ์์ ๋นจ๊ฐ ์ ๊ตฌ, ๋ ธ๋ ์ ๊ตฌ, ์ด๋ก ์ ๊ตฌ๋ฅผ ๊ฐ๊ฐ ๋ ๋ฆฝ์ ์ธ ์ค์์น๋ก ์ผ๊ณ ๋๋ ๊ฑฐ์ผ.
์ค์๋ก ๋นจ๊ฐ๋ถ์ด๋ ์ด๋ก๋ถ์ ๋์์ ์ผ๋ฉด? ๊ต์ฐจ๋ก์์ ์ฌ๊ณ ๊ฐ ๋.
useReducer๋ฐฉ์์ ์ ํธ๋ฑ ์ปจํธ๋กค๋ฌ์ "RED", "YELLOW", "GREEN" ๋ฒํผ ํ๋์ฉ๋ง ์์ด.
๊ทธ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ปจํธ๋กค๋ฌ๊ฐ ์์์ ์ด์ ๋ถ์ ๋๊ณ ์ ๋ถ์ ์ผ.
๋์์ ๋ ๋ถ์ด ์ผ์ง๋ ๊ฒ ์์ฒด๊ฐ ๊ตฌ์กฐ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํด.
โ๏ธ useReducer ํต์ฌ ๊ตฌ์กฐ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
useReducer์ Action โ Reducer โ Next State ํ๋ฆ์ ์ดํดํ๋ค- ๊ฐ๋จํ ์นด์ดํฐ๋ถํฐ ๋ณต์กํ ํผ ์ํ๊น์ง
useReducer๋ฅผ ์ ์ฉํ ์ ์๋ค
useReducer ์ 3์์:
dispatch(action) โ reducer(state, action) โ ์ state
โ โ
์ด๋ฒคํธ ๋ฐ์ ๋ฆฌ๋ ๋๋ง
๊ธฐ๋ณธ ๊ตฌ์กฐ (์นด์ดํฐ ์์):
import { useReducer } from 'react';
// 1๏ธโฃ State ํ์
์ ์
type CounterState = {
count: number;
};
// 2๏ธโฃ Action ํ์
์ ์ (์ด๋ค ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ์ ์๋๊ฐ)
type CounterAction =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET' }
| { type: 'SET'; payload: number }; // ํน์ ๊ฐ์ผ๋ก ์ค์
// 3๏ธโฃ Reducer ํจ์ (์์ ํจ์: ๊ฐ์ ์
๋ ฅ โ ํญ์ ๊ฐ์ ์ถ๋ ฅ)
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }; // ์ ๊ฐ์ฒด ๋ฐํ (๋ถ๋ณ์ฑ ์ ์ง!)
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
case 'SET':
return { count: action.payload }; // payload๋ก ์ ๋ฌ๋ ๊ฐ ์ฌ์ฉ
default:
return state; // ์ ์ ์๋ action์ ํ์ฌ ์ํ ๊ทธ๋๋ก ๋ฐํ
}
}
// 4๏ธโฃ ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>๋ฆฌ์
</button>
<button onClick={() => dispatch({ type: 'SET', payload: 100 })}>100์ผ๋ก</button>
</div>
);
}Reducer ํจ์ ์์ฑ ์์น:
// โ
์ข์ Reducer: ์์ ํจ์
function goodReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] }; // ์ ๋ฐฐ์ด ์์ฑ
default:
return state;
}
}
// โ ๋์ Reducer: ์ฌ์ด๋ ์ดํํธ ํฌํจ
function badReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
state.items.push(action.payload); // ๐ฃ ์๋ณธ ๋ฐฐ์ด ์ง์ ๋ณ๊ฒฝ โ ๋ฆฌ๋ ๋๋ง ์ ๋จ!
return state;
case 'FETCH':
fetch('/api/data'); // ๐ฃ Reducer ์์์ API ํธ์ถ โ ์ ๋ ๊ธ์ง!
return state;
}
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"Reducer๋ ์์ ํจ์์ผ. ๊ฐ์ state + action = ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ. ์ฌ์ด๋ ์ดํํธ(API ํธ์ถ, DOM ์กฐ์)๋ ์ ๋ ์ ๋ผ."
๐ useState vs useReducer ์ ํ ๊ธฐ์ค ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ด๋ค ์ํฉ์์
useState๋ฅผuseReducer๋ก ์ ํํด์ผ ํ ์ง ํ๋จํ ์ ์๋ค
์ ํ ํ๋จ ๊ธฐ์คํ:
| ์ํฉ | useState | useReducer |
|---|---|---|
| ๋ ๋ฆฝ์ ์ธ ๊ฐ ํ๋ | โ | ๊ณผํจ |
| ์๋ก ์ฐ๊ด๋ ์ํ 2-3๊ฐ | ๊ฐ๋ฅ | โ ๊ถ์ฅ |
| ์ํ ์ ํ ๋ก์ง์ด ๋ณต์กํจ | ์ด๋ ค์ | โ |
| "๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ" ์ด ์กด์ฌ | ๋ง๊ธฐ ์ด๋ ค์ | โ |
| ์ฌ๋ฌ ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ๊ฐ์ ์ํ๋ฅผ ๋ณ๊ฒฝ | ์ค๋ณต ๋ฐ์ | โ |
| ๋ก์ง์ ์ปดํฌ๋ํธ ๋ฐ์์ ํ ์คํธํ๊ณ ์ถ๋ค | ๋ถ๊ฐ๋ฅ | โ ๊ฐ๋ฅ |
์ค์ ์ ํ ์์ โ ๊ฒ์๊ธ ์๋ํฐ:
// โ
useReducer๋ก ๊ฒ์๊ธ ์๋ํฐ ์ํ ๋จธ์ ํ
// 1๏ธโฃ ๋ถ๊ฐ๋ฅํ ์กฐํฉ์ด ์๋ ๋จ์ผ status ํ์
type EditorStatus = 'idle' | 'saving' | 'saved' | 'error';
type EditorState = {
content: string;
status: EditorStatus; // ๋จ ํ๋์ ์ํ๋ง ๊ฐ๋ฅ (๋์ 2๊ฐ ๋ถ๊ฐ!)
errorMessage: string | null;
};
type EditorAction =
| { type: 'CHANGE_CONTENT'; payload: string }
| { type: 'SAVE_START' }
| { type: 'SAVE_SUCCESS' }
| { type: 'SAVE_ERROR'; payload: string };
function editorReducer(state: EditorState, action: EditorAction): EditorState {
switch (action.type) {
case 'CHANGE_CONTENT':
// ๋ด์ฉ ๋ณ๊ฒฝ ์ ์ ์ฅ๋จ ์ํ ์ด๊ธฐํ
return { ...state, content: action.payload, status: 'idle' };
case 'SAVE_START':
// ์ ์ฅ ์์: status ํ๋๋ง ๋ฐ๊พธ๋ฉด ๋ชจ๋ ์ฐ๊ด ์ํ๊ฐ ์ผ๊ด์ฑ ์ ์ง
return { ...state, status: 'saving', errorMessage: null };
case 'SAVE_SUCCESS':
return { ...state, status: 'saved' };
case 'SAVE_ERROR':
return { ...state, status: 'error', errorMessage: action.payload };
default:
return state;
}
}
// 2๏ธโฃ ์ปดํฌ๋ํธ๋ dispatch๋ง ํธ์ถ, ์ํ ์ ํ ๋ก์ง์ reducer์๋ง ์กด์ฌ
function PostEditor() {
const [state, dispatch] = useReducer(editorReducer, {
content: '',
status: 'idle',
errorMessage: null,
});
const handleSave = async () => {
dispatch({ type: 'SAVE_START' }); // ํ ์ค๋ก ๋ชจ๋ ์ฐ๊ด ์ํ ์ผ๊ด์ฑ ์๊ฒ ์ ํ
try {
await savePost(state.content);
dispatch({ type: 'SAVE_SUCCESS' });
} catch (e) {
dispatch({ type: 'SAVE_ERROR', payload: e.message });
}
// isSaving์ false๋ก ๋ฐ๊พธ๋ ๊ฑธ ๊น๋นกํ ์๊ฐ ์์! reducer๊ฐ ์์์ ์ฒ๋ฆฌ
};
return (
<div>
<textarea
value={state.content}
onChange={e => dispatch({ type: 'CHANGE_CONTENT', payload: e.target.value })}
/>
<button onClick={handleSave} disabled={state.status === 'saving'}>
{state.status === 'saving' ? '์ ์ฅ ์ค...' : '์ ์ฅ'}
</button>
{state.status === 'saved' && <span>โ
์ ์ฅ๋จ</span>}
{state.status === 'error' && <span>โ {state.errorMessage}</span>}
{/* 'saved'์ 'error'๊ฐ ๋์์ ํ์๋ ์ ์์ โ status๊ฐ ํ๋์ด๊ธฐ ๋๋ฌธ์! */}
</div>
);
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"์ฐ๊ด ์ํ๊ฐ 3๊ฐ ์ด์์ด๊ณ ๋์์ ๋ฐ๋๋ค๋ฉด,useReducer๋ก ๋จ์ผstatusํ๋๋ก ํตํฉํด๋ผ. ๋์์ ๋ ์ํ๊ฐ true์ธ ๋ฒ๊ทธ๊ฐ ๊ตฌ์กฐ์ ์ผ๋ก ์ฌ๋ผ์ง๋ค."
๐ค ์ ํ ์ํ ๋จธ์ ์ฌ๊ณ ๋ฐฉ์ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- FSM(์ ํ ์ํ ๋จธ์ ) ๊ฐ๋ ์ React ์ํ ์ค๊ณ์ ์ ์ฉํ ์ ์๋ค
- "๋ค์ ์ํ๋ก์ ์ ํ"์ ๋ช ์์ ์ผ๋ก ๋ชจ๋ธ๋งํ ์ ์๋ค
์ ํ ์ํ ๋จธ์ (FSM) โ Finite State Machine ์ด๋, ์์คํ ์ด ๊ฐ์ง ์ ์๋ ์ ํํ ์์ ์ํ์ ๊ทธ ์ฌ์ด์ ํ์ฉ๋ ์ ํ๋ง ์ ์ํ ๋ชจ๋ธ์ด์์.
๐ ์ฉ์ด: FSM(์ ํ ์ํ ๋จธ์ ) โ ํญ์ ์ ํํ ํ๋์ ์ํ์ ์๊ณ , ์ธ๋ถ ์ด๋ฒคํธ์ ์ํด ๋ค๋ฅธ ์ํ๋ก ์ ํ๋๋ ์ํ์ ๋ชจ๋ธ. ์ ํธ๋ฑ, ์ํ๊ธฐ, ์ฒดํฌ์์ ํ๋ก์ฐ ๋ฑ์ ์์ฐ์ค๋ฝ๊ฒ ์ ์ฉ๋ผ์.
๊ฒฐ์ ํ๋ก์ฐ๋ฅผ FSM์ผ๋ก ๋ชจ๋ธ๋ง:
[cart] โโ(์ฃผ์ ์
๋ ฅ ์๋ฃ)โโโ [address]
[address] โโ(๊ฒฐ์ ์๋จ ์ ํ)โโโ [payment]
[payment] โโ(๊ฒฐ์ ์์)โโโ [confirming]
[confirming] โโ(์ฑ๊ณต)โโโ [completed]
[confirming] โโ(์คํจ)โโโ [failed]
[failed] โโ(์ฌ์๋)โโโ [payment]
# ํ์ฉ๋์ง ์์ ์ ํ:
# [completed] โ [cart] : ๊ฒฐ์ ์๋ฃ ํ ์ฅ๋ฐ๊ตฌ๋๋ก ๋์๊ฐ๊ธฐ โ ๋ถ๊ฐ!
# [confirming] โ [address] : ๊ฒฐ์ ์ค ์ฃผ์ ๋ณ๊ฒฝ โ ๋ถ๊ฐ!
์ฝ๋๋ก ๊ตฌํ:
type CheckoutStatus = 'cart' | 'address' | 'payment' | 'confirming' | 'completed' | 'failed';
type CheckoutState = {
status: CheckoutStatus;
address: Address | null;
paymentMethod: PaymentMethod | null;
errorMessage: string | null;
};
type CheckoutAction =
| { type: 'ADDRESS_SUBMITTED'; payload: Address }
| { type: 'PAYMENT_METHOD_SELECTED'; payload: PaymentMethod }
| { type: 'PAYMENT_CONFIRMED' }
| { type: 'PAYMENT_SUCCEEDED' }
| { type: 'PAYMENT_FAILED'; payload: string }
| { type: 'RETRY_PAYMENT' };
function checkoutReducer(state: CheckoutState, action: CheckoutAction): CheckoutState {
// ํ์ฌ ์ํ์์ ํ์ฉ๋ action๋ง ์ฒ๋ฆฌ โ ๋ถ๊ฐ๋ฅํ ์ ํ ์๋ ์ฐจ๋จ
switch (state.status) {
case 'cart':
if (action.type === 'ADDRESS_SUBMITTED') {
return { ...state, status: 'address', address: action.payload };
}
break;
case 'address':
if (action.type === 'PAYMENT_METHOD_SELECTED') {
return { ...state, status: 'payment', paymentMethod: action.payload };
}
break;
case 'payment':
if (action.type === 'PAYMENT_CONFIRMED') {
return { ...state, status: 'confirming' };
}
break;
case 'confirming':
if (action.type === 'PAYMENT_SUCCEEDED') {
return { ...state, status: 'completed' };
}
if (action.type === 'PAYMENT_FAILED') {
return { ...state, status: 'failed', errorMessage: action.payload };
}
break;
case 'failed':
if (action.type === 'RETRY_PAYMENT') {
return { ...state, status: 'payment', errorMessage: null };
}
break;
}
return state; // ํ์ฉ๋์ง ์์ action์ ๋ฌด์ โ ์ํ ์ ์ง
}์ด๋ ๊ฒ ํ๋ฉด completed ์ํ์์ ADDRESS_SUBMITTED ๋ฅผ dispatchํด๋ ์๋ฌด ์ผ๋ ์ผ์ด๋์ง ์์์. ํ์ฉ๋์ง ์์ ์ํ ์ ํ์ด ์ฝ๋ ๊ตฌ์กฐ์ ๋งํ์์ด์.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"FSM์ ์ ํธ๋ฑ ์ปจํธ๋กค๋ฌ์ผ. ์ด๋ค ์ํ์์ ์ด๋ค ์ด๋ฒคํธ๊ฐ ์ค๋ฉด ์ด๋ ์ํ๋ก ๊ฐ๋์ง๋ง ์ ์ํ๋ฉด, ๋ถ๊ฐ๋ฅํ ์กฐํฉ์ ๊ตฌ์กฐ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํด์ง๋ค."
๐ก๏ธ TypeScript๋ก ํ์ ์์ ํ Reducer ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Discriminated Union ์ผ๋ก Action ํ์ ์ ์์ ํ๊ฒ ์ ์ํ ์ ์๋ค
- ์ปดํ์ผ ํ์์ ์๋ชป๋ action ์ฌ์ฉ์ ์ก์ ์ ์๋ค
// โ
Discriminated Union Action ํ์
type TodoAction =
| { type: 'ADD_TODO'; payload: { text: string } } // ADD๋ text ํ์
| { type: 'TOGGLE_TODO'; payload: { id: string } } // TOGGLE์ id ํ์
| { type: 'DELETE_TODO'; payload: { id: string } } // DELETE๋ id ํ์
| { type: 'CLEAR_ALL' }; // CLEAR๋ payload ์์
// TypeScript๊ฐ ๊ฐ case์์ payload ํ์
์ ์๋์ผ๋ก ์ขํ์ค
function todoReducer(state: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case 'ADD_TODO':
// ์ฌ๊ธฐ์ action.payload๋ { text: string } ํ์
์ผ๋ก ์๋ ์ถ๋ก
return [...state, { id: crypto.randomUUID(), text: action.payload.text, done: false }];
case 'TOGGLE_TODO':
// ์ฌ๊ธฐ์ action.payload๋ { id: string } ํ์
์ผ๋ก ์๋ ์ถ๋ก
return state.map(todo =>
todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload.id);
case 'CLEAR_ALL':
// ์ฌ๊ธฐ์ action.payload๋ undefined (ํ์
์๋ฌ ์์)
return [];
default:
// TypeScript: ์ด ์์ ์์ action์ never ํ์
โ ๋ชจ๋ case ์ฒ๋ฆฌ ๋ณด์ฅ
const _exhaustiveCheck: never = action;
return state;
}
}Action Creator ํจํด (์ ํ์ ):
// Action ์์ฑ ํจ์: dispatch ํธ์ถ ์ ํ์
์คํ ๋ฐฉ์ง
const todoActions = {
addTodo: (text: string): TodoAction => ({ type: 'ADD_TODO', payload: { text } }),
toggleTodo: (id: string): TodoAction => ({ type: 'TOGGLE_TODO', payload: { id } }),
deleteTodo: (id: string): TodoAction => ({ type: 'DELETE_TODO', payload: { id } }),
clearAll: (): TodoAction => ({ type: 'CLEAR_ALL' }),
};
// ์ฌ์ฉ: ๋ฌธ์์ด ์คํ ์์ด ์์ ํ๊ฒ dispatch
dispatch(todoActions.addTodo('์ ๊ฒ์๊ธ ์์ฑ'));
dispatch(todoActions.toggleTodo(postId));๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"Discriminated Union Action ํ์ ์switch๋ฌธ์์ TypeScript๊ฐ ๊ฐ case๋ง๋ค payload ํ์ ์ ์๋์ผ๋ก ์ถ๋ก ํด์ค๋ค. Action Creator๋ฅผ ์ฐ๋ฉด dispatch ํธ์ถ ์ ๋ฌธ์์ด ์คํ๋ ์ก์๋ผ ์ ์์ด."
๐ useContext + useReducer ์กฐํฉ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ปดํฌ๋ํธ ํธ๋ฆฌ ์ ์ฒด์์ ์ฐ๋ ์ ์ญ ์ํ๋ฅผ ๊ฒฝ๋ Redux ํจํด์ผ๋ก ๋ง๋ค ์ ์๋ค
- Redux ์์ด๋ ์ฑ ์ ์ญ ์ํ๋ฅผ ํ์ ์์ ํ๊ฒ ๊ด๋ฆฌํ ์ ์๋ค
useContext + useReducer ๋ Redux์ ํต์ฌ ์์ด๋์ด๋ฅผ React ๋ด์ฅ API๋ง์ผ๋ก ๊ตฌํํ๋ ํจํด์ด์์.
// โ
์ ์ญ Toast(์๋ฆผ) ์์คํ
๊ตฌ์ถ
// 1๏ธโฃ ํ์
์ ์
type ToastType = 'success' | 'error' | 'info';
type Toast = { id: string; message: string; type: ToastType };
type ToastAction =
| { type: 'ADD'; payload: Omit<Toast, 'id'> }
| { type: 'REMOVE'; payload: string }; // id ์ ๋ฌ
// 2๏ธโฃ Context ํ์
์ ์
interface ToastContextValue {
toasts: Toast[];
dispatch: React.Dispatch<ToastAction>;
}
const ToastContext = createContext<ToastContextValue | null>(null);
// 3๏ธโฃ Reducer
function toastReducer(state: Toast[], action: ToastAction): Toast[] {
switch (action.type) {
case 'ADD':
return [
...state,
{ ...action.payload, id: crypto.randomUUID() }, // id ์๋ ์์ฑ
];
case 'REMOVE':
return state.filter(t => t.id !== action.payload);
default:
return state;
}
}
// 4๏ธโฃ Provider ์ปดํฌ๋ํธ
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, dispatch] = useReducer(toastReducer, []);
return (
<ToastContext.Provider value={{ toasts, dispatch }}>
{children}
<ToastContainer toasts={toasts} dispatch={dispatch} />
</ToastContext.Provider>
);
}
// 5๏ธโฃ ์ปค์คํ
ํ
์ผ๋ก ํธ๋ฆฌํ API ์ ๊ณต
export function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within ToastProvider');
const { dispatch } = context;
return {
// dispatch๋ฅผ ์ง์ ๋
ธ์ถํ์ง ์๊ณ ์๋ฏธ ์๋ ํจ์๋ง ์ ๊ณต
success: (message: string) =>
dispatch({ type: 'ADD', payload: { message, type: 'success' } }),
error: (message: string) =>
dispatch({ type: 'ADD', payload: { message, type: 'error' } }),
info: (message: string) =>
dispatch({ type: 'ADD', payload: { message, type: 'info' } }),
remove: (id: string) =>
dispatch({ type: 'REMOVE', payload: id }),
};
}
// 6๏ธโฃ ์ด๋ ์ปดํฌ๋ํธ์์๋ ์ฌ์ฉ
function SaveButton() {
const { success, error } = useToast();
const handleSave = async () => {
try {
await savePost();
success('๊ฒ์๊ธ์ด ์ ์ฅ๋์ด์!'); // ์ ์ญ ํ ์คํธ ํ์
} catch {
error('์ ์ฅ์ ์คํจํ์ด์. ๋ค์ ์๋ํด์ฃผ์ธ์.');
}
};
return <button onClick={handleSave}>์ ์ฅ</button>;
}useContext + useReducer vs Redux ๋น๊ต:
useContext + useReducer | Redux | |
|---|---|---|
| ์ ์ ๋ณต์ก๋ | ๋ฎ์ (๋ด์ฅ API) | ๋์ (์ธ๋ถ ํจํค์ง) |
| DevTools | ์์ | Redux DevTools ๊ฐ๋ ฅํจ |
| ์ฑ๋ฅ | Context ๋ฆฌ๋ ๋๋ง ์ด์ ์์ | ์ต์ ํ ์ ๋จ |
| ์ ํฉํ ๊ท๋ชจ | ์~์ค๊ฐ ๊ท๋ชจ | ์ค~๋๊ท๋ชจ |
| ํ์ ์์ ์ฑ | TypeScript๋ก ๊ฐ๋ฅ | TypeScript๋ก ๊ฐ๋ฅ |
๐ ์ฐ๊ฒฐ ๊ณ ๋ฆฌ
์๋ฒ ๋ฐ์ดํฐ๋ 19๋ฒ ๋ฌธ์ โ TanStack Query ๋ก, UI ์ ์ญ ์ํ(Toast, Modal)๋ ์ด ํจํด์ผ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ ์ค๋ฌด ์ํคํ ์ฒ์ ์ ์์ด์์.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"useContext + useReducer๋ ๊ฒฝ๋ Redux์ผ. ์~์ค๊ฐ ๊ท๋ชจ ์ฑ์ UI ์ ์ญ ์ํ(์๋ฆผ, ๋ชจ๋ฌ, ํ ๋ง)์ ๋ฑ ๋ง๊ณ , ์๋ฒ ๋ฐ์ดํฐ๋ TanStack Query์๊ฒ ๋งก๊ฒจ."
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F ๋ก ๋ฉ์์ง ์ผ๋ถ๋ฅผ ๊ฒ์ํด๋ด.
โ ์ํ๊ฐ ์ ๋ฐ์ดํธ๋๋๋ฐ ํ๋ฉด์ด ๋ฆฌ๋ ๋๋ง ์ ๋จ
์ธ์ ๋์ค๋๊ฐ?
// reducer๊ฐ ์๋ณธ ๋ฐฐ์ด/๊ฐ์ฒด๋ฅผ ์ง์ ์์ ํจ
case 'ADD_ITEM':
state.items.push(newItem); // ๐ฅ ์๋ณธ ๋ณ๊ฒฝ โ React๊ฐ ๋ณ๊ฒฝ ๊ฐ์ง ๋ชป ํจ
return state; // ๊ฐ์ ์ฐธ์กฐ ๋ฐํ โ ๋ฆฌ๋ ๋๋ง ์์!ํด๊ฒฐ์ฑ :
case 'ADD_ITEM':
return { ...state, items: [...state.items, newItem] }; // ์ ์ฐธ์กฐ ๋ฐํโ dispatch ํ ์ํ๊ฐ ์ฆ์ ๋ฐ์ ์ ๋จ
์ธ์ ๋์ค๋๊ฐ?
dispatch({ type: 'SAVE_START' });
console.log(state.status); // ๐ฅ ์ฌ์ ํ 'idle' ์ถ๋ ฅ! dispatch๋ ๋น๋๊ธฐ์์ธ: dispatch ๋ ๋น๋๊ธฐ์ด๊ณ , ๋ฆฌ๋ ๋๋ง ํ์ ์ state๋ฅผ ์ธ ์ ์์ด์.
ํด๊ฒฐ์ฑ :
// dispatch ์งํ์ state๋ฅผ ์ฐ์ง ๋ง๊ณ , ๋ค์ ๋ ๋์์ ์ฌ์ฉ
dispatch({ type: 'SAVE_START' });
// ์ดํ ๋ก์ง์์ state.status๋ฅผ ์ฐ์ง ์๊ณ , action์ ์๋ฏธ์ ๋ง๊ฒ ์งํ
await savePost(state.content); // state.content๋ ์์ง ์ ๋ฐ๋ โ ๋ฌธ์ ์์โ useReducer ์ด๊ธฐ ์ํ ๊ณ์ฐ์ด ๋๋ฆผ (๊ฒ์ผ๋ฅธ ์ด๊ธฐํ)
์ธ์ ๋์ค๋๊ฐ?
// ์ด๊ธฐ ์ํ ๊ณ์ฐ์ด ๋น์ผ ๊ฒฝ์ฐ (localStorage ์ฝ๊ธฐ ๋ฑ)
const [state, dispatch] = useReducer(reducer, loadFromLocalStorage()); // ๋งค ๋ ๋๋ง๋ค ์คํ!ํด๊ฒฐ์ฑ :
// ์ธ ๋ฒ์งธ ์ธ์๋ก ์ด๊ธฐํ ํจ์ ์ ๋ฌ โ ์ต์ด ํ ๋ฒ๋ง ์คํ
const [state, dispatch] = useReducer(reducer, null, () => loadFromLocalStorage());๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ useReducer vs useState ์ ํ ๊ธฐ์ค
| ์ํฉ | ์ ํ |
|---|---|
| ๋ ๋ฆฝ์ ์ธ ๋จ์ ๊ฐ | useState |
| ์ฐ๊ด๋ ์ํ 3๊ฐ ์ด์์ด ๋์์ ๋ณํจ | useReducer |
| "๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ"์ ๋ง์์ผ ํจ | useReducer + FSM |
| ์ฌ๋ฌ ํธ๋ค๋ฌ๊ฐ ๊ฐ์ ์ํ๋ฅผ ๋ณต์กํ๊ฒ ์์ | useReducer |
| ๋ก์ง์ ์ปดํฌ๋ํธ ๋ฐ์์ ํ ์คํธํ๊ณ ์ถ์ | useReducer |
๐ ํต์ฌ ํจํด ์์ฝ
| ํจํด | ์ฝ๋ | ์ค๋ช |
|---|---|---|
| ๊ธฐ๋ณธ useReducer | const [state, dispatch] = useReducer(reducer, init) | ์ํ + dispatch |
| FSM status | type Status = 'idle' | 'loading' | 'success' | 'error' | ๋จ์ผ status๋ก ๋ถ๊ฐ๋ฅํ ์กฐํฉ ์ฐจ๋จ |
| ํ์ ์์ Action | type Action = | {type:'A'; payload: X} | {type:'B'} | Discriminated Union |
| ์ ์ญ ์ํ | useContext + useReducer | Context์ dispatch ์ ๋ฌ |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| โ ๋์ ์ | โ ์ข์ ์ | ์ด์ |
|---|---|---|
state.items.push(item); return state; | return { ...state, items: [...state.items, item] } | ์๋ณธ ๋ณ๊ฒฝ ์ ๋ฆฌ๋ ๋๋ง ์์ |
| Reducer ์์์ API ํธ์ถ | ์ปดํฌ๋ํธ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ์์ ํธ์ถ | Reducer๋ ์์ ํจ์์ฌ์ผ ํจ |
isSaving && isSaved ๋ถ๋ฆฌ์ธ ์กฐํฉ | status: 'saving' | 'saved' ๋จ์ผ ํ๋ | ๋ถ๊ฐ๋ฅํ ์กฐํฉ ์ฐจ๋จ |
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
'์ ์ฅ ์ค'์ด๋ฉด์ '์ ์ฅ ์๋ฃ'๊ฐ ์ด๋ป๊ฒ ๋์์ ์ฐธ์ด ๋ ์ ์๋๋ฉฐ ๋ฒ๊ทธ ๋ฆฌํฌํธ๋ฅผ ๋ฐ์์ ๋ ๋ฑ๊ณจ์ด ์ค์นํ์๋ค. ์๋ง์ useState ๋ถ๋ฆฌ์ธ ๊ฐ๋ค์ด ๋ง๋ค์ด๋ด๋ ๊ฒฝ์ฐ์ ์๋ ์ง์ฅ๋ฌธ์ด๋ ๋ค๋ฆ์์๋ค.
๐ก "์ํ๋ ๊ฐ๋ณ ์ค์์น๊ฐ ์๋๋ผ, ๊ฑฐ๋ํ ๊ธฐ๊ณ์ 'ํ์ฌ ๋ชจ๋(Status)'๋ค. FSM(์ ํ ์ํ ๋จธ์ )์ผ๋ก ๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ์ ์์ ๋ฌธ๋ฒ์ ์ผ๋ก ์ฐจ๋จํด ๋ฒ๋ ค๋ผ."
useReducer๋ฅผ ๊ทธ๋ฅ ์ํ ๋ชจ์๋๋ ๋ฐ๊ตฌ๋ ์ ๋๋ก๋ง ์๊ฐํ์๋๋ฐ, FSM์ด๋ ๊ฒฐํฉํ๋๊น ์ ํธ๋ฑ ๋ถ ์ผ์ง๋ฏ ์ํ๊ฐ ๋ฑ๋ฑ ๋จ์ด์ง๋ ๊ฒ ๋๋ฌด ์ ๊ธฐํ๋ค. ์ด์ ์๋ํฐ์ฐฝ ์ํ ๊ด๋ฆฌํ๋ค๊ฐ ๊ผฌ์ผ ์ผ์ ์์ํ ์๋
์ด๋ค.
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. useReducer ๊ฐ useState ๋ณด๋ค ์ ํฉํ ์ํฉ์?
- A) ๊ฐ๋จํ ์นด์ดํฐ (ํ๋์ ์ซ์)
- B) ํ ๊ธ ๋ฒํผ (true/false)
- C) ์ ์ฅ ์ค/์ ์ฅ๋จ/์๋ฌ ์ํ๊ฐ ์๋ก ์ฐ๊ด๋ ๊ฒ์๊ธ ์๋ํฐ
- D) ์ ๋ ฅ ํ๋์ ํ ์คํธ ๊ฐ
โ ์ ๋ต: C
- A, B, D: ๋ ๋ฆฝ์ ์ธ ๋จ์ ๊ฐ โ
useState๋ก ์ถฉ๋ถ- C: ์ฐ๊ด๋ ์ํ๋ค์ด ๋์์ ๋ณํ๊ณ ,
isSaving && isSaved๊ฐ์ ๋ถ๊ฐ๋ฅํ ์กฐํฉ์ด ์๊ธธ ์ ์์ โuseReducer+ FSM๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "์ํ๋ค์ด ํจ๊ป ๋ค๋๋ฉด
useReducer, ํผ์ ๋ค๋๋ฉดuseState."
Q2. ์๋ Reducer์ ๋ฒ๊ทธ๋ฅผ ์ฐพ์๋ณด์.
function buggyReducer(state: { items: string[] }, action: { type: 'ADD'; payload: string }) {
switch (action.type) {
case 'ADD':
state.items.push(action.payload); // 1๋ฒ
return state; // 2๋ฒ
default:
return state;
}
}์ด ์ฝ๋์ ๋ฌธ์ ์ ์?
- A)
switch๋ฌธ ๋์if๋ฌธ์ ์จ์ผ ํ๋ค - B) ์๋ณธ
state๋ฅผ ์ง์ ๋ณ๊ฒฝํ๊ณ ๊ฐ์ ์ฐธ์กฐ๋ฅผ ๋ฐํํด์ ๋ฆฌ๋ ๋๋ง์ด ์ ๋๋ค - C)
payloadํ์ ์ด ์๋ชป๋๋ค - D)
useReducer๋ ๋ฐฐ์ด ์ํ๋ฅผ ์ง์ํ์ง ์๋๋ค
โ ์ ๋ต: B
state.items.push()๋ก ์๋ณธ ๋ฐฐ์ด์ ์ง์ ๋ณ๊ฒฝํ๊ณ ,return state๋ก ๊ฐ์ ๊ฐ์ฒด ์ฐธ์กฐ๋ฅผ ๋ฐํํ๋ฉด React๊ฐ ์ํ ๋ณ๊ฒฝ์ ๊ฐ์งํ์ง ๋ชปํด ๋ฆฌ๋ ๋๋ง์ด ์ผ์ด๋์ง ์์์.์ฌ๋ฐ๋ฅธ ์ฝ๋:
case 'ADD': return { ...state, items: [...state.items, action.payload] };๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "Reducer๋ ํญ์ ์ ๊ฐ์ฒด๋ฅผ ๋ฐํํด์ผ ํ๋ค. ๊ฐ์ ์ฐธ์กฐ = ๋ณ๊ฒฝ ์์ = ๋ฆฌ๋ ๋๋ง ์์."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
useReducer์ FSM์ ์ฐ๋ฉด ์ "๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ" ๋ฒ๊ทธ๊ฐ ์ฌ๋ผ์ง๋์ง, ์์๋ฅผ ๋ค์ด ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"์ ํธ๋ฑ์ ๋นจ๊ฐ๋ถ ์ค์์น, ์ด๋ก๋ถ ์ค์์น๋ฅผ ๋ฐ๋ก ๋๋ฉด ์ค์๋ก ๋ ๋ค ์ผค ์ ์์ด. ์ด๊ฒ
useState3๊ฐ๋ก ๋ฐ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ์์ด์ผ. FSM์ ์ ํธ๋ฑ ์ปจํธ๋กค๋ฌ์ 'RED', 'GREEN' ๋ฒํผ๋ง ์์ด์, RED๋ฅผ ๋๋ฅด๋ฉด ์ปจํธ๋กค๋ฌ๊ฐ ์๋์ผ๋ก ์ด๋ก๋ถ์ ๋๊ณ ๋นจ๊ฐ๋ถ์ ์ผ. ๋์์ ๋ ๋ถ์ด ์ผ์ง๋ ๊ฒ ๊ตฌ์กฐ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํด.useReducer์์status: 'saving' | 'saved' | 'error'๋ก ํ๋์ ํ๋๋ง ์ฐ๋ฉด,status๊ฐ ํ๋์ ๊ฐ๋ง ๊ฐ์ง ์ ์์ผ๋๊น 'saving์ด๋ฉด์ ๋์์ saved'์ธ ์ํ ์์ฒด๊ฐ ์กด์ฌํ ์ ์์ด."
๐ ๋ ์์๋ณด๊ธฐ
- React ๊ณต์ ๋ฌธ์ โ Reducer๋ก ์ํ ๋ก์ง ์ถ์ถํ๊ธฐ
- XState โ React์์ FSM์ ํ๋ก๋์ ์์ค์ผ๋ก ๊ตฌํํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- 19๋ฒ ๋ฌธ์ โ ์๋ฒ ์ํ vs ํด๋ผ์ด์ธํธ ์ํ ๋ถ๋ฆฌ
- 22๋ฒ ๋ฌธ์ โ ์ปค์คํ
ํ
์ค๊ณ ์ฒ ํ โ
useReducer๋ฅผ ํ ์ผ๋ก ๊ฐ์ธ๋ ํจํด