React 19 useEffectEvent์ Activity ์๋ฒฝ ๊ฐ์ด๋: ์๋ก์ด API์ ๋ฉด์ ์ง๋ฌธ 2026
React 19.2์์ ๋์ ๋ useEffectEvent์ Activity ์ปดํฌ๋ํธ์ ๋์ ์๋ฆฌ, ์ค์ ํจํด, ๋ฉด์ ํต์ฌ ์ง๋ฌธ์ ์ฒด๊ณ์ ์ผ๋ก ๋ค๋ฃน๋๋ค.

React ์ํ๊ณ๋ ๊พธ์คํ ์งํํ๊ณ ์์ผ๋ฉฐ, 2025๋
ํ๋ฐ๊ธฐ์ ์ถ์๋ React 19.2๋ ๊ทธ๋์ ๊ฐ๋ฐ์๋ค์ด ๊ฒช์ด์จ ๋ ๊ฐ์ง ํต์ฌ ๋ฌธ์ ๋ฅผ ์ ๋ฉด์ผ๋ก ํด๊ฒฐํ์ต๋๋ค. ์ฒซ ๋ฒ์งธ๋ Effect ๋ด๋ถ์ stale closure ๋ฌธ์ ๋ฅผ ๊ทผ๋ณธ์ ์ผ๋ก ์ ๊ฑฐํ๋ useEffectEvent ํ
์ด๊ณ , ๋ ๋ฒ์งธ๋ ์ปดํฌ๋ํธ์ ์ํ๋ฅผ ๋ณด์กดํ๋ฉด์ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ๋ฆฌ๋ ๋๋ง์ ๊ฐ๋ฅํ๊ฒ ํ๋ <Activity> ์ปดํฌ๋ํธ์
๋๋ค.
์ด ๋ API๋ ๋จ์ํ ํธ์ ๊ธฐ๋ฅ์ด ์๋๋๋ค. React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ด๋ ์ดํํธ ๊ด๋ฆฌ ๋ฐฉ์๊ณผ ๋ค๋น๊ฒ์ด์ ํจํด์ ๊ทผ๋ณธ์ ์ผ๋ก ๋ณํ์ํค๊ณ ์์ผ๋ฉฐ, 2026๋ ํ๋ก ํธ์๋ ๋ฉด์ ์์ ํต์ฌ ์ง๋ฌธ์ผ๋ก ์๋ฆฌ์ก๊ณ ์์ต๋๋ค. ์ด ๊ธ์์๋ ๊ฐ API์ ๋์ ์๋ฆฌ, ์ค์ ์ฌ์ฉ ํจํด, ์ฃผ์์ฌํญ, ๊ทธ๋ฆฌ๊ณ ๋ฉด์ ๋๋น๋ฅผ ์ํ ํต์ฌ ์ง์์๋ต๊น์ง ์ฒด๊ณ์ ์ผ๋ก ๋ค๋ฃน๋๋ค.
useEffectEvent์ Activity๋ React 19.2 ์ด์์์ ์ฌ์ฉํ ์ ์์ต๋๋ค. npm install react@latest react-dom@latest๋ก ์
๋ฐ์ดํธํ์ญ์์ค. ESLint ํ๋ฌ๊ทธ์ธ eslint-plugin-react-hooks@6+๋ useEffectEvent์ ์์กด์ฑ ๋ฐฐ์ด ๊ฒ์ฌ๋ฅผ ๋ค์ดํฐ๋ธ๋ก ์ง์ํฉ๋๋ค.
Stale Closure ๋ฌธ์ : useEffectEvent ์ด์ ์ ์ธ๊ณ
React์์ useEffect๋ฅผ ์ฌ์ฉํ ๋ ๊ฐ์ฅ ๋น๋ฒํ๊ฒ ๋ฐ์ํ๋ ๋ฒ๊ทธ ์ค ํ๋๊ฐ stale closure ๋ฌธ์ ์
๋๋ค. Effect ์ฝ๋ฐฑ ํจ์๊ฐ ์์ฑ ์์ ์ ๋ณ์ ๊ฐ์ ์บก์ฒํ์ฌ, ์ดํ ํด๋น ๋ณ์๊ฐ ๋ณ๊ฒฝ๋๋๋ผ๋ ๊ณผ๊ฑฐ ๊ฐ์ ์ฐธ์กฐํ๋ ํ์์ ๋งํฉ๋๋ค.
์ ํต์ ์ธ ํด๊ฒฐ ๋ฐฉ๋ฒ์ useRef๋ฅผ ํ์ฉํ๋ ๊ฒ์ด์์ต๋๋ค. ์๋ ์ฝ๋๋ ์ฑํ
๋ฐฉ ์ฐ๊ฒฐ ํ
์์ theme ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๋ถํ์ํ ์ฌ์ฐ๊ฒฐ์ ๋ฐฉ์งํ๋ฉด์๋ ์ต์ ๊ฐ์ ์ฐธ์กฐํด์ผ ํ๋ ์ ํ์ ์ธ ์ํฉ์ ๋ณด์ฌ์ค๋๋ค.
// Before useEffectEvent: useRef workaround
import { useEffect, useRef } from 'react'
export function useChatRoom(roomId: string, theme: string) {
// Store theme in a ref to avoid stale closure
const themeRef = useRef(theme)
themeRef.current = theme
useEffect(() => {
const connection = createConnection(roomId)
connection.on('message', (msg) => {
// themeRef.current always has the latest value
logAnalytics('new_message', { roomId, theme: themeRef.current })
showNotification(msg)
})
connection.connect()
return () => connection.disconnect()
}, [roomId]) // theme intentionally excluded โ but linter warns
}์ด ํจํด์๋ ์ฌ๋ฌ ๋ฌธ์ ๊ฐ ์กด์ฌํฉ๋๋ค. useRef๋ฅผ ํตํ ์ต์ ๊ฐ ๋๊ธฐํ๋ React์ ์ ์ธ์ ๋ชจ๋ธ๊ณผ ์ด๊ธ๋๋ฉฐ, ์์กด์ฑ ๋ฐฐ์ด์์ theme๋ฅผ ์๋์ ์ผ๋ก ์ ์ธํด์ผ ํ๋ฏ๋ก ESLint์ exhaustive-deps ๊ท์น๊ณผ ์ถฉ๋ํฉ๋๋ค. ์ฝ๋๊ฐ ๋ณต์กํด์ง์๋ก ์ด๋ค ๊ฐ์ด ref๋ก ๊ด๋ฆฌ๋๊ณ ์๋์ง ์ถ์ ํ๊ธฐ ์ด๋ ค์์ง๋ฉฐ, ์ด๋ ์ ์ง๋ณด์์ฑ์ ํฌ๊ฒ ์ ํ์ํต๋๋ค.
useEffectEvent: ๋ฐ์ํ ๋ก์ง๊ณผ ๋น๋ฐ์ํ ๋ก์ง์ ๋ถ๋ฆฌ
useEffectEvent๋ ์ด ๋ฌธ์ ๋ฅผ ์ฐ์ํ๊ฒ ํด๊ฒฐํฉ๋๋ค. ์ด ํ
์ผ๋ก ์์ฑ๋ ํจ์๋ ํญ์ ์ต์ props์ state๋ฅผ ์ฝ์ง๋ง, Effect์ ์์กด์ฑ์ผ๋ก ์ทจ๊ธ๋์ง ์์ต๋๋ค. ์ฆ, "Effect๊ฐ ์ธ์ ์คํ๋ ์ง"์ "์คํ ์ ์ด๋ค ๊ฐ์ ์ฌ์ฉํ ์ง"๋ฅผ ๋ช
ํํ๊ฒ ๋ถ๋ฆฌํ ์ ์์ต๋๋ค.
// After useEffectEvent: clean separation of concerns
import { useEffect, useEffectEvent } from 'react'
export function useChatRoom(roomId: string, theme: string) {
// Effect Event: always reads latest theme, never triggers reconnect
const onMessage = useEffectEvent((msg: string) => {
logAnalytics('new_message', { roomId, theme })
showNotification(msg)
})
useEffect(() => {
const connection = createConnection(roomId)
connection.on('message', onMessage)
connection.connect()
return () => connection.disconnect()
}, [roomId]) // No linter warning โ onMessage is an Effect Event
}๋ณ๊ฒฝ๋ ์ฝ๋์์ ์ฃผ๋ชฉํ ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. onMessage๋ useEffectEvent๋ก ๋ํ๋์ด ์์ผ๋ฏ๋ก theme๊ฐ ๋ณ๊ฒฝ๋๋๋ผ๋ Effect๊ฐ ๋ค์ ์คํ๋์ง ์์ต๋๋ค. ๊ทธ๋ฌ๋ฉด์๋ onMessage ๋ด๋ถ์์๋ ํญ์ ์ต์ theme ๊ฐ์ ์ ๊ทผํ ์ ์์ต๋๋ค. useRef๊ฐ ์์ ํ ์ ๊ฑฐ๋์๊ณ , ESLint ๊ฒฝ๊ณ ๋ ๋ฐ์ํ์ง ์์ต๋๋ค.
useEffectEvent์ ๊ท์น๊ณผ ์ ์ฝ์ฌํญ
useEffectEvent๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด ๋ฐ๋์ ์ค์ํด์ผ ํ๋ ๊ท์น๋ค์ด ์์ต๋๋ค. ์ด ๊ท์น๋ค์ React์ ๋ด๋ถ ๋์๊ณผ ์ง๊ฒฐ๋๋ฏ๋ก, ๋ฉด์ ์์๋ ์ ํํ ์ดํด๋ฅผ ์๊ตฌํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
์ฒซ์งธ, useEffectEvent๋ก ์์ฑ๋ ํจ์๋ ๋ฐ๋์ useEffect ๋ด๋ถ์์ ํธ์ถํด์ผ ํฉ๋๋ค. ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ๋ ๋๋ง ๋ก์ง์์ ์ง์ ํธ์ถํ๋ฉด ์ ๋ฉ๋๋ค. ์ด ํจ์์ ์ ์ฒด์ฑ์ "Effect์ ๋น๋ฐ์ํ ๋ถ๋ถ"์ด๋ฉฐ, Effect ์ปจํ
์คํธ ๋ฐ์์๋ ๊ทธ ์๋ฏธ๊ฐ ์ฑ๋ฆฝํ์ง ์์ต๋๋ค.
๋์งธ, Effect Event ํจ์๋ฅผ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ ํ ์ props๋ก ์ ๋ฌํ ์ ์์ต๋๋ค. React ๋ด๋ถ์ ์ผ๋ก ์ด ํจ์๋ ํน์ํ ๋ผ์ดํ์ฌ์ดํด์ ๊ฐ๊ณ ์์ผ๋ฉฐ, ์ธ๋ถ๋ก ์ ๋ฌํ๋ฉด ํธ์ถ ์์ ์ ๋ํ ๋ณด์ฅ์ด ๊นจ์ง๋๋ค.
์
์งธ, useEffectEvent๋ ์ปดํฌ๋ํธ ์ต์์ ๋ ๋ฒจ ๋๋ ์ปค์คํ
ํ
๋ด๋ถ์์๋ง ์ ์ธํ ์ ์์ต๋๋ค. ์กฐ๊ฑด๋ฌธ์ด๋ ๋ฐ๋ณต๋ฌธ ์์์ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ์ด๋ ๋ค๋ฅธ React ํ
์ ๊ท์น๊ณผ ๋์ผํฉ๋๋ค.
์๋๋ ๊ฒ์ ์ถ์ ์ปดํฌ๋ํธ์์ useEffectEvent๋ฅผ ์ค์ ํ์ฉํ๋ ์์์
๋๋ค. userId๊ฐ ๋ณ๊ฒฝ๋๋๋ผ๋ ๊ฒ์ Effect๋ query์๋ง ๋ฐ์ํ๋ฉด์, ๋ถ์ ์ด๋ฒคํธ ์ ์ก ์์๋ ํญ์ ์ต์ userId๋ฅผ ์ฌ์ฉํฉ๋๋ค.
// Correct: Effect Event used inside useEffect
import { useEffect, useEffectEvent, useState } from 'react'
interface SearchTrackerProps {
query: string
userId: string
}
export function SearchTracker({ query, userId }: SearchTrackerProps) {
const [results, setResults] = useState<string[]>([])
// Track searches with current user context
const onSearchComplete = useEffectEvent((resultCount: number) => {
analytics.track('search_complete', {
query,
userId, // Always latest userId
resultCount,
timestamp: Date.now(),
})
})
useEffect(() => {
const controller = new AbortController()
fetchSearchResults(query, controller.signal).then((data) => {
setResults(data)
onSearchComplete(data.length) // Called from inside useEffect
})
return () => controller.abort()
}, [query]) // userId excluded safely โ lives in the Effect Event
return <ResultsList results={results} />
}useEffectEvent๋ ์์กด์ฑ ๋ฆฐํฐ ๊ฒฝ๊ณ ๋ฅผ ๋ฌด์ํ๊ธฐ ์ํ ๋๊ตฌ๊ฐ ์๋๋๋ค. ํน์ ๊ฐ์ด Effect์ ์ฌ์คํ ์์ ์ ์ ์ดํด์ผ ํ๋ ๊ฒฝ์ฐ, ํด๋น ๊ฐ์ ์์กด์ฑ ๋ฐฐ์ด์ ํฌํจํ๋ ๊ฒ์ด ์ฌ๋ฐ๋ฅธ ์ ๊ทผ์ ๋๋ค. useEffectEvent๋ ๋ก๊น , ๋ถ์, ์๋ฆผ ๋ฑ ๋ฐ์ํ ๊ฐ์ ์ฝ๋ Effect ์ฌ์คํ์ ์ ๋ฐํ์ง ์์์ผ ํ๋ ๋ถ์์ ๋ก์ง์๋ง ์ฌ์ฉํด์ผ ํฉ๋๋ค.
Activity ์ปดํฌ๋ํธ: ์ํ ๋ณด์กดํ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ๋ฆฌ๋ ๋๋ง
React 19.2์์ ๋์
๋ <Activity> ์ปดํฌ๋ํธ๋ ์ด์ ์ <Offscreen>์ด๋ผ๋ ์ด๋ฆ์ผ๋ก ์๋ ค์ ธ ์๋ API์ ์ ์ ๋ฒ์ ์
๋๋ค. ์ด ์ปดํฌ๋ํธ์ ํต์ฌ ๊ฐ์น๋ ํ๋ฉด์์ ์ฌ๋ผ์ง UI์ ์ํ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ณด์กดํ๋ฉด์๋, ์จ๊ฒจ์ง ์ปดํฌ๋ํธ์ Effect๋ฅผ ์ ๋ฆฌ(cleanup)ํ์ฌ ๋ฆฌ์์ค ๋์๋ฅผ ๋ฐฉ์งํ๋ ๊ฒ์
๋๋ค.
๋ค์ ํ๋ mode ์์ฑ์ ๋ฐ๋ฅธ Activity์ ๋์ ์ฐจ์ด๋ฅผ ์ ๋ฆฌํ ๊ฒ์
๋๋ค.
| ๋์ | mode="visible" | mode="hidden" |
|----------|-------------------|------------------|
| DOM ๋ ๋๋ง | ์ ์ ๋ ๋๋ง | CSS display: none ์ฒ๋ฆฌ |
| ์ปดํฌ๋ํธ ์ํ | ํ์ฑ | ๋ฉ๋ชจ๋ฆฌ์ ๋ณด์กด |
| Effect (useEffect) | ๋ง์ดํธ๋จ | ํด๋ฆฐ์
์คํ |
| ์
๋ฐ์ดํธ ์ฐ์ ์์ | ์ผ๋ฐ | ์ ํด ์๊ฐ์ ์ง์ฐ ์ฒ๋ฆฌ |
| ํ๋ฆฌ๋ ๋๋ง | ํด๋น ์์ | ๋ฎ์ ์ฐ์ ์์๋ก ๋ ๋๋ง |
๊ฐ์ฅ ๋ํ์ ์ธ ํ์ฉ ์ฌ๋ก๋ ํญ ๋ ์ด์์์ ๋๋ค. ์ฌ์ฉ์๊ฐ ํญ์ ์ ํํด๋ ์ด์ ํญ์ ํผ ์ ๋ ฅ๊ฐ, ์คํฌ๋กค ์์น, ์ปดํฌ๋ํธ ์ํ๊ฐ ๋ชจ๋ ์ ์ง๋ฉ๋๋ค.
// Activity preserves form state across tab switches
import { useState } from 'react'
import { Activity } from 'react'
interface Tab {
id: string
label: string
content: React.ReactNode
}
export function TabLayout({ tabs }: { tabs: Tab[] }) {
const [activeTab, setActiveTab] = useState(tabs[0].id)
return (
<div>
<nav className="flex gap-2 border-b border-border">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'border-b-2 border-primary' : ''}
>
{tab.label}
</button>
))}
</nav>
{tabs.map((tab) => (
<Activity key={tab.id} mode={activeTab === tab.id ? 'visible' : 'hidden'}>
{tab.content}
</Activity>
))}
</div>
)
}๊ธฐ์กด์ ์กฐ๊ฑด๋ถ ๋ ๋๋ง({activeTab === tab.id && <Content />})์ ์ฌ์ฉํ๋ฉด ํญ ์ ํ ์๋ง๋ค ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ๋๊ณ ๋ค์ ๋ง์ดํธ๋๋ฉด์ ๋ชจ๋ ์ํ๊ฐ ์ด๊ธฐํ๋ฉ๋๋ค. <Activity>๋ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ฉด์๋, mode="hidden" ์ํ์์๋ Effect๋ฅผ ์ ๋ฆฌํ์ฌ WebSocket ์ฐ๊ฒฐ์ด๋ ํ์ด๋จธ ๊ฐ์ ๋ฆฌ์์ค๊ฐ ๋ถํ์ํ๊ฒ ์ ์ง๋๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.
React / Next.js ๋ฉด์ ์ค๋น๊ฐ ๋์ จ๋์?
์ธํฐ๋ํฐ๋ธ ์๋ฎฌ๋ ์ดํฐ, flashcards, ๊ธฐ์ ํ ์คํธ๋ก ์ฐ์ตํ์ธ์.
๋ฐฑ๊ทธ๋ผ์ด๋ ํ๋ฆฌ๋ ๋๋ง๊ณผ Suspense ํตํฉ
<Activity>์ ๋ ๋ค๋ฅธ ๊ฐ๋ ฅํ ํ์ฉ์ฒ๋ ์ฌ์ฉ์๊ฐ ๋ค์์ ๋ฐฉ๋ฌธํ ๊ฐ๋ฅ์ฑ์ด ๋์ ๋ผ์ฐํธ๋ฅผ ๋ฏธ๋ฆฌ ๋ ๋๋งํ๋ ๊ฒ์
๋๋ค. Suspense์ ๊ฒฐํฉํ๋ฉด ๋ฐ์ดํฐ ํ์นญ๊น์ง ํฌํจํ ์ ์ฒด UI๋ฅผ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ค๋นํ ์ ์์ต๋๋ค.
// Pre-render the next likely route in the background
import { Activity, Suspense, use } from 'react'
interface PrerenderedRouteProps {
isActive: boolean
dataPromise: Promise<DashboardData>
}
export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) {
return (
<Activity mode={isActive ? 'visible' : 'hidden'}>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent dataPromise={dataPromise} />
</Suspense>
</Activity>
)
}
function DashboardContent({ dataPromise }: { dataPromise: Promise<DashboardData> }) {
// use() reads the cached promise โ works during hidden pre-render
const data = use(dataPromise)
return (
<div className="grid grid-cols-3 gap-4">
<MetricsCard data={data.metrics} />
<ChartPanel data={data.charts} />
<RecentActivity items={data.activity} />
</div>
)
}mode="hidden" ์ํ์์๋ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ ๋ ๋๋ง๋๋ฏ๋ก, use()๋ฅผ ํตํ Promise ์ฝ๊ธฐ์ Suspense ๋ฐ์ด๋๋ฆฌ ์ฒ๋ฆฌ๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์๋ฃ๋ฉ๋๋ค. ์ฌ์ฉ์๊ฐ ํด๋น ๋ผ์ฐํธ๋ก ์ด๋ํ๋ฉด mode๊ฐ "visible"๋ก ์ ํ๋๋ฉฐ, ์ด๋ฏธ ๋ ๋๋ง์ด ์๋ฃ๋ UI๊ฐ ์ฆ์ ํ์๋ฉ๋๋ค.
Activity์ TanStack Query ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
<Activity>๋ฅผ TanStack Query์ ํจ๊ป ์ฌ์ฉํ ๋ ๋ฐ๋์ ์์์ผ ํ ์ค์ํ ๋์ ํน์ฑ์ด ์์ต๋๋ค. mode="hidden" ์ํ์์๋ Effect๊ฐ ํด๋ฆฐ์
๋๋ฏ๋ก, useQuery ํ
์ด ๋ฐ์ดํฐ ํ์นญ์ ์ํํ์ง ์์ต๋๋ค. ์ด๋ ์๋๋ ๋์์ด์ง๋ง, ํ๋ฆฌํ์นญ์ ๊ธฐ๋ํ๋ ๊ฒฝ์ฐ ์์๊ณผ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ํ ์ ์์ต๋๋ค.
// Problem: useQuery won't fetch when Activity is hidden
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'
function UserStats() {
// This useQuery will NOT run while hidden
const { data } = useQuery({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return <StatsDisplay data={data} />
}
// Solution: prefetch data outside the Activity boundary
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
const queryClient = useQueryClient()
// Prefetch at the parent level โ runs regardless of Activity mode
queryClient.ensureQueryData({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return (
<Activity mode={showStats ? 'visible' : 'hidden'}>
<UserStats />
</Activity>
)
}ํด๊ฒฐ ๋ฐฉ๋ฒ์ <Activity> ๋ฐ์ด๋๋ฆฌ ๋ฐ๊นฅ์์, ์ฆ ๋ถ๋ชจ ์ปดํฌ๋ํธ ๋ ๋ฒจ์์ queryClient.ensureQueryData()๋ฅผ ํธ์ถํ๋ ๊ฒ์
๋๋ค. ์ด ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ฉด Activity์ mode์ ๊ด๊ณ์์ด ๋ฐ์ดํฐ๊ฐ TanStack Query ์บ์์ ๋ฏธ๋ฆฌ ์ ์ฌ๋๋ฉฐ, <Activity>๊ฐ "visible"๋ก ์ ํ๋ ๋ useQuery๋ ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ๋ฐํํฉ๋๋ค.
Activity๋ ์๋๋ฅผ ์ํด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ํธ๋ ์ด๋์คํ๋ฅผ ์๋ฐํฉ๋๋ค. ์จ๊ฒจ์ง ๊ฐ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ ์ ์ฒด DOM๊ณผ ํจ๊ป ๋ฉ๋ชจ๋ฆฌ์ ์ ์ง๋ฉ๋๋ค. ์จ๊ฒจ์ง ๋ผ์ฐํธ๊ฐ ๋ง์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ๋ชจ๋ํฐ๋งํ๋ ๊ฒ์ด ๊ถ์ฅ๋ฉ๋๋ค.
useEffectEvent์ Activity์ ๊ฒฐํฉ: ์ค์๊ฐ ๋์๋ณด๋ ํจํด
๋ API๋ฅผ ๊ฒฐํฉํ๋ฉด ์ค์๊ฐ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ์ฒ๋ฆฌํ๋ฉด์๋ ํจ์จ์ ์ธ ํญ ์ ํ์ ์ง์ํ๋ ๊ณ ๊ธ ํจํด์ ๊ตฌํํ ์ ์์ต๋๋ค. ์๋ ์์๋ ์ฌ๋ฌ ์ฑ๋์ ์ค์๊ฐ ํผ๋๋ฅผ WebSocket์ผ๋ก ์์ ํ๋, ํ์ฑ ์ฑ๋๋ง ์ฐ๊ฒฐ์ ์ ์งํ๊ณ , ๋ถ์ ์ถ์ ์๋ ํญ์ ์ต์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฌ์ฉํ๋ ๋ผ์ด๋ธ ๋์๋ณด๋์ ๋๋ค.
import { useEffect, useEffectEvent, useState } from 'react'
import { Activity } from 'react'
function LiveFeed({ channel, userId }: { channel: string; userId: string }) {
const [messages, setMessages] = useState<Message[]>([])
// Analytics tracking with latest userId โ no effect re-sync
const onNewMessage = useEffectEvent((msg: Message) => {
analytics.track('live_message', { channel, userId })
setMessages((prev) => [...prev, msg])
})
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/${channel}`)
ws.onmessage = (event) => {
const msg = JSON.parse(event.data) as Message
onNewMessage(msg)
}
return () => ws.close()
}, [channel]) // Clean reconnect only when channel changes
return <MessageList messages={messages} />
}
export function LiveDashboard({ userId }: { userId: string }) {
const [activeChannel, setActiveChannel] = useState('general')
const channels = ['general', 'alerts', 'metrics']
return (
<div>
<nav className="flex gap-2">
{channels.map((ch) => (
<button key={ch} onClick={() => setActiveChannel(ch)}>
{ch}
</button>
))}
</nav>
{channels.map((ch) => (
<Activity key={ch} mode={activeChannel === ch ? 'visible' : 'hidden'}>
<LiveFeed channel={ch} userId={userId} />
</Activity>
))}
</div>
)
}์ด ํจํด์์ ๋ API์ ์ญํ ์ ๋ช
ํํ ๊ตฌ๋ถํ ์ ์์ต๋๋ค. useEffectEvent๋ userId ๋ณ๊ฒฝ ์ WebSocket ์ฌ์ฐ๊ฒฐ์ ๋ฐฉ์งํ๋ฉด์๋ ๋ถ์ ์ด๋ฒคํธ์ ์ต์ ๊ฐ์ ๋ณด์ฅํฉ๋๋ค. <Activity>๋ ๋นํ์ฑ ์ฑ๋์ WebSocket ์ฐ๊ฒฐ์ ์๋์ผ๋ก ์ ๋ฆฌ(hidden ์ Effect ํด๋ฆฐ์
)ํ๋ฉด์๋, ํด๋น ์ฑ๋์ ๋ฉ์์ง ๋ชฉ๋ก ์ํ๋ ๋ฉ๋ชจ๋ฆฌ์ ๋ณด์กดํฉ๋๋ค. ์ฌ์ฉ์๊ฐ ์ฑ๋์ ๋ค์ ์ ํํ๋ฉด ์ด์ ๋ฉ์์ง๊ฐ ๊ทธ๋๋ก ํ์๋๊ณ , WebSocket์ด ๋ค์ ์ฐ๊ฒฐ๋์ด ์ ๋ฉ์์ง๋ฅผ ์์ ํฉ๋๋ค.
๊ธฐ์ ๋ฉด์ ํต์ฌ ์ง๋ฌธ๊ณผ ๋ต๋ณ
๋ค์์ 2026๋ React ๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ useEffectEvent์ Activity ๊ด๋ จ ํต์ฌ ์ง๋ฌธ์ ๋๋ค.
Q1: useEffectEvent๋ useCallback๊ณผ ์ด๋ป๊ฒ ๋ค๋ฆ ๋๊น?
useCallback์ ์์กด์ฑ ๋ฐฐ์ด์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋ ์๋ก์ด ํจ์๋ฅผ ์์ฑํ๋ฉฐ, ๋ฐํ๋ ํจ์๋ ์์กด์ฑ ๋ฐฐ์ด ์์ ์ ํด๋ก์ ๋ฅผ ์บก์ฒํฉ๋๋ค. Effect์ ์์กด์ฑ์ ํฌํจ๋๋ฏ๋ก ํจ์๊ฐ ์ฌ์์ฑ๋๋ฉด Effect๊ฐ ๋ค์ ์คํ๋ฉ๋๋ค. ๋ฐ๋ฉด useEffectEvent๋ ํธ์ถ ์์ ์ ์ต์ ๊ฐ์ ํญ์ ์ฝ์ผ๋ฉฐ, Effect์ ์์กด์ฑ์ผ๋ก ์ทจ๊ธ๋์ง ์์ต๋๋ค. ๋ฐ๋ผ์ Effect์ ์ฌ์คํ ์์ด ์ต์ ์ํ๋ฅผ ์ฐธ์กฐํด์ผ ํ๋ ๋น๋ฐ์ํ ๋ก์ง์ ์ ํฉํฉ๋๋ค.
Q2: Activity์ mode๊ฐ hidden์ผ๋ก ์ ํ๋๋ฉด ๋ด๋ถ Effect๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌ๋ฉ๋๊น?
mode="hidden"์ผ๋ก ์ ํ๋๋ฉด React๋ ํด๋น Activity ๋ด๋ถ์ ๋ชจ๋ useEffect ํด๋ฆฐ์
ํจ์๋ฅผ ์คํํฉ๋๋ค. ์ด๋ ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ๋ ๋์ ๋์ผํ ๋์์
๋๋ค. ๋ค๋ง ์ปดํฌ๋ํธ ์ํ(useState)์ DOM ํธ๋ฆฌ๋ ๋ฉ๋ชจ๋ฆฌ์ ๋ณด์กด๋ฉ๋๋ค. ๋ค์ mode="visible"๋ก ์ ํ๋๋ฉด Effect๊ฐ ๋ค์ ๋ง์ดํธ๋๋ฉด์ ์ค์ (setup) ํจ์๊ฐ ์คํ๋ฉ๋๋ค.
Q3: useEffectEvent ํจ์๋ฅผ ์ด๋ฒคํธ ํธ๋ค๋ฌ์์ ์ง์ ํธ์ถํ ์ ์์ต๋๊น?
ํธ์ถํ ์ ์์ต๋๋ค. useEffectEvent๋ก ์์ฑ๋ ํจ์๋ ๋ฐ๋์ useEffect ๋ด๋ถ์์๋ง ํธ์ถํด์ผ ํฉ๋๋ค. ์ด๋ฒคํธ ํธ๋ค๋ฌ์์ ์ต์ ์ํ๋ฅผ ์ฝ์ด์ผ ํ๋ ๊ฒฝ์ฐ์๋ ์ผ๋ฐ์ ์ธ ํจ์ ์ ์ธ์ด๋ useCallback์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ฌ๋ฐ๋ฅธ ์ ๊ทผ ๋ฐฉ์์
๋๋ค. Effect Event๋ "Effect์ ๋น๋ฐ์ํ ๋ถ๋ถ"์ด๋ผ๋ ํน์ ์ฉ๋๋ฅผ ์ํด ์ค๊ณ๋ ๊ฒ์
๋๋ค.
Q4: Activity mode="hidden" ์ํ์์ useQuery(TanStack Query)๊ฐ ๋์ํ์ง ์๋ ์ด์ ๋ ๋ฌด์์ ๋๊น?
TanStack Query์ useQuery๋ ๋ด๋ถ์ ์ผ๋ก useEffect๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ํ์นญ์ ํธ๋ฆฌ๊ฑฐํฉ๋๋ค. <Activity mode="hidden">์์๋ ๋ชจ๋ Effect์ ํด๋ฆฐ์
์ด ์คํ๋๋ฏ๋ก, useQuery์ ๊ตฌ๋
์ด ๋นํ์ฑํ๋ฉ๋๋ค. ์ด๋ฅผ ํด๊ฒฐํ๋ ค๋ฉด Activity ๋ฐ์ด๋๋ฆฌ ๋ฐ๊นฅ์ ๋ถ๋ชจ ์ปดํฌ๋ํธ์์ queryClient.ensureQueryData()๋ฅผ ํธ์ถํ์ฌ ๋ฐ์ดํฐ๋ฅผ ํ๋ฆฌํ์นํด์ผ ํฉ๋๋ค.
Q5: Activity ๋ด๋ถ ์ปดํฌ๋ํธ๊ฐ hidden ์ํ์์ ๋ฆฌ๋ ๋๋ง๋ฉ๋๊น?
hidden ์ํ์ Activity ๋ด๋ถ ์ปดํฌ๋ํธ๋ ๋ถ๋ชจ์ ์ํ ๋ณ๊ฒฝ์ผ๋ก ์ธํ ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ ์ ์์ง๋ง, ์ด๋ฌํ ์ ๋ฐ์ดํธ๋ "deferred" ์ฐ์ ์์๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค. ์ฆ, ๋ฉ์ธ ์ค๋ ๋๊ฐ ์ ํด ์ํ์ผ ๋๋ง ์ฒ๋ฆฌ๋๋ฏ๋ก ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ์๋ UI์ ์ฑ๋ฅ์๋ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.
Q6: useEffectEvent์ Activity๋ฅผ ํจ๊ป ์ฌ์ฉํ ๋ hidden ์ ํ ์ Effect Event ํจ์์ ๋์์ ์ด๋ป๊ฒ ๋ฉ๋๊น?
<Activity mode="hidden">์ผ๋ก ์ ํ๋๋ฉด Effect๊ฐ ํด๋ฆฐ์
๋๋ฏ๋ก, Effect ๋ด๋ถ์์ ๋ฑ๋กํ ์ฝ๋ฐฑ(์: WebSocket onmessage)๋ ํจ๊ป ํด์ ๋ฉ๋๋ค. ๋ฐ๋ผ์ hidden ์ํ์์๋ Effect Event ํจ์๊ฐ ํธ์ถ๋ ๊ฒฝ๋ก ์์ฒด๊ฐ ์ฌ๋ผ์ง๋๋ค. ๋ค์ visible๋ก ์ ํ๋๋ฉด Effect๊ฐ ์ฌ์คํ๋๊ณ , ๊ทธ ์์ ์์ Effect Event ํจ์๋ ์ต์ props์ state๋ฅผ ์ ์์ ์ผ๋ก ์ฝ์ ์ ์์ต๋๋ค.
๊ฒฐ๋ก
React 19.2์ useEffectEvent์ <Activity>๋ ๊ฐ๊ฐ ๋
๋ฆฝ์ ์ผ๋ก๋ ๊ฐ๋ ฅํ์ง๋ง, ๊ฒฐํฉํ ๋ ์ง์ ํ ๊ฐ์น๊ฐ ๋๋ฌ๋๋ API์
๋๋ค. ํต์ฌ ๋ด์ฉ์ ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- useEffectEvent๋ Effect ๋ด๋ถ์์ ์ต์ props/state๋ฅผ ์ฝ์ผ๋ฉด์๋ ๋ถํ์ํ Effect ์ฌ์คํ์ ๋ฐฉ์งํฉ๋๋ค.
useRef์ฐํ ํจํด์ ์์ ํ ๋์ฒดํ๋ฉฐ, ์์กด์ฑ ๋ฐฐ์ด ๊ด๋ จ ๋ฆฐํฐ ๊ฒฝ๊ณ ๋ฌธ์ ๋ฅผ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํฉ๋๋ค. - Activity ์ปดํฌ๋ํธ๋
mode์์ฑ์ ํตํด ์ปดํฌ๋ํธ์ ๊ฐ์์ฑ์ ์ ์ดํ๋ฉด์ ์ํ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ณด์กดํฉ๋๋ค. hidden ์ํ์์๋ Effect๊ฐ ํด๋ฆฐ์ ๋์ด ๋ฆฌ์์ค ๋์๋ฅผ ๋ฐฉ์งํ๊ณ , ์ ๋ฐ์ดํธ๋ ์ ํด ์๊ฐ์ ์ฒ๋ฆฌ๋์ด ๋ฉ์ธ UI ์ฑ๋ฅ์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค. - TanStack Query์์ ํตํฉ์์๋ Activity ๋ฐ์ด๋๋ฆฌ ๋ฐ๊นฅ์์ ํ๋ฆฌํ์นํ๋ ํจํด์ด ํ์์ ์ ๋๋ค.
- ๋ API์ ๊ฒฐํฉ์ผ๋ก ์ค์๊ฐ ๋ฐ์ดํฐ ์คํธ๋ฆผ, ๋ฉํฐ ํญ ๋์๋ณด๋, ๋ผ์ฐํธ ํ๋ฆฌ๋ ๋๋ง ๋ฑ ๋ณต์กํ ์๋๋ฆฌ์ค๋ฅผ ์ ์ธ์ ์ด๊ณ ์์ ํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
2026๋ React ๋ฉด์ ์์ ์ด ๋ API์ ๋ํ ์ง๋ฌธ์ ๋จ์ํ ๋ฌธ๋ฒ ์์ค์ ๋์ด, "์ ํ์ํ์ง"์ "์ด๋ค ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋์ง"์ ๋ํ ๊น์ ์ดํด๋ฅผ ์๊ตฌํฉ๋๋ค. ๊ฐ API์ ์ค๊ณ ์๋์ ๋ด๋ถ ๋์ ์๋ฆฌ๋ฅผ ํ์คํ ํ์ ํด ๋๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
์ฐ์ต์ ์์ํ์ธ์!
๋ฉด์ ์๋ฎฌ๋ ์ดํฐ์ ๊ธฐ์ ํ ์คํธ๋ก ์ง์์ ํ ์คํธํ์ธ์.
ํ๊ทธ
๊ณต์
๊ด๋ จ ๊ธฐ์ฌ

React Compiler 2026: ์๋ ๋ฉ๋ชจ์ด์ ์ด์ ์ ์๋ฆฌ์ ๊ธฐ์ ๋ฉด์ ์๋ฒฝ ๊ฐ์ด๋
React Compiler v1.0์ ์๋ ๋ฉ๋ชจ์ด์ ์ด์ ๋ด๋ถ ๊ตฌ์กฐ, ์ปดํ์ผ ํ์ดํ๋ผ์ธ, ์๋ ์ต์ ํ๊ฐ ํ์ํ ์๋๋ฆฌ์ค๋ฅผ ์์ธํ ๋ถ์ํฉ๋๋ค. 2026๋ React ๊ธฐ์ ๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ ํต์ฌ ์ง๋ฌธ์ ํฌ๊ด์ ์ผ๋ก ๋ค๋ฃน๋๋ค.

Next.js 16 Cache Components ์๋ฒฝ ๊ฐ์ด๋: use cache, PPR, ๋ฉด์ ์ง๋ฌธ ์ด์ ๋ฆฌ (2026)
Next.js 16์์ ๋์ ๋ Cache Components์ ํต์ฌ ๊ฐ๋ ์ธ use cache, cacheLife, cacheTag, PPR์ ์ค์ ์ฝ๋์ ๋ฉด์ ์ง๋ฌธ์ผ๋ก ๊น์ด ์๊ฒ ๋ค๋ฃจ๋ ๊ธฐ์ ๋ธ๋ก๊ทธ ๊ธ์ ๋๋ค.

ํ๋ก๋์ ์์์ React Server Components: ํจํด๊ณผ ํจ์
ํ๋ก๋์ ์์์ React Server Components: ์ค์ ์์ ๊ฒ์ฆ๋ ํจํด, ํํ ์ํฐํจํด, ๊ฒฌ๊ณ ํ Next.js 15 ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ๋๋ฒ๊น ์ ๋ต์ ๋๋ค.