앞선 글에서 **SSE(Server-Sent Events)**의 개념과 왜 이 기술이 실시간 웹 서비스에서 매력적인지 이론적인 부분을 짚어보았습니다. 하지만 백문이 불여일견이죠. 개발자에게 가장 확실한 이해는 역시 '코드를 직접 짜보는 것'입니다.
오늘은 가장 대중적인 백엔드 환경인 Node.js(Express)를 활용하여, 서버에서 클라이언트로 5초마다 실시간 메시지를 던져주는 아주 심플하지만 강력한 알림 시스템 예제를 함께 만들어보겠습니다. 제가 실제 프로젝트에서 실시간 데이터 전송 기능을 구현하며 겪었던 소소한 팁들도 함께 녹여냈으니, 천천히 따라와 보시기 바랍니다.
1. 프로젝트 환경 설정: 가벼운 시작
복잡한 설정은 필요 없습니다. Node.js가 설치되어 있다면 바로 시작할 수 있습니다. 저는 개인적으로 새로운 기술을 테스트할 때 가장 깔끔한 기본 폴더 구조를 선호합니다.
먼저, 프로젝트 폴더를 만들고 필요한 라이브러리를 설치합니다.
mkdir sse-demo
cd sse-demo
npm init -y
npm install express
Express는 HTTP 서버를 가장 직관적으로 구축할 수 있게 도와줍니다. 서버 사이드 이벤트 역시 표준 HTTP 위에서 동작하므로, 별도의 복잡한 프로토콜 라이브러리 없이 Express만으로 충분합니다.
2. 서버 사이드 구현: 핵심은 '헤더'와 '연결 유지'
이제 server.js 파일을 만들고 아래 코드를 작성해 보겠습니다. SSE의 핵심은 응답을 한 번에 끝내지 않고, 연결을 계속 열어두는(Keep-Alive) 데 있습니다.
const express = require('express');
const app = express();
const PORT = 3000;
// 정적 파일 제공 (클라이언트 HTML을 보여주기 위함)
app.use(express.static('public'));
app.get('/events', (req, res) => {
// 1. SSE를 위한 필수 헤더 설정
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
console.log('클라이언트가 연결되었습니다.');
// 2. 일정 간격으로 데이터 전송 (예: 5초마다)
const sendEvent = () => {
const data = {
message: "새로운 알림이 도착했습니다!",
timestamp: new Date().toISOString()
};
// SSE 포맷 준수: 'data: 내용\n\n'
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
const intervalId = setInterval(sendEvent, 5000);
// 3. 클라이언트가 연결을 종료했을 때 처리
req.on('close', () => {
console.log('클라이언트 연결이 종료되었습니다.');
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => {
console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다.`);
});
여기서 주목할 포인트!
제가 실무에서 가장 많이 실수했던 부분은 바로 \n\n(개행 문자 두 개)입니다. SSE 명세상 하나의 이벤트는 반드시 두 개의 개행 문자로 끝나야 클라이언트가 "아, 이제 데이터 한 덩어리가 다 왔구나"라고 인식합니다. 이 부분을 놓치면 클라이언트에서 데이터를 아예 받지 못하는 현상이 발생하니 꼭 주의하세요.
또한, 실시간 데이터 전송을 유지하기 위해 Cache-Control: no-cache를 설정하는 것도 잊지 마세요. 브라우저가 응답을 캐싱해버리면 실시간성이 깨질 수 있기 때문입니다.
3. 클라이언트 구현: EventSource의 편리함
서버가 준비되었다면, 이제 이 이벤트를 받아낼 클라이언트 화면을 만들어야겠죠? public/index.html 파일을 생성합니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>SSE 실시간 알림 테스트</title>
<style>
#notifications {
border: 1px solid #ddd;
padding: 10px;
height: 300px;
overflow-y: scroll;
background: #f9f9f9;
}
.log { margin-bottom: 5px; border-bottom: 1px dashed #ccc; padding: 5px; }
</style>
</head>
<body>
<h1>실시간 서버 알림 받아보기</h1>
<div id="status">연결 상태: 대기 중...</div>
<div id="notifications"></div>
<script>
const notificationDiv = document.getElementById('notifications');
const statusDiv = document.getElementById('status');
// 1. SSE 연결 시작
const eventSource = new EventSource('/events');
// 연결이 열렸을 때
eventSource.onopen = () => {
statusDiv.innerText = "연결 상태: 서버와 연결됨 ✅";
statusDiv.style.color = "green";
};
// 메시지를 받았을 때
eventSource.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
const newLog = document.createElement('div');
newLog.className = 'log';
newLog.innerHTML = `<strong>[${parsedData.timestamp}]</strong> ${parsedData.message}`;
notificationDiv.prepend(newLog); // 최신 메시지를 위로
};
// 에러 발생 시 (연결 끊김 등)
eventSource.onerror = (err) => {
console.error("SSE 에러 발생:", err);
statusDiv.innerText = "연결 상태: 연결 끊김/에러 ❌";
statusDiv.style.color = "red";
};
</script>
</body>
</html>
브라우저에서 제공하는 EventSource API는 정말 놀랍도록 심플합니다. 웹소켓처럼 복잡한 핸드쉐이크 과정을 직접 관리할 필요가 없죠. 특히 앞서 언급했듯, 서버가 잠시 꺼졌다가 다시 켜지면 브라우저가 알아서 재연결을 시도한다는 점이 실제 운영 환경에서 매우 든든한 요소가 됩니다.
4. 실무 경험에서 우러나온 심화 팁
단순한 예제를 넘어, 실제 상용 서비스에 SSE를 도입할 때 고려해야 할 핵심적인 경험치들을 공유해 드리고 싶습니다.
1) CORS 설정의 함정
만약 프론트엔드와 백엔드 서버의 도메인이 다르다면(예: API 서버는 https://www.google.com/search?q=api.example.com, 웹은 www.example.com), CORS 설정이 필요합니다. 이때 단순히 허용만 하는 게 아니라, credentials: true 설정이 필요한 경우(쿠키 기반 인증 시) 세심한 세팅이 요구됩니다.
2) Nginx 프록시 설정 (가장 중요!)
실제 배포 환경에서는 보통 서버 앞에 Nginx를 둡니다. Nginx의 기본 설정은 응답을 버퍼링(Buffering)하도록 되어 있습니다. 즉, 서버가 res.write로 데이터를 보내도 Nginx가 "데이터가 좀 더 모이면 한꺼번에 보내야지" 하고 붙잡아둘 수 있다는 뜻입니다. 이렇게 되면 실시간 데이터 전송이라는 말이 무색하게 알림이 뭉쳐서 오게 됩니다.
- 해결책: Nginx 설정 파일에
proxy_buffering off;와proxy_cache off;를 추가하여 스트리밍 데이터가 즉시 전달되도록 설정해야 합니다.
3) 사용자별 맞춤 알림 처리
위 예제는 모든 연결된 사용자에게 동일한 메시지를 보내지만, 실제로는 특정 사용자에게만 알림을 보내야 하는 경우가 대부분입니다. 이럴 때는 서버 메모리에 userId와 res 객체를 매핑하여 관리하거나, Redis의 Pub/Sub 기능을 연동하여 분산 환경에서도 특정 유저에게 메시지가 정확히 전달되도록 설계해야 합니다.
5. 직접 실행하고 테스트하기
이제 터미널에서 node server.js를 실행하고 브라우저로 http://localhost:3000에 접속해 보세요. 5초마다 화면에 새로운 로그가 쌓이는 것을 볼 수 있습니다.
테스트 도중 서버를 강제로 종료해 보세요. 클라이언트 화면의 상태가 '에러'로 변했다가, 서버를 다시 띄우는 순간 별도의 새로고침 없이도 다시 '연결됨' 상태로 돌아오며 데이터를 받기 시작할 것입니다. 이 서버 사이드 이벤트의 자동 재연결 기능이야말로 제가 SSE를 사랑하는 가장 큰 이유 중 하나입니다.
마치며: 완벽한 기술보다는 적절한 기술을
Node.js를 활용해 SSE를 구현하는 과정, 생각보다 어렵지 않으셨죠?
프로그래밍 세계에는 수많은 화려한 기술이 존재합니다. 하지만 제가 현업에서 뼈저리게 느낀 것은, '가장 최신 기술'이나 '가장 복잡한 기술'이 항상 정답은 아니라는 사실입니다. 서비스의 성격이 서버에서 정보를 일방적으로 전달하는 구조라면, 굳이 웹소켓의 복잡함을 감수할 필요가 없습니다.
SSE는 단순함 속에 강력한 안정성을 품고 있는 기술입니다. 이번 실습을 통해 여러분의 프로젝트에 실시간이라는 날개를 달아줄 때, SSE라는 선택지를 자신 있게 꺼내 드실 수 있기를 바랍니다.
개발 과정에서 코드가 의도대로 동작하지 않거나, 특정 프레임워크와의 연동에서 막히는 부분이 있다면 언제든 편하게 질문 남겨주세요. 제가 겪었던 수많은 시행착오가 여러분의 시간을 아껴줄 수 있다면 좋겠습니다.
작성 후기 및 회고
단순한 이론 설명을 넘어 실제 구동 가능한 코드를 중심으로 글을 구성해 보았습니다. 특히 실무자 입장에서 가장 당황하기 쉬운 Nginx 설정이나 개행 문자 규칙 등을 강조하여, 글을 읽는 분들이 실제 배포 단계까지 고려할 수 있도록 신경 썼습니다. SSE의 '단순미'가 독자분들에게 잘 전달되었길 바랍니다.