스벨트킷으로 구성된 프론트엔드와 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>