빠른 시작: SignalR Service를 사용하여 채팅방 만들기

Azure SignalR Service는 개발자가 실시간 기능으로 손쉽게 웹 애플리케이션을 빌드할 수 있게 하는 Azure 서비스입니다.

이 문서에서는 Azure SignalR Service를 시작하는 방법을 보여줍니다. 이 빠른 시작에서는 ASP.NET Core 웹앱을 사용하여 채팅 애플리케이션을 만듭니다. 이 앱은 Azure SignalR Service 리소스와 연결하여 실시간 콘텐츠 업데이트를 사용하도록 설정합니다. 웹 애플리케이션을 로컬로 호스팅하고 여러 브라우저 클라이언트에 연결합니다. 각 클라이언트는 다른 모든 클라이언트에 콘텐츠 업데이트 푸시할 수 있습니다.

이 빠른 시작의 단계를 완료하려면 아무 코드 편집기나 사용할 수 있습니다. 한 가지 옵션은 Windows, macOS 및 Linux 플랫폼에서 사용할 수 있는 Visual Studio Code입니다.

이 자습서에 대한 코드는 AzureSignalR-samples GitHub 리포지토리에서 다운로드할 수 있습니다. SignalR Service 만들기 스크립트에 따라 이 빠른 시작에 사용되는 Azure 리소스를 만들 수 있습니다.

Azure 구독이 아직 없는 경우 시작하기 전에 체험 계정을 만듭니다.

시작해 볼까요?

필수 조건

문제가 있나요? 문제 해결 가이드를 사용해 보거나 알려주세요.

Azure SignalR 리소스 만들기

이 섹션에서는 앱에 사용할 기본 Azure SignalR 인스턴스를 만듭니다. 다음 단계에서는 Azure Portal을 사용하여 새 인스턴스를 만들지만 Azure CLI를 사용할 수도 있습니다. 자세한 내용은 Azure SignalR Service CLI 참조에서 az signalr create 명령을 참조하세요.

  1. Azure Portal에 로그인합니다.
  2. 페이지의 왼쪽 상단에서 + 리소스 만들기를 선택합니다.
  3. 리소스 만들기 페이지의 검색 서비스 및 마켓플레이스 텍스트 상자에 signalr을 입력한 다음, 목록에서 SignalR Service를 선택합니다.
  4. SignalR Service 페이지에서 만들기를 선택합니다.
  5. 기본 탭에서 새 SignalR Service 인스턴스에 대한 필수 정보를 입력합니다. 다음 값을 입력합니다.
필드 제안 값 설명
구독 구독 선택 새 SignalR Service 인스턴스를 만드는 데 사용할 구독을 선택합니다.
리소스 그룹 SignalRTestResources라는 리소스 그룹 만들기 SignalR 리소스에 대한 리소스 그룹을 선택하거나 만듭니다. 기존 리소스 그룹을 사용하는 대신 이 자습서에 대한 새 리소스 그룹을 만드는 것이 유용합니다. 자습서를 완료한 후 리소스를 확보하려면 리소스 그룹을 삭제합니다.

리소스 그룹을 삭제하면 해당 그룹에 속한 리소스도 모두 삭제됩니다. 이 작업은 취소할 수 없습니다. 리소스 그룹을 삭제하기 전에 유지하려는 리소스가 포함되어 있지 않은지 확인합니다.

자세한 내용은 리소스 그룹을 사용하여 Azure 리소스 관리를 참조하세요.
리소스 이름 testsignalr SignalR 리소스에 사용할 고유한 리소스 이름을 입력합니다. 해당 지역에서 이미 testsignalr을 사용하고 있는 경우 이름이 고유해질 때까지 숫자나 문자를 추가합니다.

이름은 1~63자의 문자열로, 숫자, 영문자 및 하이픈(-) 문자만 포함할 수 있습니다. 이름은 하이픈 문자로 시작하거나 끝날 수 없고 연속되는 하이픈 문자는 유효하지 않습니다.
지역 지역 선택 새 SignalR Service 인스턴스에 적합한 지역을 선택합니다.

Azure SignalR Service는 현재 일부 지역에서 사용할 수 없습니다. 자세한 내용은 Azure SignalR Service 지역 가용성을 참조하세요.
가격 책정 계층 변경을 선택한 다음, 무료(개발/테스트 전용)를 선택합니다. 선택을 선택하여 선택한 가격 책정 계층을 확인합니다. Azure SignalR Service에는 무료, 표준 및 프리미엄의 세 가지 가격 책정 계층이 있습니다. 자습서는 필수 구성 요소에 달리 명시되지 않는 한 무료 계층을 사용합니다.

계층과 가격 책정 간의 기능 차이에 대한 자세한 내용은 Azure SignalR Service 가격 책정을 참조하세요.
서비스 모드 적절한 서비스 모드 선택 웹앱에서 SignalR 허브 논리를 호스팅하고 SignalR 서비스를 프록시로 사용하는 경우 기본값을 사용합니다. Azure Functions와 같은 서버리스 기술을 사용하여 SignalR 허브 논리를 호스팅하는 경우 서버리스를 사용합니다.

클래식 모드는 이전 버전과의 호환성만을 위한 것이므로 사용하지 않는 것이 좋습니다.

자세한 내용은 Azure SignalR Service의 서비스 모드를 참조하세요.

SignalR 자습서의 네트워킹태그 탭에서 설정을 변경할 필요가 없습니다.

  1. 기본 탭의 아래쪽에서 검토 + 만들기 단추를 선택합니다.
  2. 검토 + 만들기 탭에서 해당 값을 검토한 다음, 만들기를 선택합니다. 배포가 완료되는 데 몇 분 정도 걸립니다.
  3. 배포가 완료되면 리소스로 이동 단추를 선택합니다.
  4. SignalR 리소스 페이지의 Settings 아래 왼쪽 메뉴에서 Keys를 선택합니다.
  5. 기본 키의 연결 문자열을 복사합니다. 이 자습서의 뒷부분에서 앱을 구성하려면 이 연결 문자열이 필요합니다.

ASP.NET Core 웹앱 만들기

이 섹션에서는 .NET Core CLI(명령줄 인터페이스)를 사용하여 새 ASP.NET Core MVC 웹앱 프로젝트를 만듭니다. Visual Studio 대신 .NET Core CLI를 사용하면 Windows, macOS 및 Linux 플랫폼에서 사용할 수 있다는 이점이 있습니다.

  1. 프로젝트 폴더를 만듭니다. 이 빠른 시작에서는 chattest 폴더를 사용합니다.

  2. 새 폴더에서 다음 명령을 실행하여 프로젝트를 만듭니다.

    dotnet new web
    

프로젝트에 암호 관리자 추가

이 섹션에서는 비밀 관리자 도구를 프로젝트에 추가합니다. 비밀 관리자 도구는 개발 작업용 중요 데이터를 프로젝트 트리 외부에 저장합니다. 이 방법을 사용하면 소스 코드에서 앱 비밀을 실수로 공유하지 못하도록 방지할 수 있습니다.

  1. 폴더에서 다음 명령을 실행하여 UserSecretsId를 초기화합니다.

    dotnet user-secrets init
    
  2. Azure:SignalR:ConnectionString이라는 암호를 암호 관리자에 추가합니다.

    이 암호는 SignalR Service 리소스에 액세스하기 위한 연결 문자열을 포함합니다. Azure:SignalR:ConnectionString은 SignalR에서 연결을 설정하기 위해 찾는 기본 구성 키입니다. 다음 명령의 값을 SignalR Service 리소스에 대한 연결 문자열로 바꿉니다.

    이 명령은 csproj 파일과 동일한 디렉터리에서 실행해야 합니다.

    dotnet user-secrets set Azure:SignalR:ConnectionString "<Your connection string>"
    

    비밀 관리자는 로컬로 호스팅되는 동안 웹앱을 테스트하는 데만 사용됩니다. 자습서의 뒷부분에서는 채팅 웹앱을 Azure에 배포합니다. 웹앱이 Azure에 배포되면 비밀 관리자를 통해 연결 문자열을 저장하는 대신 애플리케이션 설정을 사용합니다.

    이 비밀은 구성 API를 사용하여 액세스됩니다. 콜론(:)은 지원되는 모든 플랫폼에서 구성 API를 통해 구성 이름에서 작동합니다. 환경별 구성을 참조하세요.

웹앱에 Azure SignalR 추가

  1. 다음 명령을 실행하여 Microsoft.Azure.SignalR NuGet 패키지에 대한 참조를 추가합니다.

    dotnet add package Microsoft.Azure.SignalR
    
  2. Program.cs를 열고 코드를 다음과 같이 업데이트합니다. Azure SignalR Service를 사용하기 위해 AddSignalR()AddAzureSignalR() 메서드를 호출합니다.

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddSignalR().AddAzureSignalR();
    var app = builder.Build();
    
    app.UseDefaultFiles();
    app.UseRouting();
    app.UseStaticFiles();
    app.MapHub<ChatSampleHub>("/chat");
    app.Run();
    

    매개 변수를 AddAzureSignalR()에 전달하지 않으면 SignalR Service 리소스 연결 문자열에 대한 기본 구성 키를 사용한다는 의미입니다. 기본 구성 키는 Azure:SignalR:ConnectionString입니다 또한 아래 섹션에서 만들 ChatSampleHub를 사용합니다.

허브 클래스 추가

SignalR에서 허브는 클라이언트에서 호출할 수 있는 메서드 세트를 노출하는 핵심 구성 요소입니다. 이 섹션에서는 다음 두 가지 방법으로 허브 클래스를 정의합니다.

  • BroadcastMessage: 이 메서드는 모든 클라이언트에 메시지를 브로드캐스트합니다.
  • Echo: 이 메서드는 호출자에게 메시지를 보냅니다.

두 메서드는 모두 ASP.NET Core SignalR SDK에서 제공하는 Clients 인터페이스를 사용합니다. 이 인터페이스를 통해 연결된 모든 클라이언트에 액세스할 수 있으므로 콘텐츠를 클라이언트로 푸시할 수 있습니다.

  1. 프로젝트 디렉터리에서 Hub라는 새 폴더를 추가합니다. 새 폴더에 ChatSampleHub.cs라는 새 허브 코드 파일을 추가합니다.

  2. 허브 클래스를 정의하고 파일을 저장하려면 ChatSampleHub.cs에 다음 코드를 추가합니다.

    using Microsoft.AspNetCore.SignalR;
    
    public class ChatSampleHub : Hub
    {
        public Task BroadcastMessage(string name, string message) =>
            Clients.All.SendAsync("broadcastMessage", name, message);
    
        public Task Echo(string name, string message) =>
            Clients.Client(Context.ConnectionId)
                    .SendAsync("echo", name, $"{message} (echo from server)");
    }
    

웹앱용 클라이언트 인터페이스 추가

이 대화방 앱용 클라이언트 사용자 인터페이스는 wwwroot 디렉터리의 index.html 파일에 HTML 및 JavaScript로 구성됩니다.

샘플 리포지토리wwwroot 폴더에서 css/site.css 파일을 복사합니다. 프로젝트의 css/site.css를 복사한 것으로 바꿉니다.

index.html이라는 wwwroot 디렉터리에 새 파일을 만들고, 다음 HTML을 새로 만든 파일에 복사하여 붙여넣습니다.

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
  <meta name="viewport" content="width=device-width">
  <meta http-equiv="Pragma" content="no-cache" />
  <meta http-equiv="Expires" content="0" />
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet" />
  <link href="css/site.css" rel="stylesheet" />
  <title>Azure SignalR Group Chat</title>
</head>
<body>
  <h2 class="text-center" style="margin-top: 0; padding-top: 30px; padding-bottom: 30px;">Azure SignalR Group Chat</h2>
  <div class="container" style="height: calc(100% - 110px);">
    <div id="messages" style="background-color: whitesmoke; "></div>
    <div style="width: 100%; border-left-style: ridge; border-right-style: ridge;">
      <textarea id="message" style="width: 100%; padding: 5px 10px; border-style: hidden;"
        placeholder="Type message and press Enter to send..."></textarea>
    </div>
    <div style="overflow: auto; border-style: ridge; border-top-style: hidden;">
      <button class="btn-warning pull-right" id="echo">Echo</button>
      <button class="btn-success pull-right" id="sendmessage">Send</button>
    </div>
  </div>
  <div class="modal alert alert-danger fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <div>Connection Error...</div>
          <div><strong style="font-size: 1.5em;">Hit Refresh/F5</strong> to rejoin. ;)</div>
        </div>
      </div>
    </div>
  </div>

  <!--Reference the SignalR library. -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>

  <!--Add script to update the page and send messages.-->
  <script type="text/javascript">
    document.addEventListener("DOMContentLoaded", function () {
      function getUserName() {
        function generateRandomName() {
          return Math.random().toString(36).substring(2, 10);
        }

        // Get the user name and store it to prepend to messages.
        var username = generateRandomName();
        var promptMessage = "Enter your name:";
        do {
          username = prompt(promptMessage, username);
          if (!username || username.startsWith("_") || username.indexOf("<") > -1 || username.indexOf(">") > -1) {
            username = "";
            promptMessage = "Invalid input. Enter your name:";
          }
        } while (!username)
        return username;
      }

      username = getUserName();
      // Set initial focus to message input box.
      var messageInput = document.getElementById("message");
      messageInput.focus();

      function createMessageEntry(encodedName, encodedMsg) {
        var entry = document.createElement("div");
        entry.classList.add("message-entry");
        if (encodedName === "_SYSTEM_") {
          entry.innerHTML = encodedMsg;
          entry.classList.add("text-center");
          entry.classList.add("system-message");
        } else if (encodedName === "_BROADCAST_") {
          entry.classList.add("text-center");
          entry.innerHTML = `<div class="text-center broadcast-message">${encodedMsg}</div>`;
        } else if (encodedName === username) {
          entry.innerHTML = `<div class="message-avatar pull-right">${encodedName}</div>` +
            `<div class="message-content pull-right">${encodedMsg}<div>`;
        } else {
          entry.innerHTML = `<div class="message-avatar pull-left">${encodedName}</div>` +
            `<div class="message-content pull-left">${encodedMsg}<div>`;
        }
        return entry;
      }

      function appendMessage(encodedName, encodedMsg) {
        var messageEntry = createMessageEntry(encodedName, encodedMsg);
        var messageBox = document.getElementById("messages");
        messageBox.appendChild(messageEntry);
        messageBox.scrollTop = messageBox.scrollHeight;
      }

      function bindConnectionMessage(connection) {
        var messageCallback = function (name, message) {
          if (!message) return;
          // Html encode display name and message.
          var encodedName = name;
          var encodedMsg = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
          appendMessage(encodedName, encodedMsg);
        };
        // Create a function that the hub can call to broadcast messages.
        connection.on("broadcastMessage", messageCallback);
        connection.on("echo", messageCallback);
        connection.onclose(onConnectionError);
      }

      function onConnected(connection) {
        console.log("connection started");
        connection.send("broadcastMessage", "_SYSTEM_", username + " JOINED");
        document.getElementById("sendmessage").addEventListener("click", function (event) {
          // Call the broadcastMessage method on the hub.
          if (messageInput.value) {
            connection.send("broadcastMessage", username, messageInput.value)
              .catch((e) => appendMessage("_BROADCAST_", e.message));
          }

          // Clear text box and reset focus for next comment.
          messageInput.value = "";
          messageInput.focus();
          event.preventDefault();
        });
        document.getElementById("message").addEventListener("keypress", function (event) {
          if (event.keyCode === 13) {
            event.preventDefault();
            document.getElementById("sendmessage").click();
            return false;
          }
        });
        document.getElementById("echo").addEventListener("click", function (event) {
          // Call the echo method on the hub.
          connection.send("echo", username, messageInput.value);

          // Clear text box and reset focus for next comment.
          messageInput.value = "";
          messageInput.focus();
          event.preventDefault();
        });
      }

      function onConnectionError(error) {
        if (error && error.message) {
          console.error(error.message);
        }
        var modal = document.getElementById("myModal");
        modal.classList.add("in");
        modal.style = "display: block;";
      }

      var connection = new signalR.HubConnectionBuilder()
        .withUrl("/chat")
        .build();
      bindConnectionMessage(connection);
      connection.start()
        .then(function () {
          onConnected(connection);
        })
        .catch(function (error) {
          console.error(error.message);
        });
    });
  </script>
</body>
</html>

index.html의 코드는 HubConnectionBuilder.build()를 호출하여 Azure SignalR 리소스에 대한 HTTP 연결을 수행합니다.

연결이 성공적이면 해당 연결이 bindConnectionMessage로 전달되고, 들어오는 콘텐츠의 클라이언트 푸시를 위한 이벤트 처리기가 추가됩니다.

HubConnection.start()는 허브와의 통신을 시작합니다. 그런 다음, onConnected()에서 단추 이벤트 처리기를 추가합니다. 이러한 처리기는 연결을 사용하여 이 클라이언트가 연결된 모든 클라이언트에 콘텐츠 업데이트를 푸시할 수 있도록 합니다.

로컬로 앱 빌드 및 실행

  1. 다음 명령을 실행하여 웹앱을 로컬로 실행합니다.

    dotnet run
    

    앱은 다음과 같이 localhost URL이 포함된 출력을 통해 로컬로 호스트됩니다.

    Building...
    info: Microsoft.Hosting.Lifetime[14]
          Now listening on: http://localhost:5000
    info: Microsoft.Hosting.Lifetime[0]
          Application started. Press Ctrl+C to shut down.
    info: Microsoft.Hosting.Lifetime[0]
          Hosting environment: Development
    
  2. 두 개의 브라우저 창을 엽니다. 각 브라우저에서 출력 창에 표시된 localhost URL(예: 위 출력 창에 표시된 대로 http://localhost:5000/)로 이동합니다. 이름을 입력하라는 메시지가 표시됩니다. 두 클라이언트에 대한 클라이언트 이름을 입력하고, 보내기 단추를 사용하여 두 클라이언트 간의 메시지 콘텐츠 푸시를 테스트합니다.

    Example of an Azure SignalR group chat

리소스 정리

다음 자습서로 계속 진행하려면 이 빠른 시작에서 만든 리소스를 유지하여 다시 사용할 수 있습니다.

빠른 시작 샘플 애플리케이션의 사용이 완료되면 요금이 청구되지 않도록 이 빠른 시작에서 만든 Azure 리소스를 삭제할 수 있습니다.

Important

리소스 그룹을 삭제하면 되돌릴 수 없으며 해당 그룹의 모든 리소스가 포함됩니다. 잘못된 리소스 그룹 또는 리소스를 자동으로 삭제하지 않도록 해야 합니다. 유지하려는 리소스가 포함된 기존 리소스 그룹에 이 샘플의 리소스를 만든 경우 리소스 그룹을 삭제하는 대신, 해당 블레이드에서 각 리소스를 개별적으로 삭제할 수 있습니다.

Azure Portal에 로그인하고 리소스 그룹을 선택합니다.

이름으로 필터링 텍스트 상자에서 리소스 그룹의 이름을 입력합니다. 이 빠른 시작의 지침에서는 SignalRTestResources라는 리소스 그룹을 사용합니다. 결과 목록의 리소스 그룹에서 줄임표(...) >리소스 그룹 삭제를 선택합니다.

Selections for deleting a resource group

리소스 그룹 삭제를 확인하는 메시지가 표시됩니다. 리소스 그룹의 이름을 입력하여 확인하고 삭제를 선택합니다.

잠시 후, 리소스 그룹 및 모든 해당 리소스가 삭제됩니다.

문제가 있나요? 문제 해결 가이드를 사용해 보거나 알려주세요.

다음 단계

이 빠른 시작에서는 새 Azure SignalR Service 리소스를 만들었습니다. 그런 다음, ASP.NET Core 웹앱에서 이를 사용하여 콘텐츠 업데이트를 연결된 여러 클라이언트에 실시간으로 푸시했습니다. Azure SignalR Service를 사용하는 방법을 자세히 알아보려면 인증을 시연하는 다음 자습서로 계속 진행하세요.