Sam Story

실시간 채팅 기능 구현에 SSE를 사용해본 기록 본문

공부기록

실시간 채팅 기능 구현에 SSE를 사용해본 기록

Sam H 2026. 3. 21. 16:47

회사에서 중고거래 플랫폼 프로젝트를 진행하면서

가장 고민이 되었던 기능중 하나가 바로 채팅 기능이였다.

 

단순히 메시지를 주고받는 기능 자체는 어렵지 않다.

HTTP 기반으로 주기적으로 데이터를 조회하는 방식이나 쪽지 기능처럼 구현하는 방법도 충분히 가능하다.

 

하지만 이 방식은 새 메시지를 확인하기 위해 지속적으로 요청을 보내야 하고

결국 실시간성과 사용자 경험 측면에서 한계가 분명했다.

 

특히 판매자와 구매자 간의 소통이 중요한 중고거래 특성상 이러한 방식은

답답한 사용자 경험으로 이어질 수밖에 없다고 판단했다.

 

그래서 최종적으로는 "실시간 대화 기능" 형태로 구현하기로 방향을 잡았다.

그렇다면 선택지는 자연스럽게 WebSocket이었다.

 

내가 알고 있는 범위 내에서는 WebSocket을 이용하는 것이 가장 일반적인 실시간 채팅 구현 방식이기 때문이다.

하지만 MVP 단계에서 별도의 소켓 서버를 운영해야 한다는 점은
현재 프로젝트 상황에서 리소스적인 부담으로 느껴졌다.

 

그러던 중 SSE(Server-Sent Events)라는 방식을 알게 되었고
내가 만들고 있는 서비스에는 이 방식이 더 적합하다고 판단했다.


1. SSE(Server-Sent Events)란?

그렇다면 SSE는 어떤 방식일까?

SSE(Server-Sent Events)는 이름 그대로

서버가 클라이언트에게 데이터를 지속적으로 보내주는 방식이다.

 

기존 HTTP 통신은 클라이언트가 요청을 보내야만 서버가 응답을 주는 구조였다.

즉 데이터를 받고 싶다면 매번 클라이언트에서 요청을 보내야 했다.

 

하지만 SSE는 한 번 연결을 맺으면

서버가 필요할 때마다 데이터를 계속 흘려보낼 수 있다.

 

좀 더 간단히 설명하면

클라이언트는 연결만 유지한 채 대기하고 있다가

서버에서 새로운 데이터가 발생하면 자동으로 전달받는 구조다.

 

이 구조 덕분에 주기적으로 요청을 보내지 않아도

실시간으로 데이터를 받을 수 있다.

 

특히 채팅처럼 새로운 데이터가 발생했을 때 즉시 전달되어야 하는 경우에

굉장히 효율적인 방식이다.

 

또 하나 중요한 특징은

SSE가 HTTP 기반 위에서 동작한다는 점이다.

WebSocket처럼 별도의 프로토콜을 사용하는 것이 아니라

기존 HTTP 연결을 그대로 활용하기 때문에

구현 난이도가 비교적 낮고 추가적인 서버 구성 없이도 사용할 수 있었다.

 

지금까지의 설명만 보면 단점이 없어 보이지만 SSE에도 분명한 한계가 존재한다.

 

SSE는 서버에서 클라이언트로만 데이터를 보낼 수 있는

서버 → 클라이언트 단방향 통신 방식이기 때문에

사실 실제 양방향 통신이 필요한 경우에는 WebSocket이 더 적합하다.

 

하지만 내가 만들고 있는 채팅 기능의 경우

메시지를 보내는 것 자체는 기존 HTTP 요청으로 처리하고

서버에서 요청을 받아 해당 클라이언트에게 응답만 전달해도 사용자 경험이나

흐름을 크게 해치지 않기 때문에 SSE 구조가 오히려 더 단순하고 효율적인 선택이었다.


2. 예제

간단한 프론트 코드와 서버 코드를 이용한 예제다.

 

Client

 

  <!DOCTYPE html>
  <html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <title>SSE 테스트</title>
  </head>
  <body>
    <h2>SSE 실시간 알림 테스트</h2>

    <input id="input" placeholder="메시지 입력" />
    <button id="sendBtn">보내기</button>

    <hr />

    <h3>수신 메시지</h3>
    <div id="messages"></div>

    <script>
      const messages = document.getElementById('messages');
      const input = document.getElementById('input');
      const sendBtn = document.getElementById('sendBtn');

      const eventSource = new EventSource('http://localhost:3000/sse');

      eventSource.onopen = () => {
        console.log('SSE 연결 성공');
      };

      eventSource.onmessage = (event) => {
        console.log('받은 데이터:', event.data);

        const data = JSON.parse(event.data);
        const p = document.createElement('p');
        p.textContent = `[${data.time}] ${data.message}`;
        messages.prepend(p);
      };

      eventSource.onerror = (error) => {
        console.error('SSE 에러:', error);
      };

      sendBtn.addEventListener('click', async () => {
        const message = input.value.trim();
        if (!message) return;

        try {
          const res = await fetch('http://localhost:3000/send', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ message }),
          });

          const result = await res.json();
          console.log('전송 결과:', result);
          input.value = '';
        } catch (err) {
          console.error('전송 실패:', err);
        }
      });
    </script>
  </body>
  </html>

 

 

Server

const express = require('express');
const app = express();

app.use(express.json());

// 간단 CORS
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');

  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }

  next();
});

let clients = [];

app.get('/sse', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  res.flushHeaders?.();

  clients.push(res);
  console.log('SSE 연결됨. 현재 클라이언트 수:', clients.length);

  res.write(`data: ${JSON.stringify({ message: 'SSE 연결 성공', time: new Date().toLocaleTimeString('ko-KR') })}\n\n`);

  req.on('close', () => {
    clients = clients.filter((client) => client !== res);
    console.log('SSE 연결 종료. 현재 클라이언트 수:', clients.length);
  });
});

app.post('/send', (req, res) => {
  const { message } = req.body;

  console.log('받은 메시지:', message);

  const data = {
    message,
    time: new Date().toLocaleTimeString('ko-KR'),
  };

  clients.forEach((client) => {
    client.write(`data: ${JSON.stringify(data)}\n\n`);
  });

  res.json({ success: true, sentTo: clients.length });
});

app.listen(3000, () => {
  console.log('서버 실행: http://localhost:3000');
});

 

 

실행결과

 

실제로 실행해보면 아래와 같이 동작한다.
메시지를 보내면 서버가 해당 이벤트를 받아 연결된 클라이언트에게 즉시 전달하는 것을 확인할 수 있었다.

실제로 메시지를 보내면 즉시 반영되는 것을 확인할 수 있다.

 

 

처음에는 실시간 기능 = WebSocket이라고만 생각했는데,
SSE를 직접 적용해보면서 꼭 그럴 필요는 없다는 걸 느꼈다.

모든 상황에서 복잡한 구조를 가져가기보다는
현재 서비스에 필요한 수준이 어디까지인지 고민하고
그에 맞는 기술을 선택하는 게 더 중요하다고 생각한다.

이번 구현에서는 SSE가 딱 그 역할을 해줬고
덕분에 비교적 간단하게 원하는 실시간 기능을 구현할 수 있었다.

'공부기록' 카테고리의 다른 글

8pt Grid System  (0) 2026.03.08
Axios  (0) 2025.05.27
2025-05-08 node.js 백엔드 기초  (1) 2025.05.08
2024-12-18 조건문을 좀 더 간결하게 작성  (2) 2024.12.18
2024-11-10 LottieAnimationView  (6) 2024.11.10