-
SSE 토이 프로젝트 - 프롬프터 만들기Study/개발 2024. 7. 18. 00:13
SSE(Server Sent Event)를 사용해보자.
서버에서 입력한 내용을 화면에 마크다운으로 띄워주는 프롬프터를 만들어본다.
github: https://github.com/JAAAAAEMKIM/practice/tree/main/sse-practice
스펙
1. 서버
- 사용자 입력을 받아 라인별로 클라이언트에 전송한다.
- SSE를 통해 구현한다.
2. 클라이언트
- 서버와 연결되어, 사용자 입력을 화면에 표시한다.
- Markdown을 사용해서 실시간으로 보여준다.
서버 개발하기
기술 스택: Bun, Typescript
Bun 선정 이유
- Typescript 바로 실행 가능
- 간단한 서버 실행
서버 실행
구현은 SSE 관련 내용의 bun github issue를 참고했다.
https://github.com/oven-sh/bun/issues/2443Bun.serve({ port: 8080, fetch(req) { if (new URL(req.url).pathname === "/sse") { // sse 요청 핸들링 return sse(req); } return new Response("Not Found", { status: 404 }); }, });
sse
요청 처리function sse(req: Request) { const { signal } = req; return new Response( // Stream을 응답으로 준다. new ReadableStream({ start(controller) { eventEmitter.on("message", (data) => { // stdin에서 입력 data를 message event에 담에 eventEmitter로 보낸다. // sendSseMessage 내에서 stream의 controller를 통해 data를 클라이언트로 전송한다. sendSseMessage(controller, data); }); signal.onabort = () => { controller.close(); }; }, }), { status: 200, headers: { // SSE에 필요한 헤더 (중요) "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, }, ); }
stdin 처리
const prompt = "Type prompt: "; process.stdout.write(prompt); // user input을 line 단위로 읽는다. for await (const line of console) { console.log(`You typed: ${line}`); // eventEmitter에 입력 데이터를 전달. eventEmitter.emit("message", line); process.stdout.write(prompt); }
sendSseMessage
구현function sendSseMessage( controller: Bun.ReadableStreamController<Uint8Array>, data: string, ) { const payload = data .split("\n") // 입력 데이터를 sse 형식에 맞게 수정 (data: [data]\n\n) .map((line) => `data: ${line}\n\n`) .join(""); controller.enqueue(Buffer.from(payload)); }
클라이언트 구현
클라이언트 구현은 간단하여 짧게 넘어간다.
- pnpm create vite 를 사용하여 빠른 시작
- Prompter 구현
- proxy 설정
import { useEffect, useRef, useState } from "react"; import Markdown from "react-markdown"; import styles from "./Prompter.module.css"; const SERVER_URL = "/sse"; const Prompter = () => { const [content, setContent] = useState(""); const eventSourceRef = useRef<EventSource>(); const articleRef = useRef<HTMLDivElement>(null); useEffect(() => { try { // EventSource를 통해 sse 이벤트를 받을 수 있다. eventSourceRef.current = new EventSource(SERVER_URL); eventSourceRef.current.onmessage = (ev) => { // 기존 content와 연결해준다. setContent((prev) => `${prev}\n${ev.data}`); }; return () => { eventSourceRef.current?.close(); }; } catch { setContent("Error"); } }, []); useEffect(() => { if (articleRef.current) { // 새로운 data가 화면에 표시될 때 보이도록 스크롤 articleRef.current.scrollIntoView({ block: "end" }); } }, [content]); return ( <article className={styles.article}> <div ref={articleRef}> <Markdown>{content}</Markdown> </div> </article> ); }; export default Prompter;
완성하면 처음에 봤던 화면을 사용해볼 수 있다.
728x90'Study > 개발' 카테고리의 다른 글
더 이상 메인 에디터로 Neovim을 쓰지 않는 이유 (1) 2024.11.23 [React Conf 2024] Demystifying Accessibility in React Apps (0) 2024.07.03 타입스크립트 5.5 베타 요약 (0) 2024.07.02 Tmux 마우스 드래그 오류 (0) 2024.07.01 Regular Expression 종류, 동작 방식, 성능 (2) 2024.03.23