비고
이 기사는 최신 버전이 아닙니다. 현재 릴리스는 이 문서의 .NET 10 버전을 참조하세요.
경고
이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조하세요. 현재 릴리스는 이 문서의 .NET 10 버전을 참조하세요.
Blazor PWA는 사용자가 앱을 적극적으로 사용하지 않는 경우에도 백 엔드 서버에서 푸시 알림 (데이터 메시지)을 수신하고 표시할 수 있습니다. 예를 들어 다른 사용자가 설치된 PWA에서 작업을 수행하거나 백 엔드 서버 앱과 직접 상호 작용하는 앱 또는 사용자가 작업을 수행할 때 푸시 알림을 보낼 수 있습니다.
푸시 알림을 사용하여 다음을 수행합니다.
- 사용자에게 중요한 일이 발생했음을 알리고 앱으로 돌아가라는 메시지를 표시합니다.
- 앱에 저장된 데이터(예: 뉴스 피드)를 업데이트하여 푸시 알림이 실행될 때 오프라인인 경우에도 사용자가 앱으로의 다음 반환 시 새 데이터를 찾습니다.
푸시 알림을 보내고, 받고, 표시하는 메커니즘은 독립적입니다 Blazor WebAssembly. 푸시 알림 보내기는 모든 기술을 사용할 수 있는 백 엔드 서버에서 구현됩니다. 클라이언트에 대한 푸시 알림 수신 및 표시는 서비스 작업자 JavaScript(JS) 파일에서 구현됩니다.
이 문서의 예제에서는 푸시 알림을 사용하여 Blazing Pizza Workshop PWA 데모 앱을 기반으로 피자 레스토랑 고객에게 주문 상태 업데이트를 제공합니다. 이 문서를 사용하기 위해 온라인 워크샵에 참여할 필요는 없지만 워크샵은 PWA 개발에 유용한 소개 Blazor 입니다.
비고
Blazing Pizza 앱은 리포지토리 패턴을 채택하여 UI 계층과 데이터 액세스 계층 간에 추상화 계층을 만듭니다. 자세한 내용은 UoW(작업 단위) 패턴 및 인프라 지속성 계층 설계를 참조하세요.
퍼블릭 및 프라이빗 키 설정
PowerShell 또는 IIS를 사용하거나 온라인 도구를 사용하여 로컬에서 푸시 알림을 보호하기 위한 암호화 공용 및 프라이빗 키를 생성합니다.
이 문서의 예제 코드에서 사용되는 자리 표시자:
-
{PUBLIC KEY}: 공개 키입니다. -
{PRIVATE KEY}: 프라이빗 키입니다.
이 문서의 C# 예제에서는 사용자 지정 키 쌍을 만들 때 사용되는 주소와 일치하도록 전자 메일 주소를 업데이트 someone@example.com 합니다.
푸시 알림을 구현할 때 암호화 키를 안전하게 관리해야 합니다.
- 키 생성: 신뢰할 수 있는 라이브러리 또는 도구를 사용하여 퍼블릭 및 프라이빗 키를 생성합니다. 약하거나 오래된 알고리즘을 사용하지 마세요.
- 키 스토리지: HSM(하드웨어 보안 모듈) 또는 암호화된 스토리지와 같은 보안 스토리지 메커니즘을 사용하여 서버에 프라이빗 키를 안전하게 저장합니다. 프라이빗 키를 클라이언트에 노출하지 않습니다.
- 키 사용: 푸시 알림 페이로드 서명에만 프라이빗 키를 사용합니다. 공개 키가 클라이언트에 안전하게 배포되었는지 확인합니다.
암호화 모범 사례에 대한 자세한 내용은 Cryptographic Services를 참조하세요.
구독 만들기
사용자에게 푸시 알림을 보내기 전에 앱은 사용자에게 사용 권한을 요청해야 합니다. 알림을 받을 수 있는 권한을 부여하면 브라우저는 앱이 사용자에게 알림을 라우팅하는 데 사용할 수 있는 토큰 집합을 포함하는 구독을 생성합니다.
앱에서 언제든지 사용 권한을 얻을 수 있지만 앱에서 알림을 구독하려는 이유가 명확할 때만 사용자에게 사용 권한을 요청하는 것이 좋습니다. 다음 예제에서는 사용자가 체크 아웃 페이지(Checkout 구성 요소)에 도착할 때 사용자에게 묻습니다. 이때 사용자가 주문에 대해 진지하다는 것이 분명하기 때문입니다.
사용자가 알림을 받는 데 동의하는 경우 다음 예제에서는 푸시 알림 구독 데이터를 서버로 보냅니다. 여기서 푸시 알림 토큰은 나중에 사용할 수 있도록 데이터베이스에 저장됩니다.
구독을 요청하는 푸시 알림 JS 파일을 추가합니다.
- 서비스 작업자 등록을 가져오려면 호출
navigator.serviceWorker.getRegistration합니다. - 구독이 있는지 확인하기 위해 호출
worker.pushManager.getSubscription합니다. - 구독이 없는 경우 함수를 사용하여 새 구독을
PushManager.subscribe만들고 새 구독의 URL 및 토큰을 반환합니다.
Blazing Pizza 앱에서 파일의 JS 이름은 pushNotifications.js 솔루션 wwwroot 클래스 라이브러리 프로젝트(Razor)의 공용 정적 자산 폴더(BlazingPizza.ComponentsLibrary)에 있습니다. 함수가 blazorPushNotifications.requestSubscription 구독을 요청합니다.
BlazingPizza.ComponentsLibrary/wwwroot/pushNotifications.js:
(function () {
const applicationServerPublicKey = '{PUBLIC KEY}';
window.blazorPushNotifications = {
requestSubscription: async () => {
const worker = await navigator.serviceWorker.getRegistration();
const existingSubscription = await worker.pushManager.getSubscription();
if (!existingSubscription) {
const newSubscription = await subscribe(worker);
if (newSubscription) {
return {
url: newSubscription.endpoint,
p256dh: arrayBufferToBase64(newSubscription.getKey('p256dh')),
auth: arrayBufferToBase64(newSubscription.getKey('auth'))
};
}
}
}
};
async function subscribe(worker) {
try {
return await worker.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerPublicKey
});
} catch (error) {
if (error.name === 'NotAllowedError') {
return null;
}
throw error;
}
}
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
})();
비고
이전 arrayBufferToBase64 함수에 대한 자세한 내용은 ArrayBuffer를 base64로 인코딩된 문자열로 변환하려면 어떻게 해야 하나요? (스택 오버플로).
구독 개체 및 알림 구독 엔드포인트가 서버에 만들어집니다. 엔드포인트는 암호화 토큰을 포함하여 푸시 알림 구독 데이터를 사용하여 클라이언트 웹 API 호출을 받습니다. 데이터는 각 앱 사용자의 데이터베이스에 저장됩니다.
Blazing Pizza 앱에서 구독 개체는 클래스입니다 NotificationSubscription .
P256dh 및 Auth 속성은 사용자의 암호화 토큰입니다.
BlazingPizza.Shared/NotificationSubscription.cs:
public class NotificationSubscription
{
public int? NotificationSubscriptionId { get; set; }
public string? UserId { get; set; }
public string? Url { get; set; }
public string? P256dh { get; set; }
public string? Auth { get; set; }
}
엔드포인트는 notifications/subscribe 앱에 있는 확장 메서드에 정의되어 있으며, 앱의 MapPizzaApi 파일에서 호출되어 웹 API 엔드포인트를 설정합니다. 푸시 알림 토큰을 포함하는 사용자의 알림 구독()NotificationSubscription은 데이터베이스에 저장됩니다. 사용자당 하나의 구독만 저장됩니다. 또는 사용자가 다른 브라우저 또는 디바이스에서 여러 구독을 등록하도록 허용할 수 있습니다.
app.MapPut("/notifications/subscribe",
[Authorize] async (
HttpContext context,
PizzaStoreContext db,
NotificationSubscription subscription) =>
{
var userId = GetUserId(context);
if (userId is null)
{
return Results.Unauthorized();
}
// Remove old subscriptions for this user
var oldSubscriptions = db.NotificationSubscriptions.Where(
e => e.UserId == userId);
db.NotificationSubscriptions.RemoveRange(oldSubscriptions);
// Store the new subscription
subscription.UserId = userId;
db.NotificationSubscriptions.Add(subscription);
await db.SaveChangesAsync();
return Results.Ok(subscription);
});
이 메서드는 BlazingPizza.Client/HttpRepository.cs서버의 SubscribeToNotifications 구독 엔드포인트에 HTTP PUT을 발급합니다.
public class HttpRepository : IRepository
{
private readonly HttpClient _httpClient;
public HttpRepository(HttpClient httpClient)
{
_httpClient = httpClient;
}
...
public async Task SubscribeToNotifications(NotificationSubscription subscription)
{
var response = await _httpClient.PutAsJsonAsync("notifications/subscribe",
subscription);
response.EnsureSuccessStatusCode();
}
}
리포지토리 인터페이스(BlazingPizza.Shared/IRepository.cs)에는 다음의 메서드 서명이 SubscribeToNotifications포함됩니다.
public interface IRepository
{
...
Task SubscribeToNotifications(NotificationSubscription subscription);
}
구독을 요청하고 구독이 설정된 경우 알림을 구독하는 방법을 정의합니다. 나중에 사용할 수 있는 데이터베이스에 구독을 저장합니다.
Checkout Blazing Pizza 앱 RequestNotificationSubscriptionAsync 의 구성 요소에서 이 메서드는 다음 작업을 수행합니다.
- 구독은 interop을 통해 JS을 호출하여
blazorPushNotifications.requestSubscription생성됩니다. 구성 요소는 IJSRuntime 서비스를 삽입하여 JS 함수를 호출합니다. -
SubscribeToNotifications구독을 저장하기 위해 메서드가 호출됩니다.
BlazingPizza.Client/Components/Pages/Checkout.razor의 경우
async Task RequestNotificationSubscriptionAsync()
{
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>(
"blazorPushNotifications.requestSubscription");
if (subscription is not null)
{
try
{
await Repository.SubscribeToNotifications(subscription);
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
}
구성 요소 Checkout에서 RequestNotificationSubscriptionAsync는 OnInitialized 라이프사이클 메서드에서 호출되며 구성 요소의 초기화 시 실행됩니다. 메서드는 비동기이지만 백그라운드에서 실행할 수 있으며 Task 반환되는 메서드는 삭제할 수 있습니다. 따라서 메서드는 구성 요소 초기화(OnInitializedAsync)를 위한 비동기 수명 주기 메서드에서 호출되지 않습니다. 이 방법은 구성 요소를 더 빠르게 렌더링합니다.
protected override void OnInitialized()
{
_ = RequestNotificationSubscriptionAsync();
}
코드의 작동 방식을 보여 주려면 Blazing Pizza 앱을 실행하고 주문하기 시작합니다. 체크 아웃 화면으로 이동하여 구독 요청을 확인합니다.
허용을 선택하고 브라우저 개발자 도구 콘솔에서 오류를 확인합니다. '의 PizzaApiExtensions 코드에서 MapPut("/notifications/subscribe"...)중단점을 설정하고 디버깅을 사용하여 앱을 실행하여 브라우저에서 들어오는 데이터를 검사할 수 있습니다. 데이터에는 엔드포인트 URL 및 암호화 토큰이 포함됩니다.
사용자가 지정된 사이트에 대한 알림을 허용하거나 차단한 후에는 브라우저에서 다시 묻지 않습니다. Google Chrome 또는 Microsoft Edge에 대한 추가 테스트 권한을 다시 설정하려면 다음 이미지와 같이 브라우저 주소 표시줄 왼쪽에 있는 "정보" 아이콘(🛈)을 선택하고 알림을다시 Ask(기본값)로 변경합니다.
알림 보내기
알림을 보내려면 서버에서 일부 복잡한 암호화 작업을 수행하여 전송 중인 데이터를 보호해야 합니다. 타사 NuGet 패키지 WebPush가 앱에 대한 복잡성의 대부분을 처리하며, 이는 Blazing Pizza 앱의 서버 프로젝트(BlazingPizza.Server)에서 사용됩니다.
이 메서드는 캡처된 구독을 사용하여 SendNotificationAsync 주문 알림을 발송합니다. 다음 코드는 알림을 디스패치하는 API를 사용합니다 WebPush . 알림의 페이로드는 JSON 직렬화되며 메시지와 URL을 포함합니다. 메시지가 사용자에게 표시되고 URL을 사용하면 사용자가 알림과 연결된 피자 주문에 도달할 수 있습니다. 다른 알림 시나리오에 필요한 대로 추가 매개 변수를 serialize할 수 있습니다.
주의
다음 예제에서는 프라이빗 키를 제공하는 보안 방법을 사용하는 것이 좋습니다. 환경에서 로컬로 작업하는 Development 경우 비밀 관리자 도구를 사용하여 프라이빗 키를 앱에 제공할 수 있습니다.
Development, Staging, 그리고 Production 환경에서 Azure Managed Identities를 사용한 Azure Key Vault를 사용할 수 있으며, 덧붙여서 인증서의 프라이빗 키를 키 자격 증명 모음에서 가져오기 위해서는 인증서가 내보낼 수 있는 프라이빗 키를 포함해야 합니다.
private static async Task SendNotificationAsync(Order order,
NotificationSubscription subscription, string message)
{
var publicKey = "{PUBLIC KEY}";
var privateKey = "{PRIVATE KEY}";
var pushSubscription = new PushSubscription(subscription.Url,
subscription.P256dh, subscription.Auth);
var vapidDetails = new VapidDetails("mailto:<someone@example.com>", publicKey,
privateKey);
var webPushClient = new WebPushClient();
try
{
var payload = JsonSerializer.Serialize(new
{
message,
url = $"myorders/{order.OrderId}",
});
await webPushClient.SendNotificationAsync(pushSubscription, payload,
vapidDetails);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error sending push notification: {ex.Message}");
}
}
앞의 예제에서는 서버에서 알림을 보낼 수 있지만 브라우저는 추가 논리 없이 알림에 반응하지 않습니다. 알림 표시는 표시 알림 섹션에서 다룹니다 .
브라우저의 개발자 도구 콘솔은 주문이 Blazing Pizza 앱에 배치된 후 10초 후에 알림이 도착했음을 나타냅니다. 애플리케이션 탭에서 푸시 메시징 섹션을 엽니다. 기록을 시작할 원을 선택합니다.
알림 표시
PWA의 서비스 작업자(service-worker.js)는 앱에서 푸시 알림을 표시하기 위해 푸시 알림을 처리해야 합니다.
Blazing Pizza 앱의 다음 push 이벤트 처리기는 활성 서비스 작업자에 대한 알림을 만들기 위해 호출 showNotification 합니다.
BlazingPizza/wwwroot/service-worker.js의 경우
self.addEventListener('push', event => {
const payload = event.data.json();
event.waitUntil(
self.registration.showNotification('Blazing Pizza', {
body: payload.message,
icon: 'img/icon-512.png',
vibrate: [100, 50, 100],
data: { url: payload.url }
})
);
});
다음 페이지가 로드된 후부터 브라우저가 Installing service worker...을(를) 기록할 때까지 이전 코드는 적용되지 않습니다. 서비스 작업자를 업데이트하는 데 어려움을 겪는 경우 브라우저의 개발자 도구 콘솔에서 애플리케이션 탭을 사용합니다.
서비스 작업자에서 업데이트를 선택하거나 등록 취소를 사용하여 다음 로드 시 새 등록을 강제로 적용합니다.
위의 코드가 적용되고 사용자가 새 주문을 하면, 주문은 앱의 내장 데모 논리에 따라 10초 후 배송 중 상태로 이동합니다. 브라우저에서 푸시 알림을 받습니다.
Google Chrome 또는 Microsoft Edge에서 앱을 사용하는 경우 사용자가 Blazing Pizza 앱을 적극적으로 사용하지 않더라도 알림이 표시됩니다. 그러나 브라우저가 실행 중이거나 다음에 브라우저가 열릴 때 알림이 표시됩니다.
설치된 PWA를 사용하는 경우 사용자가 앱을 실행하지 않는 경우에도 알림이 전달되어야 합니다.
알림 클릭 처리
notificationclick 이벤트 처리기를 등록하여 디바이스에서 푸시 알림을 선택(클릭)하는 사용자를 처리합니다.
- 를 호출
event.notification.close하여 알림을 닫습니다. - 호출
clients.openWindow하여 새 최상위 검색 컨텍스트를 만들고 메서드에 전달된 URL을 로드합니다.
Blazing Pizza 앱의 다음 예제에서는 알림과 관련된 주문에 대한 주문 상태 페이지로 사용자를 이동합니다. URL은 알림 페이로드의 event.notification.data.url 서버에서 보내는 매개 변수에 의해 제공됩니다.
서비스 작업자 파일(service-worker.js):
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});
PWA가 디바이스에 설치된 경우 PWA가 디바이스에 표시됩니다. PWA가 설치되지 않은 경우 사용자는 브라우저에서 앱의 페이지로 이동됩니다.
추가 리소스
ASP.NET Core