웹소켓 통신: 실시간 채팅과 파일 전송 구현하기🌐
현대 웹 애플리케이션에서는 사용자 간의 실시간 소통이 점점 더 중요해지고 있습니다.
채팅, 알림, 실시간 데이터 업데이트 등 다양한 기능이 사용자 경험을 한층 향상시키고 있죠.
이러한 요구를 충족시키기 위해 등장한 기술 중 하나가 바로 웹소켓(WebSocket)입니다.
웹소켓은 클라이언트와 서버 간의 지속적인 연결을 유지하여, 양방향 통신을 가능하게 해줍니다.
이를 통해 실시간으로 메시지를 주고받거나 파일을 전송하는 등의 기능을 구현할 수 있습니다.
🪁 웹소켓(WebSocket)이란?
웹소켓(WebSocket)은 실시간 양방향 통신을 가능하게 해주는 프로토콜입니다.
기존의 HTTP 요청-응답 방식과 달리,
(기존의 HTTP 요청-응답 방식은 각 요청마다 연결을 새로 설정해야 하므로,실시간 소통이 필요한 애플리케이션에서는 한계가 있었습니다.)
웹소켓은 한 번 연결되면 유지되며, 클라이언트와 서버 간에 자유롭게 또 지속적으로 데이터를 주고받을 수 있습니다.
이로 인해 클라이언트와 서버 간에 자유롭게 데이터를 주고받을 수 있으며, 필요한 순간에 즉시 통신이 이루어집니다.
웹소켓은 다양한 실시간 애플리케이션에서 매우 유용하게 사용됩니다.
예를 들어,
채팅 서비스에서는 사용자 간의 메시지를 즉시 전달할 수 있고,
실시간 알림 시스템에서는 중요한 정보를 신속하게 업데이트할 수 있습니다.
또한, 주식 시장의 시세 변동을 실시간으로 반영하여 사용자에게 가장 최신의 정보를 제공하는 데도 활용됩니다.
🚀 웹소켓 vs HTTP, 뭐가 다를까?
HTTP | WebSocket |
|
통신 방식 | 요청-응답 방식 | 지속적인 연결 유지 |
연결 유지 | 요청 시마다 연결 생성 | 한 번 연결되면 지속됨 |
데이터 흐름 | 클라이언트가 요청해야 서버 응답 | 클라이언트-서버 간 양방향 통신 가능 |
성능 | 매 요청마다 새로운 연결, 오버헤드 발생 | 연결 유지로 오버헤드 최소화 |
사용 사례 | 정적인 웹사이트, API 요청 | 채팅, 실시간 알림, 스트리밍 |
🔥 웹소켓이 필요한 이유
- HTTP는 요청을 보내야지만 응답이 가능하기 때문에, 클라이언트가 서버의 변경 사항을 확인하려면 계속해서 요청을 보내야 함 (폴링, Long Polling 필요)
- 웹소켓은 서버에서 실시간으로 데이터를 보낼 수 있어 클라이언트가 변경 사항을 즉시 받을 수 있음
- 빠른 응답 속도와 네트워크 자원 절약 가능
🎛️ 웹소켓의 주요 특징
🌀 양방향 통신:
웹소켓은 클라이언트와 서버 간에 실시간으로 데이터를 주고받을 수 있는 양방향 통신을 지원합니다.
즉, 한쪽에서 메시지를 전송하면 다른 쪽에서도 이를 즉시 수신할 수 있습니다.
이러한 통신 방식은 채팅 애플리케이션, 온라인 게임, 실시간 알림 시스템 등에서 매우 유용합니다.
예를 들어, 사용자가 메시지를 보내면 서버는 이를 즉시 받아서 다른 사용자에게 전달하고,
이 과정은 클라이언트와 서버 간의 연결이 끊어지지 않는 한 계속 유지됩니다.
⏱️ 낮은 지연 시간
웹소켓은 HTTP 프로토콜과 비교하여 낮은 지연 시간을 자랑합니다.
HTTP는 요청-응답 방식으로 동작하며, 각 요청마다 새로운 연결을 설정해야 합니다.
이 과정에서 연결 설정, 데이터 전송, 응답 수신 등의 시간이 소요되어 지연이 발생합니다.
반면, 웹소켓은 초기 연결이 이루어진 후 지속적인 연결을 유지하므로 추가적인 오버헤드 없이 데이터를 즉시 전송할 수 있습니다.
예를 들어, 게임에서는 실시간으로 사용자 위치 정보를 업데이트해야 하는데, 웹소켓은 이러한 요구에 효과적으로 대응할 수 있습니다.
🔗 연결 지속성
웹소켓은 클라이언트와 서버 간의 연결이 한 번 설정되면 해당 연결이 유지됩니다.
이는 클라이언트가 서버와의 연결을 지속적으로 유지하고 필요할 때마다 데이터를 전송할 수 있음을 의미합니다.
이로 인해 매번 새로운 연결을 설정하는 것보다 훨씬 더 효율적으로 데이터 통신을 할 수 있습니다.
예를 들어, 채팅 애플리케이션에서는 사용자가 메시지를 주고받을 때마다 연결을 새로 설정할 필요가 없으므로,
더 부드럽고 빠른 사용자 경험을 제공할 수 있는 거지요.
💡정리: WebSocket이 중요한 이유?
✅ 풀 듀플렉스(Fullduplex) → 클라이언트와 서버가 동시에 데이터를 송수신
✅ Persistent Connection → 한 번 연결되면 계속 유지됨
✅ 낮은 오버헤드 → HTTP Polling보다 리소스 사용이 적음
🛠 웹소켓의 작동 원리
웹소켓은 기본적으로 핸드셰이크(Handshake) 과정을 통해 연결을 설정합니다.
🔦 1 단계 : 웹소켓 핸드셰이크 ( 과정 )
1️⃣ 클라이언트가 서버로 웹소켓 연결 요청 (ws:// 또는 wss:// 사용)
- ✅ 클라이언트는 웹소켓 서버에 연결 요청을 보냅니다.
- ✅ 이 요청은 HTTP 프로토콜을 사용하여 이루어지며,
- ✅ 클라이언트는 Upgrade 헤더를 포함하여 웹소켓 프로토콜로의 전환을 요청합니다.
📌 클라이언트가 서버에 보낸 요청
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2️⃣ 서버가 요청을 받고, 웹소켓 프로토콜로 전환 (HTTP 101 Switching Protocols 응답)
- 서버는 이 요청을 수신하고 확인한 후, 웹소켓 프로토콜로 전환합니다.
- 성공적으로 전환되면, 서버는 클라이언트에게 승인 응답을 보냅니다.
- 이 응답에는 클라이언트의 요청에 대한 확인과 함께 새로운 연결에 대한 정보가 포함됩니다.
📌 서버의 응답
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
이후 클라이언트와 서버는 연결이 유지된 상태에서 자유롭게 데이터를 주고받을 수 있습니다.
📡 2 단계 : 연결 후 데이터 전송
1️⃣ 연결 확립
핸드셰이크가 완료되면 클라이언트와 서버 간의 연결이 확립됩니다.
이 상태에서는 양방향으로 데이터 프레임을 주고받을 수 있습니다.
2️⃣ 실시간 통신
클라이언트와 서버는 서로의 상태를 실시간으로 반영하며, 필요에 따라 데이터를 즉시 전송할 수 있습니다.
예를 들어, 사용자가 채팅 메시지를 보내면, 서버는 이를 즉시 다른 사용자에게 전달합니다.
3️⃣ 데이터 프레임
웹소켓에서는 데이터가 "프레임"이라는 단위로 전송됩니다.
각 프레임에는 메시지의 종류(텍스트, 바이너리 등)와 데이터가 포함되어 있습니다.
🛑 3 단계 : 연결 종료
1️⃣ 종료 요청
연결을 종료하려면 클라이언트 또는 서버가 연결 종료 메시지를 전송합니다.
이는 웹소켓 프로토콜에 따라 정해진 형식으로 이루어집니다
2️⃣ 안전한 종료
연결 종료 메시지를 수신한 쪽은 연결을 안전하게 종료하며, 이 과정에서도 추가적인 핸드셰이크가 필요하지 않습니다.
이는 웹소켓의 효율성을 더욱 높이는 요소입니다.
📌 종료 메시지
Close frame: [code, reason]
이러한 과정을 통해 웹소켓은 클라이언트와 서버 간의 지속적이고 효율적인 통신을 가능하게 합니다.
웹소켓의 이러한 특징은 실시간 기능이 중요한 다양한 애플리케이션에서 큰 장점을 제공합니다.
✍ 웹소켓 문법과 규칙
1️⃣ 웹소켓 연결하기
const socket = new WebSocket('ws://example.com/socket');
- ws:// → 웹소켓 연결 (보안 필요 시 wss:// 사용)
- 웹소켓 객체가 생성되면 서버와의 연결 시도 시작
2️⃣ 이벤트 핸들링 (연결, 메시지, 에러, 종료)
socket.onopen = function () {
console.log('웹소켓 연결 완료! 🎉');
socket.send('Hello, Server!'); // 서버로 데이터 전송
};
socket.onmessage = function (event) {
console.log('서버 메시지 수신: ', event.data);
};
socket.onerror = function (error) {
console.error('웹소켓 오류 발생 ❌', error);
};
socket.onclose = function () {
console.log('웹소켓 연결 종료 😢');
};
3️⃣ 데이터 보내고 받기
// 서버로 데이터 보내기
socket.send(JSON.stringify({ type: 'message', text: '안녕하세요!' }));
// 서버에서 JSON 데이터를 받을 경우
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
console.log('받은 데이터:', data);
};
📌 보낼 수 있는 데이터 타입:
- 문자열(String)
- Blob (이미지, 파일 전송)
- ArrayBuffer (바이너리 데이터 전송)
4️⃣ 웹소켓 연결 닫기
socket.close(); // 정상 종료
socket.close(1000, '정상 종료'); // 종료 코드와 메시지 포함
📌 주요 종료 코드
코드 | 의미 |
1000 | 정상 종료 |
1001 | 클라이언트가 브라우저 종료 |
1006 | 네트워크 장애 |
1011 | 서버 내부 오류 |
💠 OSI 7계층에서의 웹소켓
웹소켓은 OSI 7계층 모델에서 응용 계층과 전송 계층에서 중요한 역할을 합니다.
이를 통해 클라이언트와 서버 간의 효율적인 실시간 통신을 가능하게 합니다.
🔩 응용 계층에서의 웹소켓
💬 데이터 형식 및 프로토콜
웹소켓은 클라이언트와 서버 간의 데이터 형식을 정의합니다.
클라이언트는 웹소켓 API를 통해 메시지를 전송하며, 이때 JSON, XML 또는 텍스트 형식으로 데이터를 주고받습니다.
이 데이터 형식은 서로 다른 프로그래밍 언어와 플랫폼 간의 상호 이해를 가능하게 하여 메시지를 효율적으로 처리할 수 있도록 돕습니다.
🖥️ 웹소켓 API
웹소켓은 JavaScript와 같은 클라이언트 사이드 언어에서 사용할 수 있는 API를 제공합니다.
📌 웹소켓 API의 주요 메서드
- WebSocket.send(): 서버로 데이터를 전송합니다.
- WebSocket.onmessage: 서버로부터 메시지를 수신했을 때 호출되는 이벤트 핸들러입니다.
- WebSocket.onopen: 연결이 성공적으로 열렸을 때 호출되는 이벤트 핸들러입니다.
- WebSocket.onclose: 연결이 닫혔을 때 호출되는 이벤트 핸들러입니다.
- WebSocket.onerror: 오류가 발생했을 때 호출되는 이벤트 핸들러입니다.
📦 메시징 구조
웹소켓은 프레임 단위로 데이터를 전송합니다.
클라이언트에서 전송된 데이터는 웹소켓 프로토콜에 따라 프레임 형식으로 패키징되어 서버로 전송됩니다.
이 과정은 매우 빠르며 각 프레임은 기본적으로 데이터와 메타데이터를 포함합니다.
예를 들어, 클라이언트가 "안녕하세요"라는 메시지를 보내면 이 메시지는 웹소켓 프레임으로 감싸져 전송됩니다.
서버는 이 프레임을 해석하여 원래 메시지를 읽고, 필요에 따라 다른 클라이언트에게 전달할 수 있습니다.
🔄 상태 유지 및 세션 관리
웹소켓은 연결이 열려 있는 동안 상태를 유지합니다.
이로 인해 클라이언트와 서버는 지속적으로 데이터를 주고받을 수 있으며, 실시간으로 사용자 상호작용을 처리할 수 있습니다.
예를 들어, 채팅 애플리케이션에서는 사용자가 메시지를 입력할 때마다 서버와 클라이언트 간의 연결이 끊기지 않으므로 사용자 경험이 원활하게 유지됩니다.
이러한 상태 유지는 실시간 상호작용이 중요한 애플리케이션에서 큰 장점으로 작용합니다.
🔄 Pub/Sub 구조란?
Pub/Sub 구조는 발행(Publish)하는 주체와 구독(Subscribe)하는 주체를 분리하여 이벤트 기반 메시징을 가능하게 하는 패턴입니다. 채팅 서비스, 주식 데이터 스트리밍, 실시간 알림 시스템 등에서 자주 사용됩니다.
📌 Pub/Sub의 핵심 개념
- Publisher (발행자) → 메시지를 특정 채널(Topic)에 발행
- Broker (중개자, Message Queue) → 메시지를 적절한 수신자에게 라우팅
- Subscriber (구독자) → 특정 채널을 구독하고, 메시지를 수신
💡 왜 Pub/Sub 구조를 사용할까?
- 발행자와 구독자가 직접 연결되지 않아도 된다 → 분산 시스템에서 강력한 확장성 제공
- 비동기 이벤트 기반 아키텍처를 쉽게 구축 가능
- 서버 부하 감소 → 다수의 클라이언트가 데이터를 구독하더라도 성능 저하가 적음
🔍 WebSocket 기반 Pub/Sub 동작 원리
WebSocket과 Pub/Sub 구조가 결합되면, 서버는 Publisher(발행자)로, 클라이언트는 Subscriber(구독자)로 동작하게 됩니다. 이를 메시지 브로커(Redis, Kafka 등)와 함께 활용하면 훨씬 더 강력한 시스템을 구축할 수 있습니다.
📌 동작 과정
1️⃣ 클라이언트가 WebSocket 서버에 연결
2️⃣ 클라이언트가 특정 채널(Topic)을 구독 (Subscribe)
3️⃣ 서버가 특정 클라이언트에게 메시지를 발행 (Publish)
4️⃣ 메시지 브로커(Redis 등)를 활용해 다수의 클라이언트에게 메시지 전달
📡 프론트엔드 & 백엔드 실시간 웹소켓 통신: 클라이언트부터 S3 파일 전송까지
웹소켓을 활용하면 클라이언트와 서버 간 양방향 실시간 통신을 간단하게 구현할 수 있어요.
이번 글에서는 웹소켓(WebSocket)을 활용해 실시간 메시지를 주고받는 퍼블리시-구독(pub/sub) 방식의 채팅 시스템을 구현하고,
AWS S3를 활용한 파일 업로드까지 다뤄볼게요.
🎯 1. 프론트엔드에서 웹소켓 연결하기
웹소켓을 사용하는 방법은 생각보다 간단해요.
브라우저에서 제공하는 WebSocket API를 활용하면 바로 구현할 수 있어요.
🛠 웹소켓 기본 코드
const socket = new WebSocket('ws://서버주소:포트/ws');
// ✅ 웹소켓 연결됨
socket.addEventListener('open', () => {
console.log('✅ WebSocket 연결 성공!');
socket.send('안녕하세요, 서버!');
});
// 📩 메시지 수신
socket.addEventListener('message', (event) => {
console.log('📩 받은 메시지:', event.data);
});
// 🔴 연결 종료
socket.addEventListener('close', () => {
console.log('🔴 WebSocket 연결 종료');
});
// ⚠️ 오류 발생
socket.addEventListener('error', (event) => {
console.error('⚠️ WebSocket 오류:', event);
});
👉 이 코드의 핵심은?
- open: 웹소켓 연결 성공 시 실행
- message: 서버에서 메시지를 받으면 실행
- close: 웹소켓 연결이 종료될 때 실행
- error: 오류가 발생했을 때 실행
📌 2. 웹소켓 상태 관리하기 (React 예제)
웹소켓 연결 상태와 메시지를 관리하려면 상태 관리 라이브러리를 활용하면 좋아요.
✅ React 활용 코드
import React, { useEffect, useState } from 'react';
const useChat = () => {
const [socket, setSocket] = useState(null);
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('ws://서버주소:포트/ws');
setSocket(ws);
ws.addEventListener('message', (event) => {
setMessages((prevMessages) => [...prevMessages, event.data]);
});
ws.addEventListener('close', () => {
console.log('🔴 웹소켓 연결 종료');
});
// 컴포넌트 언마운트 시 소켓 연결 종료
return () => {
ws.close();
};
}, []);
const sendMessage = (message) => {
if (socket) {
socket.send(message);
}
};
return { messages, sendMessage };
};
const ChatComponent = () => {
const { messages, sendMessage } = useChat();
const [inputMessage, setInputMessage] = useState('');
const handleSend = () => {
sendMessage(inputMessage);
setInputMessage('');
};
return (
<div>
<div>
{messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
</div>
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
/>
<button onClick={handleSend}>전송</button>
</div>
);
};
export default ChatComponent;
👉 이 코드의 핵심은?
1️⃣ useChat 훅:
- socket과 messages 상태를 관리합니다.
- useEffect를 사용하여 웹소켓을 연결하고 메시지 수신 이벤트를 설정합니다.
- 컴포넌트가 언마운트될 때 웹소켓 연결을 종료합니다.
2️⃣ ChatComponent:
- 메시지를 입력하고 전송할 수 있는 UI를 제공합니다.
- handleSend 함수로 메시지를 전송하고 입력 필드를 초기화합니다.
이렇게하면 프론트엔드는 준비 완료입니다!
🚀 3. 백엔드(WebSocket + Spring + Kafka) 설정하기
웹소켓 서버는 Spring Boot + STOMP 프로토콜을 사용해 설정할 수 있어요.
또한, Kafka를 활용해 메시지를 비동기적으로 처리하면 확장성이 훨씬 좋아집니다.
✅ 웹소켓 설정 코드
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic"); // 메시지 브로커 설정
config.setApplicationDestinationPrefixes("/app"); // 클라이언트 메시지 라우팅
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS(); // 클라이언트 엔드포인트
}
}
👉 이 코드의 핵심은?
- /ws 엔드포인트로 클라이언트가 연결
- /app으로 들어오는 메시지를 /topic으로 브로드캐스트
- STOMP 프로토콜을 활용해 메시지를 구독/발행
📨 4. Kafka를 활용한 메시지 처리
웹소켓을 단순히 연결하는 것뿐만 아니라, Kafka를 활용해 메시지를 비동기적으로 관리하면 훨씬 효율적이에요.
✅ Kafka 프로듀서 코드
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@MessageMapping("/sendMessage")
public void sendMessage(String message) {
kafkaTemplate.send("chat-topic", message); // 메시지를 카프카에 저장
}
📌 클라이언트가 /app/sendMessage로 메시지를 보내면
→ Kafka의 chat-topic에 메시지가 저장됨
🔄 5. 프론트엔드에서 메시지 구독하기
이제 클라이언트에서 Kafka로부터 메시지를 받아 웹소켓을 통해 UI를 업데이트해볼까요?
✅ 채팅 UI에 메시지 표시
function displayMessage(message) {
const messageContainer = document.getElementById('messageContainer');
const newMessage = document.createElement('div');
newMessage.textContent = message;
messageContainer.appendChild(newMessage);
}
Kafka에서 메시지를 받아온 후, displayMessage() 함수를 호출하면 화면에 메시지가 추가돼요!
📂 6. 파일 업로드 (AWS S3 연동)
실시간 채팅에서 파일 공유 기능을 추가하고 싶다면 AWS S3를 활용하면 좋아요.
✅ 클라이언트에서 파일 업로드 요청
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
fetch('http://서버주소/api/upload', {
method: 'POST',
body: formData,
});
📌 FormData를 활용해 파일을 서버로 전송
✅ Spring에서 파일을 S3에 업로드
@PostMapping("/api/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
String fileUrl = s3Service.uploadFile(file);
return ResponseEntity.ok(fileUrl);
}
📌 클라이언트에서 보낸 파일을 S3에 저장하고 파일 URL 반환
📈 성능 최적화 및 고급 활용법
아래는 웹소켓 통신의 부하를 줄이고 속도와 성능을 개선하는 백엔드 방법입니다!
✅ 1) 다중 서버에서 WebSocket Pub/Sub 구현
- Redis의 클러스터링(Clustering) 기능을 사용하여 여러 개의 WebSocket 서버가 메시지를 공유할 수 있습니다.
- Kafka + WebSocket을 활용하면 대량의 데이터를 처리하는 실시간 시스템을 구축할 수 있습니다.
✅ 2) 메시지 브로커 활용
- Redis Pub/Sub: 단순한 메시징 시스템 구현 가능
- Kafka: 대량의 이벤트 스트리밍 시스템 구축 가능
- RabbitMQ: 메시지 큐잉을 활용한 대기열 기반 메시징 가능
✅ 3) WebSocket의 대체 기술
- SSE (Server-Sent Events): 단방향 스트리밍 (서버 → 클라이언트)
- GraphQL Subscriptions: WebSocket을 GraphQL API와 함께 사용
🎯 7. 마무리 & 정리
🚀 프론트엔드
✅ WebSocket은 양방향 실시간 통신이 가능
✅ Pub/Sub 패턴은 분산 시스템에서 확장성을 보장
✅ 실시간 채팅, 주식 데이터, 알림 시스템 등에 필수적으로 사용됨
🛠 백엔드
✅ Spring Boot + STOMP로 웹소켓 서버 구축
✅ Redis, Kafka 등의 메시지 브로커와 결합하면 성능 최적화 가능
✅ AWS S3로 파일 업로드
🌷 전설의 개발자가 되어봅시당! 🌷