Separating Events from Effects

date
2026-01-21
order
6
link
  • Separating Events from Effects โ€“ React

You will learn

  • ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์™€ effect ์ค‘์— ์„ ํƒํ•˜๋Š” ๋ฐฉ๋ฒ•
  • effect๊ฐ€ ๋ฐ˜์‘ํ˜•์ด๊ณ  ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ๊ทธ๋ ‡์ง€ ์•Š์€ ์ด์œ 
  • effect์˜ ์ผ๋ถ€ ์ฝ”๋“œ๊ฐ€ ๋ฐ˜์‘ํ˜•์ด์ง€ ์•Š๊ธธ ๋ฐ”๋ž„ ๋•Œ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•๋“ค
  • effect event๋ฅผ ์ •์˜ํ•˜๊ณ  effect ์—์„œ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•
  • effect event๋ฅผ ํ†ตํ•ด effect์—์„œ ์ตœ์‹ ์˜ props๋‚˜ state๋ฅผ ์ฝ๋Š” ๋ฐฉ๋ฒ•

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ vs ์ดํŽ™ํŠธ

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ๋ง ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ์— ์˜ํ•ด ์ด‰๋ฐœ๋˜์–ด์„œ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๊ณ , ์ดํŽ™ํŠธ๋Š” ๋™๊ธฐํ™” synchronization์ด ํ•„์š”ํ•  ๋•Œ ๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๋‹ค.

Reactive values ๋ฐ˜์‘ํ˜• ๊ฐ’

Props, State, ๊ทธ๋ฆฌ๊ณ  ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ ์ •์˜๋œ ๋ณ€์ˆ˜๋“ค์„ ๋ฐ˜์‘ํ˜• ๊ฐ’ reactive value ๋ผ๊ณ  ํ•œ๋‹ค. ์ด ์„ธ๊ฐ€์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง ๋  ๋•Œ ๋งˆ๋‹ค ๋ณ€ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์™€ ์ดํŽ™ํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ธฐ์ค€๋„ ์ด ๋ฐ˜์‘ํ˜• reactive ์—ฌ๋ถ€๋‹ค.

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์•ˆ์˜ ๋กœ์ง์€ ๋ฐ˜์‘ํ˜•์ด ์•„๋‹ˆ๋‹ค. sendMessage(message) ์—์„œ message ์ƒํƒœ๊ฐ€ ๋ณ€ํ•˜๋”๋ผ๋„ ์‹คํ–‰๋˜์ง€ ์•Š์ง€๋งŒ, ์‹คํ–‰๋  ๋•Œ์—๋Š” ๋ณ€ํ•œ ์ƒํƒœ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค. (Event handlers can read reactive values without โ€œreactingโ€ to their changes.)

๋ฐ˜๋ฉด ์ดํŽ™ํŠธ์˜ ๋กœ์ง์€ ๋ฐ˜์‘ํ˜•์ด๋‹ค. ์ฆ‰ ๋ฐ˜์‘ํ˜• ๊ฐ’๋“ค(์†์„ฑ, ์ƒํƒœ, ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ ์ •์˜๋œ ๋ณ€์ˆ˜)์ด ๋ณ€๊ฒฝ๋˜๋ฉด ํ•จ๊ป˜ ์‹คํ–‰๋œ๋‹ค.

์ดํŽ™ํŠธ์—์„œ ๋น„๋ฐ˜์‘ํ˜• ๋กœ์ง ์ œ๊ฑฐํ•˜๊ธฐ

๋”ฐ๋ผ์„œ ์ดํŽ™ํŠธ ์•ˆ์—๋Š” ์ดํŽ™ํŠธ๊ฐ€ ์˜์กดํ•˜๊ณ  ์žˆ๋Š” ๊ฐ’์— ๋ฐ˜์‘ํ•˜๋Š” ๋ฐ˜์‘ํ˜• ๋กœ์ง๋“ค๋งŒ ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ๊นŒ๋‹ค๋กœ์šด ๊ฒฝ์šฐ๊ฐ€ ์กด์žฌํ•˜๋Š”๋ฐ, ๊ฐ€๋ น ์ฑ„ํŒ…์„œ๋ฒ„ ์ž…์žฅ์‹œ ์ฑ„ํŒ…๋ฐฉ ์ž…์žฅ์„ ์•Œ๋ฆฌ๋Š” ์•Œ๋ฆผ์„ ํ‘œ์‹œํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ useEffect์•ˆ์— ์ž‘์„ฑํ•˜๊ฒŒ ๋  ๊ฒƒ์ด๋‹ค.

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    // ...

์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •ํ•œ ui ํ…Œ๋งˆ์— ๋งž๋Š” ์•Œ๋ฆผ์ฐฝ์„ ๋„์šฐ๊ธฐ ์œ„ํ•ด theme ์†์„ฑ์„ showNotification ํ•จ์ˆ˜์˜ ์ธ์ž๋กœ ์ „๋‹ฌํ•˜๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๋Š” ๊ฒฝ์šฐ theme ์„ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ theme์„ ๋ณ€๊ฒฝํ•  ๋•Œ ๋งˆ๋‹ค ์ดํŽ™ํŠธ๊ฐ€ ์‹คํ–‰๋˜๋ฉด์„œ ์ฑ„ํŒ… ์„œ๋ฒ„์™€ ๋‹ค์‹œ ์—ฐ๊ฒฐ๋˜๊ณ  ์•Œ๋ฆผ์ฐฝ์ด ๋˜ ํ‘œ์‹œ๋ ๊ฒƒ์ด๋‹ค. ์–ด๋–ป๊ฒŒ theme์„ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๋„ฃ์ง€ ์•Š์œผ๋ฉด์„œ ์ฑ„ํŒ…๋ฐฉ ์ž…์žฅ์‹œ์—๋งŒ ์•Œ๋ฆผ์ฐฝ์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์„๊นŒ?

Effect Event

์ด๋Ÿฐ ๊ฒฝ์šฐ๋ฅผ ์œ„ํ•ด ๊ฐœ๋ฐœ๋œ ํ›… useEffectEvent๊ฐ€ ์žˆ์–ด์„œ ์ด๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  // ...

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด showNotification ํ•จ์ˆ˜๋Š” ์ตœ์‹ ์ƒํƒœ์˜ theme์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , useEffect ๋Š” ์ •๋ง๋กœ ํ•„์š”ํ•œ ๊ฐ’์—๋งŒ ์˜์กดํ•  ์ˆ˜ ์žˆ๋‹ค.

function Page({ url }) {
  const { items } = useContext(ShoppingCartContext);
  const numberOfItems = items.length;

  const onVisit = useEffectEvent(visitedUrl => {
    logVisit(visitedUrl, numberOfItems);
  });

  useEffect(() => {
    onVisit(url);
  }, [url]);
  // ...
}

์œ„์™€ ๊ฐ™์€ ๊ฒฝ์šฐ์—๋„ ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€์— ๋ฐฉ๋ฌธํ•  ๋•Œ ๋งˆ๋‹ค ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด์™€ ํ•จ๊ป˜ ๋กœ๊ทธ ๊ธฐ๋ก์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋‹ค.

Effect Event ์‚ฌ์šฉ์‹œ ์ฃผ์˜์‚ฌํ•ญ

  • ์ดํŽ™ํŠธ ์•ˆ์—์„œ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.
  • ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋‚˜ ํ›…์— ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š”๋‹ค.
function Timer() {
  const [count, setCount] = useState(0);

  const onTick = useEffectEvent(() => {
    setCount(count + 1);
  });

  useTimer(onTick, 1000); // ๐Ÿ”ด Avoid: Passing Effect Events

  return <h1>{count}</h1>
}

function useTimer(callback, delay) {
  useEffect(() => {
    const id = setInterval(() => {
      callback();
    }, delay);
    return () => {
      clearInterval(id);
    };
  }, [delay, callback]); // Need to specify "callback" in dependencies
}

์—ฌ๊ธฐ์„œ๋Š” onTick์ด๋ผ๋Š” ์ดํŽ™ํŠธ ์ด๋ฒคํŠธ๋ฅผ ์ •์˜ํ•˜๊ณ  useTimer๋ผ๋Š” ํ›…์— ์ „๋‹ฌํ•˜๊ณ  ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ›…์˜ ์ดํŽ™ํŠธ์—์„œ callback์ด๋ผ๋Š” ์†์„ฑ์„ ๋ฐ›์•„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋Š” ์“ธ๋ชจ์—†๋Š” ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด๋ผ ์ข‹์ง€ ์•Š์€ ์„ค๊ณ„์ด๋‹ค.

function Timer() {
  const [count, setCount] = useState(0);
  useTimer(() => {
    setCount(count + 1);
  }, 1000);
  return <h1>{count}</h1>
}

function useTimer(callback, delay) {
  const onTick = useEffectEvent(() => {
    callback();
  });

  useEffect(() => {
    const id = setInterval(() => {
      onTick(); // โœ… Good: Only called locally inside an Effect
    }, delay);
    return () => {
      clearInterval(id);
    };
  }, [delay]); // No need to specify "onTick" (an Effect Event) as a dependency
}

์ด๋ ‡๊ฒŒ callback์œผ๋กœ ์™ธ๋ถ€์—์„œ ํ•จ์ˆ˜๋ฅผ ์ฃผ์ž…๋ฐ›๋”๋ผ๋„ ํ›… ์•ˆ์—์„œ ์ดํŽ™ํŠธ ์ด๋ฒคํŠธ๋ฅผ ์ •์˜ํ•ด์•ผ ํ•œ๋‹ค.

์˜ˆ์‹œ ๋ฌธ์ œ

2. ์ž ๊น ๋ฉˆ์ถ”๋Š” ์นด์šดํ„ฐ

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + increment);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  }, [increment]);

์ดํŽ™ํŠธ๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋˜๋ฉด์„œ ํƒ€์ด๋จธ๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ์„ค์ •๋˜๊ธฐ ๋•Œ๋ฌธ์— increment๋ฅผ ์กฐ์ •ํ•  ๋•Œ ์ž ๊น ํ”„๋ฆฌ์ง•๋˜๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•œ๋‹ค.

  const onTick = useEffectEvent(() => {
    setCount(c => c + increment);
  });

  useEffect(() => {
    const id = setInterval(() => {
      onTick();
    }, 1000);
    return () => {
      clearInterval(id);
    };
  }, []);

์ด๋ ‡๊ฒŒ increment ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜๊ณ  useEffectEvent ํ›…์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

3. ๋”œ๋ ˆ์ด ์„ค์ •์ด ์ ์šฉ ์•ˆ๋˜๋Š” ์นด์šดํ„ฐ

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);
  const [delay, setDelay] = useState(100);

  const onTick = useEffectEvent(() => {
    setCount(c => c + increment);
  });

  const onMount = useEffectEvent(() => {
    return setInterval(() => {
      onTick();
    }, delay);
  });

  useEffect(() => {
    const id = onMount();
    return () => {
      clearInterval(id);
    }
  }, []);

์ธํ„ฐ๋ฒŒ ๋”œ๋ ˆ์ด๋ฅผ ๋ณ€๊ฒฝํ•ด๋„ ์ ์šฉ์ด ๋˜์ง€ ์•Š๋Š”๋‹ค. ์ ์šฉ์ด ์•ˆ๋˜๋Š” ์ด์œ ๋Š” ์ดํŽ™ํŠธ๊ฐ€ delay ๊ฐ’์— ๋ฐ˜์‘ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.


  const onTick = useEffectEvent(() => {
    setCount(c => c + increment);
  });

  useEffect(() => {
    const id = setInterval(() => {
      onTick();
    }, delay);
    return () => {
      clearInterval(id);
    }
  }, [delay]);

onMount ์˜ ๋กœ์ง์€ ์ดํŽ™ํŠธ๊ฐ€ ๊ฐ€์ ธ์•ผํ•˜๋Š” ๋ฐ˜์‘ํ˜• ๋กœ์ง์ด๋‹ค.

4. ์•Œ๋ฆผ์ฐฝ ๋”œ๋ ˆ์ด

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Welcome to ' + roomId, theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      setTimeout(() => {
        onConnected();
      }, 2000);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

์ฑ„ํŒ…๋ฐฉ์„ general -> travel -> music ๋กœ ๋น ๋ฅด๊ฒŒ ๋‘ ๋ฒˆ ๋ณ€๊ฒฝํ•˜๋ฉด ์•Œ๋ฆผ์ฐฝ์€ music ์•Œ๋ฆผ์ฐฝ ๋‘ ๊ฐœ๊ฐ€ ๋œฌ๋‹ค. travel ํ•˜๋‚˜ music ํ•˜๋‚˜๊ฐ€ ๋œจ๋„๋ก ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค.

์ด๊ฑด ๋ฐ˜๋Œ€๋กœ ์ดํŽ™ํŠธ ์ด๋ฒคํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด์„œ ์ตœ์‹  roomId๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋‹ค. ํ•˜์ง€๋งŒ ์ดํŽ™ํŠธ ์•ˆ์— ๋„ฃ์–ด์„œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด theme๋„ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๋“ค์–ด๊ฐ€์•ผ ํ•ด์„œ ๋ถˆ๊ฐ€๋Šฅ. onConnected๊ฐ€ ์ตœ์‹  rooomId๋ฅผ ์ฝ์ง€ ๋ชปํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ? ๋ณด๋‹ˆ๊นŒ ์ •๋‹ต์€ ์ง์ ‘ onConnectedํ•จ์ˆ˜์— ์ธ์ž๋กœ ๊ฑด๋‚ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

  const onConnected = useEffectEvent(connectedRoomId => {
    showNotification('Welcome to ' + connectedRoomId, theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      setTimeout(() => {
        onConnected(roomId);
      }, 2000);
    });

๊ทธ๋Ÿฐ๋ฐ ์ถ”๊ฐ€๋กœ ์ƒ๊ฐํ•ด๋ณด๋ฉด ๊ณผ์—ฐ ์•Œ๋ฆผ์ฐฝ์„ ๊ตณ์ด ๋‘ ๋ฒˆ ๋ณด์—ฌ์ค„ ํ•„์š”๊ฐ€ ์žˆ์„๊นŒ? ๋ฌธ์ œ์—์„œ๋„ ์ถ”๊ฐ€๋กœ ๋””๋ฐ”์šด์Šค๋ฅผ ์ ์šฉํ•ด ๋งˆ์ง€๋ง‰ ์ ‘์† ์ฑ„๋„์— ๋Œ€ํ•œ ์•Œ๋ฆผ๋งŒ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹์„ ์†Œ๊ฐœํ•œ๋‹ค.

 useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    let notificationTimeoutId;
    connection.on('connected', () => {
      notificationTimeoutId = setTimeout(() => {
        onConnected(roomId);
      }, 2000);
    });
    connection.connect();
    return () => {
      connection.disconnect();
      if (notificationTimeoutId !== undefined) {
        clearTimeout(notificationTimeoutId);
      }
    };
  }, [roomId]);