React 클로저(closure) 딱 대..
함수가 정의될 때, 그 함수는 해당 시점의 user 상태 값을 "캡처"합니다. 이후에 user 상태가 업데이트되더라도, 함수 내부에서 사용되는 user는 함수가 정의된 시점의 값을 참조하기 때문에 최신 값이 반영되지 않는 것입니다.
const handleBlackListEvent = (email: string) => {
console.log("블랙리스트 이벤트", user);
if (user.email !== email) return;
handleLiveLeaveButtonClick();
handleRefetch();
};
React 함수 컴포넌트 내에서 이벤트 핸들러 함수가 생성될 때, 그 함수는 해당 시점의 상태를 캡처한다. 위 코드에서 handleBlackListEvent는 user 상태를 캡처했는데, 이 값은 함수 생성 시점의 user 값(즉, 초기값)이다. 따라서 user 상태가 갱신되어도 handleBlackListEvent는 최신 값을 참조하지 못한다.
이 문제를 해결하려면 최신 상태를 참조할 수 있는 방식을 사용한다.
몇가지 해결방법들을 소개해볼게용
해결 방법
1. useRef를 사용하여 최신 상태 참조
useRef를 사용하면 상태를 저장하되 렌더링에 영향을 미치지 않으면서 최신 값을 유지할 수 있다. 현재 이 방법을 채택해 우리 서비스에 적용했다. useRef는 값을 저장하지만 리렌더링을 트리거하지 않는다. 따라서 useRef로 저장한 값은 컴포넌트의 렌더링 주기에 영향을 미치지 않는다는 장점이 있다. 렌더링과 무관한 데이터 관리에 적합하다.
import { useEffect, useRef } from "react";
const MyComponent = () => {
const user = useRecoilValue(userState); // Recoil의 user 상태
const userRef = useRef(user);
useEffect(() => {
userRef.current = user; // 최신 user 값을 ref에 저장
}, [user]);
const handleBlackListEvent = (email: string) => {
console.log("블랙리스트 이벤트", userRef.current); // 항상 최신 user 참조
if (userRef.current.email !== email) return;
handleLiveLeaveButtonClick();
handleRefetch();
};
// 웹소켓 이벤트 등록
useEffect(() => {
const socket = new WebSocket("ws://example.com");
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
if (data.type === "blacklist") {
handleBlackListEvent(data.email);
}
});
return () => socket.close();
}, []);
return <div>....
2. useRecoilCallback과 snapshot을 이용한 최신 상태 참조
useRecoilCallback과 snapshot은 Recoil에서 최신 상태를 참조하거나, 동기적/비동기적으로 Recoil 상태를 읽고 쓰는 작업을 수행할 때 매우 유용하다. 이를 활용하면 React 클로저 문제를 우회하고, 최신 상태를 기반으로 로직을 처리할 수 있다.
useRecoilCallback과 snapshop의 특성
1️⃣ 최신 상태를 항상 보장합니다.
- useRecoilCallback은 Recoil 상태를 읽거나 쓰는 함수에서 최신 상태를 사용할 수 있게 합니다.
- React의 클로저 문제가 발생하지 않습니다.
2️⃣ 지연된 상태에 접근합니다.
- 상태를 즉시 읽거나 쓰지 않고, 특정 시점에서 필요할 때 호출할 수 있습니다.
3️⃣ Recoil 상태와 비동기 작업의 통합
- 비동기 작업 중에도 Recoil 상태를 안정적으로 읽고 업데이트할 수 있습니다.
// Recoil 상태를 참조하지 않고도 최신 상태를 가져오는 callback
const handleBlackListEvent = useRecoilCallback(
({ snapshot }) =>
(email: string) => {
// snapshot을 통해 최신 상태를 가져옴
const user = snapshot.getLoadable(userState).contents;
console.log("최신 user:", user);
if (user.email !== email) return;
handleLiveLeaveButtonClick();
handleRefetch();
},
[] // 의존성 배열을 비워 React 클로저 문제 회피
);
- 최신 상태를 동기적으로 참조 가능.
- 비동기 API 호출과 Recoil 상태를 쉽게 통합.
- 상태 읽기/쓰기를 명확히 분리하여 코드 가독성 증가.
- React 클로저 문제 회피.
클로저의 특성이 예상치 못한 문제를 유발하기도 하기도 한다는 것을 알게되었고, 기술의 본질적인 특성이 실제 코드에서 어떻게 작용하는지 이해하며 기능구현을 하는 것이 중요하다고 깨달은 사건이었다.
오래간만에 새로운 기술적 이슈를 마주하게 되어 한편으로는 설레기도 했으며, 큰 동기부여가 됐던 것 같다.