👩🏻‍💻 develop

[Next.js] SSE 브라우저 연결 오류 해결하기 (Nginx)

dev.daisy 2025. 9. 2. 14:02
이번 트러블슈팅을 통해 단순히 코드 레벨에서의 문제 해결에 그치지 않고, 전체 시스템 흐름과 인프라 환경까지 통합적으로 이해하는 시야가 필요함을 체감했습니다. 실제로 초기에는 FE와 BE 사이의 요청 · 응답 구조나 토큰 인증 방식에 문제가 있을 것이라 판단해 양측 코드를 점검했지만, 최종 원인은 클라우드 인프라(Nginx) 설정의 누락이었습니다.

이 경험을 통해 클라이언트 개발자의 시야를 넘어 전체 시스템 레이어에 대한 이해와 협업 능력이 얼마나 중요한지 절실히 느꼈고, 앞으로도 비슷한 문제가 생길 때 코드 → 브라우저 → 네트워크 → 인프라 전 과정을 유기적으로 살펴보는 습관을 갖고자 합니다 :)

 

진행 중인 프로젝트 TUNING은 사용자 간 매칭 결과를 뉴스 속보 형태로 전달하는 조직 기반 소셜 매칭 서비스입니다. 실시간 매칭 알림을 구현하기 위해 SSE(Server-Sent Events) 기술을 사용했으나, 연결 직후 즉시 종료되는 문제가 발생해 트러블슈팅을 진행했습니다.

 

이 글에서는 오류가 발생한 원인, 이를 해결하기 위해 어떤 방식으로 구조를 수정했는지, 다른 파트와 어떤 방식으로 협업했는지 정리하고자 합니다.


💥 SSE 연결 오류가 발생한 상황은?

문제를 처음 인식했을 당시의 상황은 아래와 같은데,

  • [FE] EventSourcePolyfill 사용 - Authorization 헤더 포함해서 백엔드로 GET 요청 보냄
  • [BE] SSE 연결이 성공 직후 종료, 로그상으로는 이벤트 스트림이 유지되지 않음

Signoz 로그를 확인해보니, Content-Type: text/event-stream이 응답 헤더에 포함되지 않으며 브라우저가 SSE 연결로 인식하지 못하고 끊어지는 현상이 확인되었습니다.

발생한 에러 로그

SSE connection error: ErrorEvent{type: 'error', target: EventSourcePolyfill, error: Error: No activity within 45000 milliseconds. No response received. Reconnecting.
    at onTimeout …}error: Error: No activity within 45000 milliseconds. No response received. Reconnecting.
    at onTimeout (https://local.hertz-tuning.com:3000/_next/static/chunks/node_modules__pnpm_49ca5954._.js:1552:30)
    at https://local.hertz-tuning.com:3000/_next/static/chunks/node_modules__pnpm_49ca5954._.js:1569:17target: EventSourcePolyfill{_listeners: {…}, onopen: undefined, onmessage: undefined, url: 'https://dev.hertz-tuning.com/api/sse/subscribe', onerror: ƒ,…}type: "error"[[Prototype]]: Event
overrideMethod @ hook.js:608
error @ intercept-console-error.ts:40

 

→ SSE 연결이 시도되었으나 서버로부터 올바른 응답을 받지 못해 타임아웃 → 재연결 → 재시도를 무한 반복하고 있었으며, Swagger / Postman에서는 정상 동작했지만 브라우저에서만 실패하는 상황입니다.


💡 원인 분석 및 해결 과정

처음엔 백엔드에서 text/event-stream 헤더를 누락했거나 SSE 스트림을 올바르게 유지하지 못하는 것으로 의심했습니다. 하지만 백엔드 측에 확인해본 결과, 서버에서는 정상적으로 SSE 응답 헤더를 포함하고 있었고 프론트에서 response headers로 아무 응답도 받지 못하는 상황이였지만 로그상에서도 연결 로직은 수행되고 있었습니다.

프론트엔드 구현 방식

import { EventSourcePolyfill } from 'event-source-polyfill';

const eventSource = new EventSourcePolyfill('/api/sse/subscribe', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
  withCredentials: true,
});

 

당시 프론트엔드 구현 방식은 위와 같은데, 백엔드에서 SSE API 설계 시 Authorization 헤더 기반 인증 구조를 사용중이여서, Bearer token을 넘기도록 설계를 해두었습니다.

 

기본 EventSource는 커스텀 헤더 지원이 불가해 EventSourcePolyfill을 사용중이였고, SSE 연결 직후 종료되는 원인이 EventSourcePolyfill의 내부 구현 혹은 보안 정책 충돌일 가능성이 존재하다고 판단하여 RefreshToken을 쿠키로 받아오는 방법을 제안해 수정 작업을 진행했습니다.


🔁 해결 과정: 헤더 기반 인증 → 쿠키 기반 인증 방식 전환

기존에는 EventSourcePolyfill을 사용해 SSE를 연결했습니다. 이때 인증을 위해 Authorization 헤더에 accessToken을 직접 포함했고 인증 헤더도 수동으로 설정했습니다.

 

하지만 이번에는 기본 EventSource 객체를 사용하고, 인증 방식은 HTTP-only refreshToken 쿠키를 활용하는 방식으로 변경했습니다. 이를 위해 withCredentials: true 옵션을 설정해 브라우저가 자동으로 쿠키를 포함하도록 처리했습니다.

 

이 방식은 다음과 같은 장점이 있습니다.

  • 인증 정보가 요청마다 자동으로 포함되어 코드가 간결해졌다.
  • refreshToken은 HTTP-only 속성으로 보안성이 높아졌다.
  • 수동 헤더 설정 없이도 인증 흐름이 안정적으로 유지되었다.

또한 EventSource 객체는 기본적으로 사용자 정의 헤더 설정을 지원하지 않기 때문에, 공식적으로도 쿠키 기반 인증을 사용하는 방식이 권장됩니다.

 

MDN EventSource - credentials 사용 예시

 

EventSource: EventSource() constructor

const evtSource = new EventSource("sse.php"); const eventList = document.querySelector("ul"); evtSource.onmessage = (e) => { const newElement = document.createElement("li"); newElement.textContent = `message: ${e.data}`; eventList.appendChild(newElement);

developer.mozilla.org

 

이처럼 인증 방식을 헤더에서 쿠키 기반으로 전환함으로써, SSE 연결이 더 안정적이고 안전하게 동작하도록 개선했습니다. 코드 수정 후 프론트 캐시 초기화 후 테스트 해봤을 때 Response Headers에서 응답을 잘 받아오고 있는 것을 확인했으며, Refresh Token 또한 잘 담겨서 요청을 보내고 있었다. 기존 Pending → 401이 뜨는 것으로 확인했습니다.


⚙️ 해결 방안: Nginx 설정 수정하기

하지만 이후에도 브라우저 연결 실패는 지속되었고, Nginx 설정의 문제라고 판단했습니다.

  • 서버 응답은 정상인데 브라우저에서만 연결 직후 끊기는 문제 지속됨
  • Postman에서는 정상 동작 → 서버 자체 문제는 아니라고 판단함
  • Nginx proxy 설정 누락으로 인한 스트림 인식 실패 확인

요청드린 Nginx 설정 항목

따라서 클라우드분께 아래와 같이 설정이 잘 되어 있는지 확인해달라고 요청드렸습니다.

location /api/sse/ {
    proxy_pass http://backend-server;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_buffering off;
    chunked_transfer_encoding off;
    proxy_cache off;
    keepalive_requests 1000;
}
특히 proxy_http_version 1.1과 proxy_buffering off 옵션이 없으면 브라우저는 SSE를 일반 HTTP 요청으로 오해하고 버퍼링해버리므로 실시간 스트림이 끊기는 문제가 발생한다고 합니다.

 

기존 Nginx 서버 세팅

 

→ 이렇게만 세팅되어 있었는데, sse 경로를 추가한 후 제대로 연결 되는 것으로 확인했습니다.

 

SSE 제대로 동작하는 화면


💡 내가 배운 것

이 경험을 통해 깊이 있게 배우고 체감할 수 있었던 점은 다음과 같습니다.

- SSE는 네트워크 전 구간에서 모든 설정이 맞아야만 정상적으로 작동합니다.
단순한 HTTP API 요청과 달리, 브라우저 ↔ 백엔드 ↔ 프록시(Nginx) ↔ 인증 흐름 등 전체 체인을 아우르는 시야가 필요합니다.

- 브라우저는 SSE 스트림에 매우 민감하게 반응합니다.
특히 proxy_buffering, Connection, http_version 등의 Nginx 설정이 올바르지 않으면 연결이 유지되지 않고 즉시 종료됩니다.

- Polyfill을 사용하더라도 CORS 및 스트리밍 헤더 충돌로 인해 문제가 발생할 수 있음을 경험했습니다.
브라우저에서의 실질적인 동작은 Swagger나 Postman과는 다르므로, 테스트 도구 결과만을 신뢰해서는 안 됩니다.

- Authorization 헤더 기반 인증보다, 쿠키 기반 인증 방식이 SSE와의 궁합에 있어 더 안정적임을 실감했습니다.
이를 통해 기존 구조를 리팩토링하여 보다 보안적으로 안전하고 브라우저 친화적인 방식으로 개선할 수 있었다.


- 진짜 문제 해결 능력이란 코드만 보는 것이 아니라, 전체 네트워크 흐름과 인프라 계층을 통합적으로 이해하고 접근하는 것이라는 사실을 체득했습니다.

 

깃허브 트러블슈팅 보러가기
튜닝 FE 레포지토리 보러가기