useEffectEvent

2025. 10. 13. 11:56Coding Study/React

출처 : https://react.dev/learn/separating-events-from-effects#extracting-non-reactive-logic-out-of-effects

 

Separating Events from Effects – React

The library for web and native user interfaces

react.dev

 declaring an Effect Event (효과 이벤트 선언) 참고

 

 

1. Effect의 목적과 의존성

  • useEffect의 목적: useEffect는 컴포넌트 외부의 시스템(여기서는 채팅 서버 연결)과 동기화(synchronization)하는 데 사용됩니다. 즉, 의존성 배열([roomId])에 있는 값이 바뀔 때마다 동기화(연결/재연결)를 다시 실행합니다.
  • theme의 역할: theme은 연결 자체를 다시 설정할 필요 없이, 연결 성공 시 실행되는 이벤트 핸들러(connection.on('connected', ...) 내부)에서만 필요한 값입니다. theme이 바뀔 때마다 웹소켓 연결을 끊고(disconnect) 다시 연결(connect)하는 것은 원치 않는 동작(불필요한 재연결)이며 성능에도 좋지 않습니다.
import { useState, useEffect } from 'react';
import { useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

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]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

 

 

2. useEffectEvent의 역할

useEffectEvent는 Effect 내부에서 발생하는 이벤트(여기서는 연결 성공 이벤트)에 대한 로직을 분리하는 새로운 React 훅입니다.

  1. 신선한 Props/State 접근: useEffectEvent로 감싼 함수(onConnected)는 Effect가 다시 실행되지 않더라도 항상 최신 theme 값에 접근할 수 있습니다.
  2. 비-반응형(Non-Reactive) 로직 분리: useEffectEvent로 생성된 함수는 React의 "반응형 값(Reactive Value)"으로 간주되지 않습니다. 따라서 이 함수를 Effect 내부에서 사용하더라도 의존성 배열에 포함할 필요가 없습니다. (eslint-plugin-react-hooks 린트 규칙을 준수하면서도 불필요한 재연결을 막아줍니다.)

❌ theme을 제외할 경우의 문제 (권장하지 않음)

만약 아래처럼 theme을 의존성 배열에서 일부러 제외하고 린트 경고를 무시한다면:

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    // ... 연결 설정 로직
    connection.on('connected', () => {
      showNotification('Connected!', theme); // 🔴 이 'theme'은 Effect 실행 시점의 오래된(stale) 값임
    });
    // ...
    return () => connection.disconnect();
  }, [roomId]); // ❌ 'theme'이 빠짐
}

 

 

  • useEffect가 roomId에 대해서만 실행되기 때문에, 연결이 설정된 시점의 theme 값(예: 'light')이 클로저에 갇히게 됩니다.
  • 사용자가 나중에 테마를 'dark'로 변경해도, 알림창은 여전히 연결 시점에 캡처된 오래된 theme 값('light')을 사용하게 됩니다. 이를 Stale Closure(오래된 클로저) 문제라고 합니다.

 

✅ useEffectEvent를 사용할 경우의 이점 (권장 방식)

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme); // ✅ 항상 최신 'theme' 값에 접근
  });

  useEffect(() => {
    // ... 연결 설정 로직
    connection.on('connected', () => {
      onConnected(); // onConnected는 최신 theme을 가져옴
    });
    // ...
    return () => connection.disconnect();
  }, [roomId]); // ✅ 'theme'이 없어도 됨 (불필요한 재연결 방지)
}

 

 

useEffectEvent는 theme 값이 바뀌더라도 연결을 재설정하지 않으면서(의존성 배열에 없으므로), 이벤트가 발생할 때(onConnected가 호출될 때)는 항상 최신 theme 값을 읽을 수 있도록 해줍니다.

요약하자면, useEffectEvent는 React에서 '설정 로직(Effect)''이벤트 핸들러(Effect Event)'를 명확하게 분리하여, 불필요한 재동작 없이 최신 props와 state를 안전하게 사용할 수 있게 해주는 공식적인 방법입니다.

 

 

요약

 useEffect 내부에서 사용하고자 하는  onConnected 함수는

 의존성 변수 theme 를 useEffect 의존성 배열에서 포함 할 경우 theme가 변경 될 때 마다 채팅방 연결로직이 실행 된다.

 그렇다고 의존성 배열에서 제외할 경우 오래된 theme 값을 쓰게 됨.

 이 경우 useEffectEvent 로 분리해서 사용하게 되면 항상 최신의 theme 값을 가져올 수 있다.