스벨트킷으로 구성된 프론트엔드와 nodejs로 구성된 백엔드를 소켓으로 실시간 연결한다. 평소 쉽게 네트워킹하던 restAPI와 달리 실시간으로 뚫어놓는 것이다.
웹소켓은 클라이언트와 서버 간의 양방향 통신을 가능하게 해주는 프로토콜(약속)입니다. 일반적인 HTTP 요청-응답 방식과는 달리, 웹소켓은 연결이 한 번 수립되면 클라이언트와 서버가 서로 자유롭게 데이터를 주고받을 수 있습니다. 이로 인해 실시간 애플리케이션, 예를 들어 채팅 앱이나 실시간 게임에서 매우 유용하게 사용됩니다.
쉽게 말해 횡단보도가 없고 고속도로가 뚫려있는 하이패스 통신!
양방향 통신: 클라이언트와 서버가 서로 데이터를 자유롭게 주고받을 수 있습니다.
지속적인 연결: 초기 연결이 설정된 후에는 연결이 유지되며, 추가적인 HTTP 요청 없이 데이터를 주고받을 수 있습니다.
낮은 레이턴시: 헤더 크기가 작고, 연결 수립 후 추가적인 핸드셰이크가 필요 없기 때문에 더 빠른 응답을 제공합니다.
Socket.IO는 웹소켓 프로토콜을 기반으로 한 JavaScript 라이브러리로, 실시간 웹 애플리케이션을 쉽게 개발할 수 있도록 돕습니다. Socket.IO는 웹소켓이 지원되지 않는 브라우저에서도 사용할 수 있도록 자동으로 폴백(fallback) 메커니즘을 제공합니다. 즉, 웹소켓이 불가능한 환경에서도 다른 방법(예: 폴링)을 통해 통신할 수 있습니다.
Socket.IO 연결 과정
초기 연결을 HTTP로 설정 > 웹소켓 프로토콜 업그레이드 = 실시간 통신
WebSocket의 헤더를 "업그레이드"한다는 것은 클라이언트와 서버 간의 HTTP 프로토콜을 사용하여 초기 연결을 설정한 후, 이 연결을 WebSocket 프로토콜로 변경하는 과정을 의미합니다. 이를 통해 실시간 양방향 통신을 가능하게 합니다.
주요 특징 자동 폴백: 웹소켓이 지원되지 않는 환경에서도 안정적으로 연결을 유지합니다. 이벤트 기반: 클라이언트와 서버 간의 통신이 이벤트를 통해 이루어져, 코드가 직관적이고 간단합니다. 방(room) 기능: 여러 클라이언트를 그룹으로 묶어 특정 메시지를 전송할 수 있는 기능을 제공합니다. 네임스페이스: 여러 개의 독립적인 소켓 연결을 생성하여 관리할 수 있습니다.
digda/ ├── apps/ │ ├── client/ # SvelteKit 프론트엔드 │ │ ├── src/ │ │ │ ├── lib/ │ │ │ │ └── socket.js │ │ │ ├── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ └── +page.svelte #여기서 client 연결 │ │ │ └── app.css │ │ ├── package.json │ │ ├── svelte.config.js │ │ └── vite.config.js │ │ │ └── server/ # Node.js 백엔드 │ ├── src/ │ │ └── socket/ │ │ └── server_sockets.js │ ├── index.js # 여기서 백엔드 연결 │ └── package.json
서버 준비 서버 설정: 먼저, Socket.IO 서버가 실행되고 있어야 합니다. 서버는 클라이언트가 연결 요청을 받을 준비가 되어 있어야 합니다. 이때 서버는 특정 엔드포인트(예: /socket.io/)를 통해 클라이언트의 요청을 기다립니다.
// index.js import http from "http"; import { attach_sockets } from "./lib/server_sockets.js"; // HTTP 서버 생성 const server = http.createServer(); // 서버 실행 const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); attach_sockets(server);
commonjs 모듈이 아닌 esm을 사용한다. nodejs도 가능함 http 서버만 필요하기때문에 express는 설치 하지 않는다.
서버를 실행한 뒤 별로도 만든 소켓 모듈을 실행한다.
// lib/server_socket.io import { Server } from "socket.io"; export function attach_sockets(server) { // Socket.IO 서버 초기화 const io = new Server(server, { cors: { origin: "http://localhost:5173", // SvelteKit 기본 포트 methods: ["GET", "POST"], credentials: true, }, }); io.on("connection", (socket) => { console.log(socket.id, "user connected"); socket.on("eventFromClient", (msg) => { console.log("📟 Message from client:", msg); // 클라이언트에 응답 socket.emit("eventFromServer", `서버입니다: ${msg}`); }); socket.on("disconnect", () => { console.log("User disconnected"); }); }); }
클라이언트 연결 요청 클라이언트 요청: 클라이언트가 Socket.IO 서버에 연결을 요청합니다. 이 요청은 HTTP 요청 형태로 전송됩니다. 클라이언트는 이 요청을 통해 서버와의 연결을 시작합니다.
socket을 class로 구현하여 메서드를 활용해 구조화를 높였다.
2 . 상태 관리
class SocketWrapper { #socket; #isConnected; constructor() { this.#isConnected = writable(false); } get isConnected() { return { subscribe: this.#isConnected.subscribe }; } } /** * 소켓 연결을 초기화합니다 * * @param {string} [url='http://localhost:3000'] - 서버 URL. Default is `'http://localhost:3000'` * @param {SocketOptions} [options={}] - 소켓 설정. Default is `{}` */ connect(url = 'http://localhost:3000', options = {}) { try { this.#socket = io(url, { withCredentials: true, transports: ['websocket'], reconnectionAttempts: 5, reconnectionDelay: 1000, ...options }) this.#socket.on('connect', () => { console.log('Connected with ID:', this.#socket.id) this.#isConnected.set(true) // 연결 상태 업데이트 }) this.#socket.on('disconnect', () => { console.log('Disconnected') this.#isConnected.set(false) // 연결 해제 상태 업데이트 }) // 연결 에러 이벤트 리스너 this.#socket.on('connect_error', (error) => { console.error('Connection error:', error) }) return this.#socket } catch (error) { console.error('Socket initialization error:', error) throw error } }
class SocketWrapper { emit(event, data) { this.#socket?.emit(event, data); } on(event, callback) { this.#socket?.on(event, callback); return () => this.#socket?.off(event, callback); } }
connect(url, options = {}) { try { // 연결 로직 } catch (error) { console.error('Socket initialization error:', error); this.#isConnected.set(false); throw error; } }
//+page.svelte <script> import { socket } from '$lib/socket' import { onMount } from 'svelte' onMount(() => { socket.connect()
mount 되자마자 연결해버린다.
let currentMessage = $state('') // 입력창에 입력된 메시지를 저장할 변수 let returnMessage = $state(['']) // 서버로부터 받은 메시지를 저장할 변수 onMount(() => { socket.connect() socket.on('connect', () => { console.log('🧑🏾💻 Socket connected') }) socket.on('disconnect', () => { console.log('🧑🏾💻 Socket disconnected') }) if (!socket.isConnected) { return console.log('🧑🏾💻 Socket is not connected') } const unsubscribe = socket.on('eventFromServer', (message) => { // 불변성을 유지하면서 배열 업데이트 returnMessage = [...returnMessage, message] }) return () => unsubscribe() }) function sendMessage() { if (currentMessage.trim()) { socket.emit('eventFromClient', currentMessage) currentMessage = '' } } </script> <form onsubmit={sendMessage} class="flex max-w-lg mt-10 items-center"> <Input class="w-full" bind:value={currentMessage} placeholder="Type a message..." onkeypress={(e) => e.key === 'Enter'} /> <Button type="submit">Send</Button> </form> <ul> {#each returnMessage as message} <li>{message}</li> {/each} </ul>