๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ‘จ‍๐Ÿ’ป web.dev/translates

(๋ฒˆ์—ญ) ๋ฆฌ์•กํŠธ์˜ useEffectEvent ์ดํ•ดํ•˜๊ธฐ: ์˜ค๋ž˜๋œ ํด๋กœ์ € ํ•ด๊ฒฐ์„ ์œ„ํ•œ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

by HandHand 2026. 3. 3.

 

์›๋ฌธ: Understanding React’s useEffectEvent: A Complete Guide to Solving Stale Closures

 

TL;DR

useEffectEvent๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๊ณ ๋„ Effect ๋‚ด๋ถ€์—์„œ ์ตœ์‹  props๋‚˜ ์ƒํƒœ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•ด๋‹น ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ Effect๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์˜ˆ์ œ๋กœ ์ด๋™ํ•˜๊ธฐ.

 

ํ•ต์‹ฌ ๋ฌธ์ œ(The Core Problem)

์ƒํ™ฉ์€ ์ด๋ ‡์Šต๋‹ˆ๋‹ค. WebSocket ์—ฐ๊ฒฐ, ๊ฐ„๊ฒฉ ํƒ€์ด๋จธ, ๊ตฌ๋…๊ณผ ๊ฐ™์ด ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ์ž‘์—…์„ ์„ค์ •ํ•˜๋Š” useEffect๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์„ค์ • ๋‚ด๋ถ€์—์„œ ์ฝœ๋ฐฑ์ด ์–ด๋–ค ์ƒํƒœ๊ฐ’์„ ์ฝ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ ์ƒํƒœ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์—ฐ๊ฒฐ์„ ๋Š๊ณ  ๋‹ค์‹œ ์„ค์ •ํ•˜๊ณ  ์‹ถ์ง€๋Š” ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ฐˆ๋“ฑ ์š”์†Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

  • ์ƒํƒœ๊ฐ’์„ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ํฌํ•จํ•˜๋ฉด → Effect๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋˜๊ณ  ์—ฐ๊ฒฐ์ด ์žฌ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค(๋‚˜์จ).
  • ์ƒํƒœ๊ฐ’์„ ์˜์กด์„ฑ ๋ฐฐ์—ด์—์„œ ์ œ์™ธํ•˜๋ฉด → ์ฝœ๋ฐฑ์ด ์˜ค๋ž˜๋œ ๊ฐ’์„ ๋ณด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค(์—ญ์‹œ ๋‚˜์จ).

useEffectEvent๊ฐ€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ํ˜ธ์ถœ๋  ๋•Œ ํ•ญ์ƒ ์ตœ์‹  ๊ฐ’์„ ์ฝ์ง€๋งŒ ๋ฐ˜์‘ํ˜• ์˜์กด์„ฑ์œผ๋กœ ์ทจ๊ธ‰๋˜์ง€ ์•Š๋Š” ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

 

์˜ˆ์ œ 1: ์ฑ„ํŒ…๋ฐฉ ์—ฐ๊ฒฐ

์ฑ„ํŒ… ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€๊ฐ€ ๋„์ฐฉํ•˜๋ฉด ์†Œ๋ฆฌ๋ฅผ ์žฌ์ƒํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ, ์‚ฌ์šฉ์ž๊ฐ€ ์•Œ๋ฆผ์„ ์Œ์†Œ๊ฑฐํ•˜์ง€ ์•Š์•˜์„ ๋•Œ๋งŒ ์†Œ๋ฆฌ๋ฅผ ์žฌ์ƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ž˜๋ชป๋œ ๋ฒ„์ „ (์˜ค๋ž˜๋œ ํด๋กœ์ €)

import { useEffect, useState } from "react";

function ChatRoom({ roomId }: { roomId: string }) {
  const [isMuted, setIsMuted] = useState(false);

  useEffect(() => {
    const connection = connectToRoom(roomId);

    connection.on("message", (message: string) => {
      // ๋ฒ„๊ทธ: ์ด๊ฒƒ์€ isMuted์˜ ์ดˆ๊ธฐ ๊ฐ’์„ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค
      // ์‚ฌ์šฉ์ž๊ฐ€ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํ† ๊ธ€ํ•  ๋•Œ ์—…๋ฐ์ดํŠธ๋œ ๊ฐ’์„ ์•Œ ์ˆ˜ ์—†์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค!
      if (!isMuted) {
        playSound();
      }
    });

    return () => connection.disconnect();
  }, [roomId]); // โŒ isMuted๊ฐ€ ๋ˆ„๋ฝ๋จ - ์ฝœ๋ฐฑ์ด ์˜ค๋ž˜๋œ ๊ฐ’์„ ๋ด…๋‹ˆ๋‹ค!

  return (
    <div>
      <h1>Chat Room: {roomId}</h1>
      <label>
        <input
          type="checkbox"
          checked={isMuted}
          onChange={(e) => setIsMuted(e.target.checked)}
        />
        Mute notifications
      </label>
    </div>
  );
}

 

๋ฌด์—‡์ด ์ž˜๋ชป๋˜์—ˆ์„๊นŒ์š”? ์ฝœ๋ฐฑ์€ Effect๊ฐ€ ์ฒ˜์Œ ์‹คํ–‰๋  ๋•Œ isMuted๋ฅผ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์Œ์†Œ๊ฑฐ๋ฅผ ํ† ๊ธ€ํ–ˆ๋‚˜์š”? ์ฝœ๋ฐฑ์€ ์—ฌ์ „ํžˆ ์ด์ „ ๊ฐ’์„ ๋ณด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํด๋กœ์ €(closure)๊ฐ€ ์˜ค๋ž˜๋œ ์ƒํƒœ์ธ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ƒˆ๋กœ์šด ๋ฌธ์ œ๋ฅผ ๋งŒ๋“œ๋Š” "ํ•ด๊ฒฐ์ฑ…"

"๊ทธ๋ƒฅ isMuted๋ฅผ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•˜๋ฉด ๋˜์ž–์•„!"๋ผ๊ณ  ์ƒ๊ฐํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

useEffect(() => {
  const connection = connectToRoom(roomId);

  connection.on("message", (message: string) => {
    if (!isMuted) {
      playSound();
    }
  });

  return () => connection.disconnect();
}, [roomId, isMuted]); // โŒ ์ด์ œ isMuted๊ฐ€ ์žฌ์—ฐ๊ฒฐ์„ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค!

 

์ด์ œ ์ฝœ๋ฐฑ์€ ์ตœ์‹  ๊ฐ’์„ ๋ด…๋‹ˆ๋‹ค... ํ•˜์ง€๋งŒ isMuted๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค,

 

  1. ๋ฆฌ์•กํŠธ๊ฐ€ ํด๋ฆฐ์—… ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ → connection.disconnect()
  2. ๋ฆฌ์•กํŠธ๊ฐ€ Effect๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ → connectToRoom(roomId)
  3. ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋“ฑ๋ก๋จ

์Œ์†Œ๊ฑฐ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํ† ๊ธ€ํ•˜๋ฉด ์ฑ„ํŒ…์ด ๋‹ค์‹œ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค! ์žฌ์—ฐ๊ฒฐ ์ค‘์— ๋ฉ”์‹œ์ง€๋ฅผ ๋†“์น  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๋Š” ์—ฐ๊ฒฐ์ด ์ž์ฃผ ๋Š๊ฒผ๋‹ค ๋ถ™์—ˆ๋‹ค ํ•˜๋Š” ๊ฒƒ์„ ๋ณด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋‚ญ๋น„์ด๊ณ  ๊ณ ์žฅ๋‚œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

ํ•ด๊ฒฐ์ฑ…: useEffectEvent

import { useEffect, useState, useEffectEvent } from "react";

function ChatRoom({ roomId }: { roomId: string }) {
  const [isMuted, setIsMuted] = useState(false);

  // Effect Event ์ƒ์„ฑ - ํ˜ธ์ถœ๋  ๋•Œ ์ตœ์‹  isMuted๋ฅผ ์ฝ์Œ
  const onMessage = useEffectEvent((message: string) => {
    if (!isMuted) {
      playSound();
    }
  });

  useEffect(() => {
    const connection = connectToRoom(roomId);
    connection.on("message", onMessage);
    return () => connection.disconnect();
  }, [roomId]); // โœ… roomId๋งŒ ์žฌ์—ฐ๊ฒฐ์„ ํŠธ๋ฆฌ๊ฑฐํ•จ

  return (
    <div>
      <h1>Chat Room: {roomId}</h1>
      <label>
        <input
          type="checkbox"
          checked={isMuted}
          onChange={(e) => setIsMuted(e.target.checked)}
        />
        Mute notifications
      </label>
    </div>
  );
}

 

์ด์ œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚ ๊นŒ์š”?

 

  • roomId ๋ณ€๊ฒฝ → ์žฌ์—ฐ๊ฒฐ (์ •์ƒ!)
  • isMuted ํ† ๊ธ€ → ์—ฐ๊ฒฐ์— ์•„๋ฌด๋Ÿฐ ์˜ํ–ฅ ์—†์Œ
  • ๋ฉ”์‹œ์ง€ ๋„์ฐฉ → onMessage๊ฐ€ ํ˜„์žฌ isMuted ๊ฐ’์„ ํ™•์ธ

Effect๋Š” ์˜ค์ง roomId์—๋งŒ ๊ด€์‹ฌ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค. onMessage ํ•จ์ˆ˜๋Š” ํ˜ธ์ถœ๋  ๋•Œ isMuted๋ฅผ ์ฝ์–ด, ๊ทธ ์ˆœ๊ฐ„์˜ ํ˜„์žฌ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

 

์˜ˆ์ œ 2: REST ํด๋ง ๋Œ€์‹œ๋ณด๋“œ

๋˜ ๋‹ค๋ฅธ ํ”ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” 10์ดˆ๋งˆ๋‹ค API๋ฅผ ํด๋ง(polling)ํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ์ž…๋‹ˆ๋‹ค. ์š”์ฒญ์—๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ํ† ๊ธ€ํ•  ์ˆ˜ ์žˆ๋Š” ํ•„ํ„ฐ ์˜ต์…˜์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

 

์ž˜๋ชป๋œ ๋ฒ„์ „ (ํƒ€์ด๋จธ ๋ฆฌ์…‹)

import { useEffect, useState } from "react";

function Dashboard({ teamId }: { teamId: string }) {
  const [includeArchived, setIncludeArchived] = useState(false);
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(
        `/api/team/${teamId}/tasks?archived=${includeArchived}`
      );
      const json = await response.json();
      setData(json);
    };

    fetchData(); // ์ฆ‰์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ
    const intervalId = setInterval(fetchData, 10000); // ๊ทธ ํ›„ ๋งค 10์ดˆ๋งˆ๋‹ค

    return () => clearInterval(intervalId);
  }, [teamId, includeArchived]); // โŒ ์ฒดํฌ๋ฐ•์Šค ํ† ๊ธ€ ์‹œ ํƒ€์ด๋จธ๊ฐ€ ์žฌ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค!

  return (
    <div>
      <h1>Team Dashboard</h1>
      <label>
        <input
          type="checkbox"
          checked={includeArchived}
          onChange={(e) => setIncludeArchived(e.target.checked)}
        />
        Include archived tasks
      </label>
      <ul>
        {data?.map((task) => (
          <li key={task.id}>{task.name}</li>
        ))}
      </ul>
    </div>
  );
}

 

๋ฌด์—‡์ด ์ž˜๋ชป๋˜์—ˆ์„๊นŒ์š”? ์‚ฌ์šฉ์ž๊ฐ€ "๋ณด๊ด€๋œ ์ž‘์—… ํฌํ•จ(Include archived)"์„ ํ† ๊ธ€ํ•  ๋•Œ๋งˆ๋‹ค,

 

  1. Effect ํด๋ฆฐ์—… ์‹คํ–‰ → clearInterval(intervalId)
  2. Effect ๋‹ค์‹œ ์‹คํ–‰ → ์ƒˆ๋กœ์šด ๊ฐ„๊ฒฉ(interval)์ด 0๋ถ€ํ„ฐ ์‹œ์ž‘

์‚ฌ์šฉ์ž๊ฐ€ 3์ดˆ ์•ˆ์— 5๋ฒˆ ํ† ๊ธ€ํ•˜๋ฉด, ํƒ€์ด๋จธ๋Š” 5๋ฒˆ ๋ฆฌ์…‹๋˜๊ณ  ์‹ค์ œ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํด๋ฆญ์„ ๋ฉˆ์ถœ ๋•Œ๊นŒ์ง€ ๋ฐ์ดํ„ฐ๋Š” ๊ฐ€์ ธ์™€์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

ํ•ด๊ฒฐ์ฑ…: useEffectEvent

import { useEffect, useState, useEffectEvent } from "react";

function Dashboard({ teamId }: { teamId: string }) {
  const [includeArchived, setIncludeArchived] = useState(false);
  const [data, setData] = useState(null);

  // Effect Event๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ ์ตœ์‹  includeArchived๋ฅผ ์ฝ์Œ
  const fetchData = useEffectEvent(async () => {
    const response = await fetch(
      `/api/team/${teamId}/tasks?archived=${includeArchived}`
    );
    const json = await response.json();
    setData(json);
  });

  useEffect(() => {
    fetchData();
    const intervalId = setInterval(fetchData, 10000);
    return () => clearInterval(intervalId);
  }, [teamId]); // โœ… teamId๋งŒ ๊ฐ„๊ฒฉ์„ ์žฌ์‹œ์ž‘ํ•จ

  return (
    <div>
      <h1>Team Dashboard</h1>
      <label>
        <input
          type="checkbox"
          checked={includeArchived}
          onChange={(e) => setIncludeArchived(e.target.checked)}
        />
        Include archived tasks
      </label>
      <ul>
        {data?.map((task) => (
          <li key={task.id}>{task.name}</li>
        ))}
      </ul>
    </div>
  );
}

 

์ด์ œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚ ๊นŒ์š”?

 

  • teamId ๋ณ€๊ฒฝ → ๊ฐ„๊ฒฉ ์žฌ์‹œ์ž‘, ์ƒˆ๋กœ์šด ํŒ€ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (์ •์ƒ!)
  • "๋ณด๊ด€๋œ ์ž‘์—… ํฌํ•จ" ํ† ๊ธ€ → ๊ฐ„๊ฒฉ์ด ์ค‘๋‹จ ์—†์ด ๊ณ„์† ์‹คํ–‰๋จ
  • ๋‹ค์Œ fetch → ํ˜„์žฌ includeArchived ๊ฐ’์„ ์‚ฌ์šฉ

 

๊ธฐ์กด์˜ ์ž„์‹œ ํ•ด๊ฒฐ์ฑ…: useRef

useEffectEvent ์ด์ „์—๋Š” ๊ฐ’์„ useRef์— ๋ฏธ๋Ÿฌ๋งํ•˜๋Š” ๊ฒƒ์ด ํ‘œ์ค€ ํŒจํ„ด์ด์—ˆ์Šต๋‹ˆ๋‹ค.

import { useEffect, useLayoutEffect, useState, useRef } from "react";

function Dashboard({ teamId }: { teamId: string }) {
  const [includeArchived, setIncludeArchived] = useState(false);
  const [data, setData] = useState(null);

  // 1๋‹จ๊ณ„: ref ์ƒ์„ฑ
  const includeArchivedRef = useRef(includeArchived);

  // 2๋‹จ๊ณ„: useLayoutEffect๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ref๋ฅผ state์™€ ๋™๊ธฐํ™” ์œ ์ง€
  // ์ด๋Š” useEffect ์ด์ „์— ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ref๊ฐ€ ์ตœ์‹ ์ž„์„ ๋ณด์žฅํ•จ
  useLayoutEffect(() => {
    includeArchivedRef.current = includeArchived;
  }, [includeArchived]);

  // 3๋‹จ๊ณ„: Effect์—์„œ ref ์ฝ๊ธฐ
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(
        `/api/team/${teamId}/tasks?archived=${includeArchivedRef.current}`
      );
      const json = await response.json();
      setData(json);
    };

    fetchData();
    const intervalId = setInterval(fetchData, 10000);

    return () => clearInterval(intervalId);
  }, [teamId]);

  // ... JSX
}

 

์™œ useEffect ๋Œ€์‹  useLayoutEffect๋ฅผ ์‚ฌ์šฉํ• ๊นŒ์š”?

 

"๊ทธ๋ƒฅ ๋‘ ๊ฐœ์˜ useEffect ํ›…์„ ์ˆœ์„œ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋˜๋‚˜์š”? ์ฒซ ๋ฒˆ์งธ๋Š” ref๋ฅผ ๋™๊ธฐํ™”ํ•˜๊ณ , ๋‘ ๋ฒˆ์งธ๋Š” ๊ทธ๊ฒƒ์„ ์ฝ๋Š” ์‹์œผ๋กœ์š”."๋ผ๊ณ  ๊ถ๊ธˆํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์‹ค์ œ๋กœ๋Š” ์ด๋Š” ์ข…์ข… ์ œ๋Œ€๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์„ ์–ธ๋œ ์ˆœ์„œ๋Œ€๋กœ effect๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ•จ์ •์€ ์—ฌ๊ธฐ์— ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ ๋ฌธ์„œ์— ์˜ํ•˜๋ฉด ์—ฌ๋Ÿฌ useEffect ํ›… ๊ฐ„์˜ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋ณด์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ๋Š” effect๊ฐ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ปค๋ฐ‹๋œ ํ›„์— ์‹คํ–‰๋œ๋‹ค๊ณ  ๋ช…์‹œํ•˜์ง€๋งŒ, ์—ฌ๋Ÿฌ effect ๊ฐ„์˜ ์ •ํ™•ํ•œ ์ˆœ์„œ—ํŠนํžˆ ๋™์‹œ ๋ Œ๋”๋ง, Suspense ๊ฒฝ๊ณ„ ๋˜๋Š” ๋ฏธ๋ž˜์˜ ๋ฆฌ์•กํŠธ ๋ฒ„์ „—๋Š” ๋ฆฌ์•กํŠธ์˜ ๊ณต๊ฐœ API ๊ธฐ๋Šฅ์ด ์•„๋‹™๋‹ˆ๋‹ค.

 

useLayoutEffect๋Š” ๋ช…์‹œ์ ์ธ ๋ณด์žฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด,

"useLayoutEffect๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ์‹คํ–‰๋˜๋Š” useEffect์˜ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค."

"๋ฆฌ์•กํŠธ๋Š” useLayoutEffect ๋‚ด๋ถ€์˜ ์ฝ”๋“œ์™€ ๊ทธ ์•ˆ์—์„œ ์˜ˆ์•ฝ๋œ ๋ชจ๋“  ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ์ฒ˜๋ฆฌ๋จ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค."

 

์ด๋Š” ์‹คํ–‰ ์ˆœ์„œ๊ฐ€ ์ž˜ ์ •์˜๋˜์–ด ์žˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

  1. ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง
  2. ๋ฆฌ์•กํŠธ๊ฐ€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ DOM์— ์ปค๋ฐ‹
  3. useLayoutEffect๊ฐ€ ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋จ → ref๊ฐ€ ๋™๊ธฐํ™”๋จ
  4. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๊ทธ๋ฆผ
  5. useEffect ์‹คํ–‰ → ์ด๋ฏธ ์—…๋ฐ์ดํŠธ๋œ ref๋ฅผ ์ฝ์Œ

 

๋™๊ธฐํ™”์— useLayoutEffect๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ, ๊ด€์ฐฐ๋˜์—ˆ์ง€๋งŒ ๋ช…์‹œ๋˜์ง€ ์•Š์€ ๊ตฌํ˜„ ์„ธ๋ถ€ ์‚ฌํ•ญ์ด ์•„๋‹Œ ๋ฌธ์„œํ™”๋˜๊ณ  ๋ณด์žฅ๋œ ๋™์ž‘์— ์˜์กดํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

์ด ํŒจํ„ด์˜ ๋‹จ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

  • ์ถ”๊ฐ€์ ์ธ useRef ์„ ์–ธ
  • ๋™๊ธฐํ™”๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ถ”๊ฐ€์ ์ธ useLayoutEffect
  • ์ƒํƒœ๊ฐ’์„ ์ง์ ‘ ์ฝ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ .current๋ฅผ ์ฝ์–ด์•ผ ํ•จ์„ ๊ธฐ์–ตํ•ด์•ผ ํ•จ
  • "์ œ์™ธ(escape)"์‹œ์ผœ์•ผ ํ•˜๋Š” ๋ชจ๋“  ๊ฐ’์— ๋Œ€ํ•ด ๋ฐ˜๋ณตํ•ด์•ผ ํ•จ
  • ์‹ค์ˆ˜๋กœ useEffect๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์›€ (์ž‘๋™ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ... ์–ธ์  ๊ฐ€ ๋ฉˆ์ถœ ๋•Œ๊นŒ์ง€)

 

useEffectEvent๋Š” ๋ฐ”๋กœ ์ด ํŒจํ„ด์„ ์ž๋™ํ™”ํ•˜๊ณ  ์ด๋Ÿฌํ•œ ์‹ค์ˆ˜์˜ ๊ฐ€๋Šฅ์„ฑ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

 

ํ•จ์ˆ˜ ๋™์ผ์„ฑ์— ๋Œ€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ

โš ๏ธ ํ”ํ•œ ํ˜ผ๋ž€: ๋ฐ˜ํ™˜๋œ ํ•จ์ˆ˜๋Š” ์•ˆ์ •์ ์ธ๊ฐ€์š”?

onMessage(useEffectEvent๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜)๋Š” ์•ˆ์ •์ ์ผ์ง€ ๊ถ๊ธˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. useCallback์ด ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ Œ๋”๋งํ•  ๋•Œ๋งˆ๋‹ค ๋™์ผํ•œ ์ฐธ์กฐ์ผ๊นŒ์š”?

์•„๋‹ˆ์š”, ๊ทธ๋ฆฌ๊ณ  ๊ทธ๊ฑด ์ค‘์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ๋Š” ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ๋กœ์šด onMessage ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ onMessage๋ฅผ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๋„ฃ์œผ๋ฉด, Effect๋Š” ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๋‹ค์‹œ ์‹คํ–‰๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์ž˜๋ชป ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋Š” ์‹ ํ˜ธ์ž…๋‹ˆ๋‹ค.

์—ฌ์ „ํžˆ ์ž‘๋™ํ•˜๋Š” ์ด์œ : ๋ชจ๋“  ๋ฒ„์ „์˜ onMessage (๋ Œ๋”๋ง 1, ๋ Œ๋”๋ง 2 ๋“ฑ์—์„œ์˜)๋Š” ๋™์ผํ•œ ๋‚ด๋ถ€ ref๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๋Š” ๊ทธ ref๋ฅผ ๊ณ„์† ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ตฌ๋…์ด ๋ Œ๋”๋ง 1์—์„œ ์บก์ฒ˜ํ•œ onMessage๋ฅผ ํ˜ธ์ถœํ•˜๋”๋ผ๋„, ์—ฌ์ „ํžˆ ํ˜„์žฌ ๊ฐ’์„ ๊ฐ€์ง„ ์ตœ์‹  ์ฝœ๋ฐฑ์„ ์ฝ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก : ์•ˆ์ •์„ฑ์— ๋Œ€ํ•ด ๊ฑฑ์ •ํ•˜์ง€ ๋งˆ์„ธ์š”. ๊ทœ์น™๋งŒ ๋”ฐ๋ฅด์„ธ์š”. Effect ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœํ•˜๊ณ , ์ ˆ๋Œ€๋กœ ์˜์กด์„ฑ์œผ๋กœ ๋‚˜์—ดํ•˜์ง€ ๋งˆ์„ธ์š”.

 

๋‚ด๋ถ€ ์ž‘๋™ ์›๋ฆฌ

๋งˆ๋ฒ•์€ ์—†์Šต๋‹ˆ๋‹ค. useEffectEvent๋Š” ์—ฌ๋Ÿฌ๋ถ„์ด useRef๋กœ ํ•˜๋˜ ์ผ์„ ์ •ํ™•ํžˆ ์ˆ˜ํ–‰ํ•˜์ง€๋งŒ, ๋ฆฌ์•กํŠธ๊ฐ€ ๋Œ€์‹  ์ฒ˜๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.

๊ฐœ๋…์  ๋ชจ๋ธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค(์‹ค์ œ ๋ฆฌ์•กํŠธ ๊ตฌํ˜„์€ ์•„๋‹˜).

function useEffectEvent<T extends (...args: any[]) => any>(callback: T): T {
  const latestCallbackRef = useRef(callback)

  // ๋ฆฌ์•กํŠธ๋Š” ์‹ค์ œ๋กœ ๋ Œ๋”๋ง์ด ์•„๋‹Œ ์ปค๋ฐ‹ ๋‹จ๊ณ„์—์„œ ์ด๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค
  // (๋ช…ํ™•์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ ๋‹จ์ˆœํ™”๋จ)
  latestCallbackRef.current = callback

  // ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ - ์˜๋„์ ์ž„!
  // ๋ชจ๋“  ๋ฒ„์ „์ด ๋™์ผํ•œ ref๋ฅผ ์ฝ์œผ๋ฏ€๋กœ ๋ชจ๋‘ ์ตœ์‹  ๊ฐ’์„ ์–ป์Œ
  return ((...args: Parameters<T>) => {
    return latestCallbackRef.current(...args)
  }) as T
}

 

ํ•ต์‹ฌ ํ†ต์ฐฐ: ๋ชจ๋“  ํ•จ์ˆ˜ ์ธ์Šคํ„ด์Šค๋Š” ๋™์ผํ•œ ref๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. Effect๊ฐ€ ๋ Œ๋”๋ง 1์—์„œ "์˜ค๋ž˜๋œ" ํ•จ์ˆ˜๋ฅผ ์บก์ฒ˜ํ–ˆ๋”๋ผ๋„, ์ด๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด latestCallbackRef.current๋ฅผ ์ฝ๊ฒŒ ๋˜๋ฉฐ, ์ด๋Š” ์ตœ์‹  ์ฝœ๋ฐฑ์„ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค.

 

์˜ค๋ž˜๋œ ํด๋กœ์ € ๋ฌธ์ œ ์‹œ๊ฐํ™”

 

useRef๊ฐ€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•

 

useEffectEvent๊ฐ€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•

 

๋™๋“ฑ์„ฑ

 

๊ทœ์น™ ๋ฐ ์ฃผ์˜์‚ฌํ•ญ

useEffectEvent์—๋Š” ์ค‘์š”ํ•œ ๊ทœ์น™๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค. eslint-plugin-react-hooks (๋ฒ„์ „ 6.1.1 ์ด์ƒ)๊ฐ€ ์ด๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.

 

๊ทœ์น™ 1: Effect ๋‚ด๋ถ€์—์„œ๋งŒ Effect Event๋ฅผ ํ˜ธ์ถœํ•˜์„ธ์š”

Effect Event๋Š” ๋‹จ ํ•˜๋‚˜์˜ ๋ชฉ์ ์„ ์œ„ํ•ด ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Effect ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœ๋˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

// โœ… ์˜ฌ๋ฐ”๋ฆ„: Effect ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœ๋จ
const onMessage = useEffectEvent((msg: string) => {
  console.log(msg, latestState);
});

useEffect(() => {
  socket.on("message", onMessage);
  return () => socket.off("message", onMessage);
}, []);

// โŒ ์ž˜๋ชป๋จ: ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ ํ˜ธ์ถœ๋จ
<button onClick={() => onMessage("hello")}>Click me</button>;

// โŒ ์ž˜๋ชป๋จ: ๋ Œ๋”๋ง ์ค‘์— ํ˜ธ์ถœ๋จ
return <div>{onMessage("rendered")}</div>;

 

๋ฆฌ์•กํŠธ๋Š” ์ด๋ฅผ ์ ๊ทน์ ์œผ๋กœ ๋ง‰์Šต๋‹ˆ๋‹ค. Effect ์ปจํ…์ŠคํŠธ ๋ฐ–์—์„œ Effect Event๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์ธ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ(onClick, onChange ๋“ฑ)์˜ ๊ฒฝ์šฐ useEffectEvent๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌํ•œ ํ•ธ๋“ค๋Ÿฌ๋Š” ๊ฐ ์ƒํ˜ธ์ž‘์šฉ(interaction)๋งˆ๋‹ค ํ˜„์žฌ ๊ฐ’์œผ๋กœ ์ƒˆ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

 

๊ทœ์น™ 2: ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์— Effect Event๋ฅผ ์ „๋‹ฌํ•˜์ง€ ๋งˆ์„ธ์š”

Effect Event๋ฅผ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์œ ์ง€ํ•˜์„ธ์š”.

// โŒ ์ž˜๋ชป๋จ: Effect Event๋ฅผ prop์œผ๋กœ ์ „๋‹ฌ
function Parent() {
  const onTick = useEffectEvent(() => {
    console.log(latestCount)
  })
  return <Timer onTick={onTick} /> // ์ด๋ ‡๊ฒŒ ํ•˜์ง€ ๋งˆ์„ธ์š”!
}

// โœ… ์˜ฌ๋ฐ”๋ฆ„: Effect Event๋ฅผ ๋กœ์ปฌ๋กœ ์œ ์ง€
function Parent() {
  const [count, setCount] = useState(0)
  const onTick = useEffectEvent(() => {
    console.log(count)
  })

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

  return <div>Count: {count}</div>
}

 

๊ทœ์น™ 3: ๋ฆฐํ„ฐ ๊ฒฝ๊ณ ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด useEffectEvent๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”

์ด๊ฒƒ์€ ์˜๋„์— ๊ด€ํ•œ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. "์ด ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ, Effect๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š”๊ฐ€?" ์Šค์Šค๋กœ์—๊ฒŒ ๋ฌผ์–ด๋ณด์„ธ์š”.

 

  • ์˜ˆ → ์˜์กด์„ฑ์ž…๋‹ˆ๋‹ค. ์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•˜์„ธ์š”.
  • ์•„๋‹ˆ์š” → ๋กœ์ง์„ Effect Event๋กœ ๊ฐ์‹ธ์„ธ์š”.
// โŒ ์ž˜๋ชป๋จ: page๋ฅผ ์˜์กด์„ฑ์œผ๋กœ ๋‚˜์—ดํ•˜๋Š” ๊ฒƒ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด useEffectEvent ์‚ฌ์šฉ
const fetchData = useEffectEvent(async () => {
  const data = await fetch(`/api/items?page=${page}`)
  setItems(data)
})

useEffect(() => {
  fetchData()
}, []) // "์ด์ œ deps์— page๊ฐ€ ํ•„์š” ์—†์–ด!" <- ์ž˜๋ชป๋œ ์ƒ๊ฐ!

// โœ… ์˜ฌ๋ฐ”๋ฆ„: page๋Š” ์˜์กด์„ฑ์ด์–ด์•ผ ํ•จ - ๋ณ€๊ฒฝ๋  ๋•Œ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ๋ฅผ ์›ํ•จ
useEffect(() => {
  async function fetchData() {
    const data = await fetch(`/api/items?page=${page}`)
    setItems(data)
  }
  fetchData()
}, [page])

 

์–ธ์ œ useEffectEvent๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ์š”?

Effect ๋‚ด๋ถ€์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝœ๋ฐฑ์ด ์žˆ์„ ๋•Œ ์‚ฌ์šฉํ•˜์„ธ์š”.

 

  1. ์žฌ๋“ฑ๋กํ•˜๊ณ  ์‹ถ์ง€ ์•Š์€ ๊ตฌ๋…, ํƒ€์ด๋จธ, ๋˜๋Š” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ „๋‹ฌ๋จ
  2. ํ˜ธ์ถœ๋  ๋•Œ ์ตœ์‹  props/state๋ฅผ ์ฝ์–ด์•ผ ํ•จ
  3. ๊ทธ๋Ÿฌํ•œ ๊ฐ’๋“ค์ด Effect์˜ ์žฌ์‹คํ–‰์„ ํŠธ๋ฆฌ๊ฑฐํ•˜์ง€ ์•Š์•„์•ผ ํ•จ

 

์ƒํ™ฉ ๋ฐ˜์‘ํ˜• (Effect ์‹คํ–‰ ์œ ๋ฐœ) ๋น„๋ฐ˜์‘ํ˜• (Effect Event)
์ฑ„ํŒ…๋ฐฉ ์—ฐ๊ฒฐ roomId isMuted, theme
ํด๋ง ๋Œ€์‹œ๋ณด๋“œ teamId includeArchived, sortOrder
๋ถ„์„ ๋กœ๊น… pageUrl cartItemCount, userId
์›น์†Œ์ผ“ ๋ฉ”์‹œ์ง€ socketUrl isOnline, preferences
๊ฐ„๊ฒฉ ํƒ€์ด๋จธ - (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋จ) count, step

 

๋ฆฌ์•กํŠธ ๋ฒ„์ „ ์ฐธ๊ณ  ์‚ฌํ•ญ

useEffectEvent๋Š” ๋ฆฌ์•กํŠธ 19.2์—์„œ ์•ˆ์ •์ ์ธ ๊ธฐ๋Šฅ์œผ๋กœ ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ „ ๋ฒ„์ „์„ ์‚ฌ์šฉ ์ค‘์ด๋ผ๋ฉด,

 

  • ๋ฆฌ์•กํŠธ 18.x ๋ฐ ์ด์ „: ์œ„์—์„œ ์„ค๋ช…ํ•œ useRef ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ๋ฆฌ์•กํŠธ 19.0-19.1: useEffectEvent๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ ์‹คํ—˜์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ฆฌ์•กํŠธ 19.2+: ์ž์‹  ์žˆ๊ฒŒ useEffectEvent๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

์‚ฌ์šฉ ์ค‘์ธ ๋ฆฌ์•กํŠธ ๋ฒ„์ „์„ ๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ํ™•์ธํ•˜์„ธ์š”.

npm list react

 

์š”์•ฝ

useEffectEvent๋Š” ํ•˜๋‚˜์˜ ํŠน์ • ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. Effect ์ฝœ๋ฐฑ ๋‚ด์—์„œ ์ตœ์‹  ๊ฐ’์„ ์ฝ์œผ๋ฉด์„œ๋„, ๊ทธ ๊ฐ’๋“ค์ด Effect๋ฅผ ๋‹ค์‹œ ์‹คํ–‰์‹œํ‚ค์ง€ ์•Š๋„๋ก ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๊ทธ๊ฒŒ ์ „๋ถ€์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋งˆ๋ฒ•์€ ์—†์Šต๋‹ˆ๋‹ค.

 

๋ฉ˜ํƒˆ ๋ชจ๋ธ:

  • ์˜์กด์„ฑ์˜ ๋Œ€๋‹ต: "์ด Effect๋Š” ์–ธ์ œ ๋‹ค์‹œ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š”๊ฐ€?"
  • Effect Event์˜ ๋Œ€๋‹ต: "Effect์˜ ์ฝœ๋ฐฑ์ด ์‹คํ–‰๋  ๋•Œ ์–ด๋–ค ๊ฐ’์„ ์ฝ์–ด์•ผ ํ•˜๋Š”๊ฐ€?"

์ด๋Ÿฌํ•œ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•จ์œผ๋กœ์จ ์ฝ”๋“œ๋Š” ๋” ๋ช…ํ™•ํ•ด์ง€๊ณ  ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•  ์—ฌ์ง€๊ฐ€ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.

 

์ถ”๊ฐ€ ์ฝ์„๊ฑฐ๋ฆฌ

๋ฐ˜์‘ํ˜•

๐Ÿ’ฌ ๋Œ“๊ธ€