다음을 통해 공유


자습서: 서버리스 모드에서 Azure 함수를 사용하여 채팅 앱 빌드(미리 보기)

이 자습서에서는 서버리스 모드에서 Socket.IO 서비스용 Web PubSub를 만드는 방법과 Azure 함수와 통합된 채팅 앱을 빌드하는 방법을 안내합니다.

이 자습서에서 사용된 전체 코드 샘플을 찾습니다.

중요

기본 모드에는 영구 서버가 필요하며 Azure 함수를 사용하여 기본 모드에서 Socket.IO에 대한 Web PubSub를 통합할 수 없습니다.

중요

원시 연결 문자열 데모용으로만 이 문서에 표시됩니다.

연결 문자열에는 애플리케이션이 Azure Web PubSub 서비스에 액세스하는 데 필요한 권한 부여 정보가 포함됩니다. 연결 문자열 내의 액세스 키는 서비스의 루트 암호와 비슷합니다. 프로덕션 환경에서는 항상 액세스 키를 보호합니다. Azure Key Vault를 사용하여 키를 안전하게 관리하고 회전하고 WebPubSubServiceClient를 통해 연결을 보호합니다.

액세스 키를 다른 사용자에게 배포하거나 하드 코딩하거나 다른 사용자가 액세스할 수 있는 일반 텍스트로 저장하지 않도록 합니다. 키가 손상되었다고 생각되면 키를 회전하세요.

필수 구성 요소

서버리스 모드에서 Socket.IO 리소스에 대한 Web PubSub 만들기

Socket.IO에 대한 Web PubSub를 만들려면 다음 Azure CLI 명령을 사용할 수 있습니다.

az webpubsub create -g <resource-group> -n <resource-name>--kind socketio --service-mode serverless --sku Premium_P1

로컬로 Azure 함수 프로젝트 만들기

로컬 Azure 함수 프로젝트를 시작하려면 다음 단계를 따라야 합니다.

  1. 최신 Azure 함수 핵심 도구를 설치하려면 다음 단계를 따릅니다.

  2. 터미널 창 또는 명령 프롬프트에서 다음 명령을 실행하여 SocketIOProject 폴더에 프로젝트를 만듭니다.

    func init SocketIOProject --worker-runtime javascript --model V4
    

    이 명령은 JavaScript 프로젝트를 만듭니다. SocketIOProject 폴더로 들어가서 다음 명령을 실행합니다.

  3. 현재 함수 번들에는 Socket.IO 함수 바인딩이 포함되어 있지 않으므로 패키지를 수동으로 추가해야 합니다.

    1. 함수 번들 참조를 제거하려면 host.json 파일을 편집하고 다음 줄을 제거합니다.

      "extensionBundle": {
          "id": "Microsoft.Azure.Functions.ExtensionBundle",
          "version": "[4.*, 5.0.0)"
      }
      
    2. 명령 실행:

      func extensions install -p Microsoft.Azure.WebJobs.Extensions.WebPubSubForSocketIO -v 1.0.0-beta.4
      
  4. 협상을 위한 함수를 만듭니다. 클라이언트가 서비스에 액세스하기 위한 엔드포인트와 토큰을 생성하는 데 사용되는 협상 함수입니다.

    func new --template "Http Trigger" --name negotiate
    

    src/functions/negotiate.js에 있는 파일을 열고 다음 코드로 바꿉니다.

    const { app, input } = require('@azure/functions');
    
    const socketIONegotiate = input.generic({
        type: 'socketionegotiation',
        direction: 'in',
        name: 'result',
        hub: 'hub'
    });
    
    async function negotiate(request, context) {
        let result = context.extraInputs.get(socketIONegotiate);
        return { jsonBody: result };
    };
    
    // Negotiation
    app.http('negotiate', {
        methods: ['GET', 'POST'],
        authLevel: 'anonymous',
        extraInputs: [socketIONegotiate],
        handler: negotiate
    });
    

    이 단계에서는 HTTP 트리거와 negotiate 출력 바인딩을 사용하여 SocketIONegotiation 함수를 만듭니다. 즉, HTTP 호출을 사용하여 함수를 트리거하고 SocketIONegotiation 바인딩에서 생성된 협상 결과를 반환할 수 있습니다.

  5. 메시지 처리 함수를 만듭니다.

    func new --template "Http Trigger" --name message
    

    src/functions/message.js 파일을 열고 다음 코드로 바꿉니다.

    const { app, output, trigger } = require('@azure/functions');
    
    const socketio = output.generic({
    type: 'socketio',
    hub: 'hub',
    })
    
    async function chat(request, context) {
        context.extraOutputs.set(socketio, {
        actionName: 'sendToNamespace',
        namespace: '/',
        eventName: 'new message',
        parameters: [
            context.triggerMetadata.socketId,
            context.triggerMetadata.message
        ],
        });
    }
    
    // Trigger for new message
    app.generic('chat', {
        trigger: trigger.generic({
            type: 'socketiotrigger',
            hub: 'hub',
            eventName: 'chat',
            parameterNames: ['message'],
        }),
        extraOutputs: [socketio],
        handler: chat
    });
    

    SocketIOTrigger를 사용하여 Socket.IO 클라이언트 메시지에 의해 트리거되고 SocketIO 바인딩을 사용하여 네임스페이스에 메시지를 브로드캐스트합니다.

  6. 방문을 위한 인덱스 HTML을 반환하는 함수를 만듭니다.

    1. public 아래에 src/ 폴더를 만듭니다.

    2. 다음 콘텐츠로 HTML 파일 index.html을 만듭니다.

      <html>
      
      <body>
      <h1>Socket.IO Serverless Sample</h1>
      <div id="chatPage" class="chat-container">
          <div class="chat-input">
              <input type="text" id="chatInput" placeholder="Type your message here...">
              <button onclick="sendMessage()">Send</button>
          </div>
          <div id="chatMessages" class="chat-messages"></div>
      </div>
      <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
      <script>
          function appendMessage(message) {
          const chatMessages = document.getElementById('chatMessages');
          const messageElement = document.createElement('div');
          messageElement.innerText = message;
          chatMessages.appendChild(messageElement);
          hatMessages.scrollTop = chatMessages.scrollHeight;
          }
      
          function sendMessage() {
          const message = document.getElementById('chatInput').value;
          if (message) {
              document.getElementById('chatInput').value = '';
              socket.emit('chat', message);
          }
          }
      
          async function initializeSocket() {
          const negotiateResponse = await fetch(`/api/negotiate`);
          if (!negotiateResponse.ok) {
              console.log("Failed to negotiate, status code =", negotiateResponse.status);
              return;
          }
          const negotiateJson = await negotiateResponse.json();
          socket = io(negotiateJson.endpoint, {
              path: negotiateJson.path,
              query: { access_token: negotiateJson.token }
          });
      
          socket.on('new message', (socketId, message) => {
              appendMessage(`${socketId.substring(0,5)}: ${message}`);
          })
          }
      
          initializeSocket();
      </script>
      </body>
      
      </html>
      
    3. HTML 페이지를 반환하려면 함수를 만들고 코드를 복사합니다.

      func new --template "Http Trigger" --name index
      
    4. src/functions/index.js 파일을 열고 다음 코드로 바꿉니다.

      const { app } = require('@azure/functions');
      
      const fs = require('fs').promises;
      const path = require('path')
      
      async function index(request, context) {
          try {
              context.log(`HTTP function processed request for url "${request.url}"`);
      
              const filePath = path.join(__dirname,'../public/index.html');
              const html = await fs.readFile(filePath);
              return {
                  body: html,
                  headers: {
                      'Content-Type': 'text/html'
                  }
              };
          } catch (error) {
              context.log(error);
              return {
                  status: 500,
                  jsonBody: error
              }
          }
      };
      
      app.http('index', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          handler: index
      });
      
      

로컬에서 앱을 실행하는 방법

코드를 준비한 후 지침에 따라 샘플을 실행합니다.

Azure 함수를 위한 Azure Storage 설정

Azure Functions는 로컬에서 실행하더라도 작동하려면 스토리지 계정이 필요합니다. 다음 두 옵션 중 하나를 선택합니다.

  • 무료 Azurite 에뮬레이터를 실행합니다.
  • Azure Storage 서비스를 사용합니다. 계속 사용하는 경우 비용이 발생할 수 있습니다.
  1. Azurite 설치
npm install -g azurite
  1. Azurite 스토리지 에뮬레이터를 시작:
azurite -l azurite -d azurite\debug.log
  1. AzureWebJobsStorage이(가) UseDevelopmentStorage=true(으)로 설정되어 있는지 확인합니다.

Socket.IO에 대한 Web PubSub 구성 설정

  1. 함수 앱에 연결 문자열을 추가합니다.
func settings add WebPubSubForSocketIOConnectionString "<connection string>"
  1. Socket.IO에 대한 Web PubSub에 허브 설정 추가
az webpubsub hub create -n <resource name> -g <resource group> --hub-name hub --event-handler url-template="tunnel:///runtime/webhooks/socketio" user-event-pattern="*"

연결 문자열은 Azure CLI 명령을 통해 가져올 수 있습니다.

az webpubsub key show -g <resource group> -n <resource name>

출력에는 primaryConnectionStringsecondaryConnectionString이 포함되어 있으며 둘 중 하나를 사용할 수 있습니다.

터널 설정

서버리스 모드에서는 서비스가 웹후크를 사용하여 함수를 트리거합니다. 로컬에서 앱을 실행하려고 할 때 중요한 문제는 서비스가 로컬 함수 엔드포인트에 액세스할 수 있도록 하는 것입니다.

이를 달성하는 가장 쉬운 방법은 터널 도구를 사용하는 것입니다.

  1. 터널 도구 설치:

    npm install -g @azure/web-pubsub-tunnel-tool
    
  2. 터널 실행

    awps-tunnel run --hub hub --connection "<connection string>" --upstream http://127.0.0.1:7071
    

    --upstream은 로컬 Azure 함수가 노출하는 URL입니다. 포트가 다를 수 있으며 다음 단계에서 함수를 시작할 때 출력을 확인할 수 있습니다.

샘플 앱 실행

터널 도구가 실행된 후에는 로컬에서 함수 앱을 실행할 수 있습니다.

func start

http://localhost:7071/api/index의 웹 페이지를 참조하세요.

서버리스 채팅 앱의 스크린샷.

다음 단계

다음으로, Bicep을 사용하여 ID 기반 인증을 통해 앱을 온라인에 배포해 볼 수 있습니다.