์ฐ๋ฆฌ ์๋น์ค๋ ํ๋ก ํธ(next.js - pages router)์ ๋ฐฑ์๋(spring boot)๋ก ๋ ํ๋ก์ ํธ์ด๋ค. ๊ธฐ์ ์ ๋์ ํ๊ธฐ ์ ๋ฐฑ์๋ ๊ฐ๋ฐ์์ SSE ํต์ ์ด ๊ฐ๋ฅํ ์ง ํ์ธ์ ์ํด ํ๋กํ ํ์ ํ ์คํธ๋ฅผ ์ํํ์๋ค.
ํ๋ก ํธ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ์๋ค. EventSource ๊ฐ์ฒด๋ฅผ ํ์ฉํด SSE ์ฑ๋์ ์ด๊ณ , ์๋ฒ๊ฐ ๋ณด๋ด๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ ๊ฐ๋จํ ์ฝ๋์ด๋ค. ํ๋ก์ ํธ์์๋ ๋ฐฑ์๋์์ ํต์ ์ next.config.js์ rewrites ์ค์ ์ ํตํด ํ๋ก์ํ๊ณ ์๊ธฐ ๋๋ฌธ์, SSE ์๋ํฌ์ธํธ(/sse/test)๋ ๋์ผํ ๋ฐฉ์์ผ๋ก ์์ฑํ์๋ค.
// pages/sse-example.jsx
import { useEffect } from 'react';
export default function SseExamplePage() {
useEffect(() => {
const es = new EventSource('/sse/test');
es.onopen = () => console.log('SSE ์ฐ๊ฒฐ ์ฑ๊ณต');
es.onmessage = (e) => console.log(e.data);
es.onerror = () => console.log('SSE ์ฐ๊ฒฐ ์๋ฌ');
return () => {
console.log('SSE ์ฐ๊ฒฐ ํด์ ');
es.close();
};
}, []);
return <div>SSE ํ
์คํธ ํ์ด์ง</div>;
}
//next.config.js
async rewrites() {
return [
{
source: '/sse/:path*',
destination: `${process.env.CLOUD_BACKEND_URL}/sse/:path*`,
},
]
}
ํ์ง๋ง, SSE ์ฐ๊ฒฐ์ ์ฑ๊ณตํ์ง๋ง ๋ฐฑ์๋์์ ์ ์กํ๋ ๋ฐ์ดํฐ์ ๋ํด์๋ ๋คํธ์ํฌ ์ฐฝ์์ ์ก์ ๋ฐ์ง ๋ชปํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
์ฐ๋ฆฌ์ ๊ธฐ๋๊ฒฐ๊ณผ
SSE ์ฐ๊ฒฐ์ด ๋ ํ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ฉด ๋ฐ๋ ์ฆ์ ์คํธ๋ฆฌ๋ฐ ๋์ด์ผ ํ๋ค.
ํฌ์คํธ๋งจ(๊ธฐ๋๊ฒฐ๊ณผ ์ผ์น) :
๋ฐ๋ก ๋ฐฑ์๋ ์๋ฒ์ ์์ฒญํ์ ๋์๋ ์ฐ๊ฒฐ์ด ๋์๋ง์ ๋ฉ์์ง๊ฐ ์ค๊ณ , ์ดํ ์ฐ๊ฒฐ์ ์ ์งํ๋ ๋ฉ์์ง๋ ์จ๋ค. ์ฆ SSE๊ฐ ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ๋๋ง๋ค ์๋ต์ด ๋์ฐฉํ๋ค.

๋ธ๋ผ์ฐ์ (๊ธฐ๋๊ฒฐ๊ณผ ๋ถ์ผ์น) :
๋ฐฑ์๋ ์๋ฒ ์ข ๋ฃ ํ ๋ฐ์ดํฐ๋ฅผ ํ๊บผ๋ฒ์ ์๋ต๋ฐ๋๋ค.

ํ๋ก ํธ์๋ ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ ํ
์คํธํ ๋์๋ ์ฐ๊ฒฐ ๋ด๋ด eventstream์ ๋ฐ์ดํฐ๊ฐ ์๋ค๊ฐ ๋ฐฑ์๋ ์๋ฒ ์ข
๋ฃ ํ์ ๋ฐ์ดํฐ๊ฐ ๋ํ๋๋ค.
์ฝ์์ฐฝ์๋ ๋ฐ์ดํฐ์ ๊ด๋ จ๋ ๋ก๊ทธ๊ฐ ๋จ์ง ์๋๋ค. ์๊ฐ๋๋ฅผ ๋ณด๋ฉด SSE๊ฐ ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ๋ ์๋ต ๋ฐ๋ ๊ฒ ์๋๋ผ ์ข
๋ฃํ ํ์ด๋ฐ์ ํ๊บผ๋ฒ์ ๋ฐ๊ณ ์๋ค.
trouble shooting ๐ฅ
backend URI endpoint๋ก ์ง์ ๋ ๋ฆฌ์ง ์๊ณ rewrites๋ฅผ ์ค์ ํ ๊ฒฝ์ฐ ๋ฉ์์ง๋ฅผ ๋ฐ์ง ๋ชปํ๋ ํ์
1. ๋ฌธ์ ๊ฐ์
CORS ์๋ฌ์ proxy๋ฅผ ์ํด rewrites ์ค์ ์ ํตํด ๋ค์๊ณผ ๊ฐ์ด ์ ์ฉํ์๋ค.
{
source: '/sse/:path*',
destination: `${process.env.CLOUD_BACKEND_URL}/sse/:path*`,
},
ํ์ง๋ง ํด๋น ์ ์ฉ์ผ๋ก ์ธํด ์ฑ๋์ ์ด๋ฆฌ์ง๋ง ๋ฐ์ดํฐ๋ฅผ ์ก์ ๋ฐ๋๋ฐ๋ ์คํจํ์๋ค.
Postman - ํ ๊ธ์์ฉ ์ค์๊ฐ์ผ๋ก ์๋ต์ด ๋์ฐฉ
๋ธ๋ผ์ฐ์ (rewrites ๊ฒฝ์ ) - ๋ชจ๋ ๋ด์ฉ์ด ํ๊บผ๋ฒ์ ๋์ฐฉ
์ฒ์์๋ ๋ธ๋ผ์ฐ์ ๋ ํด๋ผ์ด์ธํธ ๋ฌธ์ ์ธ ์ค ์์๋๋ฐ,
rewrites๋ฅผ ๊ฑฐ์น์ง ์๊ณ ๋ฐฑ์๋ ์๋ํฌ์ธํธ๋ก ์ง์ ์์ฒญํ๋ฉด ์ ์์ ์ผ๋ก ์คํธ๋ฆฌ๋ฐ์ด ๋๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
2. ์ค๋ฅ ์์ธ ๋ถ์
๊ฒ์์ ํตํด ํด๋น ๋ธ๋ก๊ทธ์์ ๋ฌธ์ ์ ์์ธ์ ๋ํด ์ ์ ์์๋ค.
next.js ๊ณต์ ํ์ด์ง์ next.config.js์ ์ค์ ์ค compress์ ๋ํ ๊ธ์ ๋ณด๋ฉด ์์ถ๊ณผ ๊ด๋ จ๋ ์ค์ ์ ๋ํ ์ด์ผ๊ธฐ๊ฐ ์์ฑ๋์ด ์๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก next.js๋ฅผ ์ค์น์ ์ด ์ต์ ์ true๋ก default ๋๋ค. ํด๋น ์ต์ ์ ๊ธฐ๋ฅ์ next start์ ์ฌ์ฉ์ ์ง์ ์๋ฒ๋ฅผ gzip์ผ๋ก rendered content and static files๋ฅผ ๋์์ผ๋ก ์์ถํ์ฌ ์ฌ์ฉํ๋ ๋ฐ, ์ด ์์ถ์ด ํ์ฑํ ๋๊ฒ ๋๋ฉด ์ถฉ๋ถํ ๋ฐ์ดํฐ๊ฐ ๋ชจ์ผ ๋ ๊น์ง ์๋ต์ ๋ฒํผ๋งํ๊ฒ ๋๋ค. ๋ฐ๋ผ์, ์ผ๋ฐ์ ์ธ HTTP์๋ต์์๋ ์ฑ๋ฅ์ ์ด์ ์ด ์์ง๋ง, SSE ์คํธ๋ฆฌ๋ฐ์์๋ ์ค์๊ฐ์ฑ์ ํค์น๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
3. ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
3.1 next.config.js์ compress ์ต์ ์ ๋๋ ๋ฐฉ๋ฒ
// next.config.js
module.exports = {
compress: false, // ์์ถ ๋นํ์ฑํ
};
ํด๋น ๋ฐฉ๋ฒ์ ์ฌ์ฉ ํ rewrites ์ค์ ์ผ๋ก SSEํต์ ์ ํ๊ฒ ๋๋ฉด ๋ฐ์ดํฐ๊ฐ ์ ์์ ์ผ๋ก ์ค๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
๐จ๊ทธ๋ ์ง๋ง, next.js ๊ณต์ ๋ฌธ์์์๋ ํด๋น ์ต์
์ ๋์ง ์๋ ๊ฒ์ ๊ถ์ฅํ๊ณ ์๋ค. ์์ถ์ ๋์ญํญ ์ฌ์ฉ๋์ ์ค์ด๊ณ ์ ํ๋ฆฌ์ผ์ด์
์ฑ๋ฅ์ ํฅ์ ์ํค๊ธฐ ๋๋ฌธ
๋ฐ๋ผ์ ํด๋น ์๋ํฌ์ธํธ๋ฅผ ์ํด ๊ณต์ ๋ฌธ์์์๋ ๊ถ์ฅํ์ง ์๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ ๊ฒ์ ์ง์ํ๋ ํธ์ด ์ข๋ค๊ณ ํ๋จํ๋ค.
3.2 API Routes๋ฅผ proxy ๋ก ํ์ฉํ์ฌ ๋ฐฑ์๋์ ํต์

github issue์์ ํ์ธ๋ ๋ฐ๋ก๋ Next.js๋ API Routes ์๋ต์ ๊ธฐ๋ณธ์ ์ผ๋ก gzip์ด ์๋ค.
์ ์ ํ์ผ, ํ์ด์ง ์๋ต → Next.js๊ฐ ๊ธฐ๋ณธ gzip (→ SSE ๊ฐ์ด chunk ๋จ์ ์คํธ๋ฆฌ๋ฐ์ด๋ฉด ๋ฒํผ๋ง ์ด์ ๊ฐ๋ฅ)
API Routes ์๋ต → ๊ธฐ๋ณธ gzip ์์ (→ ์ง๊ธ ์ฐ๋ ํ๋ก์ SSE์์๋ gzip ๋ฒํผ๋ง ์ด์๊ฐ ์ ์๊น)
์ถ๊ฐ๋ก, API Routes์์ ์์ฒญ(Request) ์ชฝ ์์ถ์ ๋ค๋ฃจ๋ Discussion ๋ ์์๋ค.
“As it is, API routes seem to ignore content-encoding entirely…” GitHub
์ด๊ฑด “์์ฒญ ๋ฐ๋๊ฐ gzip ์ด๋๋ผ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฒ๋ฆฌ ์ ํด์ค๋ค”๋ ์๊ธฐ๋ผ, API Routes๊ฐ ์์ถ/๋์ฝ๋ฉ ๊ด๋ จ ๊ธฐ๋ฅ์ ๊ธฐ๋ณธ ์ ๊ณตํ์ง ์๋๋ค๋ ๊ฑธ ๊ฐ์ ์ ์ผ๋ก ๋ ํ ๋ฒ ๋ณด์ฌ์ฃผ๋ ์ฌ๋ก์ด๋ค.
4. ๊ฒฐ๋ก
API Routes๋ฅผ ํตํด ํ๋ก ํธ๋ spring boot ์๋ฒ์์ event-stream์ ๊ฐ์ ธ์ค๋ ๋ฐฉ์์ ์ฑํํ๊ธฐ๋ก ํ๋ค.
์์ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
ํด๋ผ์ด์ธํธ: SSE ์ฐ๊ฒฐ ์ฝ๋ (Next.js Page)
- ๋ธ๋ผ์ฐ์ ๋ ๋ฐฑ์๋๊ฐ ์๋๋ผ Next.js API Routes(/api/sse)์ ์ฐ๊ฒฐ
- CORS, ์ธ์ฆ, gzip ๋ฌธ์ ๋ฅผ ํ๋ก ํธ์์ ์ ๊ฒฝ ์ฐ์ง ์์๋ ๋จ
- ์ค์ ์ค๋ฌด์์ ๊ฐ์ฅ ์์ ์ ์ธ ๊ตฌ์กฐ
// pages/sse-example.tsx
import { useEffect } from 'react';
export default function SseExamplePage() {
useEffect(() => {
// โ
Next.js API Routes๋ฅผ ํฅํ SSE ์ฐ๊ฒฐ
const es = new EventSource('/api/sse');
es.onopen = () => {
console.log('[SSE] ์ฐ๊ฒฐ ์ฑ๊ณต');
};
es.onmessage = (event) => {
console.log('[SSE] ์์ ๋ฐ์ดํฐ:', event.data);
};
es.onerror = (error) => {
console.error('[SSE] ์ฐ๊ฒฐ ์๋ฌ', error);
};
return () => {
console.log('[SSE] ์ฐ๊ฒฐ ํด์ ');
es.close();
};
}, []);
return <div>SSE Proxy ํ
์คํธ ํ์ด์ง</div>;
}
Next.js API Routes: SSE Proxy ์๋ฒ
- ๋ธ๋ผ์ฐ์ ์์ /api/sse ์์ฒญ ์์
- ๋ด๋ถ์ ์ผ๋ก Spring Boot SSE ์๋ฒ์ ๋ค์ ์ฐ๊ฒฐ
- ๋ฐฑ์๋์์ ์ค๋ ์คํธ๋ฆผ ๋ฐ์ดํฐ๋ฅผ ๊ทธ๋๋ก ๋ธ๋ผ์ฐ์ ๋ก ์ค๊ณ
- gzip, CORS, ์ธ์ฆ ํค๋ ๋ฌธ์ ํด๊ฒฐ์ ๋งค์ฐ ์ ๋ฆฌ
// pages/api/sse.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// 1) ๋ธ๋ผ์ฐ์ ๋ก ๋ณด๋ผ SSE ํค๋ ์ธํ
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
});
// flush ๊ฐ๋ฅํ ํ๊ฒฝ์ด๋ฉด ๋ฐ๋ก ํค๋ ํ๋ฌ์
// (์์ถ ๋ฏธ๋ค์จ์ด ๋ผ์ด์์ ๋ ๋์๋จ)
// @ts-ignore
res.flushHeaders?.();
// 2) ์ค์ ๋ฐฑ์๋ SSE ์๋ฒ ์ฃผ์
const backendSseUrl = process.env.BACKEND_SSE_URL || 'http://localhost:8080/sse';
let backendResponse;
try {
backendResponse = await axios.get(backendSseUrl, {
responseType: 'stream', // ๐ด ์ฌ๊ธฐ ์ค์: ์คํธ๋ฆผ์ผ๋ก ๋ฐ๊ธฐ
headers: {
Accept: 'text/event-stream',
// ํ์ํ๋ฉด ์ธ์ฆ ํค๋๋ ์ฌ๊ธฐ์
// Authorization: `Bearer ${token}`,
},
});
} catch (error) {
console.error('[SSE Proxy] ๋ฐฑ์๋ ์ฐ๊ฒฐ ์คํจ', error);
res.write(`event: error\ndata: backend connection error\n\n`);
res.end();
return;
}
const backendStream = backendResponse.data; // ReadableStream
// 3) ๋ฐฑ์๋ → Next API → ๋ธ๋ผ์ฐ์ ๋ก ๊ทธ๋๋ก ์ค๊ณ
backendStream.on('data', (chunk: Buffer) => {
res.write(chunk);
});
backendStream.on('end', () => {
console.log('[SSE Proxy] ๋ฐฑ์๋ ์คํธ๋ฆผ ์ข
๋ฃ');
res.end();
});
backendStream.on('error', (err: Error) => {
console.error('[SSE Proxy] ๋ฐฑ์๋ ์คํธ๋ฆผ ์๋ฌ', err);
res.end();
});
// 4) ๋ธ๋ผ์ฐ์ ๊ฐ ์ฐ๊ฒฐ์ ๋์์ ๋ ๋ฐฑ์๋ ์คํธ๋ฆผ๋ ์ ๋ฆฌ
req.on('close', () => {
console.log('[SSE Proxy] ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ ์ข
๋ฃ');
backendStream.destroy();
});
}
๋๊ธ