언어·프레임워크/React.js

[React.js] React에서 useRef를 활용한 안정적인 소켓 통신 방법

DandyNow 2025. 2. 28. 12:58
728x90
반응형

📌 React에서 useRef를 활용한 안정적인 소켓 통신

웹 애플리케이션에서 실시간 데이터를 다루려면 WebSocket을 활용하는 경우가 많다.
React에서는 보통 useEffect를 사용해 소켓을 관리하지만, 잘못된 방식으로 사용하면 불필요한 소켓 재연결, 이벤트 리스너 중복 등록, 메모리 누수 같은 문제가 발생할 수 있다.
이를 해결하기 위해 useRef를 활용하는 것이 효과적이다! 🚀


🚨 기존 코드

import { useEffect, useState } from "react";
import { io } from "socket.io-client";

const ComponentName = () => {
  const [socketState, setSocketState] = useState(null);

  useEffect(() => {
    const socket = io("http://localhost:4000"); // 🔴 리렌더링될 때마다 새로운 소켓이 생성됨

    socket.emit("event-name", { userId: 123 }); // 🔴 요청을 보냄

    socket.on("response", (data) => {
      setSocketState(data);
    });

    return () => {
      socket.off("response"); // 🔴 기존 소켓을 완전히 해제하지 않음
    };
  }, []);

  return <div>{socketState ? JSON.stringify(socketState) : "Loading..."}</div>;
};

🚨 기존 코드의 문제점

  1. 이벤트 리스너 중복 등록 가능성
    • 페이지를 다시 방문하거나, 다른 페이지를 갔다가 돌아오면
      기존 소켓이 닫히지 않은 상태에서 새로운 소켓이 생성될 수도 있음.
    • on("response")가 중복되면 이벤트 핸들러가 여러 번 실행될 위험이 있음.
  2. 소켓이 완전히 해제되지 않음
    • socket.off("response")만 호출하면 연결이 닫히지 않은 채 유지될 가능성이 있음.
    • socket.disconnect()를 호출하지 않으면 서버와의 연결이 계속 유지됨.
    • 브라우저 개발자 도구의 네트워크 탭(WebSocket)에서 확인하면 여러 개의 연결이 유지될 수도 있음.
  3. 소켓 인스턴스를 추적하기 어려움
    • const socket = io(...)로 선언하면,
      리렌더링 후 기존 소켓 인스턴스에 접근할 방법이 없음.
    • 따라서 useRef를 사용하면 더 안전하게 소켓을 관리할 수 있음.

✅ 기존 코드가 완전히 틀린 것은 아니지만, 개선하는 것이 더 안전한 이유

기존 코드에 의존성 배열이 없어서 한 번만 실행되기 때문에 계속 새로운 소켓을 만드는 문제는 없을 수도 있음.
하지만, 아래와 같은 상황에서는 여전히 문제가 발생할 가능성이 있음.

💡 예를 들어:

  • 사용자가 페이지를 떠났다가 다시 돌아오면?
  • 소켓 연결이 네트워크 문제로 끊겼다가 자동으로 다시 연결되면?
  • 개발자가 이후에 useEffect 의존성을 추가하면?

이런 상황에서 기존 코드에서는 불필요한 소켓이 여러 개 유지되거나, 리스너가 중복 등록될 가능성이 있음.
useRef를 사용하면 항상 하나의 소켓 인스턴스만 유지하면서, 이벤트 리스너도 깔끔하게 관리 가능하기 때문의 위와 같은은 문제를 예방할 수 있다.


✅ 개선된 코드 (useRef 활용)

import { useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";

const ComponentName = () => {
  const socketRef = useRef(null);
  const [socketState, setSocketState] = useState(null);
  const [socketError, setSocketError] = useState(false);

  useEffect(() => {
    if (!socketRef.current) {
      socketRef.current = io("http://localhost:4000"); // ✅ 소켓을 한 번만 연결

      socketRef.current.on("connect_error", () => {
        setSocketError(true);
      });

      socketRef.current.emit("event-name", { userId: 123 }); // ✅ 요청을 한 번만 보냄

      socketRef.current.on("response", (data) => {
        setSocketState(data);
      });
    }

    return () => {
      if (socketRef.current) {
        socketRef.current.off("response"); // ✅ 이벤트 리스너 해제
        socketRef.current.disconnect(); // ✅ 소켓 연결 종료
        socketRef.current = null;
      }
    };
  }, []);

  return (
    <div>
      <h2>Dashboard Data</h2>
      <pre>{JSON.stringify(socketState, null, 2)}</pre>
      {socketError && <p style={{ color: "red" }}>Socket Error Occurred</p>}
    </div>
  );
};

🔍 useRef를 사용한 코드의 장점

소켓이 한 번만 연결됨useRef로 소켓 인스턴스를 유지
emit("event-name")이 한 번만 실행됨 → 불필요한 요청 방지
컴포넌트 언마운트 시 disconnect()로 소켓 종료
이벤트 리스너 중복 등록 방지off("response") 추가
소켓 에러 처리 가능 (setSocketError(true))


🔹 off() 메서드는 왜 필요할까?

  • socket.on("response", callback)으로 이벤트 리스너를 등록하면, 컴포넌트가 리렌더링될 때마다 새로운 리스너가 추가됨.
  • 하지만 기존 리스너를 제거하지 않으면 중복 호출 문제와 메모리 누수가 발생할 수 있음.
  • socketRef.current.off("response")를 사용해 기존 이벤트 리스너를 제거해야 함.

📌 즉, off()를 사용하지 않으면 동일한 이벤트 리스너가 여러 개 생길 수 있고, 메모리 누수와 불필요한 API 요청이 발생할 가능성이 높아진다! 🚀


📌 결론

🚀 기존 코드가 반드시 잘못된 것은 아니지만, 안정성과 유지보수성을 고려하면 useRef를 사용하는 것이 더 안전한 방법!
특히, 소켓이 여러 번 생성되거나 이벤트 리스너가 중복 등록되는 문제를 방지할 수 있음.

🔑 소켓을 안정적으로 관리하려면?

  1. useRef를 사용해 소켓을 관리 (리렌더링이 되어도 유지됨)
  2. disconnect()를 호출해 소켓 연결을 완전히 해제
  3. off()를 사용해 이벤트 리스너를 제거 (중복 등록 방지)
  4. 에러 핸들링 추가 (connect_error 등)
728x90
반응형