ASP.NET Core SignalR은 서버와 클라이언트 간의 양방향 실시간 통신을 가능하게 하는 라이브러리입니다. 이를 통해 웹 애플리케이션에 채팅, 알림, 실시간 대시보드와 같은 기능을 쉽게 통합할 수 있습니다.
주요 NuGet 패키지는 다음과 같습니다:
Microsoft.AspNetCore.SignalR.Client: Blazor WebAssembly 등 클라이언트 애플리케이션에서 SignalR 허브에 연결할 때 사용합니다.Microsoft.AspNetCore.SignalR.StackExchangeRedis: Redis를 이용하여 SignalR 서비스를 분산 환경에 배포할 때 사용합니다.
1. SignalR의 기본적인 사용법
SignalR은 Hub 클래스를 사용하여 서버와 클라이언트 간의 연결을 관리하고 메시지 교환을 처리합니다. 클라이언트는 허브에 연결하고 메시지를 수신하거나 서버로 요청을 보낼 수 있습니다.
1.1. 서버 측 구현 (ASP.NET Core)
먼저 ASP.NET Core 애플리케이션에 SignalR 서비스를 구성하고 허브 클래스를 생성해야 합니다. 허브 클래스는 클라이언트 요청을 처리하고 클라이언트로 메시지를 전송하는 로직을 포함합니다.
// Hubs 폴더 내에 실시간 통신을 처리할 허브 클래스를 정의합니다.
// NotificationHub.cs
public class NotificationHub : Hub
{
// 클라이언트가 호출할 수 있는 메서드입니다.
public async Task SendGlobalNotification(string senderId, string content)
{
string connectionIdentifier = Context.ConnectionId; // 현재 연결된 클라이언트의 ID
string formattedMessage = $"[{connectionIdentifier}] {senderId}: {content}";
// 연결된 모든 클라이언트에게 "ReceiveNotification" 이벤트를 통해 메시지를 전송합니다.
await Clients.All.SendAsync("ReceiveNotification", formattedMessage);
}
// 클라이언트 연결/해제 시 추가 로직을 구현할 수 있습니다.
public override Task OnConnectedAsync()
{
Console.WriteLine($"클라이언트 연결됨: {Context.ConnectionId}");
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception? exception)
{
Console.WriteLine($"클라이언트 연결 해제됨: {Context.ConnectionId}, Exception: {exception?.Message}");
return base.OnDisconnectedAsync(exception);
}
}
다음으로, Program.cs 파일에서 SignalR 서비스를 등록하고 엔드포인트를 매핑합니다.
// Program.cs
var appBuilder = WebApplication.CreateBuilder(args);
// SignalR 서비스 등록
appBuilder.Services.AddSignalR();
// CORS (Cross-Origin Resource Sharing) 설정:
// Blazor WebAssembly와 같은 SPA 클라이언트에서 서버에 접근할 수 있도록 허용합니다.
string[] allowedOrigins = new[] { "https://localhost:7042", "http://localhost:5042" }; // 클라이언트 URL
appBuilder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policyBuilder =>
{
policyBuilder.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials(); // 자격 증명(쿠키, HTTP 인증) 허용
});
});
var webApp = appBuilder.Build();
webApp.UseHttpsRedirection();
webApp.UseAuthorization();
// CORS 미들웨어 활성화
webApp.UseCors();
// SignalR 허브 엔드포인트 매핑:
// 클라이언트가 '/hub/notification' 경로로 접속하면 NotificationHub가 처리합니다.
webApp.MapHub<NotificationHub>("/hub/notification");
webApp.MapControllers(); // API 컨트롤러가 있다면 매핑
webApp.Run();
참고 사항:
- SignalR은 WebSocket을 기반으로 하며, HTTP를 통한 초기 핸드셰이크(Handshake) 과정을 거칩니다.
- 허브(
Hub) 인스턴스는 요청당 생성되는 일시적인(Transient) 생명주기를 가지므로, 허브 내부에 상태를 직접 유지해서는 안 됩니다. - 다양한 클라이언트(JavaScript, .NET, Java 등)를 위한 SignalR SDK가 제공됩니다.
1.2. 클라이언트 측 구현 (Blazor WebAssembly)
Blazor WebAssembly 애플리케이션에서 SignalR 서버에 연결하고 메시지를 주고받는 예제입니다.
먼저 Microsoft.AspNetCore.SignalR.Client NuGet 패키지를 설치한 후, 아래와 같이 UI를 구성하고 코드 블록을 작성합니다.
@page "/"
<h3>사용자 이름:</h3>
<input @bind="userNameInput" />
<h3>메시지:</h3>
<input @bind="currentMessage" />
<button @onclick="SendMessage" disabled="@(!IsConnectedToHub)">전송</button>
<ul>
@foreach (var msg in receivedMessages)
{
<li>@msg</li>
}
</ul>
@code {
private HubConnection? hubClientConnection; // SignalR 허브 연결 객체
private string? userNameInput; // 사용자 입력 이름
private string? currentMessage; // 사용자 입력 메시지
private List<string> receivedMessages = new(); // 수신된 메시지 목록
protected override async Task OnInitializedAsync()
{
// 허브 연결을 설정합니다.
hubClientConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7110/hub/notification") // 서버 허브 엔드포인트 URL
.WithAutomaticReconnect() // 자동 재연결 설정
.Build();
// 서버로부터 "ReceiveNotification" 메시지를 수신할 때 실행될 핸들러를 등록합니다.
hubClientConnection.On<string>("ReceiveNotification", (messageContent) =>
{
receivedMessages.Add(messageContent);
InvokeAsync(StateHasChanged); // UI 업데이트
});
// 허브 연결을 시작합니다.
await hubClientConnection.StartAsync();
}
// 메시지 전송 버튼 클릭 시 호출됩니다.
private async Task SendMessage()
{
if (hubClientConnection is not null && userNameInput is not null && currentMessage is not null)
{
// 서버 허브의 "SendGlobalNotification" 메서드를 호출합니다.
await hubClientConnection.SendAsync("SendGlobalNotification", userNameInput, currentMessage);
currentMessage = string.Empty; // 메시지 입력 필드 초기화
}
}
// 허브 연결 상태를 확인하는 속성
public bool IsConnectedToHub =>
hubClientConnection?.State == HubConnectionState.Connected;
// 컴포넌트 소멸 시 허브 연결을 해제하여 리소스를 정리합니다.
public async ValueTask DisposeAsync()
{
if (hubClientConnection is not null)
{
await hubClientConnection.DisposeAsync();
}
}
}
2. 컨트롤러 또는 백그라운드 서비스에서 메시지 푸시
허브 외에도 ASP.NET Core 컨트롤러나 백그라운드 서비스에서 IHubContext<THub>를 주입받아 클라이언트로 메시지를 전송할 수 있습니다. 이는 서버의 특정 이벤트(예: 데이터베이스 변경)가 발생했을 때 클라이언트에 알림을 보내는 데 유용합니다.
// 서버 측: 컨트롤러에서 IHubContext를 사용하여 메시지 전송
[Route("api/[controller]")]
[ApiController]
public class MessageApiController : ControllerBase
{
private readonly IHubContext<NotificationHub> _notificationHubContext;
public MessageApiController(IHubContext<NotificationHub> notificationHubContext)
{
_notificationHubContext = notificationHubContext;
}
[HttpGet("send-server-alert")]
public async Task<ActionResult> SendServerAlert(string alertMessage)
{
// 모든 연결된 클라이언트에게 "ServerAlert" 이벤트를 통해 메시지를 전송합니다.
await _notificationHubContext.Clients.All.SendAsync("ServerAlert", $"서버 알림: {alertMessage}");
return Ok($"알림 '{alertMessage}'이(가) 전송되었습니다.");
}
}
클라이언트 측에서는 새로운 메시지 이벤트(예: "ServerAlert")에 대한 리스너를 추가해야 합니다.
// 클라이언트 측: 새로운 메시지 이벤트 리스너 추가 (Index.razor @code 블록 내부)
hubClientConnection.On<string>("ServerAlert", (alert) =>
{
receivedMessages.Add(alert);
InvokeAsync(StateHasChanged);
});
3. SignalR 연결 협상 과정 건너뛰기 (Skip Negotiation)
SignalR은 WebSocket, Server-Sent Events(SSE), Long Polling 등 여러 전송 방식을 추상화하여 사용합니다. 클라이언트는 처음에 negotiate 요청을 보내 서버와 어떤 전송 방식을 사용할지 협상(handshake)합니다. 이 과정에서 서버는 연결 정보를 생성합니다. 다중 서버 환경에서는 협상 요청을 받은 서버와 실제 연결이 수립되는 서버가 다를 수 있어 문제가 발생할 수 있습니다.
대부분의 최신 브라우저가 WebSocket을 지원하므로, 협상 과정을 건너뛰고 바로 WebSocket을 사용하도록 설정할 수 있습니다. 이는 특히 분산 환경에서 유용합니다.
// 클라이언트 측: 협상 건너뛰기 및 WebSocket 강제 사용 설정
hubClientConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7110/hub/notification", options =>
{
options.SkipNegotiation = true; // 협상 과정 건너뛰기
options.Transports = HttpTransportType.WebSockets; // WebSocket 전송 방식 강제
})
.WithAutomaticReconnect()
.Build();
4. SignalR 분산 배포 (Redis 사용)
여러 서버에 SignalR 서비스를 배포할 경우, 각 서버는 독립적인 허브 인스턴스를 가지게 됩니다. 이로 인해 특정 서버에 연결된 클라이언트만이 해당 서버를 통해 전송된 메시지를 수신하는 문제가 발생합니다. 이 문제를 해결하기 위해 Redis와 같은 백플레인(Backplane)을 사용하여 모든 SignalR 서버 인스턴스 간에 메시지를 동기화할 수 있습니다.
먼저 Microsoft.AspNetCore.SignalR.StackExchangeRedis NuGet 패키지를 설치합니다. 그리고 Program.cs에서 SignalR 서비스 등록 시 Redis 백플레인을 추가합니다.
// Program.cs (서버 측)
// Redis 백플레인을 추가하여 분산 환경에서 메시지 동기화를 지원합니다.
appBuilder.Services.AddSignalR().AddStackExchangeRedis("localhost:6379", redisOptions =>
{
redisOptions.Configuration.ChannelPrefix = "AppNotifications_"; // Redis 채널 접두사 설정
});
AddStackExchangeRedis 메서드의 첫 번째 매개변수는 Redis 서버의 주소입니다. ChannelPrefix는 이 애플리케이션의 메시지에 사용할 Redis 채널의 접두사를 지정합니다.
5. 통신 프로토콜 비교 (TCP, HTTP, WebSocket 등)
실시간 통신과 웹 서비스에서 자주 사용되는 몇 가지 통신 프로토콜의 주요 특징과 차이점을 이해하는 것은 중요합니다.
5.1. TCP, HTTP, WebSocket
- TCP (Transmission Control Protocol): OSI 모델의 전송 계층 프로토콜로, 신뢰할 수 있는 연결 지향 데이터 스트림을 제공합니다. HTTP와 WebSocket 모두 TCP 위에 구축됩니다.
- HTTP (Hypertext Transfer Protocol): OSI 모델의 응용 계층 프로토콜로, 클라이언트-서버 모델에서 요청-응답 방식으로 동작합니다. 일반적으로 무상태(stateless)이며, 각 요청 후 연결이 끊어집니다. 텍스트 기반 데이터 전송에 사용됩니다.
- WebSocket: HTTP와 마찬가지로 응용 계층 프로토콜이지만, TCP 위에 구축되어 있습니다. HTTP를 통한 초기 핸드셰이크 후, 클라이언트와 서버 간에 영구적인 양방향(full-duplex) 통신 채널을 생성합니다. 이를 통해 서버가 클라이언트에게 비동기적으로 메시지를 푸시할 수 있습니다.
5.2. HTTP와 WebSocket의 특징 및 차이점
| 특징 | HTTP | WebSocket |
|---|---|---|
| 통신 방식 | 요청-응답 (단방향) | 양방향 (Full-duplex) |
| 연결 유지 | 무상태, 연결 단절 | 상태 유지, 장기 연결 (Long-lived) |
| 데이터 형식 | 주로 텍스트 | 텍스트 및 바이너리 |
| 오버헤드 | 각 요청에 헤더 포함, 오버헤드 큼 | 초기 핸드셰이크 후 프레임 기반, 오버헤드 작음 |
| 방화벽 통과 | 일반적으로 용이 (80/443 포트) | HTTP 핸드셰이크를 사용하므로 방화벽 통과 용이 |
| 동일 출처 정책 | 적용 (CORS 필요) | 적용되지 않음 (Cross-origin 가능) |
5.3. HTTP/1.x, HTTP/2, HTTP/3
- HTTP/1.x:
- 문제점: 한 번에 하나의 요청만 처리 가능 (HOL 블로킹), 헤더 중복 전송, 텍스트 기반으로 인한 비효율성.
- HTTP/2:
- 개선점: 다중화(Multiplexing)를 통해 하나의 TCP 연결로 여러 요청 동시 처리, 헤더 압축, 서버 푸시(Server Push) 기능 도입, 바이너리 프레이밍으로 효율성 증대.
- HTTP/3:
- 개선점: TCP 대신 UDP 기반의 QUIC 프로토콜 사용. TCP의 HOL(Head-of-Line) 블로킹 문제 해결, 연결 설정 시간 단축, 향상된 혼잡 제어 및 패킷 손실 복구 능력.
5.4. RPC (Remote Procedure Call)
- RPC는 프로토콜이라기보다는 원격지에 있는 프로시저(함수/메서드)를 로컬에 있는 것처럼 호출할 수 있게 해주는 개념 또는 통신 방식입니다.
- TCP 또는 HTTP 기반으로 구현될 수 있으며, 바이너리 직렬화를 통해 데이터 전송 효율이 높습니다.
- 로드 밸런싱 기능을 내장할 수 있으며, 주로 서비스 간 고성능 통신에 사용됩니다.
- gRPC: Google이 개발한 오픈소스 RPC 프레임워크로, HTTP/2를 기반으로 하며 Protocol Buffers를 사용하여 데이터 직렬화를 수행합니다. 언어 중립적이며 고성능을 제공합니다.