[아이티센 부트캠프] 웹 소켓

이언덕·2026년 5월 4일

아이티센 부트캠프

목록 보기
82/115
post-thumbnail

웹 소켓이란 무엇인가

WebSocket은 클라이언트와 서버가 한 번 연결된 뒤, 그 연결을 유지하면서 데이터를 실시간으로 주고받기 위한 통신 방식이다.
HTML5에서 제공하는 표준 기술이며, HTTP 환경에서 시작해 하나의 TCP 연결을 통해 통신한다.
여기서 클라이언트는 보통 브라우저를 의미한다.
서버는 클라이언트의 요청을 처리하고 필요한 데이터를 보내 주는 프로그램을 의미한다.


기존 웹 통신은 대부분 HTTP를 사용한다.
HTTP는 클라이언트가 먼저 요청해야 서버가 응답하는 방식이다.
예를 들어 사용자가 버튼을 누르거나 페이지를 새로고침하면 브라우저가 서버에 요청을 보낸다.
그러면 서버는 그 요청에 대한 응답을 돌려준다.


반면 WebSocket은 한 번 연결된 뒤에는 클라이언트와 서버가 서로 자유롭게 데이터를 주고받을 수 있다.
클라이언트가 매번 새 요청을 보내지 않아도 서버가 먼저 데이터를 보낼 수 있다.
그래서 실시간 채팅, 실시간 알림, 실시간 게임처럼 즉시 반응해야 하는 기능에 사용된다.


전 이중 통신이란 무엇인가

전 이중 통신은 같은 연결 안에서 양쪽이 동시에 데이터를 보내고 받을 수 있는 통신 방식이다.
쉽게 말하면 전화 통화와 비슷하다.
전화를 하면 내가 말할 수도 있고, 상대방도 말할 수 있다.
한쪽만 계속 말하고 다른 쪽은 듣기만 하는 구조가 아니다.


단방향 통신은 한쪽 방향으로만 데이터가 흐르는 방식이다.
텔레비전 방송이나 라디오는 대표적인 단방향 통신이다.
사용자는 방송을 보거나 라디오를 들을 수는 있지만, 텔레비전 방송국이나 라디오 방송국으로 직접 데이터를 보내지는 못한다.


WebSocket은 전 이중 통신을 지원한다.
따라서 클라이언트가 서버로 메시지를 보낼 수도 있고, 서버가 클라이언트로 메시지를 보낼 수도 있다.
이 양방향 구조가 실시간 기능의 핵심이다.


HTTP와 WebSocket의 차이

HTTP는 요청과 응답이 한 묶음으로 움직인다.
클라이언트가 request를 보내면 서버가 response를 보낸다.
그 응답이 끝나면 기본적으로 그 요청의 처리는 끝난다.


예를 들어 일반 웹 페이지에서는 사용자가 게시글 목록을 요청해야 서버가 게시글 목록을 보내 준다.
사용자가 아무 요청도 하지 않으면 서버는 보통 먼저 데이터를 보내지 않는다.
이 구조는 문서나 화면을 요청해서 받아오는 데에는 잘 맞는다.


하지만 채팅처럼 새 메시지가 생기는 즉시 화면에 보여야 하는 기능에는 불편하다.
서버에 새 메시지가 생겼는지 클라이언트가 계속 물어봐야 하기 때문이다.


WebSocket은 처음에 연결을 맺은 뒤 그 연결을 유지한다.
연결이 유지되는 동안에는 클라이언트와 서버가 필요한 순간에 바로 데이터를 보낼 수 있다.
따라서 매번 새 요청을 만들 필요가 줄어든다.

HTTP는 요청과 응답이 반복되고, WebSocket은 연결을 유지한 상태에서 양방향으로 데이터를 주고받는다.
왼쪽의 HTTP는 클라이언트가 요청해야 서버가 응답할 수 있다.
오른쪽의 WebSocket은 처음 연결을 맺은 뒤, 연결이 유지되는 동안 양쪽이 데이터를 주고받을 수 있다.


하나의 TCP 연결을 유지한다는 의미

TCP는 데이터를 안정적으로 주고받기 위해 사용하는 네트워크 연결 방식이다.
이 단계에서는 TCP를 “클라이언트와 서버 사이에 만들어지는 통신 통로” 정도로 이해하면 된다.


여기서 중요한 차이는 TCP 연결을 내부적으로 재사용할 수 있는지 여부가 아니다.
초보자 기준에서는 HTTP가 요청과 응답 중심으로 동작하고, WebSocket은 연결을 유지한 상태에서 메시지를 계속 주고받는다는 흐름을 먼저 이해하면 된다.


WebSocket은 하나의 TCP 연결을 만든 뒤 그 통로를 계속 유지한다.
그래서 채팅 메시지처럼 계속 오가는 데이터를 처리할 때 유리하다.


쉽게 말하면 HTTP는 필요할 때마다 문을 두드리고 답을 받는 방식에 가깝다.
WebSocket은 문을 열어 둔 상태에서 계속 대화하는 방식에 가깝다.


정리하면 WebSocket은 실시간 양방향 통신을 위해 연결을 계속 유지하는 기술이다.
이 흐름을 이해하면 이후에 나오는 Handshake, send(), onmessage, Session도 훨씬 쉽게 이어진다.




웹 소켓의 통신 방식

WebSocket은 연결을 계속 유지하면서 데이터를 주고받는 방식이다.
그래서 연결을 시작할 때 클라이언트와 서버가 먼저 서로 확인하는 과정이 필요하다.
이 과정을 Handshake라고 한다.


Handshake는 통신을 시작하기 전에 “이제 WebSocket 방식으로 연결해도 되는지” 확인하는 절차다.
쉽게 말하면 전화를 걸었을 때 상대방이 전화를 받기 전까지는 대화가 시작되지 않는 것과 비슷하다.
상대방이 전화를 받아야 그때부터 서로 말을 주고받을 수 있다.


WebSocket은 연속적으로 데이터를 주고받는 통신 방식이기 때문에, 연결을 안정적으로 시작하기 위해 Handshake를 먼저 진행한다.
이 흐름을 알아야 뒤에서 나오는 101 Switching Protocols, Upgrade, 개발자 도구의 Network 확인도 자연스럽게 이해된다.


Handshake가 필요한 이유

WebSocket은 단순히 한 번 요청하고 응답받고 끝나는 구조가 아니다.
연결이 열리면 그 연결을 계속 유지한다.
그래서 처음 연결할 때 서버가 이 연결을 받아들일 수 있는지 확인해야 한다.


만약 서버가 WebSocket 통신을 처리할 준비가 되어 있지 않다면 연결을 열면 안 된다.
연결을 열어도 메시지를 제대로 주고받을 수 없기 때문이다.
그래서 클라이언트는 먼저 서버에 WebSocket 연결을 요청하고, 서버는 그 요청을 받아들일지 판단한다.


또한 WebSocket은 연결된 상태에서 데이터를 계속 주고받는다.
따라서 시작 단계에서 연결이 제대로 수립되었는지 확인해야 이후 데이터 전송도 안정적으로 이어질 수 있다.


즉, Handshake는 안정적인 양방향 통신을 시작하기 위한 첫 단계다.


기존 TCP 기반 프로토콜과 WebSocket의 차이

TCP는 데이터를 안정적으로 주고받기 위한 기본 통신 통로다.
많은 네트워크 통신은 TCP 연결 위에서 동작한다.
여기서 layer는 통신을 처리하는 단계라고 이해하면 된다.


기존의 다른 TCP 기반 프로토콜은 보통 TCP layer에서 연결을 수립하는 과정을 거친다.
즉, 통신의 기본 연결 자체를 TCP 단계에서 맺고 그 위에서 데이터를 주고받는다.


WebSocket도 결국 TCP 연결 위에서 동작한다.
하지만 연결을 시작하는 방식이 다르다.
WebSocket은 처음에 HTTP 요청을 기반으로 Handshake를 진행한 뒤, 연결이 성공하면 WebSocket 통신으로 전환된다.


이 차이를 초보자 기준으로 정리하면 다음과 같다.

  • 기존 TCP 기반 프로토콜은 TCP layer에서 연결 수립 과정을 거친다.
  • WebSocket은 처음에 HTTP 요청으로 연결 전환을 요청한다.
  • 서버가 받아들이면 이후에는 같은 연결을 유지하면서 계속 데이터를 주고받는다.

즉, WebSocket은 TCP 연결 위에서 동작하지만, 시작 과정에서는 HTTP 요청 기반의 Handshake를 사용한다.


HTTP Upgrade의 의미

WebSocket 연결은 처음에 HTTP 요청으로 시작한다.
이때 클라이언트는 서버에게 “이 연결을 WebSocket 통신으로 바꾸고 싶다”라고 요청한다.
이 요청을 HTTP Upgrade라고 이해하면 된다.


Upgrade는 말 그대로 통신 방식을 바꾼다는 의미다.
처음에는 HTTP로 요청을 보내지만, 서버가 허락하면 이후 통신은 WebSocket 방식으로 바뀐다.


서버가 이 전환을 받아들이면 Status Code로 101을 응답한다.
101 Switching Protocols는 프로토콜을 바꾼다는 뜻이다.
여기서는 HTTP로 시작한 연결이 WebSocket으로 전환되었다는 의미다.


개발자 도구에서 101이 보이면 WebSocket 연결 전환이 성공했다는 중요한 신호로 볼 수 있다.


WebSocket 연결 흐름

WebSocket 통신은 크게 세 단계로 이해하면 된다.

  • 첫 번째는 Handshake 단계다.
  • 두 번째는 양방향 통신 단계다.
  • 세 번째는 연결 종료 단계다.

먼저 브라우저가 서버에 HTTP Upgrade 요청을 보낸다.
서버가 이 요청을 받아들이면 101 Switching Protocols로 응답한다.
이후 연결이 열린 상태가 되고, 클라이언트와 서버는 메시지를 계속 주고받을 수 있다.
마지막에는 한쪽에서 연결 종료를 요청하고 연결이 닫힌다.

WebSocket은 처음에는 HTTP 요청으로 연결을 시작하고, 서버가 받아들이면 101 응답을 통해 WebSocket 통신으로 전환된다.
이후에는 연결을 유지한 상태에서 양방향으로 데이터를 주고받고, 마지막에는 연결 종료 과정을 거친다.


통신 방식 핵심 정리

WebSocket은 연결을 유지하는 통신 방식이기 때문에 시작 단계가 중요하다.
처음에는 HTTP 요청으로 서버에 연결 전환을 요청한다.
서버가 이를 받아들이면 101 Switching Protocols 응답이 오고, 그 뒤부터는 WebSocket 방식으로 메시지를 주고받는다.


핵심만 정리하면 다음과 같다.

  • WebSocket은 전 이중 통신을 위해 연결을 유지한다.
  • 연결을 시작할 때 Handshake가 필요하다.
  • WebSocket의 Handshake는 HTTP 요청을 기반으로 진행된다.
  • 서버가 연결 전환을 허락하면 101 Switching Protocols가 응답된다.
  • 연결이 열린 뒤에는 클라이언트와 서버가 서로 데이터를 보낼 수 있다.

정리하면 WebSocket의 통신 방식은 HTTP 요청으로 연결을 시작하고, 성공 후에는 유지된 연결을 통해 실시간 양방향 통신을 하는 구조다.




웹 소켓이 등장한 이유

초기 웹은 문서를 전달하고, 링크를 통해 다른 문서로 이동하는 목적이 컸다.
이 목적에는 HTTP 방식이 잘 맞았다.
사용자가 어떤 문서를 요청하면 서버가 그 문서를 보내 주면 되었기 때문이다.


하지만 웹에서 처리해야 하는 기능이 점점 복잡해졌다.
단순히 문서를 보여 주는 것을 넘어서, 화면이 바로 바뀌고 사용자의 행동에 즉시 반응해야 하는 기능이 많아졌다.
예를 들어 채팅 메시지는 상대방이 보내는 순간 바로 보여야 하고, 알림은 새 내용이 생기는 순간 바로 떠야 한다.


이런 기능은 기존의 HTTP 요청-응답 방식만으로는 자연스럽게 처리하기 어렵다.
그래서 WebSocket이 등장하기 전에는 Polling, Long Polling, HTTP Streaming 같은 방식으로 실시간에 가까운 기능을 구현했다.


초기 웹과 HTTP가 잘 맞았던 이유

초기 웹의 핵심 목적은 문서 전달이었다.
사용자가 어떤 페이지를 요청하면 서버가 해당 문서를 보내 준다.
사용자는 문서 안의 링크를 눌러 다른 문서로 이동한다.


이 흐름에서는 HTTP 방식이 충분히 적합하다.
클라이언트가 요청하고, 서버가 응답하면 하나의 작업이 끝난다.
문서를 가져오는 작업은 대부분 “필요할 때 요청하고 받아오기”만으로 처리할 수 있기 때문이다.


예를 들어 사용자가 게시글 목록 페이지를 연다고 생각하면 된다.
브라우저가 서버에 게시글 목록을 요청한다.
서버는 게시글 목록 화면을 응답한다.
이런 구조에서는 서버가 먼저 계속 데이터를 보낼 필요가 크지 않다.


웹에서 실시간 기능이 필요해진 이유

웹이 발전하면서 사용자는 페이지를 새로고침하지 않아도 화면이 바뀌기를 기대하게 되었다.
채팅에서는 새 메시지가 바로 보여야 한다.
SNS에서는 새 알림이 바로 떠야 한다.
멀티 플레이어 게임에서는 다른 사용자의 움직임이 거의 즉시 반영되어야 한다.


이런 기능은 공통점이 있다.
서버에 새 데이터가 생긴 순간, 클라이언트 화면에도 바로 반영되어야 한다.
즉, 클라이언트가 매번 요청할 때까지 기다리면 늦다.


실시간 기능에서는 서버와 클라이언트가 계속 연결된 상태에서 데이터를 바로 주고받는 구조가 필요하다.
이 요구를 더 직접적으로 해결하기 위해 등장한 기술이 WebSocket이다.


Polling 방식

Polling은 클라이언트가 서버에 계속 물어보는 방식이다.
예를 들어 브라우저가 3초마다 서버에 “새 메시지 있어?”라고 요청한다고 생각하면 된다.
서버에 새 메시지가 있으면 메시지를 응답하고, 없으면 없다고 응답한다.


이 방식은 구현 흐름이 단순하다.
클라이언트가 일정 시간마다 요청을 보내면 되기 때문이다.
하지만 실시간 기능에는 비효율적이다.
새 데이터가 없어도 계속 요청이 발생하기 때문이다.


예를 들어 채팅방에 아무도 메시지를 보내지 않아도 브라우저는 계속 서버에 요청한다.
사용자가 많아질수록 서버는 실제로 필요 없는 확인 요청까지 계속 처리해야 한다.


즉, Polling은 실시간처럼 보이게 만들 수는 있지만, 실제 구조는 반복 확인 방식이다.
서버가 새 데이터가 생긴 순간 바로 밀어 보내는 구조는 아니다.


Long Polling과 HTTP Streaming

Long Polling은 일반 Polling보다 오래 기다리는 방식이다.
클라이언트가 요청을 보내면 서버가 바로 응답하지 않는다.
새 데이터가 생길 때까지 잠시 기다렸다가, 데이터가 생기면 응답한다.


이 방식은 일반 Polling보다 불필요한 응답을 줄일 수 있다.
하지만 응답이 끝나면 클라이언트가 다시 요청을 보내야 한다.
결국 연결을 완전히 유지한 상태에서 자유롭게 양방향 통신을 하는 구조는 아니다.


HTTP Streaming은 서버가 응답 연결을 유지하면서 데이터를 조금씩 보내는 방식이다.
서버에서 클라이언트로 계속 데이터를 흘려보낼 수 있다는 점에서 실시간에 가까운 처리가 가능하다.
하지만 클라이언트와 서버가 서로 자유롭게 주고받는 전 이중 구조와는 차이가 있다.


이 세 방식은 WebSocket이 등장하기 전 실시간에 가까운 기능을 만들기 위해 사용된 방식이다.
하지만 WebSocket은 실시간 양방향 통신을 위해 만들어진 방식이므로 구조가 더 직접적이다.


Polling과 WebSocket의 차이

Polling과 WebSocket의 가장 큰 차이는 서버에 새 데이터가 있는지 확인하는 방식이다.
Polling은 클라이언트가 계속 서버에 물어본다.
WebSocket은 연결을 유지하고 있다가, 새 데이터가 생기면 서버가 바로 보낼 수 있다.


채팅으로 비교하면 차이가 더 쉽다.
Polling 방식에서는 브라우저가 계속 “새 채팅 있어?”라고 확인해야 한다.
WebSocket 방식에서는 누군가 메시지를 보내는 순간 서버가 연결된 사용자들에게 바로 전달할 수 있다.

Polling은 클라이언트가 계속 요청을 보내야 하지만, WebSocket은 연결을 유지한 상태에서 서버도 즉시 데이터를 보낼 수 있다.
왼쪽의 Ajax Polling은 반복적으로 GET /poll 요청을 보내고, 오른쪽의 WebSocket은 한 번 연결한 뒤 그 연결을 통해 데이터를 주고받는다.


실시간 웹 애플리케이션에서 WebSocket을 사용하는 이유

실시간 웹 애플리케이션은 서버와 클라이언트 사이의 반응 속도가 중요하다.
새 데이터가 생긴 뒤 화면에 늦게 반영되면 사용자는 답답하게 느낀다.
채팅, 게임, 공동 문서 편집처럼 여러 사용자가 동시에 상호작용하는 기능에서는 특히 그렇다.


WebSocket은 연결을 유지한 상태에서 데이터를 주고받는다.
그래서 서버에 새 데이터가 생기면 클라이언트에게 바로 보낼 수 있다.
클라이언트도 서버로 즉시 메시지를 보낼 수 있다.


이 구조 덕분에 SNS, 멀티 플레이어 게임, 공유 문서, 실시간 채팅 같은 기능에 적합하다.
정리하면 WebSocket은 실시간 웹 애플리케이션을 자연스럽게 구현하기 위해 등장한 통신 방식이다.




웹 소켓 통신의 동작 방식

WebSocket 통신은 연결을 열고, 그 연결을 유지하면서 데이터를 주고받고, 마지막에 연결을 닫는 흐름으로 동작한다.
일반적인 HTTP 통신처럼 요청 하나에 응답 하나가 끝나는 구조가 아니다.
연결이 유지되는 동안 클라이언트와 서버는 여러 번 데이터를 주고받을 수 있다.


이 차이를 이해하면 WebSocket이 왜 실시간 채팅에 적합한지 알 수 있다.
채팅에서는 한 번 메시지를 보내고 끝나는 것이 아니라, 사용자가 접속해 있는 동안 계속 메시지가 오간다.
그래서 매번 새 요청을 만드는 방식보다 연결을 유지하는 방식이 더 자연스럽다.


WebSocket 통신의 핵심은 연결을 유지한 상태에서 클라이언트와 서버가 서로 필요한 순간에 데이터를 보낼 수 있다는 점이다.
서버도 클라이언트에게 먼저 데이터를 보낼 수 있기 때문에 실시간 알림이나 채팅 메시지 전달에 적합하다.


연결을 열고 유지하고 닫는 흐름

WebSocket 통신은 크게 세 단계로 나눌 수 있다.

  • 연결을 여는 단계
  • 데이터를 주고받는 단계
  • 연결을 닫는 단계

연결을 여는 단계에서는 Opening Handshake가 진행된다.
Opening Handshake는 통신을 시작하기 전에 클라이언트와 서버가 서로 연결 가능 여부를 확인하는 과정이다.
이 과정이 성공하면 연결 상태가 CONNECTED가 된다.


CONNECTED 상태는 클라이언트와 서버가 서로 연결되어 있는 상태를 의미한다.
이 상태에서는 데이터를 계속 주고받을 수 있다.
이때 주고받는 데이터는 하나의 message 단위로 이동한다고 이해하면 된다.


마지막에는 Closing Handshake를 통해 연결을 닫는다.
Closing Handshake는 통신을 끝내기 전에 연결 종료를 서로 확인하는 과정이다.
연결이 닫히면 더 이상 데이터를 주고받을 수 없다.

WebSocket은 연결 시작과 종료에도 별도의 프로토콜 절차가 있고, 연결 중에는 메시지를 계속 주고받을 수 있다.
처음에는 HTTP UPGRADE 요청과 HTTP 101 응답이 오가고, 연결된 뒤에는 message 단위로 데이터가 이동한다.


request-response 방식과 다른 점

일반적인 HTTP 통신은 request-response 방식이다.
request-response는 클라이언트가 요청을 보내고, 서버가 그 요청에 대한 응답을 보내는 구조다.
즉, 클라이언트가 먼저 움직여야 서버가 응답할 수 있다.


예를 들어 브라우저가 게시글 목록을 요청하면 서버는 게시글 목록을 응답한다.
브라우저가 다시 댓글 목록을 요청하면 서버는 댓글 목록을 응답한다.
이처럼 HTTP는 요청과 응답을 기준으로 동작한다.


반면 WebSocket은 연결이 열린 뒤에는 요청과 응답을 한 묶음으로만 보지 않는다.
연결된 상태에서 클라이언트가 서버로 데이터를 보낼 수 있고, 서버도 클라이언트로 데이터를 보낼 수 있다.
따라서 새 데이터가 생길 때마다 클라이언트가 계속 요청하지 않아도 된다.


이 차이 때문에 WebSocket은 실시간 기능에 적합하다.
서버에 새 채팅 메시지나 새 알림이 생기면, 서버가 연결된 클라이언트에게 바로 보낼 수 있기 때문이다.


서버가 클라이언트에게 먼저 보낼 수 있다는 의미

WebSocket의 중요한 특징 중 하나는 서버가 클라이언트에게 먼저 데이터를 보낼 수 있다는 점이다.
일반적인 HTTP 흐름에서는 클라이언트가 먼저 요청하지 않으면 서버가 응답을 보내기 어렵다.
하지만 WebSocket에서는 연결이 이미 열려 있기 때문에 서버가 필요한 순간 데이터를 보낼 수 있다.


채팅 예제로 생각하면 쉽다.
사용자 A가 메시지를 보내면 그 메시지는 서버로 간다.
서버는 이미 연결되어 있는 사용자 B, 사용자 C에게 그 메시지를 바로 전달할 수 있다.


이때 사용자 B와 사용자 C가 계속 새 메시지가 있는지 물어볼 필요가 없다.
서버가 메시지를 받는 순간 연결된 클라이언트에게 바로 보내면 된다.
이 구조가 실시간 채팅의 핵심이다.

서버는 여러 클라이언트와 WebSocket 연결을 유지하면서 실시간으로 메시지를 전달할 수 있다.
채팅에서는 각 사용자의 브라우저가 서버에 연결되고, 서버가 메시지를 중간에서 전달한다.


실시간 통신이 가능하다는 의미

실시간 통신은 데이터가 생긴 뒤 바로 전달되는 통신을 의미한다.
여기서 “바로”는 사용자가 기다리거나 새로고침하지 않아도 화면에 반영되는 흐름을 말한다.


WebSocket은 연결을 유지한다.
그래서 서버와 클라이언트가 계속 통신 가능한 상태로 남아 있다.
이 상태에서는 새 데이터가 생겼을 때 바로 메시지를 보낼 수 있다.


실시간 알림을 예로 들면 서버에 새 알림이 생기는 순간 클라이언트 화면에 알림을 띄울 수 있다.
채팅을 예로 들면 상대방이 보낸 메시지를 거의 즉시 내 화면에 보여 줄 수 있다.
멀티 플레이어 게임을 예로 들면 다른 사용자의 움직임을 빠르게 반영할 수 있다.


WebSocket의 실시간성은 연결을 유지하는 구조에서 나온다.
연결이 유지되어 있기 때문에 새 데이터가 생길 때마다 통신을 다시 준비하지 않고 바로 전달할 수 있다.


동작 방식 핵심 정리

WebSocket 통신은 연결을 열고, 유지된 연결 안에서 데이터를 주고받고, 마지막에 연결을 닫는 방식으로 동작한다.
이 흐름은 일반적인 HTTP의 request-response 방식과 다르다.


핵심만 정리하면 다음과 같다.

  • Opening Handshake로 연결을 시작한다.
  • 연결이 성공하면 CONNECTED 상태가 된다.
  • 연결이 유지되는 동안 message 단위로 데이터를 주고받는다.
  • 클라이언트와 서버가 서로 데이터를 보낼 수 있다.
  • 서버도 클라이언트에게 먼저 데이터를 보낼 수 있다.
  • 마지막에는 Closing Handshake로 연결을 종료한다.

정리하면 WebSocket은 연결을 계속 유지하면서 클라이언트와 서버가 양방향으로 실시간 데이터를 주고받는 통신 방식이다.




웹 소켓 클라이언트 구현

브라우저에서는 JavaScript와 HTML5 API를 사용해 WebSocket 클라이언트를 구현한다.
여기서 클라이언트 구현이란 브라우저가 서버에 연결하고, 서버로 데이터를 보내고, 서버에서 온 데이터를 받는 코드를 작성한다는 뜻이다.


WebSocket 클라이언트의 핵심은 어렵게 볼 필요가 없다.
먼저 WebSocket 객체를 만들어 서버에 연결한다.
그 다음 send()로 데이터를 보내고, onmessage로 서버에서 온 데이터를 받는다.


즉, 클라이언트 구현의 핵심 흐름은 연결하기 → 보내기 → 받기 → 연결 상태 처리하기다.
이 흐름만 잡으면 채팅 예제의 코드도 훨씬 쉽게 읽을 수 있다.


WebSocket 객체로 서버에 연결하기

WebSocket 연결은 new WebSocket()으로 시작한다.
new는 새 객체를 만든다는 뜻이다.
객체는 여러 기능과 값을 묶어 둔 도구라고 이해하면 된다.


WebSocket 객체는 서버와 연결하고, 데이터를 보내고, 서버에서 온 데이터를 받을 수 있는 기능을 가진 객체다.
괄호 안에는 접속할 웹 소켓 서버 주소를 넣는다.

// exam01_websocket_connect.html
let ws = new WebSocket("ws://localhost:8088/chat"); // 웹 소켓 서버에 연결 요청

이 코드는 ws://localhost:8088/chat 주소로 웹 소켓 연결을 요청한다.
여기서 ws 변수에는 서버와 연결하기 위한 WebSocket 객체가 저장된다.


주소의 마지막 /chat은 서버에 만들어 둔 웹 소켓 프로그램의 매핑명이다.
즉, 클라이언트는 /chat이라는 연결 지점으로 접속하고, 서버는 그 요청을 받아 웹 소켓 통신을 시작한다.


ws와 wss 프로토콜

WebSocket 주소는 일반 웹 주소처럼 http://로 시작하지 않는다.
일반 웹 소켓 통신은 ws://로 시작한다.
보안이 적용된 웹 소켓 통신은 wss://로 시작한다.


ws는 보안 처리가 없는 웹 소켓 통신이다.
wss는 보안 처리가 적용된 웹 소켓 통신이다.
웹 주소에서 http와 https가 구분되는 것처럼, 웹 소켓 주소에서는 ws와 wss가 구분된다.


초보자 기준에서는 이렇게 이해하면 된다.

  • ws는 일반 웹 소켓 연결이다.
  • wss는 보안 웹 소켓 연결이다.
  • 실제 배포 환경에서는 보안 연결인 wss가 더 중요하게 사용된다.
// exam02_websocket_protocol.html
let normalWs = new WebSocket("ws://localhost:8088/chat"); // 일반 웹 소켓 연결
let secureWs = new WebSocket("wss://example.com/chat"); // 보안 웹 소켓 연결

이 예제는 ws와 wss 주소 형식의 차이를 보여준다.
실습에서는 보통 로컬 서버를 사용하므로 ws://localhost:8088/chat 형태를 많이 사용한다.


WebSocket URL 구조

WebSocket URL은 웹 소켓 서버에 접속하기 위한 주소다.
URL은 인터넷에서 특정 자원을 찾기 위한 주소라고 이해하면 된다.
웹 소켓 주소도 어느 서버의 어떤 웹 소켓 프로그램에 연결할지 알려 주는 역할을 한다.


예를 들어 아래 주소를 보자.

// exam03_websocket_url.html
let ws = new WebSocket("ws://localhost:8088/chat"); // 서버 주소와 매핑명을 포함한 웹 소켓 주소

이 주소는 세 부분으로 나누어 볼 수 있다.

  • ws://는 웹 소켓 통신을 사용한다는 뜻이다.
  • localhost:8088은 접속할 서버 주소와 포트 번호다.
  • /chat은 서버에 등록된 웹 소켓 매핑명이다.

localhost는 내 컴퓨터를 뜻한다.
8088은 서버가 열려 있는 포트 번호다.
포트 번호는 한 컴퓨터 안에서 어떤 프로그램으로 들어갈지 구분하는 번호라고 이해하면 된다.


즉, ws://localhost:8088/chat은 “내 컴퓨터에서 실행 중인 8088번 서버의 /chat 웹 소켓 프로그램에 연결하겠다”는 의미다.


send로 데이터 보내기

서버로 데이터를 보낼 때는 send() 메서드를 사용한다.
메서드는 객체가 가지고 있는 기능이다.
따라서 ws.send()는 ws라는 WebSocket 객체를 통해 서버로 데이터를 보내는 기능이다.

// exam04_websocket_send.html
let ws = new WebSocket("ws://localhost:8088/chat"); // 웹 소켓 서버 연결
ws.onopen = function() {
  ws.send("안녕"); // 연결 성공 후 서버로 메시지 전송
};

이 코드는 서버와 연결된 뒤 "안녕"이라는 문자열을 서버로 보낸다.
여기서 중요한 점은 send()를 아무 때나 호출하면 안 된다는 것이다.
연결이 열리기 전에 데이터를 보내려고 하면 제대로 전송되지 않을 수 있다.


그래서 위 예제에서는 onopen 안에서 send()를 호출한다.
onopen은 서버와 연결이 성공했을 때 실행되는 이벤트 핸들러다.
이벤트 핸들러는 특정 상황이 발생했을 때 실행되는 함수라고 이해하면 된다.


데이터를 보낼 때는 연결이 열린 뒤 send()를 사용한다.


onmessage로 데이터 받기

서버에서 클라이언트로 메시지가 오면 message 이벤트가 발생한다.
이 이벤트를 처리하는 속성이 onmessage다.
서버에서 받은 데이터는 이벤트 객체의 data 속성에 들어 있다.

// exam05_websocket_message.html
let ws = new WebSocket("ws://localhost:8088/chat"); // 웹 소켓 서버 연결
ws.onmessage = function(e) {
  console.log(e.data); // 서버에서 받은 메시지 출력
};

이 코드에서 e는 이벤트 객체다.
이벤트 객체는 이벤트가 발생했을 때 관련 정보를 담고 있는 객체다.
e.data에는 서버가 보낸 메시지가 들어 있다.


예를 들어 서버가 "안녕"이라는 메시지를 보내면 e.data 값도 "안녕"이 된다.
채팅 예제에서는 이 값을 화면에 출력해서 사용자가 받은 메시지를 볼 수 있게 만든다.


보내는 코드는 send(), 받는 코드는 onmessage다.
이 두 가지가 웹 소켓 클라이언트 코드에서 가장 중요하다.


웹 소켓 관련 이벤트

WebSocket은 연결 상태나 메시지 수신 상태에 따라 이벤트가 발생한다.
이벤트는 특정 일이 생겼을 때 자동으로 실행되는 흐름이다.
웹 소켓에서는 대표적으로 open, close, message, error 이벤트가 사용된다.


WebSocket은 연결, 메시지 수신, 오류, 연결 종료 상황을 이벤트 핸들러로 처리한다.
onopen, onmessage, onerror, onclose는 각각 다른 상황에서 실행된다.


open, close, message, error 이벤트는 WebSocket 연결 상태와 데이터 수신 상태를 구분해서 처리하기 위해 사용된다.
연결이 열리면 open, 연결이 닫히면 close, 메시지가 오면 message, 오류가 나면 error가 발생한다.


주요 이벤트 핸들러 코드

이벤트 핸들러는 이벤트가 발생했을 때 실행할 함수를 등록하는 방식이다.
웹 소켓에서는 연결 성공, 연결 종료, 오류 발생, 메시지 수신 상황을 각각 따로 처리할 수 있다.

// exam06_websocket_events.html
let ws = new WebSocket("ws://localhost:8088/chat"); // 웹 소켓 연결 객체 생성
ws.onopen = function(e) {
  console.log("연결 성공"); // 서버와 접속되면 실행
};
ws.onclose = function(e) {
  console.log("연결 종료"); // 서버와 접속이 해제되면 실행
};
ws.onerror = function(e) {
  console.log("오류 발생"); // 웹 소켓 오류가 생기면 실행
};
ws.onmessage = function(e) {
  console.log(e.data); // 서버에서 메시지가 오면 실행
};

이 코드는 웹 소켓에서 자주 사용하는 이벤트 핸들러를 한 번에 보여준다.
연결이 성공하면 onopen이 실행된다.
연결이 끊기면 onclose가 실행된다.
통신 중 오류가 생기면 onerror가 실행된다.
서버에서 메시지가 오면 onmessage가 실행된다.


클라이언트 구현 핵심 정리

웹 소켓 클라이언트 구현은 복잡한 구조가 아니다.
서버에 연결할 WebSocket 객체를 만들고, 그 객체에 필요한 이벤트 처리 코드를 붙이면 된다.


핵심만 정리하면 다음과 같다.

  • new WebSocket()으로 서버 연결을 요청한다.
  • 웹 소켓 주소는 ws://서버주소/매핑명 형태로 작성한다.
  • 일반 통신은 ws, 보안 통신은 wss를 사용한다.
  • 서버로 데이터를 보낼 때는 send()를 사용한다.
  • 서버에서 데이터를 받을 때는 onmessage를 사용한다.
  • 연결 성공은 onopen, 연결 종료는 onclose, 오류는 onerror로 처리한다.

정리하면 웹 소켓 클라이언트는 WebSocket 객체 하나를 중심으로 연결, 송신, 수신, 오류, 종료 상황을 처리하는 구조다.




웹 소켓 채팅 클라이언트 예제

채팅 클라이언트 예제는 브라우저에서 채팅 화면을 만들고, 사용자가 입력한 메시지를 WebSocket 서버로 보내는 코드다.
서버에서 다시 메시지가 오면 그 메시지를 화면에 출력한다.


이 예제는 단순히 버튼을 누르는 화면이 아니다.
WebSocket 연결 생성, 메시지 전송, 메시지 수신, 화면 출력이 모두 들어 있다.
그래서 앞에서 배운 new WebSocket(), send(), onmessage가 실제로 어떻게 사용되는지 확인하기 좋다.


채팅 클라이언트의 핵심은 사용자가 입력한 값을 서버로 보내고, 서버에서 받은 값을 다시 화면에 누적해서 보여 주는 것이다.
이 흐름을 잡으면 코드가 훨씬 쉽게 읽힌다.


채팅 화면 구조

채팅 화면은 크게 다섯 부분으로 나눌 수 있다.

  • 사용자 이름을 입력하는 칸
  • 채팅에 참여하는 버튼
  • 채팅 메시지가 출력되는 영역
  • 메시지를 입력하는 영역
  • 메시지를 전송하는 버튼

사용자는 먼저 이름을 입력한다.
그 다음 채팅 참여 버튼을 누르면 서버와 WebSocket 연결이 만들어진다.
연결된 뒤 메시지를 입력하고 전송 버튼을 누르면 메시지가 서버로 전송된다.


서버에서 메시지가 다시 오면 화면의 채팅 영역에 메시지가 추가된다.
내가 보낸 메시지인지, 다른 사람이 보낸 메시지인지에 따라 class 값을 다르게 지정한다.
이렇게 하면 화면에서 내 메시지와 다른 사람 메시지를 다르게 꾸밀 수 있다.


전체 코드 흐름

// exam03_chat_client.html
<body>
  <div id="chatt">
    <h1>웹 소켓 채팅</h1>
    <input type="text" id="mid" value="게스트">
    <input type="button" value="채팅참여" id="btnJoin">
    <br/>
    <div id="talk"></div>
    <div id="sendZone">
      <textarea id="msg">안녕...</textarea>
      <input type="button" value="전송" id="btnSend">
    </div>
  </div>
  <script>
    function getId(id) {
      return document.getElementById(id); // id로 화면 요소 찾기
    }

    let data = {}; // 서버로 보낼 데이터를 담을 객체
    let ws; // 웹 소켓 연결 객체

    let mid = getId("mid"); // 사용자 이름 입력칸
    let btnJoin = getId("btnJoin"); // 채팅 참여 버튼
    let btnSend = getId("btnSend"); // 전송 버튼
    let talk = getId("talk"); // 메시지 출력 영역
    let msg = getId("msg"); // 메시지 입력 영역

    btnJoin.onclick = function() {
      ws = new WebSocket("ws://" + location.host + "/chat"); // 현재 서버의 /chat으로 연결

      ws.onmessage = function(msg) {
        let data = JSON.parse(msg.data); // 문자열 메시지를 객체로 변환
        let css;

        if (data.mid == mid.value) {
          css = "class=me"; // 내가 보낸 메시지
        } else {
          css = "class=other"; // 다른 사람이 보낸 메시지
        }

        let item = `<div ${css}>
          <span><b>${data.mid}</b></span> [ ${data.date} ]<br/>
          <span>${data.msg}</span>
        </div>`;

        talk.innerHTML += item; // 화면에 메시지 추가
        talk.scrollTop = talk.scrollHeight; // 스크롤을 아래로 이동
      };

      this.style.color = "blue"; // 참여 상태 표시
      this.value = "채팅참여중"; // 버튼 글자 변경
    };

    msg.onkeyup = function(ev) {
      if (ev.keyCode == 13) {
        send(); // Enter 키를 누르면 전송
      }
    };

    btnSend.onclick = function() {
      send(); // 전송 버튼을 누르면 전송
    };

    function send() {
      if (msg.value.trim() != "") {
        data.mid = getId("mid").value; // 사용자 이름 저장
        data.msg = msg.value; // 메시지 내용 저장
        data.date = new Date().toLocaleString(); // 현재 시간 저장

        let temp = JSON.stringify(data); // 객체를 문자열로 변환
        ws.send(temp); // 서버로 메시지 전송
      }

      msg.value = ""; // 입력칸 비우기
    }
  </script>
</body>

이 코드는 채팅 클라이언트 전체 흐름을 보여 준다.
처음에는 화면 요소를 변수에 저장한다.
그 다음 채팅 참여 버튼을 눌렀을 때 WebSocket 연결을 만든다.
메시지를 받으면 onmessage가 실행되고, 메시지를 보낼 때는 send() 함수가 실행된다.


코드에서 헷갈리기 쉬운 변수 이름

이 코드에서 초보자가 헷갈리기 쉬운 부분은 변수 이름이다.
바깥에 있는 msg는 메시지를 입력하는 textarea를 가리킨다.
반면 ws.onmessage = function(msg)에서 괄호 안의 msg는 서버에서 메시지를 받았을 때 전달되는 이벤트 객체다.
이 둘은 이름은 같지만 역할이 다르다.


또한 바깥의 data는 서버로 보낼 데이터를 담는 객체다.
반면 onmessage 안에서 JSON.parse(msg.data)로 만든 data는 서버에서 받은 문자열을 객체로 바꾼 값이다.
이 코드에서는 이름이 같아서 처음 보면 헷갈릴 수 있지만, 위치가 다르기 때문에 각각 다른 값으로 동작한다.


이 단계에서는 이렇게 이해하면 된다.

  • 바깥의 msg는 입력창이다.
  • onmessage 안의 msg는 서버에서 온 메시지 이벤트다.
  • 바깥의 data는 보낼 데이터다.
  • onmessage 안의 data는 받은 데이터다.

즉, 같은 이름이 보여도 코드 위치에 따라 역할이 다르므로, 입력값인지 수신값인지 구분해서 읽어야 한다.


화면 요소를 변수에 저장하는 이유

코드에서 getId() 함수는 화면 요소를 쉽게 찾기 위해 만든 함수다.
document.getElementById()는 화면에서 특정 id를 가진 요소를 찾는 기능이다.
예를 들어 getId("mid")는 id가 mid인 입력칸을 찾는다.


찾은 화면 요소는 변수에 저장한다.
이렇게 하면 같은 요소를 여러 번 사용할 때 코드를 짧고 읽기 쉽게 만들 수 있다.

// exam04_get_element.html
function getId(id) {
  return document.getElementById(id); // id로 화면 요소 찾기
}

let mid = getId("mid"); // 사용자 이름 입력칸 저장
let msg = getId("msg"); // 메시지 입력 영역 저장

이 예제에서 mid는 사용자 이름 입력칸을 가리킨다.
msg는 메시지 입력 영역을 가리킨다.
이후 코드에서는 mid.value, msg.value처럼 입력된 값을 꺼내 사용할 수 있다.


채팅 참여 버튼을 눌렀을 때의 흐름

채팅 참여 버튼을 누르면 btnJoin.onclick에 등록된 함수가 실행된다.
onclick은 사용자가 버튼을 클릭했을 때 실행할 동작을 지정하는 이벤트 핸들러다.


이 함수 안에서 가장 중요한 코드는 new WebSocket("ws://" + location.host + "/chat")이다.
이 코드는 현재 접속 중인 서버의 /chat 주소로 WebSocket 연결을 요청한다.


location.host는 현재 브라우저가 접속한 서버 주소와 포트 번호를 의미한다.
예를 들어 현재 주소가 localhost:8088이라면, "ws://" + location.host + "/chat"은 ws://localhost:8088/chat이 된다.

// exam05_join_chat.html
btnJoin.onclick = function() {
  ws = new WebSocket("ws://" + location.host + "/chat"); // 현재 서버의 /chat으로 연결
  this.style.color = "blue"; // 버튼 글자색 변경
  this.value = "채팅참여중"; // 버튼 문구 변경
};

이 코드는 채팅 참여 버튼을 눌렀을 때 서버와 연결하고, 버튼 상태를 참여 중으로 바꾼다.
즉, 버튼 클릭은 단순한 화면 변경이 아니라 서버와 채팅 연결을 시작하는 동작이다.


서버에서 메시지를 받는 흐름

서버에서 메시지가 오면 ws.onmessage가 실행된다.
이때 서버에서 받은 메시지는 msg.data에 들어 있다.
채팅 예제에서는 서버에서 받은 데이터가 JSON 문자열 형태라고 보고 처리한다.


JSON은 여러 값을 문자열 형태로 묶어 주고받기 위한 데이터 형식이다.
채팅 메시지에는 사용자 이름, 메시지 내용, 보낸 시간이 함께 필요하다.
이런 여러 값을 하나로 묶어 보내기 위해 JSON을 사용한다.

// exam06_receive_message.html
ws.onmessage = function(msg) {
  let data = JSON.parse(msg.data); // 받은 JSON 문자열을 객체로 변환
  console.log(data.mid); // 보낸 사람 이름 확인
  console.log(data.msg); // 메시지 내용 확인
  console.log(data.date); // 보낸 시간 확인
};

JSON.parse()는 문자열을 객체로 바꾸는 기능이다.
서버에서 받은 메시지는 문자열이기 때문에, 화면에 필요한 값을 꺼내 쓰려면 객체로 바꿔야 한다.
객체로 바꾸면 data.mid, data.msg, data.date처럼 각각의 값을 꺼낼 수 있다.


내 메시지와 다른 사람 메시지를 구분하는 흐름

채팅 화면에서는 내가 보낸 메시지와 다른 사람이 보낸 메시지를 다르게 보여 줄 수 있다.
이 예제에서는 서버에서 받은 메시지의 mid 값과 현재 입력칸의 mid.value 값을 비교한다.


두 값이 같으면 내가 보낸 메시지로 본다.
그래서 class=me를 적용한다.
두 값이 다르면 다른 사람이 보낸 메시지로 본다.
그래서 class=other를 적용한다.

// exam07_message_owner.html
let css;

if (data.mid == mid.value) {
  css = "class=me"; // 내가 보낸 메시지
} else {
  css = "class=other"; // 다른 사람이 보낸 메시지
}

이 코드는 메시지의 작성자를 기준으로 화면 표시 방식을 나누는 코드다.
class는 화면 요소에 적용할 스타일 이름이다.
따라서 me와 other에 서로 다른 스타일을 주면 내 메시지와 다른 사람 메시지를 다르게 보이게 만들 수 있다.


메시지를 화면에 출력하는 흐름

서버에서 받은 메시지는 화면에 누적해서 출력된다.
이 예제에서는 메시지 하나를 HTML 문자열로 만든 뒤, talk.innerHTML에 추가한다.


innerHTML은 특정 화면 요소 안의 HTML 내용을 의미한다.
talk.innerHTML += item은 기존 채팅 내용 뒤에 새 메시지 item을 추가한다는 뜻이다.

// exam08_print_message.html
let item = `<div ${css}>
  <span><b>${data.mid}</b></span> [ ${data.date} ]<br/>
  <span>${data.msg}</span>
</div>`; // 화면에 추가할 메시지 HTML 생성

talk.innerHTML += item; // 기존 채팅 내용 뒤에 새 메시지 추가
talk.scrollTop = talk.scrollHeight; // 스크롤을 가장 아래로 이동

talk.scrollTop = talk.scrollHeight는 채팅창 스크롤을 아래로 내리는 코드다.
새 메시지가 계속 아래에 추가되기 때문에, 최신 메시지를 바로 보이게 하려면 스크롤을 아래로 이동해야 한다.


메시지를 서버로 보내는 흐름

메시지를 보낼 때는 send() 함수가 실행된다.
이 함수는 전송할 메시지가 비어 있는지 먼저 확인한다.
그 다음 사용자 이름, 메시지 내용, 현재 시간을 객체에 담는다.


그 객체를 JSON.stringify()로 문자열로 바꾼 뒤 ws.send()로 서버에 보낸다.
여기서 JSON.stringify()는 객체를 JSON 문자열로 바꾸는 기능이다.
WebSocket으로 보낼 데이터를 문자열 형태로 정리하기 위해 사용한다.

// exam09_send_message.html
function send() {
  if (msg.value.trim() != "") {
    data.mid = getId("mid").value; // 사용자 이름 저장
    data.msg = msg.value; // 메시지 내용 저장
    data.date = new Date().toLocaleString(); // 현재 시간 저장

    let temp = JSON.stringify(data); // 객체를 JSON 문자열로 변환
    ws.send(temp); // 서버로 메시지 전송
  }

  msg.value = ""; // 입력칸 비우기
}

trim()은 문자열 양쪽의 공백을 제거하는 기능이다.
msg.value.trim() != ""는 공백만 있는 메시지는 보내지 않겠다는 뜻이다.


메시지를 보낸 뒤에는 msg.value = ""로 입력칸을 비운다.
그래야 다음 메시지를 새로 입력하기 편하다.


엔터 키와 전송 버튼 처리

이 예제에서는 메시지를 보내는 방법이 두 가지다.
하나는 Enter 키를 누르는 방식이다.
다른 하나는 전송 버튼을 클릭하는 방식이다.


두 방식 모두 마지막에는 같은 send() 함수를 호출한다.
이렇게 하면 메시지 전송 로직을 한곳에서 관리할 수 있다.

// exam10_send_event.html
msg.onkeyup = function(ev) {
  if (ev.keyCode == 13) {
    send(); // Enter 키를 누르면 메시지 전송
  }
};

btnSend.onclick = function() {
  send(); // 전송 버튼을 누르면 메시지 전송
};

onkeyup은 키보드에서 키를 눌렀다가 뗐을 때 실행되는 이벤트 핸들러다.
ev.keyCode == 13은 눌린 키가 Enter 키인지 확인하는 조건이다.
전송 버튼을 클릭할 때도 send()를 호출하므로, 두 입력 방식의 결과는 같다.


채팅 클라이언트 핵심 정리

채팅 클라이언트 예제는 여러 코드가 섞여 있지만 흐름은 단순하다.
화면 요소를 찾고, 서버에 연결하고, 메시지를 보내고, 받은 메시지를 화면에 출력한다.


핵심만 정리하면 다음과 같다.

  • getId()로 화면 요소를 찾는다.
  • 채팅 참여 버튼을 누르면 WebSocket 연결을 만든다.
  • 서버에서 메시지가 오면 onmessage가 실행된다.
  • 받은 JSON 문자열은 JSON.parse()로 객체로 바꾼다.
  • 내가 보낸 메시지와 다른 사람 메시지를 class로 구분한다.
  • 전송할 메시지는 JSON.stringify()로 문자열로 바꾼다.
  • ws.send()로 서버에 메시지를 보낸다.
  • Enter 키와 전송 버튼은 모두 send() 함수를 호출한다.

정리하면 채팅 클라이언트는 화면 입력값을 JSON 문자열로 만들어 서버에 보내고, 서버에서 받은 JSON 문자열을 다시 화면에 출력하는 구조다.




스프링 부트 웹 소켓 서버 구현

WebSocket 채팅이 동작하려면 클라이언트 코드만 있어서는 안 된다.
브라우저가 ws://localhost:8088/chat으로 연결을 요청하면, 서버 쪽에도 이 요청을 받아 줄 WebSocket 서버 프로그램이 있어야 한다.


Spring Boot에서는 WebSocket 서버를 구현할 수 있다.
서버는 클라이언트가 접속했을 때 연결을 받고, 클라이언트가 메시지를 보냈을 때 그 메시지를 처리하고, 연결이 끊겼을 때 정리 작업을 한다.


서버 구현의 핵심은 클라이언트의 연결 요청을 받을 주소를 만들고, 연결·메시지 수신·연결 종료 상황을 각각 메서드로 처리하는 것이다.
이 구조를 이해하면 뒤에 나오는 채팅 서버 예제도 쉽게 읽을 수 있다.


Spring Boot에서 WebSocket을 사용한다는 의미

Spring Boot는 웹 애플리케이션을 빠르게 만들 수 있도록 도와주는 Java 기반 프레임워크다.
프레임워크는 개발자가 자주 사용하는 구조와 기능을 미리 준비해 둔 틀이라고 이해하면 된다.


WebSocket 서버를 직접 처음부터 만들려면 연결 처리, 메시지 처리, 종료 처리 같은 부분을 모두 세세하게 구현해야 한다.
하지만 Spring Boot에서는 WebSocket 관련 기능을 추가하고, 정해진 어노테이션을 사용하면 서버 프로그램을 더 쉽게 만들 수 있다.


여기서 어노테이션은 코드 위에 붙여서 역할을 표시하는 문법이다.
예를 들어 @ServerEndpoint는 이 클래스가 WebSocket 서버의 연결 지점이라는 뜻을 나타낸다.

Spring 환경에서도 WebSocket을 활용할 수 있고, 필요하면 SockJS 같은 보조 기술과 함께 사용할 수 있다.
SockJS는 브라우저나 연결 환경이 제한적일 때 WebSocket을 더 안정적으로 사용할 수 있도록 도와주는 기술이다.


WebSocket 의존성 설정

의존성은 프로젝트에서 사용할 외부 기능을 추가하는 설정이다.
Spring Boot 프로젝트에서 WebSocket 기능을 사용하려면 spring-boot-starter-websocket 의존성을 추가해야 한다.


이 의존성이 있어야 Spring Boot가 제공하는 WebSocket 관련 기능을 사용할 수 있다.
의존성이 없으면 WebSocket 서버를 만들기 위한 클래스나 설정을 제대로 사용할 수 없다.

// exam_build.gradle
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-websocket' // 웹 소켓 기능 추가
}

이 설정은 Spring Boot 프로젝트에 WebSocket 기능을 추가한다.
implementation은 이 프로젝트에서 해당 라이브러리를 사용하겠다는 의미다.


Spring WebSocket Messaging

Spring은 단순한 WebSocket 연결뿐 아니라 메시징 구조도 지원한다.
메시징 구조는 단순히 데이터를 주고받는 것에서 더 나아가, 메시지를 목적지에 맞게 처리하는 구조를 의미한다.


예를 들어 채팅방이 여러 개라면 모든 메시지를 모든 사용자에게 보내면 안 된다.
특정 채팅방에 있는 사용자에게만 메시지를 보내야 한다.
이런 구조를 더 체계적으로 만들 때 STOMP 같은 메시징 방식이 함께 사용될 수 있다.

Spring WebSocket Messaging은 Servlet 기반 WebSocket 애플리케이션을 만들고, SockJS, STOMP와 함께 사용할 수 있는 기능을 제공한다.
이번 예제는 먼저 기본 WebSocket 연결과 메시지 처리 흐름을 이해하는 데 초점을 둔다.


ServerEndpoint 클래스가 필요한 이유

클라이언트가 WebSocket으로 서버에 연결하려면 서버 쪽에 연결을 받을 지점이 있어야 한다.
이 연결 지점을 Endpoint라고 한다.
Endpoint는 클라이언트가 접속할 수 있는 서버의 입구라고 이해하면 된다.


@ServerEndpoint는 해당 클래스가 WebSocket 서버의 입구 역할을 한다는 표시다.
괄호 안에는 클라이언트가 접속할 주소를 적는다.
예를 들어 @ServerEndpoint(value="/chat")이면 클라이언트는 /chat 주소로 접속한다.

// examWebSocketServer.java
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Service;

@Service
@ServerEndpoint(value="/chat")
public class WebSocketServer {
  // /chat 주소로 들어오는 웹 소켓 연결을 처리하는 클래스
}

이 코드에서 /chat은 클라이언트가 접속할 매핑명이다.
클라이언트 코드의 ws://localhost:8088/chat에서 마지막 /chat과 서버의 @ServerEndpoint(value="/chat")가 서로 연결된다.


ServerEndpointExporter 빈 등록이 필요한 이유

Spring에서 빈은 Spring이 직접 만들고 관리하는 객체다.
기본적으로 Spring 빈은 싱글톤으로 관리된다.
싱글톤은 하나의 객체를 만들어 두고 여러 곳에서 함께 사용하는 방식이다.


보통 @Service, @Component 같은 어노테이션이 붙은 클래스는 Spring이 객체로 만들어 관리한다.
그래서 다른 객체가 필요할 때 @Autowired 같은 방식으로 의존 객체를 주입받을 수 있다.
의존 객체 주입은 필요한 객체를 직접 만들지 않고 Spring에게 받아 쓰는 방식이라고 이해하면 된다.


하지만 @ServerEndpoint가 붙은 클래스는 일반적인 Spring 빈 생성 방식과 다르게 동작할 수 있다.
웹 소켓 연결이 생성될 때마다 @ServerEndpoint 클래스의 인스턴스가 만들어지고 관리되기 때문이다.
인스턴스는 실제로 메모리에 만들어진 객체라고 이해하면 된다.


이 차이 때문에 @ServerEndpoint 클래스 안에서 @Autowired가 붙은 멤버들이 기대한 대로 초기화되지 않을 수 있다.
그래서 클라이언트로부터 웹 소켓 접속 요청이 들어올 때마다 @ServerEndpoint 클래스가 웹 소켓 엔드포인트로 동작할 수 있도록 초기화 설정이 필요하다.


이때 사용하는 설정이 ServerEndpointExporter 빈 등록이다.
ServerEndpointExporter는 @ServerEndpoint가 붙은 클래스를 찾아서 WebSocket 서버 엔드포인트로 등록해 주는 역할을 한다.

// examWebSocketConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Component
public class WebSocketConfig {
  @Bean
  public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter(); // @ServerEndpoint 클래스를 웹 소켓 엔드포인트로 등록
  }
}

이 설정 파일은 WebSocket 서버 프로그램을 사용할 수 있도록 준비하는 코드다.
@Component는 이 클래스를 Spring이 관리하는 객체로 등록하겠다는 의미다.
@Bean은 메서드가 반환하는 객체를 Spring 빈으로 등록하겠다는 의미다.


WebSocket 서버 클래스의 기본 구조

WebSocket 서버 클래스는 연결 상황에 따라 실행될 메서드를 가지고 있어야 한다.
대표적으로 연결 성공, 메시지 수신, 연결 종료 상황을 처리한다.


이때 사용하는 어노테이션은 세 가지다.

  • @OnOpen은 클라이언트가 접속했을 때 실행된다.
  • @OnMessage는 클라이언트가 메시지를 보냈을 때 실행된다.
  • @OnClose는 클라이언트 연결이 종료되었을 때 실행된다.
// examWebSocketBasicServer.java
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Service;

@Service
@ServerEndpoint(value="/chat")
public class WebSocketBasicServer {
  @OnOpen
  public void onOpen(Session s) {
    System.out.println("접속 시작"); // 클라이언트가 연결되면 실행
  }

  @OnMessage
  public void onMessage(String msg, Session session) throws Exception {
    System.out.println(msg); // 클라이언트가 보낸 메시지를 받으면 실행
  }

  @OnClose
  public void onClose(Session s) {
    System.out.println("접속 종료"); // 클라이언트 연결이 닫히면 실행
  }
}

이 코드는 WebSocket 서버 클래스의 기본 형태를 보여 준다.
클라이언트가 /chat으로 접속하면 onOpen()이 실행된다.
클라이언트가 메시지를 보내면 onMessage()가 실행된다.
연결이 끊기면 onClose()가 실행된다.


Session 객체의 의미

Session은 클라이언트와 서버 사이의 연결 정보를 나타내는 객체다.
객체는 관련된 값과 기능을 묶어 둔 코드 단위라고 이해하면 된다.


채팅으로 생각하면 사용자 한 명이 서버에 접속할 때 하나의 연결이 만들어진다.
이 연결 정보를 서버에서 다루기 위해 사용하는 것이 Session이다.
여러 사용자가 접속하면 여러 개의 Session이 생긴다.


서버는 이 Session을 이용해 특정 클라이언트에게 메시지를 보낼 수 있다.
채팅 서버에서는 접속한 사용자들의 Session을 모아 두었다가, 한 사용자가 메시지를 보내면 모든 Session에 메시지를 다시 보내는 방식으로 동작한다.


즉, Session은 서버가 연결된 클라이언트를 구분하고 메시지를 보내기 위해 필요한 연결 정보다.


서버 구현 핵심 정리

Spring Boot에서 WebSocket 서버를 구현하려면 먼저 의존성을 추가해야 한다.
그 다음 ServerEndpointExporter를 빈으로 등록해 @ServerEndpoint 클래스가 웹 소켓 엔드포인트로 동작할 수 있게 해야 한다.
마지막으로 실제 서버 클래스에서 연결, 메시지, 종료 상황을 처리한다.


핵심만 정리하면 다음과 같다.

  • spring-boot-starter-websocket 의존성을 추가한다.
  • @ServerEndpoint는 웹 소켓 서버의 접속 주소를 만든다.
  • ServerEndpointExporter는 @ServerEndpoint 클래스를 웹 소켓 엔드포인트로 등록한다.
  • @OnOpen은 클라이언트 접속 시 실행된다.
  • @OnMessage는 메시지 수신 시 실행된다.
  • @OnClose는 연결 종료 시 실행된다.
  • Session은 클라이언트와 서버 사이의 연결 정보를 나타낸다.

정리하면 Spring Boot 웹 소켓 서버는 클라이언트가 접속할 주소를 만들고, 연결·메시지 수신·종료 상황을 각각 메서드로 처리하는 구조다.




웹 소켓 채팅 서버 예제

채팅 서버 예제는 클라이언트가 보낸 메시지를 서버가 받은 뒤, 현재 접속 중인 모든 클라이언트에게 다시 보내는 구조다.
겉으로 보면 채팅 메시지가 여러 브라우저에 동시에 보이는 기능이지만, 내부 흐름은 접속자 저장, 메시지 수신, 전체 전송, 접속 종료 정리로 나뉜다.


클라이언트는 ws://localhost:8088/chat 주소로 서버에 접속한다.
서버에서는 @ServerEndpoint(value="/chat")이 붙은 클래스가 이 연결을 받는다.
즉, 클라이언트의 /chat 연결 요청과 서버의 /chat 매핑이 서로 맞아야 통신이 시작된다.


채팅 서버의 핵심은 접속한 클라이언트의 Session을 저장해 두고, 한 클라이언트가 보낸 메시지를 저장된 모든 Session에게 다시 보내는 것이다.
이 구조를 이해하면 실시간 채팅이 왜 여러 사용자에게 동시에 보이는지 알 수 있다.


전체 서버 코드 흐름

// examWebSocketChatt.java
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@Service
@ServerEndpoint(value="/chat")
public class WebSocketChatt {
  private static Set<Session> clientSet = Collections.synchronizedSet(new HashSet<Session>()); // 접속한 클라이언트 세션 저장

  @OnOpen
  public void onOpen(Session s) {
    if (!clientSet.contains(s)) {
      clientSet.add(s); // 새 세션 추가
      System.out.println("[세션 오픈] " + s); // 접속 로그 출력
    } else {
      System.out.println("이미 연결된 세션임!!!"); // 중복 접속 확인
    }
  }

  @OnMessage
  public void onMessage(String msg, Session session) throws Exception {
    System.out.println("[수신 메시지] " + msg); // 클라이언트가 보낸 메시지 출력

    for (Session s : clientSet) {
      System.out.println("[송신 메시지] " + msg); // 전송할 메시지 출력
      s.getBasicRemote().sendText(msg); // 접속 중인 클라이언트에게 메시지 전송
    }
  }

  @OnClose
  public void onClose(Session s) {
    System.out.println("[세션 종료] " + s); // 종료 로그 출력
    clientSet.remove(s); // 종료된 세션 제거
  }
}

이 코드는 /chat 주소로 들어오는 WebSocket 연결을 처리하는 서버 코드다.
클라이언트가 접속하면 onOpen()이 실행된다.
클라이언트가 메시지를 보내면 onMessage()가 실행된다.
클라이언트 연결이 종료되면 onClose()가 실행된다.


서버는 접속한 클라이언트의 Session을 clientSet에 저장한다.
그리고 메시지를 받으면 clientSet에 들어 있는 모든 Session을 하나씩 꺼내 같은 메시지를 다시 보낸다.
그래서 한 사람이 보낸 채팅 메시지가 다른 사람의 화면에도 나타난다.


ServerEndpoint와 chat 매핑

@ServerEndpoint(value="/chat")은 이 클래스가 /chat 주소의 웹 소켓 서버 역할을 한다는 뜻이다.
클라이언트가 ws://localhost:8088/chat으로 연결하면 이 클래스가 연결을 처리한다.

// examServerEndpointMapping.java
@Service
@ServerEndpoint(value="/chat")
public class WebSocketChatt {
  // /chat 주소로 들어오는 웹 소켓 연결을 처리
}

클라이언트 주소와 서버 매핑은 서로 맞아야 한다.
클라이언트가 /chat으로 접속하는데 서버가 /socket으로 열려 있으면 연결되지 않는다.


이 관계는 아래처럼 이해하면 된다.

  • 클라이언트 연결 주소: ws://localhost:8088/chat
  • 서버 매핑 주소: @ServerEndpoint(value="/chat")

마지막 경로인 /chat이 서로 맞아야 클라이언트와 서버가 연결된다.


접속한 클라이언트를 Session으로 저장하는 이유

Session은 클라이언트와 서버 사이의 연결 정보를 나타내는 객체다.
채팅에서는 사용자 한 명이 서버에 접속할 때 하나의 Session이 만들어진다고 이해하면 된다.


서버가 메시지를 여러 사용자에게 보내려면 누가 접속해 있는지 알아야 한다.
그래서 접속한 사용자의 Session을 모아 둔다.
이 예제에서는 clientSet이 접속자 목록 역할을 한다.

// examClientSet.java
private static Set<Session> clientSet = Collections.synchronizedSet(new HashSet<Session>()); // 접속 세션 목록

Set은 중복을 허용하지 않는 자료구조다.
자료구조는 데이터를 어떤 방식으로 저장할지 정한 틀이다.
Set을 사용하면 같은 Session이 중복으로 들어가는 것을 막을 수 있다.


여기서 static이 붙은 이유도 중요하다.
static은 객체마다 따로 생기는 값이 아니라, 클래스 차원에서 함께 사용하는 값이라는 의미다.


@ServerEndpoint 클래스는 웹 소켓 연결이 만들어질 때마다 인스턴스가 새로 생성될 수 있다.
인스턴스는 실제로 메모리에 만들어진 객체를 의미한다.
만약 접속자 목록이 각 인스턴스마다 따로 만들어지면, 서버가 전체 접속자를 한곳에서 관리하기 어렵다.


채팅 서버는 한 사용자가 보낸 메시지를 모든 접속자에게 다시 보내야 한다.
그래서 모든 연결이 같은 clientSet을 함께 보도록 static으로 선언한다.
즉, static clientSet은 접속한 모든 클라이언트의 Session을 하나의 공통 목록으로 관리하기 위한 구조다.


HashSet은 Set의 한 종류다.
중복 없는 목록을 저장할 때 사용할 수 있다.
Collections.synchronizedSet()은 여러 클라이언트가 동시에 접속하거나 메시지를 보낼 때 목록이 꼬이지 않도록 동기화된 Set으로 감싸는 기능이다.


여기서 동기화는 여러 작업이 동시에 접근할 때 충돌이 나지 않도록 순서를 맞추는 처리라고 이해하면 된다.
채팅 서버는 여러 사용자가 동시에 접속할 수 있으므로 접속자 목록을 안전하게 관리해야 한다.


즉, clientSet은 현재 서버에 접속해 있는 클라이언트들의 연결 목록이다.


OnOpen에서 접속 세션 추가하기

@OnOpen은 클라이언트가 웹 소켓 서버에 접속했을 때 실행된다.
채팅 화면에서 사용자가 채팅 참여 버튼을 누르면 클라이언트는 /chat으로 연결을 요청하고, 서버에서는 onOpen()이 실행된다.

// examOnOpen.java
@OnOpen
public void onOpen(Session s) {
  if (!clientSet.contains(s)) {
    clientSet.add(s); // 접속한 세션을 목록에 추가
    System.out.println("[세션 오픈] " + s); // 접속 로그 출력
  } else {
    System.out.println("이미 연결된 세션임!!!"); // 이미 목록에 있는 세션
  }
}

contains()는 목록 안에 해당 값이 있는지 확인하는 기능이다.
clientSet.contains(s)는 clientSet 안에 현재 접속한 Session이 이미 있는지 확인한다.


!clientSet.contains(s)는 아직 이 Session이 목록에 없다는 뜻이다.
따라서 새로 접속한 클라이언트라면 clientSet.add(s)로 목록에 추가한다.


이 처리가 필요한 이유는 간단하다.
서버가 나중에 메시지를 보낼 때, 이 목록을 보고 누구에게 메시지를 보낼지 결정하기 때문이다.
채팅 서버에서 접속자 목록을 저장하는 일은 메시지를 보낼 대상을 준비하는 일이다.


OnMessage에서 메시지 받기

@OnMessage는 클라이언트가 서버로 메시지를 보냈을 때 실행된다.
클라이언트 코드에서 ws.send(temp)가 실행되면 서버의 onMessage()가 호출된다.

// examOnMessageReceive.java
@OnMessage
public void onMessage(String msg, Session session) throws Exception {
  System.out.println("[수신 메시지] " + msg); // 클라이언트가 보낸 메시지 확인
}

msg에는 클라이언트가 보낸 메시지가 들어온다.
채팅 클라이언트 예제에서는 사용자 이름, 메시지 내용, 시간을 JSON.stringify()로 문자열로 바꿔 보냈다.
따라서 서버가 받는 msg도 JSON 문자열이다.


예를 들어 클라이언트가 아래와 같은 문자열을 보낼 수 있다.

// 출력결과
// {"mid":"게스트","msg":"안녕","date":"2026. 5. 4."}

서버는 이 문자열을 받아 로그로 출력한다.
이 예제에서는 서버가 메시지 내용을 분석하지 않고, 받은 문자열 그대로 다시 클라이언트들에게 보낸다.


모든 클라이언트에게 메시지 보내기

채팅에서는 한 사람이 보낸 메시지를 접속 중인 다른 사람들도 볼 수 있어야 한다.
그래서 서버는 받은 메시지를 현재 접속 중인 모든 클라이언트에게 다시 보낸다.


이때 사용하는 구조가 반복문이다.
반복문은 같은 작업을 여러 번 실행할 때 사용하는 문법이다.
여기서는 clientSet에 저장된 Session을 하나씩 꺼내 메시지를 보내기 위해 사용한다.

// examBroadcastMessage.java
@OnMessage
public void onMessage(String msg, Session session) throws Exception {
  System.out.println("[수신 메시지] " + msg); // 받은 메시지 출력

  for (Session s : clientSet) {
    System.out.println("[송신 메시지] " + msg); // 보낼 메시지 출력
    s.getBasicRemote().sendText(msg); // 각 클라이언트에게 메시지 전송
  }
}

for (Session s : clientSet)은 clientSet에 저장된 Session을 하나씩 꺼내겠다는 뜻이다.
꺼낸 Session은 반복문 안에서 s라는 이름으로 사용한다.


s.getBasicRemote().sendText(msg)는 해당 Session의 클라이언트에게 문자열 메시지를 보내는 코드다.
즉, 서버가 클라이언트 브라우저로 메시지를 보내는 부분이다.


이 예제에서는 보낸 사람을 포함한 모든 접속자에게 메시지를 보낸다.
그래서 내가 보낸 메시지도 내 화면에 다시 출력된다.
클라이언트는 그 메시지를 받은 뒤 mid 값을 비교해서 내 메시지인지 다른 사람 메시지인지 구분한다.


브로드캐스트란 무엇인가

브로드캐스트는 하나의 메시지를 여러 대상에게 보내는 방식이다.
채팅 서버에서는 한 클라이언트가 보낸 메시지를 접속 중인 모든 클라이언트에게 보내므로 브로드캐스트 구조라고 볼 수 있다.


쉽게 말하면 한 사람이 채팅방에 말을 하면, 같은 채팅방에 있는 모든 사람이 그 말을 듣는 것과 같다.
서버는 중간에서 메시지를 받고, 접속자 목록을 기준으로 모두에게 전달한다.


이 예제의 브로드캐스트 흐름은 다음과 같다.

  • 사용자 한 명이 메시지를 보낸다.
  • 서버의 onMessage()가 메시지를 받는다.
  • 서버는 clientSet에 저장된 Session을 하나씩 꺼낸다.
  • 각 Session에게 같은 메시지를 보낸다.
  • 각 클라이언트의 onmessage가 실행되어 화면에 메시지가 출력된다.

브로드캐스트는 실시간 채팅에서 한 사람의 메시지를 여러 사용자 화면에 동시에 보여 주기 위한 핵심 구조다.


OnClose에서 접속 세션 제거하기

@OnClose는 클라이언트와 서버의 연결이 종료되었을 때 실행된다.
사용자가 브라우저를 닫거나 연결이 끊기면 해당 클라이언트의 Session은 더 이상 사용할 수 없다.

// examOnClose.java
@OnClose
public void onClose(Session s) {
  System.out.println("[세션 종료] " + s); // 종료된 세션 출력
  clientSet.remove(s); // 접속 목록에서 세션 제거
}

remove()는 목록에서 특정 값을 제거하는 기능이다.
clientSet.remove(s)는 종료된 Session을 접속자 목록에서 제거한다.


이 처리가 필요한 이유는 서버가 끊어진 클라이언트에게 메시지를 보내려고 하지 않게 하기 위해서다.
만약 종료된 Session을 계속 목록에 두면, 서버는 존재하지 않는 연결에도 메시지를 보내려고 할 수 있다.
그러면 오류가 생기거나 불필요한 처리가 늘어난다.


따라서 연결이 종료되면 반드시 접속자 목록에서도 해당 Session을 제거해야 한다.


채팅 서버 실행 흐름

채팅 서버의 전체 실행 흐름은 접속, 저장, 수신, 전송, 종료로 정리할 수 있다.
이 흐름은 클라이언트 코드와 연결해서 보면 더 쉽게 이해된다.


클라이언트가 채팅 참여 버튼을 누르면 new WebSocket("ws://" + location.host + "/chat")이 실행된다.
그러면 서버의 @OnOpen 메서드가 실행되고, 서버는 해당 클라이언트의 Session을 저장한다.


클라이언트가 메시지를 보내면 ws.send(temp)가 실행된다.
그러면 서버의 @OnMessage 메서드가 실행되고, 서버는 받은 메시지를 모든 Session에게 다시 보낸다.


클라이언트가 연결을 종료하면 서버의 @OnClose 메서드가 실행된다.
그러면 서버는 종료된 Session을 접속자 목록에서 제거한다.

// 출력결과
// [세션 오픈] Session 정보
// [수신 메시지] {"mid":"게스트","msg":"안녕","date":"2026. 5. 4."}
// [송신 메시지] {"mid":"게스트","msg":"안녕","date":"2026. 5. 4."}
// [세션 종료] Session 정보

실제 Session 정보는 실행 환경마다 다르게 출력된다.
중요한 것은 접속, 수신, 송신, 종료 흐름이 순서대로 확인된다는 점이다.


채팅 서버 핵심 정리

채팅 서버 예제는 코드가 길어 보이지만 역할은 명확하다.
접속한 클라이언트를 저장하고, 메시지를 받으면 모든 클라이언트에게 다시 보내고, 연결이 종료되면 목록에서 제거한다.


핵심만 정리하면 다음과 같다.

  • @ServerEndpoint(value="/chat")은 /chat 웹 소켓 연결을 처리한다.
  • clientSet은 접속 중인 클라이언트의 Session 목록이다.
  • @OnOpen은 새 클라이언트가 접속했을 때 실행된다.
  • clientSet.add(s)는 접속한 클라이언트를 목록에 저장한다.
  • @OnMessage는 클라이언트가 메시지를 보냈을 때 실행된다.
  • s.getBasicRemote().sendText(msg)는 클라이언트에게 메시지를 보낸다.
  • @OnClose는 연결이 종료되었을 때 실행된다.
  • clientSet.remove(s)는 종료된 클라이언트를 목록에서 제거한다.

정리하면 채팅 서버는 접속자의 Session을 관리하면서, 받은 메시지를 모든 접속자에게 다시 보내는 브로드캐스트 구조로 동작한다.




브라우저에서 웹 소켓 연결 확인하기

WebSocket 연결이 성공했는지는 브라우저 개발자 도구에서 확인할 수 있다.
채팅 화면이 보인다고 해서 무조건 WebSocket 연결이 성공한 것은 아니다.
화면은 열렸지만 서버와의 연결이 실패했을 수도 있다.


그래서 실습에서는 채팅 화면뿐만 아니라 개발자 도구의 Network 탭을 함께 확인해야 한다.
Network 탭은 브라우저가 서버와 어떤 요청을 주고받았는지 보여 주는 곳이다.
여기서 /chat 연결 요청이 보이고, 상태 코드가 101이면 WebSocket 연결 전환이 성공한 것이다.


WebSocket 연결 확인의 핵심은 Network 탭에서 Status Code가 101 Switching Protocols인지 확인하는 것이다.
101은 처음 HTTP로 시작한 연결이 WebSocket 통신으로 전환되었다는 뜻이다.


Network 탭에서 chat 요청 확인하기

WebSocket 연결 요청은 채팅 참여 버튼을 누르는 순간 발생한다.
따라서 개발자 도구의 Network 탭을 먼저 열어 둔 뒤 채팅 참여 버튼을 누르는 것이 좋다.
그래야 브라우저가 서버로 보낸 /chat 연결 요청을 놓치지 않고 확인할 수 있다.


Network 탭은 웹 페이지가 서버와 주고받은 요청 목록을 보여 준다.
일반 웹 페이지 요청, 이미지 요청, AJAX 요청, WebSocket 연결 요청 등을 확인할 수 있다.
WebSocket 실습에서는 이 목록에서 chat 요청을 찾으면 된다.

Network 탭에서 chat 요청의 상태 코드가 101이고 타입이 websocket이면 WebSocket 연결이 성공한 것이다.
오른쪽 화면에 chat 요청이 보이고, Status가 101, Type이 websocket으로 표시되어 있다.


Status Code 101의 의미

Status Code는 서버가 요청에 대해 어떤 결과를 돌려주었는지 알려 주는 숫자다.
일반 웹 페이지에서는 200이 성공을 의미하는 경우가 많다.
하지만 WebSocket 연결에서는 101이 중요하다.


101 Switching Protocols는 프로토콜을 전환한다는 뜻이다.
프로토콜은 통신 규칙이라고 이해하면 된다.
처음에는 HTTP 규칙으로 연결 요청을 보냈지만, 서버가 이를 받아들이면 WebSocket 규칙으로 바뀐다.


즉, 101은 단순히 페이지가 잘 열렸다는 뜻이 아니다.
HTTP 연결을 WebSocket 연결로 바꾸는 데 성공했다는 뜻이다.


따라서 WebSocket 실습에서 연결 성공 여부를 볼 때는 101이 가장 중요한 확인 기준이다.


Type이 websocket인지 확인하기

Network 탭에는 요청의 종류를 나타내는 Type 정보도 표시된다.
여기서 Type이 websocket이면 해당 요청이 일반 HTTP 요청이 아니라 WebSocket 연결 요청이라는 뜻이다.


일반 페이지 요청은 보통 document, 이미지 요청은 png나 jpeg, 비동기 요청은 fetch 또는 xhr처럼 표시될 수 있다.
반면 WebSocket 연결은 websocket으로 표시된다.


이 값을 확인하면 현재 보고 있는 chat 요청이 진짜 WebSocket 연결인지 구분할 수 있다.
Status가 101이고 Type이 websocket이면 연결 성공 여부를 더 확실하게 판단할 수 있다.


Headers 탭에서 Request URL 확인하기

chat 요청을 클릭하면 오른쪽에서 자세한 정보를 볼 수 있다.
그중 Headers 탭은 요청 주소와 응답 상태, 요청 헤더, 응답 헤더를 확인하는 곳이다.


먼저 Request URL을 확인한다.
Request URL이 ws://localhost:8088/chat으로 되어 있다면 클라이언트가 /chat 웹 소켓 서버로 연결을 요청했다는 뜻이다.


이 주소는 클라이언트 코드의 아래 흐름과 연결된다.

  • 클라이언트 코드: new WebSocket("ws://" + location.host + "/chat")
  • 실제 요청 주소: ws://localhost:8088/chat
  • 서버 매핑 주소: @ServerEndpoint(value="/chat")

이 세 값에서 마지막 경로인 /chat이 서로 맞아야 한다.
클라이언트가 /chat으로 요청하는데 서버가 다른 주소로 열려 있으면 연결이 실패한다.


Connection과 Upgrade 헤더 확인하기

Headers 탭에서는 Connection: upgrade와 Upgrade: websocket도 확인할 수 있다.
이 값들은 HTTP 연결을 WebSocket 연결로 바꾸는 과정과 관련이 있다.


Connection: upgrade는 현재 연결을 업그레이드하겠다는 의미다.
여기서 업그레이드는 기존 연결을 다른 통신 방식으로 전환한다는 뜻이다.


Upgrade: websocket은 어떤 방식으로 전환할지를 나타낸다.
즉, 이 값은 연결을 websocket 방식으로 바꾸겠다는 의미다.

101 Switching Protocols, Connection: upgrade, Upgrade: websocket은 HTTP 연결이 WebSocket 통신으로 전환되었음을 보여준다.
Request URL이 ws://localhost:8088/chat이고, 응답 상태가 101이면 /chat 웹 소켓 연결이 정상적으로 열린 것이다.


연결 성공 여부를 판단하는 순서

WebSocket 연결을 확인할 때는 화면만 보지 말고 개발자 도구에서 순서대로 확인하는 것이 좋다.
확인 순서는 다음과 같다.

  • 개발자 도구의 Network 탭을 먼저 연다.
  • 채팅 참여 버튼을 누른다.
  • 요청 목록에서 chat 요청을 찾는다.
  • Status가 101인지 확인한다.
  • Type이 websocket인지 확인한다.
  • Headers 탭에서 Request URL이 ws://localhost:8088/chat인지 확인한다.
  • Connection: upgrade가 있는지 확인한다.
  • Upgrade: websocket이 있는지 확인한다.

이 항목들이 맞으면 WebSocket 연결이 정상적으로 열린 것으로 볼 수 있다.
특히 101, websocket, upgrade 세 가지가 핵심이다.


연결이 안 될 때 먼저 확인할 것

WebSocket 연결이 되지 않으면 먼저 주소가 맞는지 확인해야 한다.
클라이언트가 요청하는 주소와 서버의 매핑 주소가 다르면 연결되지 않는다.


예를 들어 클라이언트가 ws://localhost:8088/chat으로 요청한다면 서버에는 @ServerEndpoint(value="/chat")이 있어야 한다.
만약 서버가 @ServerEndpoint(value="/socket")으로 되어 있다면 /chat 요청을 받을 수 없다.


다음으로 서버가 실행 중인지 확인해야 한다.
서버가 꺼져 있으면 브라우저가 아무리 연결을 요청해도 받을 대상이 없다.
또한 포트 번호가 맞는지도 확인해야 한다.
실습 서버가 8088에서 실행 중인데 클라이언트가 다른 포트로 요청하면 연결되지 않는다.


확인할 내용은 다음과 같다.

  • 서버가 실행 중인지 확인한다.
  • 클라이언트 주소가 ws://localhost:8088/chat인지 확인한다.
  • 서버 매핑이 @ServerEndpoint(value="/chat")인지 확인한다.
  • 포트 번호 8088이 실제 실행 포트와 같은지 확인한다.
  • 개발자 도구의 Network 탭을 먼저 열고 채팅 참여 버튼을 눌렀는지 확인한다.

연결 실패를 확인할 때는 화면보다 Network 탭의 요청 주소, 상태 코드, 타입을 먼저 보는 것이 정확하다.


브라우저 연결 확인 핵심 정리

브라우저에서 WebSocket 연결을 확인하는 핵심은 개발자 도구의 Network 탭이다.
채팅 화면이 보이는 것만으로는 연결 성공을 확실히 알 수 없다.
반드시 chat 요청의 상태와 헤더를 확인해야 한다.


핵심만 정리하면 다음과 같다.

  • Network 탭을 먼저 열고 채팅 참여 버튼을 누른다.
  • Network 탭에서 chat 요청을 찾는다.
  • Status Code가 101 Switching Protocols인지 확인한다.
  • Type이 websocket인지 확인한다.
  • Request URL이 ws://localhost:8088/chat인지 확인한다.
  • Connection: upgrade는 연결 전환 요청을 의미한다.
  • Upgrade: websocket은 WebSocket으로 전환한다는 의미다.
  • 101은 HTTP에서 WebSocket으로 프로토콜 전환이 성공했다는 뜻이다.

정리하면 WebSocket 연결 성공은 개발자 도구에서 101, websocket, upgrade 값을 확인하면 된다.




전체 핵심 정리

WebSocket은 실시간 양방향 통신을 만들기 위한 기술이다.
기존 HTTP처럼 클라이언트가 요청하고 서버가 응답하는 흐름만으로는 채팅, 실시간 알림, 실시간 게임처럼 즉시 반응해야 하는 기능을 자연스럽게 처리하기 어렵다.


WebSocket은 처음에 HTTP 요청을 기반으로 연결을 시작한다.
서버가 이 연결을 받아들이면 101 Switching Protocols 응답을 통해 WebSocket 통신으로 전환된다.
그 뒤에는 연결을 유지한 상태에서 클라이언트와 서버가 서로 데이터를 주고받는다.


정리하면 WebSocket은 처음에는 HTTP로 연결을 시작하지만, 연결이 성공한 뒤에는 유지된 연결을 통해 실시간 양방향 통신을 수행하는 방식이다.


WebSocket 전체 흐름 한 번에 보기

WebSocket의 전체 흐름은 어렵게 볼 필요가 없다.
연결을 열고, 데이터를 주고받고, 마지막에 연결을 닫는 구조다.


흐름을 순서대로 정리하면 다음과 같다.

  • 클라이언트가 WebSocket 서버 주소로 연결을 요청한다.
  • 이 요청은 처음에 HTTP Upgrade 요청으로 시작된다.
  • 서버가 요청을 받아들이면 101 Switching Protocols를 응답한다.
  • 연결이 열리면 CONNECTED 상태가 된다.
  • 연결이 유지되는 동안 클라이언트와 서버가 메시지를 주고받는다.
  • 통신이 끝나면 Closing Handshake를 통해 연결을 닫는다.

이 흐름에서 가장 중요한 단계는 Handshake다.
Handshake는 클라이언트와 서버가 WebSocket으로 통신해도 되는지 먼저 확인하는 절차다.
이 절차가 성공해야 이후에 실시간 메시지 전송이 가능하다.


HTTP와 WebSocket을 구분하는 기준

HTTP와 WebSocket은 모두 웹에서 사용되는 통신 방식이지만, 동작 목적이 다르다.
HTTP는 요청과 응답 중심이다.
클라이언트가 먼저 request를 보내면 서버가 response를 보낸다.


반면 WebSocket은 연결 유지 중심이다.
한 번 연결된 뒤에는 클라이언트와 서버가 서로 필요한 순간에 데이터를 보낼 수 있다.
그래서 서버가 클라이언트에게 먼저 메시지를 보내는 것도 가능하다.


초보자 기준에서는 이렇게 구분하면 된다.

  • HTTP는 필요할 때마다 요청하고 응답받는 방식이다.
  • WebSocket은 연결을 열어 둔 상태에서 계속 대화하는 방식이다.
  • HTTP는 문서나 화면을 요청해서 받아오는 데 적합하다.
  • WebSocket은 채팅이나 알림처럼 즉시 반응해야 하는 기능에 적합하다.

즉, HTTP는 요청-응답에 강하고, WebSocket은 실시간 양방향 통신에 강하다.


클라이언트 구현 핵심

브라우저 쪽에서는 JavaScript와 HTML5 API를 사용해 WebSocket 클라이언트를 구현한다.
클라이언트는 서버에 연결하고, 메시지를 보내고, 서버에서 온 메시지를 받아 화면에 출력한다.


클라이언트 구현의 핵심은 다음과 같다.

  • new WebSocket()으로 서버에 연결한다.
  • 일반 연결은 ws를 사용한다.
  • 보안 연결은 wss를 사용한다.
  • 서버로 데이터를 보낼 때는 send()를 사용한다.
  • 서버에서 데이터를 받을 때는 onmessage를 사용한다.
  • 연결 성공은 onopen에서 처리한다.
  • 연결 종료는 onclose에서 처리한다.
  • 오류 발생은 onerror에서 처리한다.

채팅 클라이언트 예제에서는 사용자가 입력한 이름, 메시지 내용, 시간을 객체에 담는다.
그 객체를 JSON.stringify()로 문자열로 바꾼 뒤 send()로 서버에 보낸다.
서버에서 메시지가 다시 오면 JSON.parse()로 객체로 바꾸고 화면에 출력한다.


서버 구현 핵심

Spring Boot에서 WebSocket 서버를 구현하려면 먼저 spring-boot-starter-websocket 의존성을 추가해야 한다.
그 다음 @ServerEndpoint가 붙은 클래스를 만들고, 클라이언트가 접속할 매핑 주소를 지정한다.


서버 구현의 핵심은 다음과 같다.

  • @ServerEndpoint(value="/chat")은 /chat 주소의 웹 소켓 연결을 처리한다.
  • ServerEndpointExporter는 @ServerEndpoint 클래스를 웹 소켓 엔드포인트로 등록한다.
  • @OnOpen은 클라이언트가 접속했을 때 실행된다.
  • @OnMessage는 클라이언트가 메시지를 보냈을 때 실행된다.
  • @OnClose는 클라이언트 연결이 종료되었을 때 실행된다.
  • Session은 클라이언트와 서버 사이의 연결 정보를 나타낸다.

채팅 서버 예제에서는 접속한 클라이언트의 Session을 Set에 저장한다.
그리고 한 클라이언트가 메시지를 보내면 서버는 저장된 모든 Session에게 같은 메시지를 다시 보낸다.
이 구조를 브로드캐스트라고 한다.


채팅 예제의 전체 데이터 흐름

채팅 예제는 클라이언트와 서버가 서로 따로 움직이는 코드가 아니다.
클라이언트 코드와 서버 코드가 /chat 연결을 기준으로 이어진다.


전체 흐름은 다음과 같다.

  • 사용자가 채팅 참여 버튼을 누른다.
  • 클라이언트가 ws://localhost:8088/chat으로 연결을 요청한다.
  • 서버의 @ServerEndpoint(value="/chat")가 연결을 받는다.
  • 서버의 @OnOpen이 실행되고 Session이 저장된다.
  • 사용자가 메시지를 입력하고 전송한다.
  • 클라이언트가 JSON.stringify()로 메시지를 문자열로 바꾼다.
  • 클라이언트가 ws.send()로 서버에 메시지를 보낸다.
  • 서버의 @OnMessage가 메시지를 받는다.
  • 서버가 모든 Session에게 sendText()로 메시지를 다시 보낸다.
  • 클라이언트의 onmessage가 실행된다.
  • 클라이언트가 JSON.parse()로 받은 문자열을 객체로 바꾼다.
  • 화면의 채팅 영역에 메시지가 출력된다.

채팅 예제의 핵심은 클라이언트가 메시지를 보내고, 서버가 그 메시지를 모든 접속자에게 다시 보내고, 각 클라이언트가 받은 메시지를 화면에 출력하는 흐름이다.


개발자 도구에서 확인할 핵심

WebSocket 실습에서는 코드만 보는 것보다 브라우저 개발자 도구에서 연결 상태를 확인하는 것이 중요하다.
채팅 화면이 보이더라도 실제 WebSocket 연결이 성공했는지는 Network 탭에서 확인해야 한다.


확인할 핵심은 다음과 같다.

  • Network 탭을 먼저 연다.
  • 채팅 참여 버튼을 누른다.
  • 요청 목록에서 chat 요청을 찾는다.
  • Status Code가 101 Switching Protocols인지 확인한다.
  • Type이 websocket인지 확인한다.
  • Request URL이 ws://localhost:8088/chat인지 확인한다.
  • Connection: upgrade를 확인한다.
  • Upgrade: websocket을 확인한다.

101 Switching Protocols는 HTTP 연결이 WebSocket 통신으로 전환되었다는 뜻이다.
따라서 실습에서 연결 성공을 판단할 때 가장 중요한 값은 101이다.


최종 정리

WebSocket은 실시간 기능을 만들기 위한 통신 방식이다.
처음에는 HTTP 요청으로 연결을 시작하지만, 연결이 성공하면 같은 연결을 유지하면서 클라이언트와 서버가 양방향으로 데이터를 주고받는다.


마지막으로 핵심만 다시 정리하면 다음과 같다.

  • WebSocket은 HTML5 표준 기술이다.
  • WebSocket은 HTTP 환경에서 시작된다.
  • WebSocket은 하나의 TCP 연결을 유지한다.
  • WebSocket은 전 이중 통신을 지원한다.
  • Handshake는 연결을 시작하기 위한 확인 절차다.
  • 101 Switching Protocols는 WebSocket 전환 성공을 의미한다.
  • 클라이언트는 new WebSocket(), send(), onmessage를 중심으로 동작한다.
  • 서버는 @ServerEndpoint, @OnOpen, @OnMessage, @OnClose, Session을 중심으로 동작한다.
  • 채팅 서버는 접속한 Session을 관리하고, 받은 메시지를 모든 클라이언트에게 다시 보낸다.

결국 WebSocket은 실시간 채팅처럼 서버와 클라이언트가 계속 연결되어 있어야 하는 기능을 만들기 위한 핵심 통신 방식이다.

0개의 댓글