Argano のジェソンです。 server sent eventを実装する方法について紹介します。
Server Sent Event(SSE)란?
서버에서 클라이언트 단방향 통신으로써 서버가 원할 때 클라이언트에게 이벤트를 사용해 데이터를 송신할 수 있는 기술입니다.
Express에서 구현
express 환경에서 구현해보겠습니다.
서버에서는 클라이언트에게 데이터를 보내줄 API를 만듭니다. 여기에서 주의할 점은
- response 헤더에 Content-Type 을’text/event-stream"으로 설정하여 응답해야 클라이언트에서 SSE 이벤트 수신을 할 수 있습니다.
- 각 이벤트는 "\n"으로 끝나는 텍스트 블록으로 전송됩니다.
- SSE 표준은 오직 GET만을 허용하기 때문에 GET으로 API를 작성해줍니다.
app.js
const express = require("express");
const app = express();
app.get('/sse', (req, res) => {
res
.setHeader("Content-Type", "text/event-stream") // 이벤트 스트림 컨텐츠 타입 선언
.setHeader("Connection", "keep-alive") // 업데이트를 가져오는데 사용할 커넥션 설정
.setHeader("Cache-Control", "no-cache") // 캐시를 비활성화
setInterval(() => {
res
.status(200)
.write(
'event: message\n'+
'data: hello Argano\n\n'
);
}, 2000);
});
app.listen(3000, () => {
console.log("start express server on port 3000");
});
클라이언트에서는 EventSource 라는 객체를 이용하여 서버에서 송신한 이벤트 데이터를 수신합니다. onopen 이벤트는 처음 1회만 발화하고 그 이후 onmessage로 서버로부터 온 데이터를 수신합니다. IE 초기버전 이외에는 모든 브라우저에서 SSE를 지원하지만 만일의 상황을 대비해 SSE를 지원하는지 체크 해주는것이 좋습니다.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>index.html</title>
</head>
<body>
<h1>index page</h1>
<div>
<button id="request-button">Request!</button>
<div id="sse-data"></div>
</div>
</body>
<script>
// SSE 지원여부 체크
if(typeof(EventSource) !== "undefined") {
console.log("sse available");
} else {
console.log("sse not available");
}
const requestButton = document.getElementById("request-button");
requestButton.onclick = () => {
const sseDataElement = document.getElementById("sse-data");
const eventSource = new EventSource("/sse");
eventSource.onopen = () => {
console.info("open");
}
eventSource.onmessage = (event) => {
const p = document.createElement("p");
p.textContent = event.data;
sseDataElement.append(p);
}
eventSource.onerror = () => {
console.error("error");
eventSource.close();
}
}
</script>
</html>
nextjs14 app router에서 구현
nextjs 14버전 app router를 사용하는 환경에서 구현해보겠습니다.
Express와 마찬가지로 Content-Type에는 반드시 "text/event-stream"를 설정해주도록 합니다.
app/api/route.ts
export const dynamic = "force-dynamic";
export async function GET() {
return new Response(
"event: message\n" +
"data: hello Argano\n\n",
{
headers: {
Connection: "keep-alive",
"Content-type": "text/event-stream",
"Cache-Control": "no-cache"
}
}
);
}
클라이언트컴포넌트에선 useState를이용해 서버컴포넌트로부터 수신한 데이터를 화면에 표시해줍니다.
app/home.tsx
"use client"
import styles from "./page.module.css";
import { useState } from "react";
export default function Home() {
const [data, setData] = useState("");
const handleClick = async () => {
setString("");
const eventSource = new EventSource("/api");
eventSource.onopen = () => {
console.info("Open EventSource");
};
eventSource.onmessage = (event: MessageEvent<any>) => {
console.info("Message: ", event.data);
setData(event.data);
}
eventSource.onerror = (event: Event) => {
console.error("error");
eventSource.close();
}
}
return (
<main className={styles.main}>
<div className={styles.description}>
<button
onClick={handleClick}
>
server sent event call
</button>
<p className={styles.card}>{data}</p>
</div>
</main>
);
}
ReadableStream 오브젝트를 이용하여 구현하기
이번에는 ReadableStream 객체를 이용하여 구현해보겠습니다.
서버컴포넌트쪽에서 readableStream을 사용하여 body에 담아 전송하면 클라이언트 쪽에서 EventSource 객체로 수신이 가능합니다.
app/api/route.ts
export const dynamic = "force-dynamic";
function sleep(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
}
export async function GET() {
const datas = ["H", "e", "l", "l", "o", "A", "r", "g", "a", "n", "o"];
const readableStream = new ReadableStream({
start(controller) {
(async () => {
for (const d of datas) {
controller.enqueue(`data: ${d}\n\n`);
await sleep(300);
}
controller.close();
})();
}
});
return new Response(readableStream, {
headers: {
Connection: "keep-alive",
"Content-type": "text/event-stream",
"Cache-Control": "no-cache",
}
});
}
클라이언트컴포넌트에선 마찬가지로 useState를이용해 서버컴포넌트로부터 수신한 데이터를 화면에 표시해줍니다. 여기서 주의할 점은 nextjs 13.4부터 react.StrictMode가 default로 true가 되어있기 때문에 next dev로 개발할 때 setState나 useEffect가 두 번 실행되는 것을 조심할 필요가 있습니다.
app/home.tsx
"use client"
import styles from "./page.module.css";
import { useState } from "react";
export default function Home() {
const [content, setContent] = useState<string>("");
const handleClick = async () => {
const eventSource = new EventSource("/api");
eventSource.onopen = () => {
setContent("");
console.info("Open EventSource");
};
eventSource.onmessage = (event: MessageEvent<any>) => {
console.info("Message: ", event.data);
setContent((prevState) => {
return prevState + event.data;
});
}
eventSource.onerror = (event: Event) => {
console.error("error");
eventSource.close();
}
}
return (
<main className={styles.main}>
<div className={styles.description}>
<button
onClick={handleClick}
>
server sent event call
</button>
<p className={styles.card}>{content}</p>
</div>
</main>
);
}
이상으로 Server Sent Event를 구현하는 방법에 대해 알아보았습니다. 읽어주셔서 감사합니다.