ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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/2443

    Bun.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));
    }

    클라이언트 구현

    클라이언트 구현은 간단하여 짧게 넘어간다.

    1. pnpm create vite 를 사용하여 빠른 시작
    2. Prompter 구현
    3. 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
Designed by Tistory.