2025. 10. 13. 11:56ㆍCoding 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 훅입니다.
- 신선한 Props/State 접근: useEffectEvent로 감싼 함수(onConnected)는 Effect가 다시 실행되지 않더라도 항상 최신 theme 값에 접근할 수 있습니다.
- 비-반응형(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 값을 가져올 수 있다.
'Coding Study > React' 카테고리의 다른 글
| React Compiler ( 리액트 컴파일러 ) (0) | 2025.12.06 |
|---|---|
| React Hook Form - register (1) | 2025.08.13 |
| 회원 가입 페이지 Joi , react-hook-form 을 사용한 유효성 검증 (4) | 2025.08.10 |
| Joi (유효성 검증 라이브러리) (1) | 2025.08.09 |
| useActionstate (0) | 2025.05.15 |