Escape Hatches : 이벤트와 효과 분리하기
이벤트 핸들러는 동일한 인터랙션을 다시 수행할 때만 다시 실행됩니다. 이벤트 핸들러와 달리 이펙트는 prop이나 상태 변수처럼 읽은 값이 마지막 렌더링 때와 다른 경우 다시 동기화합니다. 때로는 두 가지 동작을 혼합하여 일부 값에는 반응하지만 다른 값에는 반응하지 않는 이펙트를 원할 수도 있습니다. 이 페이지에서는 이를 수행하는 방법을 설명합니다.
학습 내용
- 이벤트 핸들러와 이펙트 중에서 선택하는 방법
- 이펙트는 유동적이고 이벤트 핸들러는 유동적이지 않은 이유
- 이펙트 코드의 일부가 반응하지 않도록 하려면 어떻게 해야 할까요?
- 이펙트 이벤트가 무엇이며, 이펙트에서 추출하는 방법
- 이펙트 이벤트를 사용하여 이펙트에서 최신 props와 state를 읽는 방법
이벤트 핸들러와 이펙트 중 선택하기
먼저 Event handler 와 Effect의 차이점을 정리해 보겠습니다.
채팅방 컴포넌트를 구현한다고 가정해 봅시다. 요구 사항은 다음과 같습니다:
- 컴포넌트는 선택한 채팅방에 자동으로 연결되어야 합니다.
- '보내기' 버튼을 클릭하면 채팅에 메시지를 보내야 합니다.
이미 코드를 구현했지만 어디에 넣어야 할지 잘 모르겠다고 가정해 봅시다. 이벤트 핸들러를 사용해야 할까요, 아니면 이펙트를 사용해야 할까요? 이 질문에 답해야 할 때마다 코드가 실행되어야 하는 이유를 고려하세요.
특정 상호작용에 대한 응답으로 실행되는 이벤트 핸들러
사용자 입장에서는 특정 '보내기' 버튼을 클릭했기 때문에 메시지가 전송되어야 합니다. 다른 시간이나 다른 이유로 메시지를 보내면 사용자는 오히려 화를 낼 것입니다. 그렇기 때문에 메시지 전송은 이벤트 핸들러를 사용해야 합니다. 이벤트 핸들러를 사용하면 특정 상호작용을 처리할 수 있습니다:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>;
</>
);
}
이벤트 핸들러를 사용하면 사용자가 버튼을 누를 때만 sendMessage(message)가 실행되도록 할 수 있습니다.
동기화가 필요할 때마다 Effect 실행
컴포넌트를 채팅방에 계속 연결해 두어야 한다는 점도 기억하세요. 이 코드는 어디에 쓰일까요?
이 코드를 실행하는 이유는 특정한 상호작용 때문이 아닙니다. 사용자가 채팅방 화면으로 이동한 이유나 방법은 중요하지 않습니다. 이제 사용자가 채팅방 화면을 보고 상호작용할 수 있게 되었으니 컴포넌트는 선택한 채팅 서버에 계속 연결되어 있어야 합니다. 채팅방 컴포넌트가 앱의 초기 화면이고 사용자가 아무런 상호작용을 수행하지 않은 경우에도 여전히 연결이 필요합니다. 이것이 바로 이펙트입니다:
function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
이 코드를 사용하면 사용자가 수행한 특정 상호작용에 관계없이 현재 선택된 채팅 서버에 대한 연결이 항상 활성화되어 있는지 확인할 수 있습니다. 사용자가 앱을 열기만 했든, 다른 방을 선택했든, 다른 화면으로 이동했다가 다시 돌아왔든, 이 Effect는 컴포넌트가 현재 선택된 방과 동기화된 상태를 유지하고 필요할 때마다 다시 연결되도록 보장합니다.
import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
function handleSendClick() {
sendMessage(message);
}
return (
<>
<h1>Welcome to the {roomId} room!</h1>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
export default function App() {
const [roomId, setRoomId] = useState('general');
const [show, setShow] = 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>
<button onClick={() => setShow(!show)}>
{show ? 'Close chat' : 'Open chat'}
</button>
{show && <hr />}
{show && <ChatRoom roomId={roomId} />}
</>
);
}
유동 값과 유동적인 로직
직관적으로 이벤트 핸들러는 버튼을 클릭하는 등 항상 '수동'으로 트리거된다고 말할 수 있습니다. 반면에 이펙트는 "자동"으로 동기화 상태를 유지하는 데 필요한 만큼 자주 실행되고 다시 실행됩니다.
이에 대해 더 정확하게 생각할 수 있는 방법이 있습니다.
컴포넌트 본문 내부에 선언된 props, state, 변수를 유동적인 값이라고 합니다. 이 예제에서 serverUrl은 리액티브 값이 아니지만 roomId와 message는 리액티브 값입니다. 이들은 렌더링 데이터 흐름에 참여합니다:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}
이와 같은 유동값은 재렌더링으로 인해 변경될 수 있습니다. 예를 들어, 사용자가 메시지를 편집하거나 드롭다운에서 다른 roomId를 선택할 수 있습니다. 이벤트 핸들러와 Effect는 변경에 다르게 반응합니다:
이벤트 핸들러 내부의 로직은 반응하지 않습니다. 사용자가 동일한 상호작용(예: 클릭)을 다시 수행하지 않는 한 다시 실행되지 않습니다. 이벤트 핸들러는 변경에 '반응'하지 않고 유동값을 읽을 수 있습니다.
Effect 내부의 로직은 유동적입니다. Effect에서 유동적인 값을 읽는 경우 종속성으로 지정해야 합니다. 그런 다음 다시 렌더링하면 해당 값이 변경되면 React가 새 값으로 이펙트의 로직을 다시 실행합니다.
이 차이를 설명하기 위해 이전 예제를 다시 살펴보겠습니다.
이벤트 핸들러 내부의 로직은 반응하지 않습니다.
이 코드 줄을 살펴보세요. 이 로직이 동적이어야 할까요, 아닐까요?
// ...
sendMessage(message);
// ...
사용자의 관점에서 볼 때 메시지가 변경되었다고 해서 메시지를 보내겠다는 뜻은 아닙니다. 사용자가 입력 중이라는 의미일 뿐입니다. 즉, 메시지를 전송하는 로직은 유동적이어서는 안 됩니다. 유저가 입력중인값이 변경되었다는 이유만으로 자동으로 실행되어서는 안 됩니다. 이것이 바로 이벤트 핸들러에 속하는 이유입니다:
function handleSendClick() {
sendMessage(message);
}
이벤트 핸들러는 유동적이지 않으므로 사용자가 보내기 버튼을 클릭할 때만 sendMessage(message)가 실행됩니다.
유동적 Effect 내부의 로직
이제 이 줄로 돌아가 보겠습니다:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...
사용자 입장에서 보면, roomId가 변경되었다는 것은 다른 룸에 연결하고 싶다는 의미입니다. 즉, 방에 연결하기 위한 로직은 유동적이어야 합니다. 이러한 코드 줄은 유동적인 값을 '자동으로 따라가고', 값이 달라지면 다시 실행되기를 원합니다. 이것이 바로 이펙트에 속하는 이유입니다:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
이펙트는 유동적이므로 createConnection(serverUrl, roomId) 및 connection.connect()는 roomId의 모든 고유 값에 대해 실행됩니다. 이펙트는 채팅 연결을 현재 선택된 방에 동기화합니다.
Effect에서 비반응성 로직 추출하기
반응적 로직과 비반응적 로직을 혼합하고 싶을 때는 상황이 더 까다로워집니다.
예를 들어 사용자가 채팅에 연결할 때 알림을 표시하고 싶다고 가정해 봅시다. props에서 현재 테마(어둡거나 밝은)를 읽어 올바른 색상으로 알림을 표시할 수 있습니다:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...
그러나 테마는 유동적인 값이므로(렌더링을 다시 하면 변경될 수 있음) Effect에서 읽는 모든 유동값은 종속성으로 선언해야 합니다. 이제 테마를 Effect의 종속성으로 지정해야 합니다:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ 모든 종속성이 선언됨
// ...
이 예제를 실행하면서 이 사용자 경험의 문제점을 발견할 수 있는지 확인해 보세요:
import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]);
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'}
/>
</>
);
}
roomId가 변경되면 예상대로 채팅이 다시 연결됩니다. 하지만 테마도 종속 요소이므로 어두운 테마와 밝은 테마 사이를 전환할 때마다 채팅도 다시 연결됩니다. 좋지 않죠!
다시 말해, 아래의 코드가 유동적인 Effect 내부에 있더라도 반응하지 않기를 원합니다:
// ...
showNotification('Connected!', theme);
// ...
이 비반응형 로직을 주변의 반응형 이펙트로부터 분리할 수 있는 방법이 필요합니다.
Effect Event 선언하기
개발 중
이 섹션에서는 아직 안정된 버전의 React로 출시되지 않은 실험적인 API에 대해 설명합니다.
이 비반응형 로직을 이펙트에서 추출하려면 useEffectEvent라는 특수 Hook을 사용합니다:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...
여기서 onConnected를 Effect Event라고 합니다. Effect 로직의 일부이지만 이벤트 핸들러와 훨씬 더 비슷하게 동작합니다. 그 안의 로직은 반응하지 않으며 항상 props와 state의 최신 값을 "확인"합니다.
이제 이펙트 내부에서 onConnected Effect Event를 호출할 수 있습니다:
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]); // ✅ 모든 종속성이 선언됨
// ...
이렇게 하면 문제가 해결됩니다. Effect의 종속성 목록에서 onConnected를 제거해야 한다는 점에 유의하세요. Effect Event는 유동적 이벤트가 아니므로 종속성에서 생략해야 합니다.
새 동작이 예상대로 작동하는지 확인합니다:
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as 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'}
/>
</>
);
}
Effect Event는 Event Handeler와 매우 유사하다고 생각할 수 있습니다. 가장 큰 차이점은 이벤트 핸들러는 사용자 상호작용에 대한 응답으로 실행되는 반면, 이펙트 이벤트는 이펙트에서 사용자가 트리거한다는 점입니다. 이펙트 이벤트를 사용하면 이펙트의 반응성과 반응해서는 안 되는 코드 사이의 "연결고리를 끊을 수 있습니다".
Effect Event로 최신 props 및 state 읽기
개발 중
이 섹션에서는 아직 안정된 버전의 React로 출시되지 않은 실험적인 API에 대해 설명합니다.
Effect Event를 사용하면 종속성 린터를 억제하고 싶을 수 있는 많은 패턴을 수정할 수 있습니다.
예를 들어 페이지 방문을 기록하는 Effect가 있다고 가정해 보겠습니다:
function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}
나중에 사이트에 여러 경로를 추가합니다. 이제 페이지 컴포넌트가 현재 경로가 포함된 URL props를 받습니다. 로그 방문 호출의 일부로 URL을 전달하고 싶지만 의존성 린터가 불평합니다:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect에 누락된 종속성: 'url'이 있습니다.
// ...
}
코드로 무엇을 하고 싶은지 생각해 보세요. 각 URL은 서로 다른 페이지를 나타내므로 서로 다른 URL에 대해 별도의 방문을 기록하려고 합니다. 즉, 이 logVisit 호출은 URL에 대해 유동적이어야 합니다. 그렇기 때문에 이 경우 종속성 린터를 따르고 URL을 종속성으로 추가하는 것이 합리적입니다:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ 모든 종속성이 선언됨
// ...
}
이제 모든 페이지 방문과 함께 장바구니에 있는 품목 수를 포함하려고 한다고 가정해 보겠습니다:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect에 누락된 종속성: 'numberOfItems'이 있습니다.
// ...
}
Effect 내부에서 numberOfItems를 사용했기 때문에 린터는 이를 종속성으로 추가하도록 요청합니다. 그러나 logVisit 호출이 numberOfItems와 관련하여 반응하지 않기를 원합니다. 사용자가 장바구니에 무언가를 넣고 numberOfItems가 변경된다고 해서 사용자가 페이지를 다시 방문했다는 의미는 아닙니다. 즉, 페이지 방문은 어떤 의미에서 '이벤트'입니다. 이는 정확한 순간에 발생합니다.
코드를 두 부분으로 나눕니다:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => { // EffectEvent를 선언함
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅모든 종속성이 선언됨
// ...
}
여기서 onVisit은 이펙트 이벤트입니다. 그 안의 코드는 반응적이지 않습니다. 따라서 변경 시 주변 코드가 다시 실행될 것을 걱정할 필요 없이 numberOfItems(또는 다른 유동적인 값!)를 사용할 수 있습니다.
반면에 Effect 자체는 유동적으로 유지됩니다. 이펙트 내부의 코드는 URL 프로퍼티를 사용하므로 이펙트는 다른 URL로 다시 렌더링할 때마다 다시 실행됩니다. 그러면 onVisit 이펙트 이벤트가 호출됩니다.
결과적으로 URL이 변경될 때마다 logVisit을 호출하고 항상 최신 numberOfItems를 읽게 됩니다. 그러나 numberOfItems가 자체적으로 변경되면 코드가 다시 실행되지 않습니다.
Note
인자 없이 onVisit()을 호출하고 그 안에 있는 URL을 읽을 수 있는지 궁금할 수 있습니다:
const onVisit = useEffectEvent(() => {
logVisit(url, numberOfItems);
});
useEffect(() => {
onVisit();
}, [url]);
이 방법도 작동하지만 이 URL을 Effect Event에 명시적으로 전달하는 것이 좋습니다. Effect Event에 인자로 url을 전달하면 다른 URL을 가진 페이지를 방문하는 것이 사용자 관점에서 별도의 "이벤트"를 구성한다는 의미입니다. 방문한 URL은 발생한 '이벤트'의 일부입니다:
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]);
이제 Effect Event가 명시적으로 visitedUrl을 "요청"하므로 실수로 Effect 종속성에서 URL을 제거할 수 없습니다. URL 종속성을 제거하면(별개의 페이지 방문이 하나로 계산되게 함) 린터에서 이에 대한 경고를 표시합니다. URL과 관련하여 onVisit이 반응하기를 원하므로 (반응하지 않는) 내부에서 URL을 읽는 대신 Effect에서 URL을 전달합니다.
이는 Effect 내부에 비동기 로직이 있는 경우 특히 중요합니다:
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // 방문 기록 지연
}, [url]);
여기서 onVisit 내부의 url은 (이미 변경되었을 수 있는) 최신 URL에 해당하지만 visitedUrl은 원래 이 Effect(및 이 onVisit 호출)를 실행하게 만든 URL에 해당합니다.
Deep Dive - 대신 의존성 린터를 억제해도 괜찮나요?
기존 코드베이스에서는 때때로 다음과 같이 린트 규칙이 억제된 것을 볼 수 있습니다:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 이런 식으로 린터를 억누르지 마세요:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}
useEffectEvent가 React의 안정적인 일부가 된 이후에는 린터를 억제하지 않는 것이 좋습니다.
이 규칙을 억제할 때의 첫 번째 단점은 코드에 도입한 새로운 유동적인 종속성에 Effect가 "반응"해야 할 때 React가 더 이상 경고하지 않는다는 것입니다. 앞의 예제에서는 React가 알려주었기 때문에 의존성에 URL을 추가했습니다. 린터를 비활성화하면 해당 Effect에 대한 향후 편집에 대해 더 이상 이러한 알림을 받지 않게 됩니다. 이것은 버그로 이어집니다.
다음은 린터를 비활성화했을 때 발생하는 혼란스러운 버그의 예시입니다. 이 예제에서 handelMove 함수는 현재 canMove 상태 변수 값을 읽어 점이 커서를 따라야 하는지 여부를 결정해야 합니다. 하지만 handleMove 내부에서 canMove는 항상 참입니다.
그 이유가 보이시나요?
import { useState, useEffect } from 'react';
export default function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [canMove, setCanMove] = useState(true);
function handleMove(e) {
if (canMove) {
setPosition({ x: e.clientX, y: e.clientY });
}
}
useEffect(() => {
window.addEventListener('pointermove', handleMove);
return () => window.removeEventListener('pointermove', handleMove);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<label>
<input type="checkbox"
checked={canMove}
onChange={e => setCanMove(e.target.checked)}
/>
The dot is allowed to move
</label>
<hr />
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity: 0.6,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
</>
);
}
이 코드의 문제는 의존성 린터를 억제하는 데 있습니다. 억제를 제거하면 이 Effect가 handleMove 함수에 종속되어야 한다는 것을 알 수 있습니다. handleMove는 컴포넌트 본문 내부에서 선언되어 유동적인 값이 되기 때문입니다. 모든 유동값은 종속성으로 지정해야 하며, 그렇지 않으면 시간이 지나면 낡아질 수 있습니다!
원본 코드 작성자는 Effect가 어떤 유동값에도 의존([])하지 않는다고 말함으로써 React에 "거짓말"을 했습니다. 이것이 바로 React가 canMove가 변경된 후 Effect를 다시 동기화하지 않은 이유입니다(그리고 handleMove도 함께). React가 Effect를 재동기화하지 않았기 때문에 리스너로 첨부된 handleMove는 초기 렌더링 중에 생성된 handleMove 함수입니다. 초기 렌더링 동안 canMove는 참이었기 때문에 초기 렌더링의 handleMove는 영원히 그 값을 보게 됩니다.
린터를 억제하지 않으면 오래된 값으로 인한 문제가 발생하지 않습니다.
useEffectEvent를 사용하면 린터에 "거짓말"을 할 필요가 없으며 코드가 예상대로 작동합니다:
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
export default function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [canMove, setCanMove] = useState(true);
const onMove = useEffectEvent(e => {
if (canMove) {
setPosition({ x: e.clientX, y: e.clientY });
}
});
useEffect(() => {
window.addEventListener('pointermove', onMove);
return () => window.removeEventListener('pointermove', onMove);
}, []);
return (
<>
<label>
<input type="checkbox"
checked={canMove}
onChange={e => setCanMove(e.target.checked)}
/>
The dot is allowed to move
</label>
<hr />
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity: 0.6,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
</>
);
}
그렇다고 해서 useEffectEvent가 항상 올바른 해결책이라는 의미는 아닙니다. 반응성을 원하지 않는 코드 줄에만 적용해야 합니다. 위의 샌드박스에서는 Effect의 코드가 canMove와 관련하여 반응하는 것을 원하지 않았습니다. 그렇기 때문에 이펙트 이벤트를 추출하는 것이 합리적입니다.
린터를 억제하는 다른 올바른 대안에 대해서는 이펙트 종속성 제거하기를 읽어보세요.
Effect Event의 제한 사항
개발 중
이 섹션에서는 아직 안정된 버전의 React로 출시되지 않은 실험적인 API에 대해 설명합니다.
효과 이벤트는 사용 방법이 매우 제한적입니다:
- Effect 내부에서만 호출하세요.
- 다른 컴포넌트나 Hook으로 전달해서는 안 됩니다.
예를 들어 다음과 같이 Effect Event를 선언하고 전달하지 마세요:
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000); // 🔴 피하기: Effect Event를 전달하는 것
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // 종속성에서 "콜백"을 지정해야 합니다.
}
대신 항상 이펙트 이벤트를 사용하는 이펙트 바로 옆에 이펙트 이벤트를 선언하세요:
function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ 좋음: 오직 Effect 안에서만 사용하세요.
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // onTick은 Effect Event이므로 종속성 배열에 필요하지않습니다.
}
이펙트 이벤트는 이펙트 코드의 비반응성 "조각"입니다. 이벤트는 이를 사용하는 이펙트 옆에 있어야 합니다.
요약
- Event handler는 특정 상호작용에 대한 응답으로 실행됩니다.
- Effect는 동기화가 필요할 때마다 실행됩니다.
- Event handler 내부의 로직은 반응적이지 않습니다.
- Effect 내부의 로직은 유동적(반응적)입니다.
- 비반응적 로직을 Effect에서 Effect Event로 이동할 수 있습니다.
- Effect 내부에서만 Effect Event를 호출하세요.
- Effect Event를 다른 컴포넌트나 Hook에 전달하지 마세요.
과제 1 / 4: 업데이트되지 않는 변수 수정하기
이 타이머 컴포넌트는 매초마다 증가하는 카운트 상태 변수를 유지합니다. 증가하는 값은 증분 상태 변수에 저장됩니다. 더하기 및 빼기 버튼으로 증분 변수를 제어할 수 있습니다.
그러나 더하기 버튼을 몇 번 클릭해도 카운터는 여전히 매초마다 1씩 증가합니다. 이 코드에 어떤 문제가 있을까요? Effect 코드에서 증분이 항상 1과 같은 이유는 무엇인가요? 실수를 찾아서 수정하세요.
https://codesandbox.io/s/fix-a-variable-that-doesnt-update-jljjyb?file=/App.js
과제 2/4: 멈추는 카운터 수정하기
이 타이머 컴포넌트는 매초마다 증가하는 카운트 상태 변수를 유지합니다. 증가하는 값은 증분 상태 변수에 저장되며, 더하기 및 빼기 버튼으로 제어할 수 있습니다. 예를 들어 더하기 버튼을 아홉 번 누르면 이제 초당 카운트가 1이 아니라 10씩 증가하는 것을 확인할 수 있습니다.
이 사용자 인터페이스에는 작은 문제가 있습니다. 더하기 또는 빼기 버튼을 초당 한 번보다 빠르게 계속 누르면 타이머 자체가 일시 정지하는 것처럼 보일 수 있습니다. 마지막으로 버튼을 누른 후 1초가 지나야만 타이머가 다시 시작됩니다. 이런 현상이 발생하는 이유를 찾아 타이머가 중단 없이 매초마다 틱되도록 문제를 해결하세요.
https://codesandbox.io/s/fix-a-freezing-counter-8ogwb1?file=/App.js
과제 3/4: 조정할 수 없는 지연 수정하기
이 예제에서는 간격 지연을 사용자 지정할 수 있습니다. 이는 두 개의 버튼으로 업데이트되는 지연 상태 변수에 저장됩니다. 그러나 지연이 1000밀리초(즉, 1초)가 될 때까지 '더하기 100밀리초' 버튼을 눌러도 타이머가 여전히 매우 빠르게(100밀리초마다) 증가하는 것을 알 수 있습니다. 마치 지연에 대한 변경 사항이 무시되는 것과 같습니다. 버그를 찾아서 수정하세요.
https://codesandbox.io/s/fix-a-non-adjustable-delay-x48x8c?file=/App.js
과제 4/4: 지연된 알림 수정하기
대화방에 참여하면 이 컴포넌트는 알림을 표시합니다. 하지만 알림이 즉시 표시되지는 않습니다. 대신 알림이 인위적으로 2초 지연되어 사용자가 UI를 둘러볼 수 있는 기회를 갖도록 합니다.
이 방법은 거의 작동하지만 버그가 있습니다. 드롭다운을 '일반'에서 '여행'으로 변경한 다음 '음악'으로 매우 빠르게 변경해 보세요. 충분히 빠르게 변경하면 (예상대로!) 두 개의 알림이 표시되지만 둘 다 "음악에 오신 것을 환영합니다"라고 표시됩니다.
'일반'에서 '여행'으로 전환한 다음 '음악'으로 매우 빠르게 전환할 때 첫 번째 알림은 "여행에 오신 것을 환영합니다"이고 두 번째 알림은 "음악에 오신 것을 환영합니다"로 표시되도록 수정하세요. (알림에 올바른 객실이 표시되도록 이미 설정한 경우, 후자의 알림만 표시되도록 코드를 변경하세요.)
https://codesandbox.io/s/fix-a-delayed-notification-xed7ry?file=/App.js
발표요약)
1. 이벤트 핸들러는 유저의 상호작용에 사용하고, 이펙트는 변수가 바뀌었을때 동기화되어 실행이 필요할때 사용한다.
2. Effect Event는 아직 React에 정식으로 발표된 안정적인 api는 아니지만, Effect 내부에서 사용하지만 반응하고싶지 않을 때 유용하게 사용할 수 있다. Effect Event는 Effect내부에서만 사용할수있고, 바로 근처에 선언해야하며, 다른 컴포넌트로 전달할 수 없다.