React 19 useEffectEvent์™€ Activity ์™„๋ฒฝ ๊ฐ€์ด๋“œ: ์ƒˆ๋กœ์šด API์™€ ๋ฉด์ ‘ ์งˆ๋ฌธ 2026

React 19.2์—์„œ ๋„์ž…๋œ useEffectEvent์™€ Activity ์ปดํฌ๋„ŒํŠธ์˜ ๋™์ž‘ ์›๋ฆฌ, ์‹ค์ „ ํŒจํ„ด, ๋ฉด์ ‘ ํ•ต์‹ฌ ์งˆ๋ฌธ์„ ์ฒด๊ณ„์ ์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

React 19 useEffectEvent and Activity API deep dive

React ์ƒํƒœ๊ณ„๋Š” ๊พธ์ค€ํžˆ ์ง„ํ™”ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, 2025๋…„ ํ•˜๋ฐ˜๊ธฐ์— ์ถœ์‹œ๋œ React 19.2๋Š” ๊ทธ๋™์•ˆ ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ฒช์–ด์˜จ ๋‘ ๊ฐ€์ง€ ํ•ต์‹ฌ ๋ฌธ์ œ๋ฅผ ์ •๋ฉด์œผ๋กœ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ๋Š” Effect ๋‚ด๋ถ€์˜ stale closure ๋ฌธ์ œ๋ฅผ ๊ทผ๋ณธ์ ์œผ๋กœ ์ œ๊ฑฐํ•˜๋Š” useEffectEvent ํ›…์ด๊ณ , ๋‘ ๋ฒˆ์งธ๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ๋ณด์กดํ•˜๋ฉด์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ”„๋ฆฌ๋ Œ๋”๋ง์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” <Activity> ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

์ด ๋‘ API๋Š” ๋‹จ์ˆœํ•œ ํŽธ์˜ ๊ธฐ๋Šฅ์ด ์•„๋‹™๋‹ˆ๋‹ค. React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ๊ด€๋ฆฌ ๋ฐฉ์‹๊ณผ ๋„ค๋น„๊ฒŒ์ด์…˜ ํŒจํ„ด์„ ๊ทผ๋ณธ์ ์œผ๋กœ ๋ณ€ํ™”์‹œํ‚ค๊ณ  ์žˆ์œผ๋ฉฐ, 2026๋…„ ํ”„๋ก ํŠธ์—”๋“œ ๋ฉด์ ‘์—์„œ ํ•ต์‹ฌ ์งˆ๋ฌธ์œผ๋กœ ์ž๋ฆฌ์žก๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธ€์—์„œ๋Š” ๊ฐ API์˜ ๋™์ž‘ ์›๋ฆฌ, ์‹ค์ „ ์‚ฌ์šฉ ํŒจํ„ด, ์ฃผ์˜์‚ฌํ•ญ, ๊ทธ๋ฆฌ๊ณ  ๋ฉด์ ‘ ๋Œ€๋น„๋ฅผ ์œ„ํ•œ ํ•ต์‹ฌ ์งˆ์˜์‘๋‹ต๊นŒ์ง€ ์ฒด๊ณ„์ ์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

React 19.2 ์ด์ƒ ํ•„์š”

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 ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ถˆํ•„์š”ํ•œ ์žฌ์—ฐ๊ฒฐ์„ ๋ฐฉ์ง€ํ•˜๋ฉด์„œ๋„ ์ตœ์‹  ๊ฐ’์„ ์ฐธ์กฐํ•ด์•ผ ํ•˜๋Š” ์ „ํ˜•์ ์ธ ์ƒํ™ฉ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

hooks/useChatRoom.tstsx
// 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๊ฐ€ ์–ธ์ œ ์‹คํ–‰๋ ์ง€"์™€ "์‹คํ–‰ ์‹œ ์–ด๋–ค ๊ฐ’์„ ์‚ฌ์šฉํ• ์ง€"๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

hooks/useChatRoom.tstsx
// 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๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

components/SearchTracker.tsxtsx
// 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 ์˜ค์šฉ ์ฃผ์˜

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) | ๋งˆ์šดํŠธ๋จ | ํด๋ฆฐ์—… ์‹คํ–‰ | | ์—…๋ฐ์ดํŠธ ์šฐ์„ ์ˆœ์œ„ | ์ผ๋ฐ˜ | ์œ ํœด ์‹œ๊ฐ„์— ์ง€์—ฐ ์ฒ˜๋ฆฌ | | ํ”„๋ฆฌ๋ Œ๋”๋ง | ํ•ด๋‹น ์—†์Œ | ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋กœ ๋ Œ๋”๋ง |

๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ธ ํ™œ์šฉ ์‚ฌ๋ก€๋Š” ํƒญ ๋ ˆ์ด์•„์›ƒ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ํƒญ์„ ์ „ํ™˜ํ•ด๋„ ์ด์ „ ํƒญ์˜ ํผ ์ž…๋ ฅ๊ฐ’, ์Šคํฌ๋กค ์œ„์น˜, ์ปดํฌ๋„ŒํŠธ ์ƒํƒœ๊ฐ€ ๋ชจ๋‘ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

components/TabLayout.tsxtsx
// 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๋ฅผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ค€๋น„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

components/PrerenderedRoute.tsxtsx
// 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 ํ›…์ด ๋ฐ์ดํ„ฐ ํŽ˜์นญ์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์˜๋„๋œ ๋™์ž‘์ด์ง€๋งŒ, ํ”„๋ฆฌํŽ˜์นญ์„ ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฒฝ์šฐ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

components/UserDashboard.tsxtsx
// 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์œผ๋กœ ์ˆ˜์‹ ํ•˜๋˜, ํ™œ์„ฑ ์ฑ„๋„๋งŒ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜๊ณ , ๋ถ„์„ ์ถ”์ ์—๋Š” ํ•ญ์ƒ ์ตœ์‹  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ ๋Œ€์‹œ๋ณด๋“œ์ž…๋‹ˆ๋‹ค.

components/LiveDashboard.tsxtsx
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
#react-19
#useEffectEvent
#activity
#hooks
#interview

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

React Compiler 2026 automatic memoization interview questions

React Compiler 2026: ์ž๋™ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์˜ ์›๋ฆฌ์™€ ๊ธฐ์ˆ  ๋ฉด์ ‘ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

React Compiler v1.0์˜ ์ž๋™ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๋‚ด๋ถ€ ๊ตฌ์กฐ, ์ปดํŒŒ์ผ ํŒŒ์ดํ”„๋ผ์ธ, ์ˆ˜๋™ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ƒ์„ธํžˆ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. 2026๋…„ React ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” ํ•ต์‹ฌ ์งˆ๋ฌธ์„ ํฌ๊ด„์ ์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Next.js 16 Cache Components ์•„ํ‚คํ…์ฒ˜์™€ use cache ๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ๊ธฐ์ˆ  ๋‹ค์ด์–ด๊ทธ๋žจ

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: ํŒจํ„ด๊ณผ ํ•จ์ •

ํ”„๋กœ๋•์…˜์—์„œ์˜ React Server Components: ์‹ค์ „์—์„œ ๊ฒ€์ฆ๋œ ํŒจํ„ด, ํ”ํ•œ ์•ˆํ‹ฐํŒจํ„ด, ๊ฒฌ๊ณ ํ•œ Next.js 15 ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๋””๋ฒ„๊น… ์ „๋žต์ž…๋‹ˆ๋‹ค.