Ver2.5 자바스크립트 웹소켓: 실시간 웹의 심장을 뛰게 하는 기술 🚀

자바스크립트 웹소켓: 실시간 웹의 심장을 뛰게 하는 기술 🚀
새로고침(F5)의 시대는 끝났다! 이제 서버와 클라이언트가 실시간으로 찐친처럼 대화하는 웹소켓의 세계로 당신을 초대합니다.
안녕, 코딩하는 친구들! 👋 오늘 우리가 다룰 주제는 진짜 '요즘 웹'의 핵심, 바로 웹소켓(WebSocket)이야. 혹시 라이브 채팅, 실시간 주식 차트, 온라인 멀티플레이어 게임 같은 걸 보면서 '이거 대체 어떻게 만드는 거지?' 궁금해 본 적 없어? 화면을 새로고침하지도 않았는데 데이터가 슥슥 바뀌는 마법 말이야. 그 마법의 주문이 바로 '웹소켓'이라는 거지.
옛날 웹은 좀 답답했어. 클라이언트(우리 브라우저)가 "서버님, 뭐 새로운 소식 없수?" 하고 물어봐야만 서버가 "어, 이거 받아" 하고 대답해주는 식이었거든. 이걸 HTTP 요청-응답 모델이라고 해. 마치 내가 친구한테 계속 "지금 뭐해?", "이제 뭐해?", "5초 지났는데 뭐해?" 하고 카톡을 보내야만 답장을 받을 수 있는 상황이랑 비슷해. 얼마나 피곤해? 😅
하지만 웹소켓은 달라. 한번 연결을 딱! 맺으면, 그 다음부터는 서버든 클라이언트든 상관없이 아무 때나 서로에게 메시지를 보낼 수 있는 '직통 전화' 같은 거야. 서버가 "야, 새로운 뉴스 떴다!" 하고 먼저 알려줄 수도 있고, 내가 "나 방금 메시지 보냈어!" 하고 바로 쏠 수도 있지. 이 양방향(Full-Duplex) 통신 덕분에 우리는 진짜 '살아있는' 웹 애플리케이션을 만들 수 있게 된 거야. 자, 그럼 이제부터 이 멋진 기술의 A to Z를 함께 파헤쳐 보자고! 준비됐어? Let's go! 💨
1. 웹소켓 이전의 세상: 눈물겨운 실시간 흉내내기 😢
웹소켓이 등장하기 전, 개발자들은 어떻게든 실시간 비슷한 경험을 만들어보려고 정말 눈물겨운 노력을 했어. 이른바 'HTTP를 이용한 꼼수'들이지. 왜 이런 방법들이 필요했고, 또 어떤 한계가 있었는지 알아야 웹소켓의 위대함을 제대로 느낄 수 있겠지? 한번 살펴보자.
(1) 폴링 (Polling): "아직이야? 아직이야?" 무한 질문봇
가장 단순하고 무식한(?) 방법이야. 클라이언트가 일정한 시간 간격(예: 매 2초)으로 서버에게 계속해서 "새 데이터 있어?"라고 물어보는 거지. 마치 차 뒷좌석에 앉은 아이가 2초마다 "아빠, 다 왔어?" 하고 묻는 것과 똑같아.
장점: 구현이 아주 간단해. 그냥 `setInterval` 함수 하나면 끝나니까.
단점: 이건 뭐, 단점 종합선물세트야. 🎁
첫째, 엄청난 비효율. 새로운 데이터가 있든 없든 클라이언트는 계속 서버를 호출해야 해. 99%의 요청이 "아니, 아직 없어"라는 의미 없는 응답을 받기 위해 네트워크 자원을 낭비하는 셈이지. 서버 입장에서도 별일 없는데 계속 문 두드리는 손님 때문에 피곤해 죽을 지경이야.
둘째, 딜레이 발생. 만약 2초 간격으로 폴링하는데, 데이터가 0.1초 시점에 생겼다면? 클라이언트는 1.9초를 기다렸다가 다음 요청을 보내야만 그 데이터를 받을 수 있어. '실시간'이라고 부르기엔 좀 민망한 지연이지.
// 클라이언트 측의 간단한 폴링 예시
setInterval(async () => {
try {
const response = await fetch('/api/get-new-data');
const data = await response.json();
if (data.hasNewData) {
// 새로운 데이터가 있으면 화면을 업데이트한다.
updateUI(data.content);
}
} catch (error) {
console.error('폴링 중 에러 발생:', error);
}
}, 2000); // 2초마다 무한 반복
(2) 롱 폴링 (Long Polling): 인내심 강한 대기조
폴링의 비효율을 조금이나마 개선하기 위해 나온 방법이 '롱 폴링'이야. 이건 클라이언트가 일단 서버에 "새 데이터 있어?"라고 요청을 보내. 근데 서버는 새 데이터가 없으면 바로 "없어"라고 응답하지 않고, 요청을 붙잡아두고 기다리는 거야. 그러다가 마침내 새 데이터가 생기면! 그제서야 클라이언트에게 "자, 여기 데이터!" 하고 응답을 보내주지.
클라이언트는 응답을 받자마자 화면을 업데이트하고, 곧바로 다시 서버에 다음 요청을 보내서 또 대기 상태에 들어가. 마치 식당에서 주문하고 음식이 나올 때까지 하염없이 기다리다가, 음식을 받자마자 "다음 음식 주세요" 하고 다시 주문하는 손님 같달까?
장점: 일반 폴링보다 불필요한 요청이 훨씬 적어. 데이터가 있을 때만 응답이 오니까. 딜레이도 폴링보다는 적은 편이야.
단점: 여전히 한계는 명확해.
첫째, 서버 리소스 문제. 서버는 수많은 클라이언트의 요청을 계속 붙들고 있어야 해. 연결 하나하나가 서버의 메모리와 스레드를 차지하니까, 동시 접속자가 많아지면 서버가 힘들어하는 건 마찬가지야.
둘째, 복잡성 증가. 타임아웃 처리 등 서버와 클라이언트 양쪽에서 관리해야 할 로직이 복잡해져. 연결이 중간에 끊어지면 어떻게 할 건지, 서버가 너무 오래 응답이 없으면 어떻게 할 건지 등등... 고려할 게 많아.
셋째, 여전한 HTTP 오버헤드. 메시지 하나를 받을 때마다 새로운 HTTP 연결을 맺어야 하므로, 매번 불필요한 HTTP 헤더 정보가 오고 가야 해. 데이터 자체는 몇 바이트 안 되는데, 헤더가 수백 바이트를 차지하는 배보다 배꼽이 더 큰 상황이 발생하지.
💡 잠깐! Comet? 롱 폴링과 비슷한 스트리밍 방식들을 통칭해서 'Comet'이라고 부르기도 했어. 웹소켓이 표준화되기 전, 실시간 웹을 구현하려는 개발자들의 처절한 몸부림의 역사라고 할 수 있지. 📜
(3) 서버-센트 이벤트 (Server-Sent Events, SSE): 서버의 일방적인 라디오 방송
SSE는 이름 그대로 '서버가 보내는 이벤트'야. 이건 클라이언트가 서버에 "나 이제부터 네 소식 들을게" 하고 구독 요청을 한번 보내면, 서버가 그 연결을 계속 유지하면서 필요할 때마다 클라이언트에게 데이터를 '밀어주는(push)' 방식이야. 서버 → 클라이언트 방향으로만 데이터 전송이 가능한 단방향 통신이지.
마치 라디오 방송국(서버)이 청취자(클라이언트)에게 계속해서 방송을 송출하는 것과 비슷해. 청취자는 듣기만 할 뿐, 방송국에 말을 걸 수는 없잖아?
장점: 웹소켓보다 가볍고 구현이 간단해. HTTP 프로토콜을 그대로 사용하기 때문에 기존 서버 인프라와 호환성도 좋아. 그리고 연결이 끊어졌을 때 자동으로 재연결하는 기능이 내장되어 있어서 아주 편리해.
단점: 치명적인 단점은 역시 '단방향'이라는 것. 클라이언트가 서버에게 데이터를 보내려면 별도의 HTTP 요청(POST 등)을 또 날려야 해. 실시간 알림, 뉴스 피드처럼 서버가 일방적으로 데이터만 보내주면 되는 상황에서는 최고의 선택이지만, 채팅처럼 양쪽이 모두 활발하게 대화해야 하는 경우에는 반쪽짜리 해결책일 뿐이야.
// 클라이언트 측의 SSE 예시
const eventSource = new EventSource('/api/subscribe');
// 'message'는 기본 이벤트 타입
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('새로운 메시지 도착:', data);
updateFeed(data);
};
// 서버에서 지정한 커스텀 이벤트('news')를 수신
eventSource.addEventListener('news', (event) => {
const newsData = JSON.parse(event.data);
console.log('새로운 뉴스 도착:', newsData);
showNewsAlert(newsData);
});
eventSource.onerror = (error) => {
console.error('SSE 에러 발생:', error);
// EventSource는 에러 발생 시 자동으로 재연결을 시도한다.
};
자, 이제 알겠지? 웹소켓이 등장하기 전까지 개발자들은 이런 '땜질'식 처방으로 버텨왔어. 하지만 진짜 실시간 양방향 통신에 대한 갈증은 점점 더 커져만 갔지. 그리고 마침내, 그 갈증을 해소해 줄 구원투수가 등판하게 돼. ⚾
2. 구원투수의 등판: 웹소켓(WebSocket)의 모든 것
앞서 살펴본 방법들의 한계를 극복하기 위해 2011년, IETF에 의해 RFC 6455라는 표준으로 웹소켓이 등장했어. 이건 기존의 HTTP와는 완전히 다른, 새로운 프로토콜이야. 웹소켓의 핵심 철학은 딱 두 가지로 요약할 수 있어.
1. 지속적인 연결 (Persistent Connection): 한번 연결되면, 누가 끊기 전까지는 계속 그 길이 열려있다.
2. 양방향 통신 (Full-Duplex Communication): 그 열린 길 위로 서버와 클라이언트가 동시에, 아무 때나 데이터를 주고받을 수 있다.
이 두 가지 특징 덕분에 우리는 마침내 '진짜 실시간'을 구현할 수 있게 된 거야. 더 이상 "다 왔어?"라고 묻거나, 하염없이 음식을 기다릴 필요가 없어진 거지. 그냥 연결해놓고, 할 말 생기면 바로 하면 돼! 😎
(1) 웹소켓의 시작: 특별한 악수, 핸드셰이크(Handshake)
웹소켓은 완전히 새로운 프로토콜이지만, 시작은 HTTP를 통해 이루어져. 이걸 '핸드셰이크' 과정이라고 해. 클라이언트가 서버에게 "저기... 혹시 웹소켓으로 대화 가능하신가요?"라고 물어보는 과정이야. 이때 클라이언트는 일반적인 HTTP 요청처럼 보이지만, 몇 가지 특별한 헤더를 담아서 보내.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
여기서 핵심은 `Upgrade: websocket`과 `Connection: Upgrade` 헤더야. 이게 바로 "이 HTTP 연결을 웹소켓 연결로 업그레이드해주세요!"라는 신호지. `Sec-WebSocket-Key`는 서버가 진짜 웹소켓 요청인지 확인하기 위한 보안 키 같은 거야.
이 요청을 받은 서버가 웹소켓을 지원한다면, 다음과 같은 응답을 보내줘.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
`101 Switching Protocols` 상태 코드가 바로 "오케이! 콜! 지금부터 프로토콜을 웹소켓으로 전환한다!"라는 뜻이야. 서버는 클라이언트가 보낸 `Sec-WebSocket-Key`를 특정 알고리즘으로 계산해서 `Sec-WebSocket-Accept` 헤더에 담아 보내주는데, 클라이언트는 이걸 보고 '아, 내가 요청한 그 서버가 맞구나'하고 확인하게 돼.
이 핸드셰이크가 성공적으로 끝나면, 둘 사이의 HTTP 연결은 사라지고 그 자리에 TCP 기반의 새로운 웹소켓 연결이 짠! 하고 생겨나는 거야. 이제부터 둘은 이 길을 통해 자유롭게 대화할 수 있어. 🤝
(2) 프로토콜: `ws://` 와 `wss://`
웹소켓은 `http://` 대신 `ws://` 라는 새로운 스킴(Scheme)을 사용해. 그리고 `https://`가 HTTP의 보안 버전이듯이, 웹소켓의 보안 버전은 `wss://` (WebSocket Secure)야. `wss://`는 TLS(SSL) 암호화 위에서 동작하기 때문에 중간에 누가 데이터를 훔쳐보거나 변조하는 것을 막아줘.
3. 실전! 자바스크립트 WebSocket API 파헤치기 💻
이론은 충분히 알았으니 이제 직접 코드를 만져볼 시간이야. 다행히도 브라우저는 아주 쓰기 편한 `WebSocket` API를 기본으로 제공하고 있어. 별도의 라이브러리 설치 없이 바로 사용할 수 있다는 말씀! 자바스크립트로 웹소켓을 다루는 건 생각보다 훨씬 간단해.
(1) 웹소켓 객체 생성: 연결의 시작
가장 먼저 할 일은 `WebSocket` 생성자를 사용해서 새로운 웹소켓 객체를 만드는 거야. 첫 번째 인자로는 접속할 서버의 주소를, 두 번째 인자(선택)로는 서브 프로토콜을 지정할 수 있어.
// 기본 웹소켓 연결 (보안 X)
const socket = new WebSocket('ws://localhost:8080');
// 보안 웹소켓 연결 (wss 사용)
const secureSocket = new WebSocket('wss://api.example.com/chat');
// 서브 프로토콜 지정 (서버와 미리 약속된 프로토콜)
// 예를 들어, 채팅 버전 1을 의미하는 'chat-v1'과
// 알림 전용 프로토콜 'notifications'를 지원한다고 가정
const protocolSocket = new WebSocket('wss://api.example.com/socket', ['chat-v1', 'notifications']);
이렇게 `new WebSocket(...)` 코드가 실행되는 순간, 브라우저는 백그라운드에서 서버와 핸드셰이크를 시도해. 그리고 이 과정에서 발생하는 여러 상황에 대응하기 위해 우리는 이벤트 리스너를 등록해야 해.
(2) 웹소켓 이벤트: 연결의 상태를 알려주는 신호들
웹소켓 객체는 연결의 수명주기(Lifecycle) 동안 여러 이벤트를 발생시켜. 우리는 이 이벤트들을 캐치해서 적절한 동작을 수행하면 돼. 가장 중요한 4대 이벤트는 다음과 같아.
1. `open` 이벤트: 연결 성공! 🎉
서버와 핸드셰이크가 성공적으로 완료되고, 이제 데이터를 주고받을 준비가 되었을 때 딱 한 번 발생해. 연결이 성공했을 때 사용자에게 환영 메시지를 보내거나, 초기 데이터를 요청하는 등의 작업을 여기서 처리하면 좋아.
socket.onopen = (event) => {
console.log('웹소켓 서버와 연결되었습니다!');
// 연결 성공 기념으로 서버에 메시지 보내기
socket.send('안녕하세요, 서버님! 저 연결됐어요!');
};
2. `message` 이벤트: 데이터 수신! 💌
웹소켓의 꽃! 서버로부터 메시지를 수신할 때마다 발생해. 이 이벤트가 발생하면, `event` 객체의 `data` 프로퍼티를 통해 서버가 보낸 데이터를 확인할 수 있어. 데이터는 보통 문자열(String) 형태이지만, 바이너리 데이터(Blob, ArrayBuffer) 형식일 수도 있어.
socket.onmessage = (event) => {
console.log('서버로부터 메시지를 수신했습니다.');
console.log('수신된 데이터:', event.data);
// 보통 서버와 JSON 형태의 문자열을 주고받는 것이 일반적이다.
try {
const message = JSON.parse(event.data);
console.log('파싱된 메시지:', message);
// 메시지 타입에 따라 다른 동작 수행
if (message.type === 'chat') {
appendChatMessage(message.payload);
} else if (message.type === 'notification') {
showNotification(message.payload);
}
} catch (error) {
console.error('수신된 데이터가 JSON 형식이 아닙니다:', event.data);
}
};
✨ 실무 꿀팁: JSON을 사용하세요! 서버와 클라이언트가 데이터를 주고받을 때는 순수한 텍스트보다는 JSON 형식의 문자열을 사용하는 것이 국룰이야. 왜냐하면 구조화된 데이터를 보내기 훨씬 편하기 때문이지. 위 코드처럼 `type` 필드를 둬서 메시지의 종류를 구분하고, `payload` 필드에 실제 데이터를 담는 식으로 약속하면 코드가 훨씬 깔끔해지고 확장성도 좋아져. 서버 개발자와 미리 이 데이터 구조(스키마)를 잘 협의하는 것이 프로젝트 성공의 지름길이야!
3. `error` 이벤트: 문제 발생! 😱
통신 중에 에러가 발생했을 때 호출돼. 예를 들어, 네트워크가 갑자기 끊기거나 서버 주소가 잘못되었을 때 발생할 수 있지. `error` 이벤트가 발생하면 보통 곧이어 `close` 이벤트가 발생해.
socket.onerror = (error) => {
console.error('웹소켓 에러가 발생했습니다:', error);
// 사용자에게 에러 상황을 알려주는 UI 처리 등을 할 수 있다.
};
4. `close` 이벤트: 연결 종료! 👋
웹소켓 연결이 종료되었을 때 발생해. 클라이언트가 `socket.close()`를 호출해서 정상적으로 종료했거나, 서버가 연결을 끊었거나, 혹은 네트워크 문제로 연결이 끊어졌을 때 모두 발생해. `event` 객체에는 왜 연결이 종료되었는지에 대한 정보가 담겨 있어.
socket.onclose = (event) => {
console.log('웹소켓 연결이 종료되었습니다.');
if (event.wasClean) {
console.log(`정상적으로 종료됨, 코드=${event.code}, 이유=${event.reason}`);
} else {
// 예: 프로세스가 죽거나 네트워크가 끊긴 경우
console.error(`비정상적으로 종료됨, 코드=${event.code}`);
// 여기서 재연결 로직을 시도해볼 수 있다.
handleReconnection();
}
};
`event.code`는 종료 코드를 나타내는 숫자인데, `1000`은 정상 종료를 의미하고, 그 외의 코드들은 다양한 에러 상황을 나타내. (예: `1006`은 비정상적인 종료)
(3) 웹소켓 메소드와 프로퍼티: 우리가 할 수 있는 일들
이벤트가 '듣는' 것이었다면, 메소드는 우리가 직접 '말하는' 거야. 웹소켓 API는 아주 간단해서 메소드는 딱 두 개만 기억하면 돼.
1. `socket.send(data)`: 서버로 데이터 전송하기
서버에게 메시지를 보내는 가장 중요한 메소드야. `onopen` 이벤트가 발생한 이후에만 사용할 수 있어. `data` 인자로는 문자열, `Blob`, `ArrayBuffer`, `TypedArray` 등을 보낼 수 있어. 위에서 말했듯이, 보통은 JSON 객체를 `JSON.stringify()`로 문자열로 변환해서 보내는 방식을 가장 많이 사용해.
// 연결이 열려있는 상태인지 확인 후 메시지 전송
if (socket.readyState === WebSocket.OPEN) {
const message = {
type: 'privateMessage',
payload: {
to: 'user123',
text: '오늘 저녁에 시간 돼?'
}
};
socket.send(JSON.stringify(message));
} else {
console.warn('웹소켓이 연결되어 있지 않습니다. 메시지를 보낼 수 없습니다.');
}
2. `socket.close([code], [reason])`: 연결 끊기
웹소켓 연결을 정상적으로 종료하고 싶을 때 사용해. 선택적으로 종료 코드(`code`)와 종료 이유(`reason`)를 함께 보낼 수 있어.
// 더 이상 웹소켓을 사용하지 않을 때
socket.close(1000, '사용자가 로그아웃했습니다.');
그리고 현재 웹소켓의 상태를 알 수 있는 유용한 프로퍼티도 있어.
`socket.readyState`: 현재 연결 상태 확인
이 프로퍼티는 현재 웹소켓 연결 상태를 나타내는 숫자를 반환해. `send()` 메소드를 호출하기 전에 이 값을 확인하면 예기치 않은 에러를 방지할 수 있어.
- `WebSocket.CONNECTING` (0): 연결 중. `new WebSocket()` 호출 직후의 상태.
- `WebSocket.OPEN` (1): 연결됨. `onopen` 이벤트 발생 후. 데이터를 주고받을 수 있는 유일한 상태.
- `WebSocket.CLOSING` (2): 연결 종료 중. `close()` 메소드 호출 후.
- `WebSocket.CLOSED` (3): 연결 종료됨. `onclose` 이벤트 발생 후.
이 정도면 자바스크립트 웹소켓 API의 핵심은 거의 다 마스터한 셈이야! 어때, 생각보다 별거 없지? 😉 이제 이 지식을 바탕으로 간단한 실시간 채팅 앱을 만들어보면서 개념을 확실히 다져보자.
4. 실전 예제: 10분 만에 만드는 실시간 채팅방 💬
백문이 불여일견! 직접 만들어보는 것만큼 좋은 공부는 없지. 웹소켓의 가장 클래식한 예제인 실시간 채팅 애플리케이션을 만들어보자. 여기서는 클라이언트(HTML/JS) 코드와, 서버 역할을 해줄 간단한 Node.js 코드를 모두 보여줄게. Node.js를 잘 몰라도 괜찮아. '아, 서버는 저런 식으로 클라이언트들을 관리하고 메시지를 뿌려주는구나' 정도로 이해하면 충분해.
만약 백엔드 구현이 막막하다면, 걱정하지 마. **재능넷** 같은 플랫폼에 가면 실력 있는 백엔드 개발자들에게 도움을 받거나, 관련 강의를 통해 직접 배워볼 수도 있으니 길은 얼마든지 있어.
(1) 프론트엔드: 채팅 UI와 클라이언트 로직 (HTML & JS)
먼저 사용자가 메시지를 보고 입력할 수 있는 간단한 화면을 만들어야겠지? `index.html` 파일을 하나 만들자.
`index.html`
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>초간단 웹소켓 채팅</title>
<style>
body { font-family: sans-serif; }
#messages { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; margin-bottom: 10px; }
#chat-form { display: flex; }
#message-input { flex-grow: 1; padding: 5px; }
#send-button { padding: 5px 10px; }
</style>
</head>
<body>
<h1>실시간 웹소켓 채팅방</h1>
<div id="messages"></div>
<form id="chat-form">
<input type="text" id="message-input" placeholder="메시지를 입력하세요..." autocomplete="off">
<button id="send-button">전송</button>
</form>
<script src="app.js"></script>
</body>
</html>
이제 이 HTML을 조작할 자바스크립트 파일, `app.js`를 작성할 차례야. 여기서 웹소켓 연결과 이벤트 처리가 모두 이루어져.
`app.js`
// DOM 요소 가져오기
const messagesDiv = document.getElementById('messages');
const chatForm = document.getElementById('chat-form');
const messageInput = document.getElementById('message-input');
// 1. 웹소켓 서버에 연결
// 실제 운영 환경에서는 'wss://your-server.com'과 같이 보안 소켓을 사용해야 합니다.
const socket = new WebSocket('ws://localhost:8080');
// 메시지를 화면에 추가하는 헬퍼 함수
function addMessage(message) {
const messageElement = document.createElement('div');
messageElement.textContent = message;
messagesDiv.appendChild(messageElement);
// 새 메시지가 오면 스크롤을 맨 아래로 이동
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// 2. 웹소켓 이벤트 리스너 등록
socket.onopen = () => {
addMessage('서버에 연결되었습니다.');
};
socket.onmessage = (event) => {
// 서버로부터 받은 메시지를 화면에 표시
addMessage(`서버: ${event.data}`);
};
socket.onclose = () => {
addMessage('서버와의 연결이 끊어졌습니다.');
};
socket.onerror = (error) => {
console.error('WebSocket Error:', error);
addMessage('에러가 발생했습니다. 콘솔을 확인하세요.');
};
// 3. 폼 제출(메시지 전송) 이벤트 처리
chatForm.addEventListener('submit', (event) => {
event.preventDefault(); // 폼 제출 시 페이지가 새로고침되는 것을 방지
const message = messageInput.value;
if (message.trim() === '') return; // 빈 메시지는 보내지 않음
// 사용자가 입력한 메시지를 서버로 전송
socket.send(message);
// 내가 보낸 메시지도 화면에 표시
addMessage(`나: ${message}`);
// 입력창 비우기
messageInput.value = '';
});
(2) 백엔드: Node.js와 `ws` 라이브러리로 만드는 간단한 서버
이제 클라이언트들의 연결을 받고, 한 클라이언트가 보낸 메시지를 다른 모든 클라이언트에게 전달(브로드캐스팅)해주는 서버가 필요해. Node.js 환경에서는 `ws`라는 아주 인기 있는 라이브러리를 사용하면 웹소켓 서버를 쉽게 만들 수 있어.
먼저, 터미널에서 Node.js 프로젝트를 시작하고 `ws` 라이브러리를 설치해야 해.
# 프로젝트 폴더를 만들고 이동
mkdir websocket-chat-server
cd websocket-chat-server
# npm 프로젝트 초기화
npm init -y
# ws 라이브러리 설치
npm install ws
그리고 `server.js` 파일을 만들어서 아래 코드를 작성하자.
`server.js`
const WebSocket = require('ws');
// 8080 포트에 웹소켓 서버를 생성
const wss = new WebSocket.Server({ port: 8080 });
console.log('웹소켓 서버가 8080 포트에서 실행 중입니다...');
// wss.clients는 현재 서버에 연결된 모든 클라이언트의 Set(집합)
// Set을 사용하면 중복 없이 클라이언트를 관리할 수 있다.
wss.on('connection', (ws) => {
// 새로운 클라이언트가 연결되었을 때 실행될 콜백 함수
// 'ws'는 개별 클라이언트와의 연결을 나타내는 웹소켓 객체
console.log('새로운 클라이언트가 연결되었습니다.');
// 연결된 클라이언트에게 환영 메시지 보내기
ws.send('채팅방에 오신 것을 환영합니다!');
// 클라이언트로부터 메시지를 수신했을 때 실행될 이벤트 리스너
ws.on('message', (message) => {
console.log(`수신된 메시지: ${message}`);
// 브로드캐스팅: 메시지를 보낸 클라이언트를 제외한
// 모든 클라이언트에게 메시지를 전달한다.
wss.clients.forEach((client) => {
// client는 wss.clients Set에 있는 각 클라이언트 소켓
// 메시지를 보낸 클라이언트(ws)가 아니고, 연결이 열려있는 상태(OPEN)일 때만
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(String(message)); // Buffer를 문자열로 변환하여 전송
}
});
});
// 클라이언트와의 연결이 끊어졌을 때 실행될 이벤트 리스너
ws.on('close', () => {
console.log('클라이언트와의 연결이 끊어졌습니다.');
});
// 에러 처리
ws.on('error', (error) => {
console.error('클라이언트 소켓 에러:', error);
});
});
이제 서버를 실행하고, 웹 브라우저에서 `index.html` 파일을 열어보자.
# 터미널에서 서버 실행
node server.js
브라우저 탭을 두 개 열어서 각각 `index.html`에 접속해봐. 한쪽 탭에서 메시지를 입력하고 '전송' 버튼을 누르면, 거의 즉시 다른 쪽 탭의 채팅창에도 해당 메시지가 나타나는 걸 볼 수 있을 거야. 이게 바로 웹소켓을 이용한 실시간 통신이야! 🥳
5. 프로를 위한 웹소켓: 고급 주제와 베스트 프랙티스
기본적인 사용법을 익혔다면, 이제 실제 서비스에서 마주칠 수 있는 문제들을 해결하고 더 안정적인 웹소켓 애플리케이션을 만들기 위한 고급 기술들을 알아볼 차례야. 이런 디테일이 주니어와 시니어 개발자를 가르는 차이를 만들지. 🧐
(1) 연결 유지의 기술: 하트비트 (Heartbeat)
웹소켓 연결은 이론적으로는 계속 유지되지만, 현실은 녹록지 않아. 중간에 있는 라우터나 프록시 서버, 방화벽 등이 오랫동안 아무런 데이터도 오가지 않는 '유휴(idle)' 연결을 자기 마음대로 끊어버리는 경우가 종종 발생해. "얘네 아무 말도 안 하는 거 보니까 죽었나 보다" 하고 선을 잘라버리는 거지.
이런 비극을 막기 위해 주기적으로 아주 작은 데이터를 주고받아 "나 아직 살아있어!"라고 신호를 보내는 기술이 바로 하트비트(Heartbeat) 또는 핑퐁(Ping-Pong)이야. 심장이 규칙적으로 뛰면서 생명을 유지하는 것과 같다고 해서 붙은 이름이지. ❤️
웹소켓 프로토콜 자체에 핑(Ping)/퐁(Pong) 프레임이 정의되어 있지만, 애플리케이션 레벨에서 직접 구현하는 것도 일반적이야. 클라이언트가 주기적으로 'ping' 메시지를 보내면, 서버는 'pong' 메시지로 응답하는 식이지.
// 클라이언트 측 하트비트 구현 예시
let heartbeatInterval;
socket.onopen = () => {
console.log('연결 성공!');
// 30초마다 서버에 'ping' 메시지를 보내는 인터벌 시작
heartbeatInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
// 서버와 약속된 하트비트 메시지
socket.send(JSON.stringify({ type: 'ping' }));
console.log('Ping sent');
}
}, 30000); // 30초
};
socket.onclose = () => {
console.log('연결 종료!');
// 연결이 끊기면 인터벌도 정리
clearInterval(heartbeatInterval);
};
// 서버는 'ping' 메시지를 받으면 'pong'으로 응답하거나,
// 'ping'이 일정 시간 이상 오지 않으면 클라이언트 연결이 끊긴 것으로 간주할 수 있다.
(2) 불사조 만들기: 자동 재연결 로직
네트워크는 절대 믿을 수 없는 존재야. 사용자가 지하철을 타거나, 와이파이가 불안정하거나, 서버가 잠깐 재시작하는 등 다양한 이유로 웹소켓 연결은 언제든 끊어질 수 있어. 이때 "연결이 끊겼습니다."라는 메시지만 덩그러니 보여주고 끝낸다면 사용자 경험이 매우 나쁘겠지?
프로다운 애플리케이션은 연결이 끊어졌을 때 자동으로 재연결을 시도해야 해. 이때 무작정 1초마다 계속 재연결을 시도하면 서버에 부담을 줄 수 있으니, 점차 시도 간격을 늘려나가는 '지수 백오프(Exponential Backoff)' 전략을 사용하는 것이 좋아.
// 클라이언트 측 자동 재연결 로직 예시
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
const initialReconnectDelay = 1000; // 1초
function connect() {
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
console.log('웹소켓 연결 성공!');
reconnectAttempts = 0; // 연결 성공 시 시도 횟수 초기화
// ... 기존 onopen 로직 ...
};
socket.onclose = (event) => {
if (!event.wasClean) { // 비정상 종료 시에만 재연결 시도
console.log('웹소켓 연결이 비정상적으로 종료되었습니다. 재연결을 시도합니다...');
if (reconnectAttempts < maxReconnectAttempts) {
const reconnectDelay = initialReconnectDelay * Math.pow(2, reconnectAttempts);
console.log(`${reconnectDelay / 1000}초 후에 재연결합니다.`);
setTimeout(connect, reconnectDelay);
reconnectAttempts++;
} else {
console.error('최대 재연결 횟수를 초과했습니다.');
}
}
};
// ... onmessage, onerror 로직 ...
}
// 최초 연결 시작
connect();
(3) 확장성(Scalability) 고려하기
사용자가 수십, 수백 명일 때는 우리가 만든 간단한 Node.js 서버 하나로도 충분해. 하지만 수만, 수십만 명의 동시 접속자를 감당해야 하는 서비스라면? 서버 한 대로 버틸 수 없으니 여러 대의 서버로 확장(Scale-out)해야겠지.
이때 문제가 발생해. 유저 A는 서버 1에 접속해 있고, 유저 B는 서버 2에 접속해 있다고 가정해보자. A가 보낸 메시지를 B에게 전달하려면 서버 1이 서버 2에게 "이 메시지 좀 B한테 전해줘"라고 알려줘야 해. 즉, 웹소켓 서버들 간의 통신이 필요해지는 거야.
이 문제를 해결하기 위해 보통 Redis의 Pub/Sub 기능이나 RabbitMQ, Kafka 같은 메시지 큐 시스템을 '백플레인(Backplane)'으로 사용해.
1. 유저 A가 서버 1에 메시지를 보낸다.
2. 서버 1은 그 메시지를 Redis의 특정 채널에 발행(Publish)한다.
3. 서버 1, 서버 2를 포함한 모든 웹소켓 서버들은 그 채널을 구독(Subscribe)하고 있다.
4. 모든 서버는 Redis로부터 메시지를 받고, 자신이 담당하고 있는 클라이언트들 중 해당 메시지를 받아야 할 클라이언트에게 전달한다.
이렇게 하면 어떤 클라이언트가 어떤 서버에 붙어있든 상관없이 전체적으로 메시지를 브로드캐스팅할 수 있게 돼. 이건 웹소켓 아키텍처링에서 매우 중요한 개념이니 꼭 기억해둬!
6. 웹소켓의 라이벌과 친구들: 언제 무엇을 써야 할까?
웹소켓이 강력한 건 사실이지만, 모든 상황에 맞는 만병통치약은 아니야. 때로는 다른 기술이 더 적합할 수 있어. 웹소켓과 자주 비교되는 기술들을 살펴보며 '기술 선택의 지혜'를 길러보자.
(1) 웹소켓 vs. 서버-센트 이벤트 (SSE)
- 공통점: 서버가 클라이언트에게 데이터를 밀어주는(Push) 실시간 기술.
- 차이점: 웹소켓은 양방향, SSE는 단방향(서버→클라이언트).
- 언제 SSE를 쓸까?
실시간 주식 시세, 스포츠 경기 스코어, 뉴스 속보, 서버 상태 모니터링 대시보드처럼 클라이언트가 서버에 데이터를 보낼 일이 거의 없고, 오직 서버로부터 데이터만 받으면 되는 경우. SSE는 웹소켓보다 가볍고, HTTP 기반이라 구현이 더 간단하며, 자동 재연결 기능도 내장되어 있어 이런 시나리오에 아주 적합해. - 언제 웹소켓을 쓸까?
채팅, 온라인 게임, 협업 편집 툴처럼 클라이언트와 서버가 빈번하게 데이터를 주고받아야 하는 모든 경우. 양방향 통신이 필수적이라면 고민할 필요 없이 웹소켓이야.
(2) 웹소켓 vs. WebRTC
- 공통점: 저지연(Low-latency) 실시간 통신을 위한 기술.
- 차이점: 웹소켓은 클라이언트-서버 구조. 모든 데이터가 중앙 서버를 거쳐 가. 반면 WebRTC(Web Real-Time Communication)는 주로 P2P(Peer-to-Peer), 즉 클라이언트(브라우저)끼리 서버를 거치지 않고 직접 통신하는 기술이야.
- 언제 WebRTC를 쓸까?
화상 회의, 음성 통화, 대용량 파일 P2P 전송처럼 클라이언트 간에 직접 대용량의 미디어 스트림을 주고받아야 할 때. 서버를 거치지 않으니 지연 시간이 극도로 짧고 서버 트래픽 비용을 아낄 수 있어. (물론 P2P 연결을 맺기 위한 초기 과정, 즉 '시그널링'에는 웹소켓 서버가 종종 사용돼. 둘은 경쟁자라기보단 협력 관계일 때가 많아!) - 언제 웹소켓을 쓸까?
대부분의 실시간 애플리케이션. 중앙 서버가 모든 데이터 흐름을 제어하고, 상태를 관리하고, 비즈니스 로직을 처리해야 하는 경우에는 웹소켓이 정답이야. 게임 서버, 채팅 서버 등이 대표적이지.
🤔 그럼 AJAX는 이제 안 쓰나요? 아니, 절대! `fetch`나 `XMLHttpRequest`를 이용한 비동기 HTTP 요청(흔히 AJAX라고 부르는)은 여전히 웹 개발의 근간이야. 페이지에 처음 진입할 때 초기 데이터를 불러오거나, 폼을 제출하거나, 사용자가 버튼을 눌렀을 때처럼 '일회성'으로 서버와 통신이 필요할 때는 HTTP 요청이 가장 적합하고 효율적이야. 실시간성이 필요 없는 곳에 굳이 웹소켓 연결을 유지하는 건 낭비일 뿐이지. 상황에 맞는 적절한 도구를 선택하는 것, 그게 바로 실력 있는 개발자의 모습이야. 👍
7. 마치며: 새로운 시대의 문을 연 웹소켓
자, 지금까지 정말 긴 여정을 함께했어. HTTP의 한계에서 출발해 웹소켓의 탄생 배경과 동작 원리, 자바스크립트 API를 활용한 실전 예제, 그리고 더 나은 서비스를 위한 고급 주제들까지. 이제 여러분은 웹소켓이 단순한 기술 하나가 아니라, 웹을 정적인 문서의 집합에서 동적인 소통의 공간으로 바꿔놓은 '패러다임의 전환'이라는 걸 느꼈을 거야.
새로고침 버튼에 의존하던 과거의 웹은 이제 역사의 뒤안길로 사라지고 있어. 사용자는 이제 모든 것이 즉각적으로 반응하고, 다른 사람들과 실시간으로 연결되기를 기대해. 웹소켓은 바로 그 기대를 현실로 만들어주는 핵심 열쇠지. 🔑
오늘 배운 내용을 바탕으로 여러분만의 작은 토이 프로젝트를 시작해보는 건 어떨까? 간단한 채팅방도 좋고, 실시간 투표 시스템도 좋아. 직접 코드를 짜고, 연결을 맺고, 메시지를 주고받는 경험을 통해 웹소켓은 더 이상 낯선 기술이 아닌, 여러분의 든든한 무기가 될 거야. 이 기술을 마스터한다면, 여러분의 가치는 개발자 시장에서 더욱 빛나게 될 것이고, **재능넷**과 같은 플랫폼에서 여러분의 능력을 필요로 하는 더 많은 기회를 만나게 될지도 몰라.
실시간 웹의 시대는 이제 막 시작됐어. 그리고 그 중심에는 바로 웹소켓이 있지. 이 흥미진진한 흐름에 올라타서, 세상을 연결하는 멋진 애플리케이션을 만들어보자고! 🔥
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.