๐Ÿงฉ 23. useReducer & ์ƒํƒœ ๋จธ์‹ : ๋ณต์žกํ•œ ์ƒํƒœ ๋กœ์ง์˜ ํ•ด๋ฐฉ

๐Ÿ“‹ ๊ฐœ์š”

isSaving๊ณผ isSaved๊ฐ€ ๋™์‹œ์— true๊ฐ€ ๋˜๋Š” ๋ฒ„๊ทธ๋Š” ์™œ ์ƒ๊ธฐ๋Š”๊ฐ€? useReducer์™€ ์œ ํ•œ ์ƒํƒœ ๋จธ์‹ (FSM) ์‚ฌ๊ณ ๋ฐฉ์‹์œผ๋กœ '๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ์กฐํ•ฉ'์„ ์ฝ”๋“œ ๊ตฌ์กฐ ์ž์ฒด๋กœ ์ฐจ๋‹จํ•˜๋Š” ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 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๊ฐœ๋ฟ์ด์—์š”:

isSavingisSavedisError์‹ค์ œ ์˜๋ฏธ
falsefalsefalse๋Œ€๊ธฐ ์ค‘ (idle) โœ…
truefalsefalse์ €์žฅ ์ค‘ (saving) โœ…
falsetruefalse์ €์žฅ๋จ (saved) โœ…
falsefalsetrue์˜ค๋ฅ˜ (error) โœ…
truetruefalse๋ถˆ๊ฐ€๋Šฅ! โŒ
truefalsetrue๋ถˆ๊ฐ€๋Šฅ! โŒ
falsetruetrue๋ถˆ๊ฐ€๋Šฅ! โŒ
truetruetrue๋ถˆ๊ฐ€๋Šฅ! โŒ

๋ถˆ๊ฐ€๋Šฅํ•œ ์กฐํ•ฉ 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 ๋กœ ์ „ํ™˜ํ•ด์•ผ ํ• ์ง€ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค

์ „ํ™˜ ํŒ๋‹จ ๊ธฐ์ค€ํ‘œ:

์ƒํ™ฉuseStateuseReducer
๋…๋ฆฝ์ ์ธ ๊ฐ’ ํ•˜๋‚˜โœ…๊ณผํ•จ
์„œ๋กœ ์—ฐ๊ด€๋œ ์ƒํƒœ 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 + useReducerRedux
์…‹์—… ๋ณต์žก๋„๋‚ฎ์Œ (๋‚ด์žฅ 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

๐Ÿ“‹ ํ•ต์‹ฌ ํŒจํ„ด ์š”์•ฝ

ํŒจํ„ด์ฝ”๋“œ์„ค๋ช…
๊ธฐ๋ณธ useReducerconst [state, dispatch] = useReducer(reducer, init)์ƒํƒœ + dispatch
FSM statustype Status = 'idle' | 'loading' | 'success' | 'error'๋‹จ์ผ status๋กœ ๋ถˆ๊ฐ€๋Šฅํ•œ ์กฐํ•ฉ ์ฐจ๋‹จ
ํƒ€์ž… ์•ˆ์ „ Actiontype Action = | {type:'A'; payload: X} | {type:'B'}Discriminated Union
์ „์—ญ ์ƒํƒœuseContext + useReducerContext์— 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์„ ์“ฐ๋ฉด ์™œ "๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ์กฐํ•ฉ" ๋ฒ„๊ทธ๊ฐ€ ์‚ฌ๋ผ์ง€๋Š”์ง€, ์˜ˆ์‹œ๋ฅผ ๋“ค์–ด ์„ค๋ช…ํ•ด๋ด.

์˜ˆ์‹œ ๋‹ต๋ณ€:

"์‹ ํ˜ธ๋“ฑ์— ๋นจ๊ฐ„๋ถˆ ์Šค์œ„์น˜, ์ดˆ๋ก๋ถˆ ์Šค์œ„์น˜๋ฅผ ๋”ฐ๋กœ ๋‘๋ฉด ์‹ค์ˆ˜๋กœ ๋‘˜ ๋‹ค ์ผค ์ˆ˜ ์žˆ์–ด. ์ด๊ฒŒ useState 3๊ฐœ๋กœ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ. FSM์€ ์‹ ํ˜ธ๋“ฑ ์ปจํŠธ๋กค๋Ÿฌ์— 'RED', 'GREEN' ๋ฒ„ํŠผ๋งŒ ์žˆ์–ด์„œ, RED๋ฅผ ๋ˆ„๋ฅด๋ฉด ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ ์ดˆ๋ก๋ถˆ์„ ๋„๊ณ  ๋นจ๊ฐ„๋ถˆ์„ ์ผœ. ๋™์‹œ์— ๋‘ ๋ถˆ์ด ์ผœ์ง€๋Š” ๊ฒŒ ๊ตฌ์กฐ์ ์œผ๋กœ ๋ถˆ๊ฐ€๋Šฅํ•ด. useReducer ์—์„œ status: 'saving' | 'saved' | 'error' ๋กœ ํ•˜๋‚˜์˜ ํ•„๋“œ๋งŒ ์“ฐ๋ฉด, status ๊ฐ€ ํ•˜๋‚˜์˜ ๊ฐ’๋งŒ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ 'saving์ด๋ฉด์„œ ๋™์‹œ์— saved'์ธ ์ƒํƒœ ์ž์ฒด๊ฐ€ ์กด์žฌํ•  ์ˆ˜ ์—†์–ด."


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ