SignalR 실시간 통신
SignalR Hub를 연동하여 채팅이나 알림 같은 실시간 기능을 Blazor에서 구현합니다.
📌 학습 목표
- SignalR의 동작 원리와 WebSocket 기반 실시간 통신 흐름을 설명할 수 있습니다.
- 서버에 Hub 클래스를 작성하고
Program.cs에 등록할 수 있습니다. - Blazor 컴포넌트에서 HubConnection을 생성하고 연결·해제를 관리할 수 있습니다.
- 클라이언트에서 서버로 메시지를 전송하고, 서버에서 모든 클라이언트로 브로드캐스트할 수 있습니다.
- 연결 상태를 UI에 반영하고, 컴포넌트 소멸 시 리소스를 올바르게 정리할 수 있습니다.
📝 개념 설명
1. SignalR이란?
SignalR은 ASP.NET Core에서 제공하는 실시간 통신 라이브러리입니다. HTTP 폴링처럼 클라이언트가 반복해서 서버를 확인하는 방식이 아니라, 서버가 직접 클라이언트를 푸시(push)하여 데이터를 전달합니다.
SignalR은 브라우저 환경에 따라 전송 방식을 자동으로 선택합니다. 지원되는 경우 WebSocket을 우선 사용하고, 그렇지 않으면 Server-Sent Events, Long Polling 순으로 폴백(fallback)합니다. 개발자는 전송 방식을 직접 신경 쓰지 않아도 됩니다.
2. Hub의 역할
Hub는 SignalR의 핵심 추상화입니다. 서버 측에서 Hub 클래스를 정의하면, 연결된 모든 클라이언트를 대상으로 메서드를 호출하거나 특정 클라이언트에만 메시지를 보낼 수 있습니다. 클라이언트는 Hub에 연결한 후, 서버가 호출하는 메서드 이름에 핸들러를 등록합니다.
(Blazor)
(ChatHub)
(A, B, C…)
3. Blazor Server vs Blazor WebAssembly에서 SignalR
Blazor Server는 이미 내부적으로 SignalR을 사용합니다. 추가 패키지 없이 서버 Hub를 직접 참조할 수 있어 설정이 단순합니다.
Blazor WebAssembly는 별도의 SignalR 클라이언트 패키지(Microsoft.AspNetCore.SignalR.Client)를 설치해야 합니다. 브라우저가 WebAssembly를 실행하고, 그 안에서 JavaScript interop 없이 C#으로 SignalR 연결을 관리합니다.
💡 예제 & 실습
Step 1. 패키지 설치
Blazor WebAssembly 프로젝트(또는 클라이언트 프로젝트)에 SignalR 클라이언트 패키지를 추가합니다.
dotnet add package Microsoft.AspNetCore.SignalR.Client
Blazor Server 프로젝트라면 이 패키지는 이미 포함되어 있습니다.
Step 2. 서버 — Hub 클래스 작성
Hub를 상속한 클래스를 만들고, 클라이언트가 호출할 메서드를 정의합니다.
아래 예제는 채팅 메시지를 받아 모든 클라이언트에 브로드캐스트하는 ChatHub입니다.
// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;
public class ChatHub : Hub
{
// 클라이언트가 "SendMessage"를 호출하면 이 메서드가 실행됩니다
public async Task SendMessage(string user, string message)
{
// 연결된 모든 클라이언트의 "ReceiveMessage" 메서드를 호출합니다
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Step 3. 서버 — Hub 등록 (Program.cs)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// SignalR 서비스 등록
builder.Services.AddSignalR();
var app = builder.Build();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// Hub 엔드포인트 매핑
app.MapHub<ChatHub>("/chathub");
app.Run();
Step 4. Blazor 컴포넌트 — 연결 및 메시지 수신
HubConnection을 생성하고, 서버에서 호출할 메서드 이름(ReceiveMessage)에 핸들러를 등록합니다.
IAsyncDisposable을 구현하여 컴포넌트가 사라질 때 연결을 닫습니다.
@page "/chat"
@using Microsoft.AspNetCore.SignalR.Client
@implements IAsyncDisposable
<h3>실시간 채팅</h3>
<div>
<input @bind="_userName" placeholder="이름" />
<input @bind="_inputMessage" placeholder="메시지 입력" />
<button @onclick="SendMessage" disabled="@(!IsConnected)">전송</button>
</div>
<ul>
@foreach (var msg in _messages)
{
<li>@msg</li>
}
</ul>
<p>상태: @(_hubConnection?.State.ToString() ?? "미연결")</p>
@code {
private HubConnection? _hubConnection;
private List<string> _messages = new();
private string _userName = "익명";
private string _inputMessage = string.Empty;
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.WithAutomaticReconnect() // 자동 재연결 활성화
.Build();
// 서버가 "ReceiveMessage"를 호출하면 이 핸들러가 실행됩니다
_hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
_messages.Add($"[{user}] {message}");
InvokeAsync(StateHasChanged); // UI 갱신
});
await _hubConnection.StartAsync();
}
private async Task SendMessage()
{
if (_hubConnection is not null)
{
await _hubConnection.SendAsync("SendMessage", _userName, _inputMessage);
_inputMessage = string.Empty;
}
}
public bool IsConnected =>
_hubConnection?.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (_hubConnection is not null)
await _hubConnection.DisposeAsync();
}
}
WithUrl(Navigation.ToAbsoluteUri("/chathub"))—NavigationManager로 절대 URL을 구성합니다. 배포 환경에서 경로가 달라져도 안전합니다.WithAutomaticReconnect()— 네트워크 단절 시 자동으로 재연결을 시도합니다._hubConnection.On<T1, T2>("메서드명", 핸들러)— 서버가 해당 이름으로 호출할 때 실행될 클라이언트 핸들러를 등록합니다.InvokeAsync(StateHasChanged)— SignalR 콜백은 별도 스레드에서 실행되므로, UI 갱신은 반드시InvokeAsync를 통해 Blazor 렌더링 컨텍스트로 전달해야 합니다.
Step 5. 특정 클라이언트·그룹에 메시지 보내기
모든 클라이언트가 아닌 특정 대상에게 메시지를 보낼 수도 있습니다.
public class ChatHub : Hub
{
// 발신자를 제외한 나머지에게 전송
public async Task SendMessageExceptSelf(string user, string message)
{
await Clients.Others.SendAsync("ReceiveMessage", user, message);
}
// 특정 ConnectionId에게만 전송
public async Task SendPrivate(string connectionId, string message)
{
await Clients.Client(connectionId).SendAsync("ReceiveMessage", "비공개", message);
}
// 그룹에 추가 후 그룹 전체에 전송
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).SendAsync("ReceiveMessage", "시스템", $"{Context.ConnectionId}님이 입장했습니다.");
}
}
Step 6. 연결 상태 이벤트 처리
재연결 중이거나 연결이 끊어졌을 때 사용자에게 상태를 알려줄 수 있습니다.
_hubConnection.Reconnecting += error =>
{
_statusMessage = "재연결 중...";
InvokeAsync(StateHasChanged);
return Task.CompletedTask;
};
_hubConnection.Reconnected += connectionId =>
{
_statusMessage = "재연결 완료";
InvokeAsync(StateHasChanged);
return Task.CompletedTask;
};
_hubConnection.Closed += error =>
{
_statusMessage = "연결 종료";
InvokeAsync(StateHasChanged);
return Task.CompletedTask;
};
⚠️ 자주 틀리는 것 / 주의사항
-
StateHasChanged를 직접 호출하면 안 됩니다.
SignalR 콜백은 Blazor의 동기화 컨텍스트 밖에서 실행됩니다.StateHasChanged()를 직접 부르면 예외가 발생합니다. 반드시InvokeAsync(StateHasChanged)를 사용하세요. -
HubConnection을 DisposeAsync로 반드시 해제해야 합니다.
컴포넌트가 사라져도 연결이 유지되면 서버 리소스가 낭비됩니다.@implements IAsyncDisposable을 선언하고DisposeAsync에서_hubConnection.DisposeAsync()를 호출하세요. -
Hub 메서드 이름 대소문자를 정확히 맞춰야 합니다.
On<T>("ReceiveMessage", ...)에서 등록하는 메서드 이름과 서버의SendAsync("ReceiveMessage", ...)이름이 다르면 핸들러가 실행되지 않습니다. 대소문자까지 동일해야 합니다. -
Blazor WebAssembly에서 CORS 설정을 확인하세요.
클라이언트와 서버가 다른 출처(origin)에 있다면 서버에 CORS 정책을 추가해야 SignalR 연결이 허용됩니다. -
WithAutomaticReconnect()는 초기 연결 실패를 처리하지 않습니다.
자동 재연결은 기존 연결이 끊어진 뒤에만 동작합니다.StartAsync()단계에서 서버가 없으면 예외가 발생하므로, 초기 연결 시try-catch로 예외를 처리해야 합니다.
🎯 마무리
SignalR은 Blazor 애플리케이션에서 실시간 기능을 구현하는 가장 자연스러운 방법입니다.
서버에서는 Hub 클래스를 작성하고 엔드포인트를 등록하는 것이 전부입니다.
클라이언트에서는 HubConnection을 빌드하고, 서버가 호출할 메서드 이름에 핸들러를 등록한 뒤 StartAsync()로 연결합니다.
채팅처럼 간단한 예제부터 시작해 그룹 기능과 인증 기반 연결로 확장할 수 있습니다. 컴포넌트 생명주기에 따라 연결을 올바르게 시작하고 해제하는 습관을 들이면, 메모리 누수 없이 안정적인 실시간 기능을 구현할 수 있습니다.
- SignalR은 WebSocket 기반으로 서버→클라이언트 푸시 통신을 지원하며, 브라우저 환경에 따라 전송 방식을 자동 선택합니다.
- 서버에서는
Hub를 상속한 클래스를 작성하고,Program.cs에AddSignalR()과MapHub<T>()를 등록합니다. - 클라이언트에서는
HubConnectionBuilder로 연결 객체를 만들고,.On<T>()으로 핸들러를 등록한 뒤StartAsync()로 연결합니다. - SignalR 콜백에서 UI를 갱신할 때는 반드시
InvokeAsync(StateHasChanged)를 사용합니다. - 컴포넌트 소멸 시
IAsyncDisposable.DisposeAsync()에서 HubConnection을 해제하여 리소스를 반납합니다. Clients.All/Clients.Others/Clients.Group()으로 브로드캐스트 대상을 세밀하게 조정할 수 있습니다.