Java Script & Type Script/TIL

React 클로저(closure) 딱 대..

쩰라 2024. 12. 26. 23:17
728x90
함수가 정의될 때, 그 함수는 해당 시점의 user 상태 값을 "캡처"합니다. 이후에 user 상태가 업데이트되더라도, 함수 내부에서 사용되는 user는 함수가 정의된 시점의 값을 참조하기 때문에 최신 값이 반영되지 않는 것입니다.
 
 
2024년 12월 26일.. 
웹소켓 통신으로 받아오는 다량의 실시간 데이터 중 꽤나 중요한 파트인 블랙리스트 강퇴를 정상적으로 수행하지 못한 나의 함수에 대해 Chat GPT는 React의 클로저의 작동과 관계된 문제라고했다,,,
블랙리스트가 추가됐다는 이벤트를 해당 블랙리스트 유저의 이메일과 함께 받으면 바로 접속된 유저 중 해당 이메일을 가진 유저 (블랙리스트 당사자)를 퇴장시켜야했다. 아래 함수는 블랙리스트 이벤트를 받으면 실행되는 함수이다. 
const handleBlackListEvent = (email: string) => {
     console.log("블랙리스트 이벤트", user); 
     if (user.email !== email) return;
     handleLiveLeaveButtonClick();
     handleRefetch(); 
 };
 
 
 
이 함수 바깥에서의 user(Recoil값)는 정상적으로 출력되는데 함수안에서 user는 기본값으로 나온다. 
user는 컴포넌트 최상단에 위치한 UserCheck로직을 통해 계정정보를 담고있는 아톰이다.

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. useRecoilCallbacksnapshot을 이용한 최신 상태 참조

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 클로저 문제 회피.

 

 

 

 

클로저의 특성이 예상치 못한 문제를 유발하기도 하기도 한다는 것을 알게되었고, 기술의 본질적인 특성이 실제 코드에서 어떻게 작용하는지 이해하며 기능구현을 하는 것이 중요하다고 깨달은 사건이었다.

오래간만에 새로운 기술적 이슈를 마주하게 되어 한편으로는 설레기도 했으며, 큰 동기부여가 됐던 것 같다.

728x90