최근 React에서 Socket.IO를 사용해 보았습니다.
Socket.IO를 통한 실시간 통신을 구현해보며 Custom Hook을 이용하면 더 편리한 개발이 가능할 것이라 생각했습니다.
이에 대한 구현기를 공유해보려 합니다.
Socket.IO?
- HTTP의 한계
HTTP는 요청한대로 응답을 보내주기만 하는 단순한 프로토콜입니다.
이처럼 요청에 대해서만 응답을 보낼 수 있는 HTTP의 특징으로 채팅, 게임 등의 실시간 통신에 매우 비효율적이었습니다.
예를 들어, 채팅 서비스에서 계속 메시지를 받기만 하는 상황은 구현하기 힘들었습니다.
또한 HTTP는 매 요청과 응답마다 connection과 disconnection의 과정을 반복해야 하기에, 유사한 통신을 반복해야 한다는 문제가 있었습니다.
- WebSocket
HTTP의 실시간 통신 문제를 해결하기 위해 HTML5부터 WebSocket이 등장했습니다.
WebSocket 프로토콜을 사용하면 서버와 브라우저 간 연결을 유지한 상태로 데이터를 교환할 수 있습니다.
실시간 양방향 통신을 지원하며, 한번 연결이 수립되면 클라이언트와 서버 모두 자유롭게 데이터를 보낼 수 있는 것입니다.
- WebSocket은 HTTP와 같은 OSI 모델의 7계층에 위치하는 프로토콜이며, 4계층의 TCP에 의존합니다.
- HTTP를 이용해서 연결을 수립하며, 연결된 이후에도 연결에 사용했던 80과 443포트를 이용합니다.
- Socket.IO
WebSocket은 HTML5의 기술이기 때문에 오래된 버전의 웹 브라우저는 지원하지 않았습니다. (현재는 대부부분의 브라우저에서 지원합니다.)
이를 해결하기 위해 나온 여러 기술 중 하나가 바로 Socket.IO입니다.
Socket.IO는 Node.js 기반으로 만들어진 기술로, 거의 모든 웹 브라우저와 모바일 장치를 지원하는 실시간 통신 지원 라이브러리입니다.
특히 Socket.IO는 채팅방에 특화된 라이브러리입니다. (room이라는 기능을 이용해 여러 개의 채팅방을 만들 수 있고, 소켓에 연결된 전체 클라이언트에게 브로드캐스팅하기 용이합니다.)
- 그래서 왜 선택했나?
사용자가 많은 대규모 애플리케이션의 경우 매우 빠르게 작동하며 통신할 때 아주 적은 데이터를 이용하는 WebSocket이 좋겠습니다.
하지만 실시간 통신 구현 경험이 없는 현 시점과 구현을 위해 주어진 시간을 고려하여 쉽고 빠른 개발을 제공하는 Socket.IO 라이브러리를 선택했습니다. (Socket.IO는 WebSocket을 더욱 편리하게 사용할 수 있게 지원합니다.)
Express + Socket.IO 서버 구동
먼저 Socket.IO를 설치합니다.
npm install --save -D socket.io
그리고 서버 실행 파일 app.ts를 정의하여 서버 인스턴스를 만들고 이를 socket 실행 함수에 전달합니다.
// app.ts
import express from "express";
import { createServer } from "http";
import dotenv from "dotenv";
import socket from "./socket";
dotenv.config();
const app = express();
const server = createServer(app);
server.listen(process.env.PORT, () => {
console.log("start! express server");
socket(server);
});
export default app;
인자로 받은 서버 인스턴스를 사용하여 Socket 서버를 생성하고 connection을 수행합니다.
connection 후 콜백 함수 인자 socket과 Socket 서버 인스턴스 io를 활용하여 이벤트 통신을 합니다.
// Socket/index.ts
import { Server } from "socket.io";
import { Server as httpServer } from "http";
export default function (server: httpServer) {
const io = new Server(server);
io.on("connection", (socket) => {
// 지정한 이벤트 명에 대한 데이터를 받고 콜백함수를 실행한다.
socket.on('받을 이벤트 명', (data) => {
});
// 이벤트 명을 지정하고 sender 클라이언트에게 데이터를 보낸다.
socket.emit('전송할 이벤트 명', data);
// 이벤트 명을 지정하고 sender 클라이언트 제외 모든 클라이언트에게 브로드캐스팅한다.
socket.broadcast.emit('전송할 이벤트 명', data);
// 이벤트 명을 지정하고 모든 클라이언트에게 브로드캐스팅한다.
io.emit('전송할 이벤트 명', data);
});
}
Socket.IO 이벤트 통신 방법은 위 주석을 참고해주시면 감사하겠습니다.
Socket Context 구현
이제 React에서 Socket.IO를 사용해봅시다.
먼저 Socket.IO Client API를 설치합니다.
npm install --save -D socket.io-client
Socket 연결 후 해당 연결을 유지하며 사용하는 것이 효율적입니다.
따라서 Socket 연결이 필요할 때 한번 연결하며, 이를 하위 컴포넌트에서 모두 사용할 수 있어야 합니다.
하지만 매번 Props로 Socket 연결 인스턴스를 전달해주게 되면, Props Drilling의 문제가 발생할 수 있습니다.
따라서 Context API를 사용합니다!
// SocketContext.tsx
import React from "react";
import io from "socket.io-client";
const socket = io(`${process.env.BASE_URL}`, {
transports: ["websocket"],
autoConnect: false,
});
const SocketContext = React.createContext(socket);
function SocketProvider({ children }: React.PropsWithChildren) {
React.useEffect(() => {
socket.connect();
return () => {
socket.disconnect();
};
}, []);
return (
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
);
}
export { SocketContext, SocketProvider };
Socket 연결 인스턴스를 생성합니다. (transports, autoConnect 등 옵션은 공식 문서를 참고해주시기 바랍니다.)
useEffect Hook을 이용하여 SocketProvider가 마운트될 시 연결을 수행하고 언마운트될 시 연결을 끊도록 합니다.
또한 Socket 연결 인스턴스를 SocketProvider의 value 속성으로 넣어 모든 하위 컴포넌트 사용할 수 있도록 합니다.
useSocketSender Hook 구현
아래는 실시간으로 데이터를 서버측에 송신하는 Custom Hook을 구현한 코드입니다.
import { useCallback, useContext } from "react";
import { SocketContext } from "src/contexts/socketContext";
function useSocketSender(channel: string) {
const socket = useContext(SocketContext);
const emitter = (data: unknown) => {
socket.emit(channel, data);
};
return useCallback(emitter, [channel]);
}
export default useSocketSender;
- useContext Hook을 이용하여 Socket 연결 인스턴스를 얻습니다.
- useCallBack Hook을 이용하여 channel(이벤트 명)이 바뀌지 않는 한, emitter 함수를 재사용합니다.
특정 컴포넌트에서 아래와 같이 사용할 수 있습니다.
import useSocketSender from "hooks/useSocketSender";
function TestComponent() {
const sender = useSocketSender(이벤트 명);
const handleClick = () => {
sender(데이터);
}
return ( ... );
}
export default testComponent;
한 이벤트 명에 대한 sender를 생성하고, 이를 간편하게 사용할 수 있습니다!
sender(데이터1);
sender(데이터2);
sender(데이터1);
sender(데이터3);
useSocketReceiver Hook 구현
이번에는 실시간으로 데이터를 수신하는 Custom Hook입니다.
import { useContext, useEffect } from "react";
import { SocketContext } from "src/contexts/socketContext";
function useSocketReceiver(channel: string, onReceive: (data: unknown) => void) {
const socket = useContext(SocketContext);
useEffect(() => {
socket.on(channel, onReceive);
return () => {
socket.off(channel, onReceive);
};
}, [channel, onReceive]);
}
export default useSocketReceiver;
- useContext Hook을 이용하여 Socket 연결 인스턴스를 얻습니다.
- useEffect Hook을 이용하여 컴포넌트가 마운트될 시 onReceive(이벤트 수신 콜백함수)를 바인딩하고, 언마운트될 시 onReceive 바인딩을 해제합니다.
useSocketReceiver Hook은 특정 컴포넌트에서 아래와 같이 사용될 수 있습니다.
import useSocketReceiver from "hooks/useSocketReceiver";
function TestComponent() {
useSocketReceiver(이벤트 명, (data: 타입) => {
// data 핸들링
});
return ( ... );
}
export deafult TestComponent;
.
.
.
어떤가요? Socket.IO 관심사의 Context와 Hook을 구현하니 어느 컴포넌트에서든 효율적으로 실시간 통신 이벤트를 사용할 수 있을 것 같지 않나요?
현재 함께 페어 프로그래밍을 하는 파트너 팀원께서도 Socket.IO Custom Hook 구현에 대한 아이디어를 너무나 좋게 봐주셨습니다 😁
더 개선할 수 있는 부분이 있다면 댓글 주시면 감사하겠습니다!