[React.js] React에서 useRef를 활용한 안정적인 소켓 통신 방법
📌 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>;
};
🚨 기존 코드의 문제점
- 이벤트 리스너 중복 등록 가능성
- 페이지를 다시 방문하거나, 다른 페이지를 갔다가 돌아오면
→ 기존 소켓이 닫히지 않은 상태에서 새로운 소켓이 생성될 수도 있음. on("response")
가 중복되면 이벤트 핸들러가 여러 번 실행될 위험이 있음.
- 페이지를 다시 방문하거나, 다른 페이지를 갔다가 돌아오면
- 소켓이 완전히 해제되지 않음
socket.off("response")
만 호출하면 연결이 닫히지 않은 채 유지될 가능성이 있음.socket.disconnect()
를 호출하지 않으면 서버와의 연결이 계속 유지됨.- 브라우저 개발자 도구의 네트워크 탭(WebSocket)에서 확인하면 여러 개의 연결이 유지될 수도 있음.
- 소켓 인스턴스를 추적하기 어려움
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
를 사용하는 것이 더 안전한 방법!
특히, 소켓이 여러 번 생성되거나 이벤트 리스너가 중복 등록되는 문제를 방지할 수 있음.
🔑 소켓을 안정적으로 관리하려면?
useRef
를 사용해 소켓을 관리 (리렌더링이 되어도 유지됨)disconnect()
를 호출해 소켓 연결을 완전히 해제off()
를 사용해 이벤트 리스너를 제거 (중복 등록 방지)- 에러 핸들링 추가 (
connect_error
등)