
์๋ฌธ: 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๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค,
- ๋ฆฌ์กํธ๊ฐ ํด๋ฆฐ์
ํจ์๋ฅผ ์คํ →
connection.disconnect() - ๋ฆฌ์กํธ๊ฐ Effect๋ฅผ ๋ค์ ์คํ →
connectToRoom(roomId) - ์๋ก์ด ๋ฉ์์ง ํธ๋ค๋ฌ๊ฐ ๋ฑ๋ก๋จ
์์๊ฑฐ ์ฒดํฌ๋ฐ์ค๋ฅผ ํ ๊ธํ๋ฉด ์ฑํ ์ด ๋ค์ ์ฐ๊ฒฐ๋ฉ๋๋ค! ์ฌ์ฐ๊ฒฐ ์ค์ ๋ฉ์์ง๋ฅผ ๋์น ์๋ ์์ต๋๋ค. ์๋ฒ๋ ์ฐ๊ฒฐ์ด ์์ฃผ ๋๊ฒผ๋ค ๋ถ์๋ค ํ๋ ๊ฒ์ ๋ณด๊ฒ ๋ฉ๋๋ค. ์ด๋ ๋ญ๋น์ด๊ณ ๊ณ ์ฅ๋ ๊ฒ์ ๋๋ค.
ํด๊ฒฐ์ฑ : 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)"์ ํ ๊ธํ ๋๋ง๋ค,
- Effect ํด๋ฆฐ์
์คํ →
clearInterval(intervalId) - 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 ๋ด๋ถ์ ์ฝ๋์ ๊ทธ ์์์ ์์ฝ๋ ๋ชจ๋ ์ํ ์ ๋ฐ์ดํธ๊ฐ ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ฉด์ ๋ค์ ๊ทธ๋ฆฌ๊ธฐ ์ ์ ์ฒ๋ฆฌ๋จ์ ๋ณด์ฅํฉ๋๋ค."
์ด๋ ์คํ ์์๊ฐ ์ ์ ์๋์ด ์์์ ์๋ฏธํฉ๋๋ค.
- ์ปดํฌ๋ํธ ๋ ๋๋ง
- ๋ฆฌ์กํธ๊ฐ ๋ณ๊ฒฝ ์ฌํญ์ DOM์ ์ปค๋ฐ
useLayoutEffect๊ฐ ๋๊ธฐ์ ์ผ๋ก ์คํ๋จ → ref๊ฐ ๋๊ธฐํ๋จ- ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ฉด์ ๊ทธ๋ฆผ
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 ๋ด๋ถ์ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋ฐฑ์ด ์์ ๋ ์ฌ์ฉํ์ธ์.
- ์ฌ๋ฑ๋กํ๊ณ ์ถ์ง ์์ ๊ตฌ๋ , ํ์ด๋จธ, ๋๋ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ๋ฌ๋จ
- ํธ์ถ๋ ๋ ์ต์ props/state๋ฅผ ์ฝ์ด์ผ ํจ
- ๊ทธ๋ฌํ ๊ฐ๋ค์ด 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์ ์ฝ๋ฐฑ์ด ์คํ๋ ๋ ์ด๋ค ๊ฐ์ ์ฝ์ด์ผ ํ๋๊ฐ?"
์ด๋ฌํ ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํจ์ผ๋ก์จ ์ฝ๋๋ ๋ ๋ช ํํด์ง๊ณ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ ์ฌ์ง๊ฐ ์ค์ด๋ญ๋๋ค.
์ถ๊ฐ ์ฝ์๊ฑฐ๋ฆฌ
- React Docs: useEffectEvent Reference - ๊ณต์ API ๋ฌธ์
- React Docs: Separating Events from Effects - ์ฌ์ธต์ ์ธ ๊ฐ๋ ๊ฐ์ด๋
- React Docs: useEffect Reference - ํฌ๊ด์ ์ธ Effect ๋ฌธ์
- MDN: Closures - ์ค๋๋ ํด๋ก์ (Stale Closures)์ ๋ฐฐ๊ฒฝ์ด ๋๋ ์๋ฐ์คํฌ๋ฆฝํธ ๊ฐ๋ ์ดํดํ๊ธฐ
๐ฌ ๋๊ธ