자습서: Azure Web PubSub 서비스를 사용하여 채팅 앱 만들기

메시지 게시 및 구독 자습서에서는 Azure Web PubSub를 통해 메시지를 게시하고 구독하는 기본 사항을 알아봅니다. 이 자습서에서는 Azure Web PubSub의 이벤트 시스템을 배우고 이를 사용하여 실시간 통신 기능을 갖춘 완전한 웹 애플리케이션을 빌드합니다.

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

  • Web PubSub 서비스 인스턴스 만들기
  • Azure Web PubSub에 대한 이벤트 처리기 설정 구성
  • 앱 서버에서 이벤트를 처리하고 실시간 채팅 앱 빌드

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

사전 요구 사항

  • 이렇게 설정하려면 버전 2.22.0 이상의 Azure CLI가 필요합니다. Azure Cloud Shell을 사용하는 경우 최신 버전이 이미 설치되어 있습니다.

Azure Web PubSub 인스턴스 만들기

리소스 그룹 만들기

리소스 그룹은 Azure 리소스가 배포 및 관리되는 논리적 컨테이너입니다. az group create 명령을 사용하여 eastus 위치에서 myResourceGroup이라는 리소스 그룹을 만듭니다.

az group create --name myResourceGroup --location EastUS

Web PubSub 인스턴스 만들기

az extension add를 실행하여 webpubsub 확장을 현재 버전으로 설치하거나 업그레이드합니다.

az extension add --upgrade --name webpubsub

Azure CLI az webpubsub create 명령을 사용하여 만든 리소스 그룹에 Web PubSub를 만듭니다. 다음 명령은 EastUS의 리소스 그룹 myResourceGroup 아래에 무료 Web PubSub 리소스를 만듭니다.

Important

각 Web PubSub 리소스에는 고유한 이름이 있어야 합니다. 다음 예제에서 <your-unique-resource-name>을 Web PubSub의 이름으로 바꿉니다.

az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1

이 명령의 출력에는 새로 만든 리소스의 속성이 표시됩니다. 아래에 나열된 두 개의 속성을 기록합니다.

  • 리소스 이름: 위의 --name 매개 변수에 제공한 이름입니다.
  • hostName: 이 예제에서 호스트 이름은 <your-unique-resource-name>.webpubsub.azure.com/입니다.

이때 Azure 계정은 이 새 리소스에서 모든 작업을 수행할 권한이 있는 유일한 계정입니다.

나중에 사용할 수 있도록 ConnectionString 가져오기

Important

연결 문자열에는 애플리케이션이 Azure Web PubSub 서비스에 액세스하는 데 필요한 권한 부여 정보가 포함됩니다. 연결 문자열 내의 액세스 키는 서비스의 루트 암호와 비슷합니다. 프로덕션 환경에서는 항상 액세스 키를 보호해야 합니다. Azure Key Vault를 사용하여 키를 안전하게 관리하고 회전합니다. 액세스 키를 다른 사용자에게 배포하거나 하드 코딩하거나 다른 사용자가 액세스할 수 있는 일반 텍스트로 저장하지 않도록 합니다. 키가 손상되었다고 생각되면 키를 교체하세요.

Azure CLI az webpubsub key 명령을 사용하여 서비스의 ConnectionString을 가져옵니다. <your-unique-resource-name> 자리 표시자를 Azure Web PubSub 인스턴스 이름으로 바꿉니다.

az webpubsub key show --resource-group myResourceGroup --name <your-unique-resource-name> --query primaryConnectionString --output tsv

나중에 사용할 수 있게 연결 문자열을 복사합니다.

가져온 ConnectionString을 복사하여 나중에 자습서에서 읽을 환경 변수 WebPubSubConnectionString에 설정합니다. 아래의 <connection-string>을 가져온 ConnectionString으로 바꿉니다.

export WebPubSubConnectionString="<connection-string>"
SET WebPubSubConnectionString=<connection-string>

프로젝트 설정

필수 조건

애플리케이션 만들기

Azure Web PubSub에는 서버 역할과 클라이언트 역할이 있습니다. 이 개념은 웹 애플리케이션의 서버 역할 및 클라이언트 역할과 비슷합니다. 서버는 클라이언트를 관리하고, 클라이언트 메시지를 수신 대기하고, 응답해야 합니다. 클라이언트는 사용자 메시지를 보내고 서버에서 메시지를 받고 최종 사용자를 위해 메시지를 시각화해야 합니다.

이 자습서에서는 실시간 채팅 웹 애플리케이션을 빌드합니다. 실제 웹 애플리케이션에서는 클라이언트를 인증하고 애플리케이션 UI에 대한 정적 웹 페이지를 제공하는 것도 서버의 역할에 포함됩니다.

ASP.NET Core 8을 사용하여 웹 페이지를 호스트하고 들어오는 요청을 처리합니다.

먼저 chatapp 폴더에 ASP.NET Core 웹앱을 만들겠습니다.

  1. 새 웹앱을 만듭니다.

    mkdir chatapp
    cd chatapp
    dotnet new web
    
  2. 정적 웹 페이지 호스팅을 지원하려면 app.UseStaticFiles() Program.cs를 추가합니다.

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    
    app.UseStaticFiles();
    
    app.Run();
    
  3. HTML 파일을 만들고 wwwroot/index.html로 저장합니다. 나중에 채팅 앱의 UI에 사용할 것입니다.

    <html>
      <body>
        <h1>Azure Web PubSub Chat</h1>
      </body>
    </html>
    

dotnet run --urls http://localhost:8080을 실행하여 서버를 테스트하고 브라우저에서 http://localhost:8080/index.html에 액세스할 수 있습니다.

협상 엔드포인트 추가

메시지 게시 및 구독 자습서에서 구독자는 연결 문자열을 직접 사용합니다. 실제 애플리케이션에서는 연결 문자열이 서비스에 대한 모든 작업을 수행할 수 있는 높은 권한을 갖기 때문에 클라이언트와 연결 문자열을 공유하는 것은 안전하지 않습니다. 이제 서버가 연결 문자열을 사용하고 클라이언트가 액세스 토큰이 포함된 전체 URL을 가져올 수 있도록 negotiate 엔드포인트를 노출하도록 하겠습니다. 이러한 방식으로 서버는 무단 액세스를 방지하기 위해 negotiate 엔드포인트 앞에 권한 부여 미들웨어를 추가할 수 있습니다.

먼저 종속성을 설치합니다.

dotnet add package Microsoft.Azure.WebPubSub.AspNetCore

이제 클라이언트가 토큰을 생성하기 위해 호출할 /negotiate 엔드포인트를 추가하겠습니다.

using Azure.Core;
using Microsoft.Azure.WebPubSub.AspNetCore;
using Microsoft.Azure.WebPubSub.Common;
using Microsoft.Extensions.Primitives;

// Read connection string from environment
var connectionString = Environment.GetEnvironmentVariable("WebPubSubConnectionString");
if (connectionString == null)
{
    throw new ArgumentNullException(nameof(connectionString));
}

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddWebPubSub(o => o.ServiceEndpoint = new WebPubSubServiceEndpoint(connectionString))
    .AddWebPubSubServiceClient<Sample_ChatApp>();
var app = builder.Build();

app.UseStaticFiles();

// return the Client Access URL with negotiate endpoint
app.MapGet("/negotiate", (WebPubSubServiceClient<Sample_ChatApp> service, HttpContext context) =>
{
    var id = context.Request.Query["id"];
    if (StringValues.IsNullOrEmpty(id))
    {
        context.Response.StatusCode = 400;
        return null;
    }
    return new
    {
        url = service.GetClientAccessUri(userId: id).AbsoluteUri
    };
});
app.Run();

sealed class Sample_ChatApp : WebPubSubHub
{
}

AddWebPubSubServiceClient<THub>()는 협상 단계에서 WebPubSubServiceClient<THub> 클라이언트 연결 토큰을 생성하고 허브 메서드에서 허브 이벤트가 트리거될 때 서비스 REST API를 호출하는 데 사용할 수 있는 서비스 클라이언트 를 삽입하는 데 사용됩니다. 이 토큰 생성 코드는 토큰을 생성할 때 인수(userId)를 하나 더 전달하는 것을 제외하고 메시지 게시 및 구독 자습서에서 사용한 코드와 비슷합니다. 사용자 ID를 사용하여 클라이언트 ID를 식별할 수 있으므로 메시지를 받을 때 어디서 오는 메시지인지 알 수 있습니다.

이 코드는 이전 단계에서 설정한 환경 변수 WebPubSubConnectionString에서 연결 문자열을 읽습니다.

dotnet run --urls http://localhost:8080을 사용하여 서버를 다시 실행합니다.

http://localhost:8080/negotiate?id=user1에 액세스하여 이 API를 테스트할 수 있으며 액세스 토큰과 함께 Azure Web PubSub의 전체 URL을 제공합니다.

이벤트 처리

Azure Web PubSub에서는 클라이언트 쪽에서 특정 작업이 발생하는 경우(예: 클라이언트 연결, 연결, 연결 끊김 또는 클라이언트가 메시지 보내기) 서비스가 서버에 알림을 보내 이러한 이벤트에 반응할 수 있도록 합니다.

이벤트는 Webhook 형식으로 서버에 전달됩니다. Webhook는 애플리케이션 서버를 통해 제공 및 노출되며 Azure Web PubSub 서비스 쪽에서 등록됩니다. 이 서비스는 이벤트가 발생할 때마다 webhook를 호출합니다.

Azure Web PubSub는 CloudEvents에 따라 이벤트 데이터를 설명합니다.

아래에서는 클라이언트가 연결될 때 connected 시스템 이벤트를 처리하고 클라이언트가 채팅 앱을 빌드하기 위해 메시지를 보낼 때 message 사용자 이벤트를 처리합니다.

이전 단계에서 설치한 AspNetCore용 Web PubSub SDK Microsoft.Azure.WebPubSub.AspNetCore도 CloudEvents 요청을 구문 분석하고 처리하는 데 도움이 될 수 있습니다.

먼저, app.Run() 앞에 이벤트 처리기를 추가합니다. 이벤트에 대한 엔드포인트 경로를 지정합니다. 여기서는 /eventhandler로 지정하겠습니다.

app.MapWebPubSubHub<Sample_ChatApp>("/eventhandler/{*path}");
app.Run();

이제 이전 단계에서 만든 Sample_ChatApp 클래스 내에 Web PubSub 서비스를 호출하는 데 사용하는 WebPubSubServiceClient<Sample_ChatApp>와 작동하는 생성자를 추가합니다. 그리고 connected 이벤트가 트리거될 때 응답하려면 OnConnectedAsync()을, 클라이언트의 메시지를 처리하려면 OnMessageReceivedAsync()를 사용합니다.

sealed class Sample_ChatApp : WebPubSubHub
{
    private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;

    public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
    {
        _serviceClient = serviceClient;
    }

    public override async Task OnConnectedAsync(ConnectedEventRequest request)
    {
        Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
    }

    public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
    {
        await _serviceClient.SendToAllAsync(RequestContent.Create(
        new
        {
            from = request.ConnectionContext.UserId,
            message = request.Data.ToString()
        }),
        ContentType.ApplicationJson);

        return new UserEventResponse();
    }
}

위 코드에서는 서비스 클라이언트를 사용하여 SendToAllAsync에 참여하는 모든 사용자에게 JSON 형식의 알림 메시지를 브로드캐스트합니다.

웹 페이지 업데이트

이제 연결하고, 메시지를 보내고, 받은 메시지를 페이지에 표시하는 논리를 추가하도록 index.html을 업데이트하겠습니다.

<html>
  <body>
    <h1>Azure Web PubSub Chat</h1>
    <input id="message" placeholder="Type to chat...">
    <div id="messages"></div>
    <script>
      (async function () {
        let id = prompt('Please input your user name');
        let res = await fetch(`/negotiate?id=${id}`);
        let data = await res.json();
        let ws = new WebSocket(data.url);
        ws.onopen = () => console.log('connected');

        let messages = document.querySelector('#messages');
        
        ws.onmessage = event => {
          let m = document.createElement('p');
          let data = JSON.parse(event.data);
          m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
          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>

위 코드에서 브라우저의 네이티브 WebSocket API를 사용하여 연결하고 WebSocket.send()를 사용하여 메시지를 보내고 WebSocket.onmessage를 사용하여 수신된 메시지를 수신 대기하는 것을 볼 수 있습니다.

또한 클라이언트 SDK를 사용하여 서비스에 연결할 수도 있으며, 이를 통해 자동 다시 연결, 오류 처리 등의 기능을 활용할 수 있습니다.

이제 채팅이 작동하려면 한 단계만 남았습니다. Web PubSub 서비스에서 관심 있는 이벤트와 이벤트를 보낼 위치를 구성하겠습니다.

이벤트 처리기 설정

Web PubSub 서비스에 이벤트 처리기를 설정하여 서비스에 이벤트를 보낼 위치를 알려 줍니다.

웹 서버가 로컬로 실행될 때 인터넷에 액세스할 수 있는 엔드포인트가 없는 경우 Web PubSub 서비스가 localhost를 호출하려면 어떻게 해야 하나요? 일반적으로 두 가지 방법이 있습니다. 하나는 일반 터널 도구를 사용하여 localhost를 공개적으로 노출하는 것이고, 다른 하나는 awps-tunnel을 사용하여 도구를 통해 Web PubSub 서비스의 트래픽을 로컬 서버로 터널링하는 것입니다.

이 섹션에서는 Azure CLI를 사용하여 이벤트 처리기를 설정하고 awps-tunnel을 사용하여 트래픽을 localhost로 라우팅합니다.

허브 설정 구성

Web PubSub가 awps-tunnel의 터널 연결을 통해 메시지를 라우팅하도록 tunnel 체계를 사용하도록 URL 템플릿을 설정합니다. 이 문서에 설명된 대로 포털 또는 CLI에서 이벤트 처리기를 설정할 수 있습니다. 여기서는 CLI를 통해 설정합니다. 이전 단계에서 설정한 대로 /eventhandler 경로의 이벤트를 수신 대기하므로 URL 템플릿을 tunnel:///eventhandler로 설정합니다.

Azure CLI az webpubsub hub create 명령을 사용하여 Sample_ChatApp 허브에 대한 이벤트 처리기 설정을 만듭니다.

Important

<your-unique-resource-name>을 이전 단계에서 만든 Web PubSub 리소스의 이름으로 바꿉니다.

az webpubsub hub create -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected"

awps-tunnel을 로컬에서 실행

awps-tunnel 다운로드 및 설치

이 도구는 Node.js 버전 16 이상에서 실행됩니다.

npm install -g @azure/web-pubsub-tunnel-tool

서비스 연결 문자열 사용 및 실행

export WebPubSubConnectionString="<your connection string>"
awps-tunnel run --hub Sample_ChatApp --upstream http://localhost:8080

웹 서버 실행

이제 모든 것이 설정되었습니다. 웹 서버를 실행하고 실제로 채팅 앱을 사용하겠습니다.

이제 dotnet run --urls http://localhost:8080을 사용하여 서버를 실행합니다.

이 자습서의 전체 코드 샘플은 여기서 찾을 수 있습니다.

http://localhost:8080/index.html을(를) 여십시오. 사용자 이름을 입력하고 채팅을 시작할 수 있습니다.

connect 이벤트 처리기를 사용한 지연 인증

이전 섹션에서는 협상 엔드포인트를 사용하여 클라이언트가 Web PubSub 서비스에 연결하기 위한 Web PubSub 서비스 URL과 JWT 액세스 토큰을 반환하는 방법을 보여 주었습니다. 예를 들어, 리소스가 제한된 에지 디바이스와 같은 경우 클라이언트는 Web PubSub 리소스에 직접 연결하는 것을 선호할 수 있습니다. 이러한 경우 클라이언트 인증을 지연하도록 connect 이벤트 처리기를 구성하고, 클라이언트에 사용자 ID를 할당하고, 클라이언트가 조인되면 클라이언트가 조인하는 그룹을 할당하고, 클라이언트가 갖는 권한과 클라이언트에 대한 WebSocket 응답으로 WebSocket 하위 프로토콜을 구성하는 등의 작업을 수행할 수 있습니다. 자세한 내용은 연결 이벤트 처리기 사양을 참조하세요.

이제 connect 이벤트 처리기를 사용하여 협상 섹션에서 수행하는 것과 유사한 작업을 수행하겠습니다.

허브 설정 업데이트

먼저 connect 이벤트 처리기도 포함하도록 허브 설정을 업데이트하겠습니다. JWT 액세스 토큰이 없는 클라이언트가 서비스에 연결할 수 있도록 익명 연결도 허용해야 합니다.

Azure CLI az webpubsubhub update 명령을 사용하여 Sample_ChatApp 허브에 대한 이벤트 처리기 설정을 만듭니다.

Important

<your-unique-resource-name>을 이전 단계에서 만든 Web PubSub 리소스의 이름으로 바꿉니다.

az webpubsub hub update -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --allow-anonymous true --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected" system-event="connect"

연결 이벤트를 처리하도록 업스트림 논리 업데이트

이제 연결 이벤트를 처리하도록 업스트림 논리를 업데이트하겠습니다. 협상 엔드포인트를 제거할 수도 있습니다.

데모 목적으로 엔드포인트 협상에서 수행하는 작업과 유사하게 쿼리 매개 변수에서 ID도 읽습니다. 연결 이벤트에서는 원래 클라이언트 쿼리가 연결 이벤트 요청 본문에 보존됩니다.

Sample_ChatApp 클래스 내에서 OnConnectAsync()를 재정의하여 connect 이벤트를 처리합니다.

sealed class Sample_ChatApp : WebPubSubHub
{
    private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;

    public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
    {
        _serviceClient = serviceClient;
    }

    public override ValueTask<ConnectEventResponse> OnConnectAsync(ConnectEventRequest request, CancellationToken cancellationToken)
    {
        if (request.Query.TryGetValue("id", out var id))
        {
            return new ValueTask<ConnectEventResponse>(request.CreateResponse(userId: id.FirstOrDefault(), null, null, null));
        }

        // The SDK catches this exception and returns 401 to the caller
        throw new UnauthorizedAccessException("Request missing id");
    }

    public override async Task OnConnectedAsync(ConnectedEventRequest request)
    {
        Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
    }

    public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
    {
        await _serviceClient.SendToAllAsync(RequestContent.Create(
        new
        {
            from = request.ConnectionContext.UserId,
            message = request.Data.ToString()
        }),
        ContentType.ApplicationJson);

        return new UserEventResponse();
    }
}

직접 연결하려면 index.html을 업데이트합니다.

이제 Web PubSub 서비스에 직접 연결하도록 웹 페이지를 업데이트하겠습니다. 한 가지 언급할 점은 이제 데모 목적으로 Web PubSub 서비스 엔드포인트가 클라이언트 코드에 하드 코딩되어 있다는 것입니다. 아래 HTML의 서비스 호스트 이름 <the host name of your service>를 자체 서비스의 값으로 업데이트하세요. 서버에서 Web PubSub 서비스 엔드포인트 값을 가져오는 것이 여전히 유용할 수 있으며, 클라이언트가 연결되는 위치에 더 많은 유연성과 제어 가능성을 제공합니다.

<html>
  <body>
    <h1>Azure Web PubSub Chat</h1>
    <input id="message" placeholder="Type to chat...">
    <div id="messages"></div>
    <script>
      (async function () {
        // sample host: mock.webpubsub.azure.com
        let hostname = "<the host name of your service>";
        let id = prompt('Please input your user name');
        let ws = new WebSocket(`wss://${hostname}/client/hubs/Sample_ChatApp?id=${id}`);
        ws.onopen = () => console.log('connected');

        let messages = document.querySelector('#messages');
        
        ws.onmessage = event => {
          let m = document.createElement('p');
          let data = JSON.parse(event.data);
          m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
          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>

서버를 다시 실행

이제 서버를 다시 실행하고 이전 지침에 따라 웹 페이지를 참조하세요. awps-tunnel을 중지한 경우 터널 도구를 다시 실행하시기 바랍니다.

다음 단계

이 자습서에서는 Azure Web PubSub 서비스에서 이벤트 시스템이 작동하는 방식에 대한 기본 개념을 제공합니다.

다른 자습서를 확인하여 서비스 사용 방법을 자세히 알아보세요.