반응형
클라이언트
useEffect(() => {
let eventSource: EventSource | null = null;
const connect = () => {
// 기존 연결 닫기
if (eventSource) {
eventSource.close();
eventSource = null;
}
// SSE 연결 설정
eventSource = new EventSource(
`/api/notification?userPkey=${userInfo.pkey}&userLevel=${userInfo.level}`
);
// 메시지 수신 이벤트
eventSource.onmessage = (event) => {
const { message } = JSON.parse(event.data);
// Notification API 처리
if (Notification.permission === "granted") {
new Notification("건축 견적 AI 솔루션 알림", {
body: message,
icon: "/icon.png",
});
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
new Notification("건축 견적 AI 솔루션 알림", {
body: message,
icon: "/icon.png",
});
}
});
}
};
// 오류 및 재연결 처리
eventSource.onerror = () => {
console.warn("SSE 연결 오류, 재연결 시도");
eventSource?.close();
eventSource = null;
setTimeout(connect, 3000); // 3초 후 재연결 시도
};
};
// 사용자 정보가 유효한 경우 SSE 연결 시작
if (userInfo.pkey) {
connect();
}
// 컴포넌트 언마운트 시 SSE 연결 닫기
return () => {
if (eventSource) {
eventSource.close();
eventSource = null;
}
};
}, [userInfo]);
Next.js api
let clients: {
controller: ReadableStreamDefaultController;
userPkey: string;
userLevel: string;
}[] = [];
export async function GET(req: Request) {
try {
// 쿼리 파라미터에서 userPkey 가져오기
const url = new URL(req.url);
const userPkey = url.searchParams.get("userPkey");
const userLevel = url.searchParams.get("userLevel");
if (!userPkey) {
return new Response("pkey is required", { status: 400 });
}
// SSE 헤더 설정
const headers = {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
};
// ReadableStream 생성
const stream = new ReadableStream({
start(controller) {
// 새 클라이언트를 연결 (요청이 들어오면 클라이언트로 처리)
if (!clients.find((d) => Number(d.userPkey) === Number(userPkey)))
clients.push({ controller, userPkey, userLevel: userLevel || "1" });
// 연결 종료 시 클라이언트 제거
req.signal.addEventListener("abort", () => {
clients = clients.filter(
(client) => client.controller !== controller
);
});
},
});
return new Response(stream, { headers });
} catch (error) {
console.error("GET 요청 오류:", error);
return new Response(JSON.stringify({ error: "서버 오류 발생" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
// POST 요청: 알림 전송
export async function POST(req: Request) {
try {
const { message, userPkey } = await req.json();
if (!message || !userPkey) {
return new Response(
JSON.stringify({ error: "메시지와 userPkey가 제공되지 않았습니다." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
clients
.filter(
(client) =>
Number(client.userPkey) === Number(userPkey) ||
client.userLevel === "3"
)
.forEach((client) => {
try {
client.controller.enqueue(`data: ${JSON.stringify({ message })}\n\n`);
} catch (error) {
console.error("클라이언트 전송 오류:", error);
}
});
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("POST 요청 오류:", error);
return new Response(JSON.stringify({ error: "서버 오류 발생" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
오류 발생 / 504 (Gateway Timeout)
Nginx로 프록시를 설정해서 사용하는데 SSE를 구현하고 배포 후 504 (Gateway Timeout) 에러가 발생
server {
listen ~~~ ;
server_name ~~~ ;
location / {
proxy_pass ~~~ ; # 최종 목적지 서버
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE를 위한 설정
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s; # 연결 유지 시간
proxy_send_timeout 3600s;
}
}
반응형