개발 단계에서 코드를 변경하고 저장하면 새로고침이 없이도 변경사항이 반영되는 경험은 이제 어쩌면 당연한 개발환경이 된 것 같다. 처음 개발을 시작했을 때 ‘npm run dev
를 하면 변경사항이 자동반영 된다’라는 걸 알고도 그런가보다 하고 넘어간 뒤로는 이에 대해 특별히 고찰해보지 않았다. 심지어는 웹팩으로 만들어진 디자인 시스템을 vite로 마이그레이션 하고 HMR 시간을 단축했던 경험이 있는데, 그때에도 정확히 어떤 원리로 번들러를 바꿈으로써 HMR 시간을 단축할 수 있었는지 이해하려하지 않고, 그 결과물에만 심취해있었던 것 같다.
그러다 최근 nextJS로 만들어진 웹앱을 개발하는 도중 콘솔을 보다가 웹소켓에서 [HMR] connected
라는 로그를 남긴 것이 눈에 들어왔다. 웹소켓은 실시간으로 LLM 응답을 가져올 때 사용하는 걸 본 적이 있어서 그 존재는 알고 있었지만 웹소켓이 어떤 원리로 작동되는지, 또 개발 서버에서, 그러니까 HMR과 어떤 관련이 있는지는 알지 못했다.
이번 글에서는 웹소켓이 탄생한 배경과 개념, 웹소켓과 HMR의 관계, 나아가 번들러별로 HMR 성능 차이가 왜 발생하는지, 그 trade off는 어떤 것들이 있는지 알아보려고 한다.
1. HTTP의 한계와 웹소켓의 탄생
1) HTTP의 한계
웹소켓이 만들어진 배경은 근본적으로 HTTP의 한계에서 시작된다. HTTP의 구조에서는 브라우저(클라이언트)가 서버에게 요청을 보내야만 서버는 그 요청에 대해 (한 번만)응답을 할 수 있다. 응답이 끝나면 연결은 종료되거나, 유지되더라도 서버는 요청 없이 응답을 보낼 수 없게 되어있다.
HTTP가 만들어진 당시만해도 실시간성이 보장되어야할 이유는 크게 없었기 때문에 (그리고 다른 여러가지 이유로..) 이렇게 단순한 모델로 설계되었지만, 이후 웹이 발전하면서 점차 실시간통신, 양방향 통신이 가능한 모델이 필요해졌다.
이를 위해 AJAX, Comet, SSE 등 다양한 시도가 있었지만 이는 모두 위와 같은 HTTP의 한계를 근본적으로 해결한다기보다 HTTP의 틀 안에서 실시간 통신을 하는 것처럼 보이도록 우회한 방식들이었다. 그러다보니 이러한 방식들은 통신지연이 발생하거나, 매 요청/응답마다 HTTP 헤더를 붙여 오버헤드가 발생하거나, 연결을 반복 또는 유지하면서 네트워크 리소스가 낭비되는 등의 부작용이 따랐다.
2) 웹소켓의 등장
웹이 발전함에 따라 실시간성에 대한 요구는 커져만갔고, 위에서 살펴본 것과 같이 기존 방식들은 한계점이 명확하니, 근본적인 해결책이 필요해졌다. 그래서 등장한 것이 바로 웹소켓이다. 웹소켓은 아래에서 더 구체적으로 살펴보겠지만 HTTP 위에서 동작하면서도 양방향 통신과 지속 연결을 가능하게 하는 새로운 실시간 통신 프로토콜로서 2011년 RFC 6455로 표준화 되었다. 비로소 HTTP의 “요청이 없으면 서버는 응답하지 않는 구조”를 “프로토콜 업그레이드”를 통해 근본적으로 개편한 첫 번째 표준 기술이 탄생하게 된 것이다.
2. 웹소켓의 작동방식
그렇다면 웹소켓은 정확히 어떻게 작동하며, 이것이 기존에 있었던 양방향 통신을 가능하게 하기 위한 시도들과 어떻게 다른지 살펴보자.
1) 웹소켓의 연결방식
웹소켓에 연결하기 위해서 브라우저는 헤더에 Upgrade: websocket
을 포함하여 HTTP 요청을 보낸다.
초기 요청 예시 (클라이언트 → 서버):
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
그런데 좀 이상하다. 분명 웹소켓은 HTTP의 한계를 극복하는 새로운 프로토콜인데 왜 HTTP로 시작하여 굳이 upgrade를 통해 연결을 해야할까? 이는 웹소켓이 웹 브라우저 안에서 동작하기 때문이다. 대부분의 브라우저, 프록시, 방화벽, CDN 등은 HTTP를 기준으로 설계되어있기 때문에 만약 웹소켓이 처음부터 완전히 별개의 포트를 사용하거나 HTTP를 통하지 않으면, 방화벽에 막히거나, CDN이 요청을 무시하거나, 브라우저가 보안 정책상 차단당할 가능성이 크다. 따라서 웹소켓은 HTTP로부터 독립적인 SMTP나 FTP와 달리 HTTP로 시작하여 웹소켓 프로토콜로 upgrade하도록 설계되어있는 것이다.
아무튼 그렇게 요청을 보내 서버가 101 Switching Protocols
로 응답을 해오면, 그 순간부터 HTTP는 끝나고, 웹소켓 전용 프로토콜로 업그레이드 된다.
서버 응답:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

HMR을 위해 웹소켓이 요청과 응답을 하고 있다는 것을 network 탭에서 확인할 수 있다.
2) 지속연결
웹소켓으로 연결이 완료되면, TCP 소켓은 끊어지지 않고 계속 열린 상태로 유지된다. 이제 HTTP 처럼 매번 요청/응답을 새로 맺지 않아도 되는 것이다. 따라서 최소한의 오버헤드로 저지연 메시지를 전송할 수 있게 되며, HTTP와 비교했을 때 고성능의 실시간 통신을 구현할 수 있게 된다.
3) 웹소켓의 메시지 교환
웹소켓의 가장 큰 특징 중 하나는 서버와 클라이언트가 모두 자유롭게 메시지를 주고받을 수 있는 양방향 통신이라는 점이다. 즉, 서버가 먼저 클라이언트에게 알릴 수도 있고, 클라이언트가 먼저 상태를 보내는 것도 가능하다. 이는 HTTP와 대조되는데, HTTP는 기본적으로 단방향 모델에 기반해있기 때문이다. (정확히 말하면 HTTP도 물론 TCP 위에 있기 때문에 양방향 통신이 가능하지만, 프로토콜 설계 자체가 클라이언트의 요청이 있을 때 서버가 응답하는 단방향 구조로 제한되어 있다.)
메시지를 교환하는 방식에서도 이 둘은 분명한 차이를 보인다. HTTP는 본래 문서 전송을 위해 설계된 프로토콜이기 때문에 작은 데이터를 요청하려 해도 꽤 긴 길이의 헤더를 붙여야한다. 이에 비해 웹소켓은 훨씬 작은 단위인 프레임으로 메시지를 교환하기 때문에 불필요한 오버헤드가 발생하지 않게 된다. 웹소켓의 프레임은 2-14바이트 수준으로 상대적으로 빠르고 효율적으로 통신이 가능해진다.
4) 웹소켓의 제어프레임

HMR을 위해 웹소켓이 연결되면 네트워크 탭에서 주기적으로 ping을 보내는 것을 확인할 수 있다.
웹소켓은 위에서 서술하였듯 연결을 지속한다는 특징이 있어, 이를 위해 추가로 “제어 프레임”을 함께 정의한다. 제어프레임은 연결을 지속하고 관리하기 위함인데, 대표적으로 ping/pong
은 주기적으로 주고받으며 연결이 잘 되어있는지 확인하여 연결이 끊기지 않게 하기 위한 용도이고, close
는 클라이언트와 서버가 서로 주고 받아 연결을 안전하게 종료하는 용도이다.
이렇게 WebSocket은 양방향 통신과 지속 연결이 가능하다는 점을 바탕으로 실시간 채팅, 주식 현황판 등 실시간성이 중요한 서비스에 필수적인 기술로 자리매김하였다. 더 나아가 이 기술은 개발 경험(DX)에도 큰 영향을 주었는데, 바로 HMR에서 서버와 브라우저 간 변경 사항을 실시간으로 전달하는 채널로 활용되면서, 개발자가 “저장 → 즉시 반영”이라는 자연스러운 개발 환경을 누릴 수 있게 된 것이다.
3. HMR과 웹소켓
1) HMR이란 무엇인가?
HMR(Hot Module Replacement)은 코드를 저장했을 때 전체 새로고침 없이도 브라우저에 변경 사항을 반영하는 기능이다. 단순히 페이지를 reload하는 LiveReload와 달리, HMR은 변경된 모듈만 교체하기 때문에 상태를 유지한 채로 UI만 업데이트할 수 있다. 이렇듯 HMR의 핵심목표는 “개발 서버가 코드 변경을 감지 → 브라우저는 즉시 반영”라고 볼 수 있는데, 그 목적을 지연과 오버헤드 없이 달성할 수 있는 최적의 방법은 웹소켓을 활용하는 것이다.

nextJS로 작성된 웹앱에서 개발서버를 동작시키면 웹소켓을 통해 HMR이 실행되는 것을 알 수 있다.
웹소켓을 활용하는 HMR는 아래와 같은 순서로 동작하며 즉시성을 보장하게 된다.
- 브라우저는 개발 서버와 웹소켓 연결을 맺고
- 서버가 파일 변경을 감지하면 웹소켓으로 클라이언트에 알림을 푸시한다.
- 클라이언트는 그 이벤트를 받아 해당 모듈만 교체한다.
2) 의존성 그래프
HMR이 동작하려면 어떤 모듈이 바뀌었는지를 정확히 알아야하는데, 예를 들어 Button.tsx
를 수정했다고 한다면, Button.tsx
만 바꾸면 되는지, 상위 모듈인 App.tsx
도 다시 실행해야 하는지를 알아야 한다. 이를 알아내기 위해 필요한 것이 모듈 간의 관계도, 즉 “의존성 그래프”이다. 의존성 그래프는 번들러가 모듈을 파싱하면서 자동으로 만들어 관리한다. 어떤 방식으로 그래프를 구축/활용하는지는 번들러마다 조금씩 다르다. (Webpack은 빌드 시 전역 그래프를, Vite는 브라우저의 ESM import 체인을 활용하는 식이다.) 이 글에서는 깊은 내부 구현까지는 다루지 않고, 큰 흐름만 살펴보겠다.
3) 번들러별 차이점
요즘 쓰이는 거의 모든 번들러는 HMR을 구현하기 위해 공통적으로 웹소켓을 사용한다. 다만 모듈을 교체하는 방식에서 차이가 있다.
Webpack
Webpack은 자체적으로 모듈 시스템을 만들어 애플리케이션을 관리한다. 우리가 작성한 import
/ export
문법은 실제 번들링 과정에서 __webpack_require__
, __webpack_modules__
같은 전역 객체와 런타임 코드로 변환된다. 즉, 브라우저는 진짜 ESM을 실행하는 게 아니라 Webpack이 만들어놓은 작은 가상 모듈 시스템을 돌리고 있는 셈이다.
// __webpack_require__ 정의 (간략화 버전)
var __webpack_modules__ = {
/* 모듈 id → 함수 */
};
var __webpack_module_cache__ = {};
function __webpack_require__(id) {
if (__webpack_module_cache__[id]) {
return __webpack_module_cache__[id].exports;
}
var module = (__webpack_module_cache__[id] = { exports: {} });
__webpack_modules__[id](module, module.exports, __webpack_require__);
return module.exports;
}
HMR이 동작할 때는 이 Webpack이 삽입한 런타임 코드가 중요한 역할을 한다. 서버가 특정 모듈이 변경되었다고 알리면, Webpack은 해당 모듈의 새 팩토리 함수를 __webpack_modules__
에 덮어씌우고, 이전 실행 결과가 담긴 캐시를 지운 뒤 다시 실행한다. 이렇게 하면 전체를 다시 불러올 필요 없이 필요한 모듈만 교체할 수 있다.
다만 이런 방식은 모든 모듈이 하나의 큰 번들 안에 묶여 관리되기 때문에, 프로젝트가 커질수록 HMR 반영 속도가 점점 느려진다는 단점이 있다. 대신 안정성과 풍부한 플러그인 생태계 덕분에 여전히 대규모 프로젝트에서는 많이 사용된다.
Vite
Vite는 전혀 다른 접근을 취한다. 이는 Vite가 개발된 배경을 살펴보면 더 명확해지는데, Vite가 내부에서 활용하는 esbuild는 초고속 빌드/변환을 위한 도구이지만, 단독으로는 개발 서버나 HMR을 제공하지 않는다. 그래서 Vite는 개발 서버와 HMR을 직접 구현하고, 트랜스파일(예: TS/JSX)과 의존성 프리번들링에는 esbuild를 써서 속도를 끌어올린다.
개발 모드에서 Vite는 최신 브라우저의 네이티브 ESM을 전제로 동작한다. 브라우저가 App.tsx
를 요청하면 서버는 그 파일을 esbuild로 빠르게 변환(트랜스파일)해 돌려주고, 파일 안의 import './Button.tsx'
같은 의존성은 브라우저가 직접 import 체인을 따라가며 개별적으로 요청한다. 즉, 브라우저의 ESM 로딩을 최대한 활용하는 구조다.
이 덕분에 HMR도 단순하다. 어떤 모듈이 바뀌면 서버는 그 파일만 새로 트랜스파일해주고, 브라우저는 그 모듈만 다시 import해서 교체한다. 전체 번들을 다시 만들 필요가 없으니 반영 속도는 거의 즉각적이고, 개발자는 저장 → 즉시 반영이라는 쾌적한 DX를 누릴 수 있다.
다만 최신 브라우저의 ESM을 전제로 설계되었기 때문에, 모든 환경을 Webpack만큼 광범위하게 커버하기는 어렵다는 점은 주의해야 한다.
4. 결론
지금까지 살펴본 것처럼, 웹이 발전하면서 실시간성이 중요해지자 2011년 새로운 프로토콜로 자리 잡은 WebSocket은 단순히 채팅이나 주식 현황판 같은 서비스뿐만 아니라, 개발자의 DX까지 바꿔놓은 핵심 기술이 되었다. HMR은 WebSocket 덕분에 서버가 코드 변경을 감지하자마자 브라우저에 알릴 수 있고, 브라우저는 바뀐 모듈만 갈아끼우면서도 상태를 유지할 수 있다. 오늘날 대부분의 번들러가 HMR을 지원하지만, 구현 방식에는 차이가 있으므로 프로젝트의 특성에 따라 적절하게 선택해야할 것이다.
어찌 보면 당연하게 사용해왔던 HMR이 어떻게 가능했는지 웹소켓이 탄생한 배경부터, 작동원리, 그리고 번들러별로 HMR이 구현되어있는 차이점까지 살펴보면서 더 깊이 있게 이해할 수 있었다. 왠지 앞으로는 변동사항이 즉각적으로 반영되는 앱을 보며 감사한 마음이 들 것 같다…