자습서: Azure Functions 및 Azure Web PubSub 서비스를 사용하여 서버리스 실시간 채팅 앱 만들기

Azure Web PubSub 서비스를 사용하면 WebSocket 및 게시-구독 패턴을 사용하여 실시간 메시징 웹 애플리케이션을 쉽게 빌드할 수 있습니다. Azure Functions는 인프라를 관리하지 않고 코드를 실행할 수 있는 서버리스 플랫폼입니다. 이 자습서에서는 Azure Web PubSub 서비스 및 Azure Functions를 사용하여 실시간 메시징과 게시-구독 패턴으로 서버리스 애플리케이션을 빌드하는 방법에 대해 알아봅니다.

이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.

  • 서버리스 실시간 채팅 앱 빌드
  • Web PubSub 함수 트리거 바인딩 및 출력 바인딩 작업
  • Azure Function 앱에 함수 배포
  • Azure 인증 구성
  • 애플리케이션에 이벤트 및 메시지를 라우팅하도록 Web PubSub 이벤트 처리기 구성

필수 조건

Azure를 구독하고 있지 않다면 시작하기 전에 Azure 체험 계정을 만듭니다.

Azure에 로그인

Azure 계정을 사용하여 https://portal.azure.com/에서 Azure Portal에 로그인합니다.

Azure Web PubSub 서비스 인스턴스 만들기

애플리케이션이 Azure의 Web PubSub 서비스 인스턴스에 연결됩니다.

  1. Azure Portal의 왼쪽 위에 있는 새로 만들기 단추를 선택합니다. 새 화면의 검색 상자에서 Web PubSub를 입력하고 Enter를 누릅니다. (Web 범주에서 Azure Web PubSub를 검색할 수도 있습니다.)

    포털에서 Azure Web PubSub를 검색하는 스크린샷

  2. 검색 결과에서 Web PubSub를 선택한 다음, 만들기를 선택합니다.

  3. 다음 설정을 입력합니다.

    설정 제안 값 설명
    리소스 이름 전역적으로 고유한 이름 새 Web PubSub 서비스 인스턴스를 식별하는 전역적으로 고유한 이름입니다. 유효한 문자는 a-z, A-Z, 0-9-입니다.
    구독 구독 이 새 Web PubSub 서비스 인스턴스가 생성되는 Azure 구독입니다.
    리소스 그룹 myResourceGroup Web PubSub 서비스 인스턴스를 만들 새 리소스 그룹의 이름입니다.
    위치 미국 서부 가까운 지역을 선택합니다.
    가격 책정 계층 Free 먼저 Azure Web PubSub 서비스 평가판을 사용할 수 있습니다. Azure Web PubSub 서비스 가격 책정 계층에 대해 자세히 알아보세요.
    단위 수 - 단위 수는 Web PubSub 서비스 인스턴스가 허용할 수 있는 연결 수를 지정합니다. 각 단위는 최대 1,000개의 동시 연결을 지원합니다. 표준 계층에서만 구성할 수 있습니다.

    포털에서 Azure Web PubSub 인스턴스를 만드는 스크린샷.

  4. 만들기를 선택하여 Web PubSub 서비스 인스턴스 배포를 시작하세요.

함수 만들기

  1. Azure Functions Core Tools가 설치되어 있는지 확인합니다. 그런 다음, 프로젝트에 대한 빈 디렉터리를 만듭니다. 이 작업 디렉터리 아래에서 명령을 실행합니다.

    func init --worker-runtime javascript --model V4
    
  2. Microsoft.Azure.WebJobs.Extensions.WebPubSub설치

    Web PubSub 지원을 가져오려면 host.json의 extensionBundle을 확인하고 버전 4.* 이상으로 업데이트합니다.

    {
      "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[4.*, 5.0.0)"
      }
    }
    
  3. 클라이언트에 대한 정적 웹 페이지를 읽고 호스팅하는 index 함수를 만듭니다.

    func new -n index -t HttpTrigger
    
    • src/functions/index.js를 업데이트하고 다음 코드를 복사합니다.
      const { app } = require('@azure/functions');
      const { readFile } = require('fs/promises');
      
      app.http('index', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          handler: async (context) => {
              const content = await readFile('index.html', 'utf8', (err, data) => {
                  if (err) {
                      context.err(err)
                      return
                  }
              });
      
              return { 
                  status: 200,
                  headers: { 
                      'Content-Type': 'text/html'
                  }, 
                  body: content, 
              };
          }
      });
      
  4. 클라이언트가 액세스 토큰과 함께 서비스 연결 URL을 얻을 수 있도록 negotiate 함수를 만듭니다.

    func new -n negotiate -t HttpTrigger
    

    참고 항목

    이 샘플에서는 In this sample, we use Microsoft Entra ID 사용자 ID 헤더 x-ms-client-principal-name을 사용하여 userId를 검색합니다. 그리고 이는 로컬 함수에서 작동하지 않습니다. 비워두거나, 로컬로 재생할 때 userId를 가져오거나 생성하는 다른 방법으로 변경할 수 있습니다. 예를 들어 클라이언트에서 사용자 이름을 입력하고, negotiate 함수를 호출하여 서비스 연결 URL을 가져올 때 ?user={$username}과 같은 쿼리에 해당 사용자 이름을 전달할 수 있습니다. 그리고 negotiate 함수에서 userId{query.user} 값으로 설정합니다.

    • src/functions/negotiate를 업데이트하고 다음 코드를 복사합니다.
      const { app, input } = require('@azure/functions');
      
      const connection = input.generic({
          type: 'webPubSubConnection',
          name: 'connection',
          userId: '{headers.x-ms-client-principal-name}',
          hub: 'simplechat'
      });
      
      app.http('negotiate', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          extraInputs: [connection],
          handler: async (request, context) => {
              return { body: JSON.stringify(context.extraInputs.get('connection')) };
          },
      });
      
  5. 서비스를 통해 클라이언트 메시지를 브로드캐스트하는 message 함수를 만듭니다.

    func new -n message -t HttpTrigger
    
    • src/functions/message.js를 업데이트하고 다음 코드를 복사합니다.
      const { app, output, trigger } = require('@azure/functions');
      
      const wpsMsg = output.generic({
          type: 'webPubSub',
          name: 'actions',
          hub: 'simplechat',
      });
      
      const wpsTrigger = trigger.generic({
          type: 'webPubSubTrigger',
          name: 'request',
          hub: 'simplechat',
          eventName: 'message',
          eventType: 'user'
      });
      
      app.generic('message', {
          trigger: wpsTrigger,
          extraOutputs: [wpsMsg],
          handler: async (request, context) => {
              context.extraOutputs.set(wpsMsg, [{
                  "actionName": "sendToAll",
                  "data": `[${context.triggerMetadata.connectionContext.userId}] ${request.data}`,
                  "dataType": request.dataType
              }]);
      
              return {
                  data: "[SYSTEM] ack.",
                  dataType: "text",
              };
          }
      });
      
  6. 프로젝트 루트 폴더에서 클라이언트 단일 페이지(index.html)를 추가하고 콘텐츠를 복사합니다.

    <html>
      <body>
        <h1>Azure Web PubSub Serverless Chat App</h1>
        <div id="login"></div>
        <p></p>
        <input id="message" placeholder="Type to chat..." />
        <div id="messages"></div>
        <script>
          (async function () {
            let authenticated = window.location.href.includes(
              "?authenticated=true"
            );
            if (!authenticated) {
              // auth
              let login = document.querySelector("#login");
              let link = document.createElement("a");
              link.href = `${window.location.origin}/.auth/login/aad?post_login_redirect_url=/api/index?authenticated=true`;
              link.text = "login";
              login.appendChild(link);
            } else {
              // negotiate
              let messages = document.querySelector("#messages");
              let res = await fetch(`${window.location.origin}/api/negotiate`, {
                credentials: "include",
              });
              let url = await res.json();
              // connect
              let ws = new WebSocket(url.url);
              ws.onopen = () => console.log("connected");
              ws.onmessage = (event) => {
                let m = document.createElement("p");
                m.innerText = event.data;
                messages.appendChild(m);
              };
              let message = document.querySelector("#message");
              message.addEventListener("keypress", (e) => {
                if (e.charCode !== 13) return;
                ws.send(message.value);
                message.value = "";
              });
            }
          })();
        </script>
      </body>
    </html>
    

Azure 함수 앱 만들기 및 배포

함수 코드를 Azure에 배포하기 전에 다음 세 가지 리소스를 만들어야 합니다.

  • 리소스 그룹 - 관련 리소스에 대한 논리 컨테이너입니다.
  • 스토리지 계정 - 함수에 대한 상태 및 기타 정보를 유지 관리합니다.
  • 함수 앱 - 함수 코드를 실행할 수 있는 환경을 제공합니다. 함수 앱은 로컬 함수 프로젝트에 매핑되며, 함수를 논리적 단위로 그룹화하여 리소스를 더 쉽게 관리, 배포 및 공유할 수 있습니다.

다음 명령을 사용하여 이러한 항목을 만듭니다.

  1. 아직 로그인하지 않은 경우 Azure에 로그인합니다.

    az login
    
  2. 리소스 그룹을 만들거나, Azure Web PubSub 서비스 중 하나를 다시 사용하여 건너뛸 수 있습니다.

    az group create -n WebPubSubFunction -l <REGION>
    
  3. 범용 스토리지 계정을 리소스 그룹 및 지역에 만듭니다.

    az storage account create -n <STORAGE_NAME> -l <REGION> -g WebPubSubFunction
    
  4. Azure에서 함수 앱을 만듭니다.

    az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime node --runtime-version 18 --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
    

    참고 항목

    Azure Functions 런타임 버전 설명서를 확인하여 --runtime-version 매개 변수를 지원되는 값으로 설정하세요.

  5. 함수 프로젝트를 Azure에 배포합니다.

    Azure에서 함수 앱을 성공적으로 만들었으면 이제 func azure functionapp publish 명령을 사용하여 로컬 함수 프로젝트를 배포할 준비가 되었습니다.

    func azure functionapp publish <FUNCIONAPP_NAME>
    
  6. 함수 앱의 WebPubSubConnectionString을 구성합니다.

    먼저 Azure Portal에서 Web PubSub 리소스를 찾고 아래에 있는 연결 문자열을 복사합니다. 그런 다음, Azure Portal>설정>구성에서 함수 앱 설정으로 이동합니다. 그리고 애플리케이션 설정에 이름이 WebPubSubConnectionString이며 값은 Web PubSub 리소스 연결 문자열인 새 항목을 추가합니다.

Event Handler Web PubSub 서비스 구성

이 샘플에서는 WebPubSubTrigger를 사용하여 서비스 업스트림 요청을 수신 대기합니다. 따라서 Web PubSub는 대상 클라이언트 요청을 보내기 위해 함수의 엔드포인트 정보를 인식하고 있어야 합니다. 그리고 Azure Function 앱에는 확장 관련 웹후크 방법에 대한 보안을 위한 시스템 키가 필요합니다. 이전 단계에서 message 함수를 사용하여 함수 앱을 배포하면 시스템 키를 얻을 수 있습니다.

Azure Portal -> 함수 앱 리소스 찾기 ->앱 키 ->시스템 키 ->webpubsub_extension로 이동합니다. 값을 <APP_KEY>로 복사합니다.

함수 시스템 키 가져오기의 스크린샷.

Azure Web PubSub 서비스에 Event Handler를 설정합니다. Azure Portal -> Web PubSub 리소스 찾기 ->설정으로 이동합니다. 사용 중인 하나의 함수에 새 허브 설정 매핑을 추가합니다. <FUNCTIONAPP_NAME><APP_KEY>를 사용자 고유의 값으로 바꿉니다.

  • 허브 이름: simplechat
  • URL 템플릿: https://<FUNCTIONAPP_NAME>.azurewebsites.net/runtime/webhooks/webpubsub?code=<APP_KEY>
  • 사용자 이벤트 패턴: *
  • 시스템 이벤트: -(이 샘플에서는 구성할 필요가 없음)

이벤트 처리기를 설정하는 스크린샷.

클라이언트 인증을 사용하도록 구성

Azure Portal -> 함수 앱 리소스 찾기 ->인증으로 이동합니다. Add identity provider을 클릭합니다. App Service 인증 설정인증되지 않은 액세스 허용으로 설정합니다. 그러면 익명 사용자가 인증을 위해 리디렉션하기 전에 클라이언트 인덱스 페이지를 방문할 수 있습니다. 그런 다음 저장을 선택합니다.

여기서는 negotiate 함수에서 x-ms-client-principal-nameuserId로 사용할 ID 공급자로 Microsoft를 선택합니다. 또한 링크에 따라 다른 ID 공급자를 구성할 수 있으며, 이에 따라 negotiate 함수에서 userId 값을 업데이트해야 합니다.

애플리케이션 사용해 보기

이제 함수 앱(https://<FUNCTIONAPP_NAME>.azurewebsites.net/api/index)에서 페이지를 테스트할 수 있습니다. 스냅샷을 참조하세요.

  1. login을 클릭하여 자신을 인증합니다.
  2. 입력 상자에서 채팅 메시지를 입력합니다.

메시지 함수에서 호출자의 메시지를 모든 클라이언트에 브로드캐스트하고 [SYSTEM] ack 메시지와 함께 호출자를 반환합니다. 따라서 샘플 채팅 스냅샷에서 처음 4개 메시지는 현재 클라이언트에서 제공되었으며, 마지막 2개 메시지는 다른 클라이언트에서 제공되었습니다.

채팅 샘플의 스크린샷.

리소스 정리

이 앱을 계속 사용하지 않으려면 다음 단계에 따라 이 문서에서 만든 리소스를 모두 삭제하세요. 요금은 발생되지 않습니다.

  1. Azure Portal에서 맨 왼쪽에 있는 리소스 그룹을 선택한 다음, 만든 리소스 그룹을 선택합니다. 대신 검색 상자를 사용하여 이름으로 리소스 그룹을 찾을 수 있습니다.

  2. 열린 창에서 리소스 그룹을 선택한 다음, 리소스 그룹 삭제를 선택합니다.

  3. 새 창에서 삭제할 리소스 그룹의 이름을 입력한 다음, 삭제를 선택합니다.

다음 단계

이 빠른 시작에서는 서버리스 채팅 애플리케이션을 실행하는 방법을 알아보았습니다. 이제 자체 애플리케이션을 빌드할 수 있습니다.