TypeScript 및 Azure를 사용하는 서버리스 마이크로 서비스가 포함된 간단한 채팅 앱

소개



우리 팀은 인프라를 마이그레이션하면서 서버리스 마이크로 서비스 아키텍처를 프로젝트에 적용할 계획이므로 이에 대해 배우기 시작했습니다. 그리고 아키텍처를 보여주는 최소한의 예제 앱이 많지 않은 것 같습니다. 그래서 며칠 전에 서버리스 마이크로서비스 데모로 간단한 채팅 앱을 개발하려고 했습니다. 이 기사에서는 구현의 관점에서 몇 가지 사항을 공유합니다.

다음은 repo 입니다.

참고: 이 기사에서 마이크로서비스 아키텍처에 대한 개념 설명은 건너뛰겠습니다. (이미 그것에 관한 많은 기사가 있으며 찾을 수 있습니다.) 아이디어에 대해 더 빨리 알고 싶다면 먼저 이 리소스를 읽을 수 있습니다.
  • Microservices architecture design
  • Building serverless microservices in Azure - sample architecture

  • 앱 작동 방식



    프로젝트가 정말 간단해졌습니다. 사용자는 양식에 이름을 입력하고 채팅 화면으로 이동합니다. 사용자가 채팅에 참여하면 알림이 전송됩니다. (이 앱에는 인증 및 유효성 검사 부분이 없습니다.)

    다음은 스크린샷입니다.





    기술 스택


  • 타입스크립트
  • 리액트
  • Azure 함수
  • Azure 시그널R
  • MongoDB(MongoDB용 Azure Cosmos DB API)
  • Azure 서비스 버스

  • 프로젝트 아키텍처



    다음은 프로젝트 아키텍처의 모습입니다.



    이 앱은 사용자 서비스와 채팅 서비스의 두 가지 서비스로 구성됩니다.
  • 채팅에 참여하는 사용자를 처리하는 사용자 서비스
  • 채팅으로 보낼 메시지를 처리하는 채팅 서비스

  • 두 서비스 모두 Azure Functions를 기반으로 합니다. 메시지 브로커(Azure Service Bus)는 사용자가 채팅에 참여하여 채팅 서비스에 알림을 보내도록 지시할 때 사용됩니다. SignalR 메시지는 새 메시지가 Cosmos DB에 저장될 때마다 Azure SignalR로 전송되며 클라이언트 앱은 메시지를 수신하고 채팅 화면에 표시되는 메시지를 업데이트합니다.

    사용자 서비스



    사용자 서비스( src/functions/Users/ )를 살펴보겠습니다.
    사용자 서비스에는 사용자 API가 있습니다. 사용자가 채팅 화면에 들어올 때 사용자 정보를 Cosmos DB에 저장한 다음 Azure Service Bus의 NewUsersQueue라는 큐에 메시지를 보내 채팅 서비스에서 채팅에 참여한 사람을 파악하고 알림을 보낼 수 있도록 합니다.
    user/index.ts
    import { AzureFunction, Context, HttpRequest } from "@azure/functions";
    import { ServiceBusClient } from "@azure/service-bus";
    
    import { connectDB } from "../db";
    import { User } from "../models/user";
    
    const httpTrigger: AzureFunction = async (
      context: Context,
      req: HttpRequest
    ): Promise<void> => {
      const sbClient = new ServiceBusClient(process.env["SERVICE_BUS_CONNECTION_STRING"]);
    
      try {
        await connectDB();
    
        switch (req.method) {
          case "POST":
            if (req?.body?.name) {
              const user = User.build({ name: req.body.name });
              const savedUser = await user.save();
    
              const sender = sbClient.createSender("NewUsersQueue");
              const message = {
                body: JSON.stringify(savedUser),
              };
              await sender.sendMessages(message);
              await sender.close();
    
              context.res = {
                body: savedUser,
              };
            } else {
              throw "Invalid parameter";
            }
            break;
          default:
            throw `${req.method} is not allowed`;
        }
      } catch (err) {
        context.log(`Error: ${err}`);
        context.res = {
          status: 500,
          body: err,
        };
      } finally {
        await sbClient.close();
      }
    };
    
    export default httpTrigger;
    
    


    채팅 서비스



    다음으로 채팅 서비스( src/functions/Chat/ )를 살펴보겠습니다.
    채팅 서비스에는 메시지 API, 협상 기능 및 newUserNotification 기능이 있습니다.

    메시지 API는 GET 요청으로 Cosmos DB에서 메시지 문서를 검색하고 메시지 문서를 Cosmos DB에 저장한 다음 POST 요청으로 newMessage 이벤트가 있는 SignalR 메시지를 Azure SignalR로 보냅니다.
    message/index.ts
    import { AzureFunction, Context, HttpRequest } from "@azure/functions";
    import { ReadPreference } from "mongodb";
    import { connectDB } from "../db";
    import { Message } from "../models/message";
    
    const httpTrigger: AzureFunction = async (
      context: Context,
      req: HttpRequest
    ): Promise<void> => {
      try {
        await connectDB();
    
        switch (req.method) {
          case "GET":
            const query = Message.find({}).read(ReadPreference.NEAREST);
            const messages = await query.exec();
            context.res = {
              body: messages,
            };
            break;
          case "POST":
            const { uid, from, body } = req?.body;
            if (uid && from && body) {
              const message = Message.build({ uid: uid, from: from, body: body });
              const savedMessage = await message.save();
              context.bindings.signalRMessages = [
                {
                  target: "newMessage",
                  arguments: [savedMessage],
                },
              ];
              context.res = {
                body: savedMessage,
              };
            } else {
              throw "Invalid parameter";
            }
            break;
          default:
            throw `${req.method} is not allowed`;
        }
      } catch (err) {
        context.log(`Error: ${JSON.stringify(err)}`);
        context.res = {
          status: 500,
          body: err,
        };
      }
    };
    
    export default httpTrigger;
    
    


    협상 함수는 클라이언트 앱이 Azure SignalR에 연결할 수 있도록 액세스 토큰 및 연결 정보를 반환합니다.
    negotiate/index.ts
    import { AzureFunction, Context, HttpRequest } from "@azure/functions";
    
    const httpTrigger: AzureFunction = async (
      context: Context,
      req: HttpRequest,
      connectionInfo: any
    ): Promise<void> => {
      context.res.body = connectionInfo;
    };
    
    export default httpTrigger;
    
    


    newUserNotification 함수는 Azure Service Bus의 NewUsersQueue 메시지에 의해 트리거됩니다. 함수가 트리거된 후 클라이언트 앱의 알림처럼 보이는 메시지 문서를 Cosmos DB에 저장한 다음 newMessage 이벤트와 함께 SignalR 메시지를 Azure SignalR로 보냅니다.
    newUserNotification/index.ts
    import { AzureFunction, Context } from "@azure/functions";
    import { connectDB } from "../db";
    import { Message } from "../models/message";
    
    const serviceBusQueueTrigger: AzureFunction = async (
      context: Context,
      mySbMsg: any
    ): Promise<void> => {
      try {
        await connectDB();
        const newUser = JSON.parse(mySbMsg);
        const message = Message.build({
          from: "",
          body: `${newUser.name} joined the chat.`,
        });
        const result = await message.save();
        context.bindings.signalRMessages = [{
          target: "newMessage",
          arguments: [result]
        }];
      } catch (err) {
        context.log(`Error: ${JSON.stringify(err)}`);
      }
    };
    
    export default serviceBusQueueTrigger;
    
    


    프런트엔드



    마지막으로 React 클라이언트 앱( src/frontend/ )을 살펴보겠습니다.
    api 폴더 안에 있는 파일은 서비스에 대한 API 호출을 실행하는 역할을 합니다.
    components/Chat/index.tsx 파일에서 SignalR 클라이언트 라이브러리를 사용한 구현이 있고 Azure SignalR에서 newMessage 이벤트로 메시지를 수신하는 것을 볼 수 있습니다.

    const connectSignalR = async (): Promise<void> => {
      try {
        const signalRInfo = await negotiateAPI.post();
        if (signalRInfo) {
          const options = {
            accessTokenFactory: () => signalRInfo.accessToken,
          };
          connection = new signalR.HubConnectionBuilder()
            .withUrl(signalRInfo.url, options)
            .configureLogging(signalR.LogLevel.None)
            .build();
          connection.on("newMessage", (msg: Message) => {
            setMessages((prev) => [...prev, msg]);
          });
          connection.start();
        }
      } catch (err) {
        console.log(err);
      }
    };
    
    


    결론



    실제 응용 프로그램의 경우 아키텍처가 훨씬 더 복잡해야 하지만 이 데모 응용 프로그램이 몇 가지 기본 개념을 이해하는 데 도움이 되기를 바랍니다.

    repo 페이지에 프로젝트 설정 가이드를 작성했으므로 이 프로젝트를 로컬에서 실행하려면 this을 참조하십시오.

    언제든지 피드백을 주세요!

    좋은 웹페이지 즐겨찾기