C# .NET Core Blazor 핵심 포인트 완벽 정리

c# .NET Core Blazor 핵심 포인트 정리
📚 시작하기 전에

c# .NET Core Blazor 핵심 포인트 정리

지금까지 배운 모든 기술을 핵심 정리하여 실전 Blazor 웹 애플리케이션을 개발할 수 있도록 꼭 알아야 하는 핵심 포인트만 정리합니다.
📌 현재 모델: claude-sonnet-4-6 — 이 글은 C# .NET Core Blazor 강의 커리큘럼의 최종편입니다. 지금까지 학습한 모든 핵심 개념을 체계적으로 정리하여 실전 Blazor 웹 애플리케이션 개발에 바로 활용할 수 있도록 구성하였습니다.

🎯 학습 목표

  1. Blazor 호스팅 모델(Server / WebAssembly / Hybrid)의 차이와 선택 기준을 명확히 이해합니다.
  2. 컴포넌트 생명주기의 각 단계와 올바른 활용 패턴을 정리합니다.
  3. 데이터 바인딩, 이벤트 처리, 라우팅, 의존성 주입의 핵심 패턴을 종합적으로 확립합니다.
  4. 상태 관리, 폼 유효성 검사, HTTP 통신, JavaScript Interop을 실전에 적용하는 방법을 숙지합니다.
  5. 자주 발생하는 실수를 파악하고 실전 Best Practice를 습득합니다.

1. Blazor 호스팅 모델 비교

Blazor는 C#으로 웹 UI를 구성하는 .NET 프레임워크입니다. 프로젝트 성격에 따라 세 가지 호스팅 모델 중 선택합니다.

항목Blazor ServerBlazor WebAssemblyBlazor Hybrid
실행 위치서버 (.NET 런타임)브라우저 (WebAssembly)네이티브 앱 (MAUI 등)
초기 로드빠름느림 (런타임 다운로드)빠름
오프라인 지원
서버 상시 연결SignalR 필수불필요 (API만)불필요
서버 부하높음 (세션 유지)낮음낮음
적합한 사례기업 내부 시스템, 실시간 앱공개 SPA, PWA데스크톱·모바일 앱

2. 컴포넌트 구조와 생명주기

2.1 .razor 파일 기본 구조

Blazor의 기본 단위는 .razor 파일입니다. 디렉티브(지시문), HTML 마크업, @code 블록이 하나의 파일에 결합됩니다.

@page "/products"
@using MyApp.Models
@inject IProductService ProductService

<h2>@Title</h2>

@if (isLoading)
{
    <p>데이터를 불러오는 중...</p>
}
else
{
    <ul>
    @foreach (var item in products)
    {
        <li>@item.Name — @item.Price.ToString("N0")원</li>
    }
    </ul>
}

@code {
    [Parameter] public string Title { get; set; } = "상품 목록";

    private List<Product> products = new();
    private bool isLoading = true;

    protected override async Task OnInitializedAsync()
    {
        products = await ProductService.GetAllAsync();
        isLoading = false;
    }
}

2.2 컴포넌트 생명주기

🔄 Blazor 컴포넌트 생명주기 흐름
① 컴포넌트 인스턴스 생성
② SetParametersAsync — 매개변수 수신 및 설정
③ OnInitialized / OnInitializedAsync — 최초 1회 초기화
④ OnParametersSet / OnParametersSetAsync — 매개변수 변경마다 실행
⑤ 렌더링 (BuildRenderTree)
⑥ OnAfterRender / OnAfterRenderAsync — DOM 완성 후 (firstRender 구분)
⑦ Dispose — 컴포넌트 소멸 (IDisposable 구현 시)
@implements IDisposable

@code {
    // ③ 최초 1회 — API 호출, 데이터 로드에 사용
    protected override async Task OnInitializedAsync()
    {
        data = await MyService.GetDataAsync();
    }

    // ④ 매개변수 변경마다 — 부모로부터 새 값을 받을 때
    protected override void OnParametersSet()
    {
        filteredData = data.Where(x => x.Category == Category).ToList();
    }

    // ⑥ DOM 완성 후 — JS Interop, 차트 초기화 등
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync("initChart", "chart-container");
        }
    }

    // ⑦ 이벤트 구독 해제, 리소스 정리
    public void Dispose()
    {
        AppState.OnChange -= StateHasChanged;
    }
}

3. 데이터 바인딩과 이벤트 처리

3.1 바인딩 종류

  • 단방향 바인딩: @PropertyName — C# 값을 HTML에 출력
  • 양방향 바인딩: @bind="PropertyName" — 입력 요소와 값 동기화 (기본: onchange)
  • 실시간 바인딩: @bind="name" @bind:event="oninput" — 키 입력마다 반영
  • 컴포넌트 바인딩: @bind-Value="value" — 자식 컴포넌트 ValueChanged 패턴
<!-- 단방향: 표시만 -->
<p>검색어: @keyword</p>

<!-- 양방향: 포커스 이탈 시 업데이트 -->
<input @bind="keyword" />

<!-- 실시간: 타이핑마다 업데이트 -->
<input @bind="keyword" @bind:event="oninput" />

@code {
    private string keyword = "";
}

3.2 이벤트 처리

<button @onclick="HandleClick">클릭</button>
<input @onchange="HandleChange" />
<input @oninput="e => liveText = e.Value?.ToString()" />

@code {
    private void HandleClick(MouseEventArgs e)
    {
        Console.WriteLine($"클릭: ({e.ClientX}, {e.ClientY})");
    }

    private void HandleChange(ChangeEventArgs e)
    {
        inputValue = e.Value?.ToString() ?? "";
    }
}

3.3 부모 ↔ 자식 컴포넌트 통신

<!-- 자식 컴포넌트: SearchBox.razor -->
@code {
    [Parameter] public string Value { get; set; } = "";
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    [Parameter] public EventCallback OnSearch { get; set; }

    private async Task OnInput(ChangeEventArgs e)
    {
        // 부모에게 값 전달 (@bind-Value 패턴 지원)
        await ValueChanged.InvokeAsync(e.Value?.ToString());
    }
}

<!-- 부모 컴포넌트 -->
<SearchBox @bind-Value="searchKeyword" OnSearch="DoSearch" />
<p>현재 검색어: @searchKeyword</p>

4. 라우팅과 내비게이션

<!-- 복수 라우트 등록 가능 -->
@page "/products"
@page "/products/{Category}"
@page "/products/{Id:int}"

<!-- NavLink: 활성 경로 자동 CSS 처리 -->
<NavLink href="/products" Match="NavLinkMatch.All" class="nav-item">
    전체 상품
</NavLink>

@inject NavigationManager Nav

@code {
    [Parameter] public string? Category { get; set; }
    [Parameter] public int Id { get; set; }

    private void GoToDetail(int id) => Nav.NavigateTo($"/products/{id}");

    // 쿼리스트링 읽기 (.NET 7+)
    [SupplyParameterFromQuery] public string? Sort { get; set; }
}
💡 라우트 제약 조건 종류
  • {Id:int} — 정수
  • {Name:alpha} — 알파벳
  • {Guid:guid} — GUID
  • {Price:decimal} — 소수
  • {*catchAll} — 나머지 전체 경로

5. 의존성 주입 (의존성 주입(DI))

// Program.cs — 서비스 등록
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddSingleton<IAppCache, AppCache>();
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
builder.Services.AddHttpClient<IApiService, ApiService>(
    c => c.BaseAddress = new Uri("https://api.example.com/"));

// 컴포넌트에서 @inject 디렉티브로 주입
@inject IProductService ProductService
@inject NavigationManager Nav
@inject IJSRuntime JS

// code-behind (.razor.cs) 에서는 [Inject] 속성 사용
public partial class ProductList : ComponentBase
{
    [Inject] private IProductService ProductService { get; set; } = default!;
}
수명인스턴스 생성 시점적합한 사용 예
Singleton앱 전체 단 1개전역 캐시, 설정, 로거
Scoped연결(Circuit)당 1개DB 컨텍스트, 장바구니 상태
Transient요청마다 새 인스턴스경량 유틸, 이메일 발송

6. 상태 관리

6.1 CascadingValue / CascadingParameter

<!-- 상위 레이아웃에서 공급 -->
<CascadingValue Value="currentTheme">
    <CascadingValue Value="currentUser" Name="User">
        @Body
    </CascadingValue>
</CascadingValue>

<!-- 깊은 하위 컴포넌트에서 수신 -->
@code {
    [CascadingParameter] private string CurrentTheme { get; set; } = "light";
    [CascadingParameter(Name = "User")] private UserInfo? User { get; set; }
}

6.2 상태 컨테이너 패턴 (Scoped Service)

// AppState.cs — Scoped 서비스로 등록
public class AppState
{
    public int CartCount { get; private set; }
    public event Action? OnChange;

    public void AddToCart(int quantity = 1)
    {
        CartCount += quantity;
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}

// 컴포넌트에서 구독 및 해제
@inject AppState AppState
@implements IDisposable

<span>장바구니: @AppState.CartCount</span>

@code {
    protected override void OnInitialized()
    {
        AppState.OnChange += StateHasChanged; // 상태 변경 시 리렌더링
    }

    public void Dispose()
    {
        AppState.OnChange -= StateHasChanged; // 메모리 누수 방지
    }
}

7. 폼 처리와 유효성 검사

// 모델 클래스 — DataAnnotations 활용
public class RegisterModel
{
    [Required(ErrorMessage = "이름은 필수입니다")]
    [StringLength(50, MinimumLength = 2, ErrorMessage = "2~50자 사이로 입력하세요")]
    public string Name { get; set; } = "";

    [Required, EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다")]
    public string Email { get; set; } = "";

    [Required, Range(0, 150)]
    public int Age { get; set; }
}

<!-- EditForm: OnValidSubmit = 검증 통과 시만 실행 -->
<EditForm Model="model" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div>
        <label>이름</label>
        <InputText @bind-Value="model.Name" />
        <ValidationMessage For="() => model.Name" />
    </div>
    <div>
        <label>이메일</label>
        <InputText @bind-Value="model.Email" />
        <ValidationMessage For="() => model.Email" />
    </div>
    <div>
        <label>나이</label>
        <InputNumber @bind-Value="model.Age" />
        <ValidationMessage For="() => model.Age" />
    </div>
    <button type="submit">가입</button>
</EditForm>

@code {
    private RegisterModel model = new();

    private async Task HandleSubmit()
    {
        await UserService.RegisterAsync(model);
        Nav.NavigateTo("/welcome");
    }
}

8. HTTP 통신과 API 연동

// Program.cs
builder.Services.AddHttpClient<IProductApiService, ProductApiService>(
    c => c.BaseAddress = new Uri(builder.Configuration["ApiBase"]!));

// ProductApiService.cs
public class ProductApiService : IProductApiService
{
    private readonly HttpClient _http;
    public ProductApiService(HttpClient http) => _http = http;

    public Task<List<Product>> GetAllAsync()
        => _http.GetFromJsonAsync<List<Product>>("/api/products") ?? Task.FromResult(new List<Product>());

    public Task<Product?> GetByIdAsync(int id)
        => _http.GetFromJsonAsync<Product>($"/api/products/{id}");

    public Task CreateAsync(Product product)
        => _http.PostAsJsonAsync("/api/products", product);

    public Task UpdateAsync(Product product)
        => _http.PutAsJsonAsync($"/api/products/{product.Id}", product);

    public Task DeleteAsync(int id)
        => _http.DeleteAsync($"/api/products/{id}");
}

// 컴포넌트에서 사용 — 로딩 상태 처리 포함
@inject IProductApiService ApiService

@code {
    private List<Product> products = new();
    private bool isLoading = true;
    private string? errorMessage;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            products = await ApiService.GetAllAsync();
        }
        catch (HttpRequestException ex)
        {
            errorMessage = $"데이터 로드 실패: {ex.Message}";
        }
        finally
        {
            isLoading = false;
        }
    }
}

9. JavaScript Interop

@inject IJSRuntime JS

@code {
    // C# → JS 단방향 호출 (반환값 없음)
    private async Task ShowAlert()
        => await JS.InvokeVoidAsync("alert", "Blazor에서 호출!");

    // C# → JS 값 반환 호출
    private async Task<string> GetLocalStorageItem(string key)
        => await JS.InvokeAsync<string>("localStorage.getItem", key);

    // DOM 조작은 반드시 OnAfterRenderAsync(firstRender) 에서
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await JS.InvokeVoidAsync("initializeMap", "map-container");
    }
}

// wwwroot/js/app.js — JS → C# 역방향 호출
// window.callDotNet = async (data) => {
//   await DotNet.invokeMethodAsync("MyApp", "ReceiveFromJS", data);
// };

// C# 수신 메서드 — [JSInvokable] 필수
[JSInvokable]
public static Task ReceiveFromJS(string payload)
{
    Console.WriteLine($"JS에서 수신: {payload}");
    return Task.CompletedTask;
}

10. 인증과 인가

// Program.cs — ASP.NET Core Identity + Blazor 인증
builder.Services.AddAuthentication();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
});

// 컴포넌트 수준 인가 — @attribute 어트리뷰트
@attribute [Authorize]
@attribute [Authorize(Roles = "Admin,Manager")]
@attribute [Authorize(Policy = "AdminOnly")]

// 템플릿 수준 조건부 렌더링
<AuthorizeView>
    <Authorized>
        <p>안녕하세요, @context.User.Identity?.Name 님!</p>
        <button @onclick="Logout">로그아웃</button>
    </Authorized>
    <NotAuthorized>
        <a href="/login">로그인이 필요합니다</a>
    </NotAuthorized>
</AuthorizeView>

<AuthorizeView Roles="Admin">
    <Authorized><AdminPanel /></Authorized>
</AuthorizeView>

// 코드에서 인증 상태 확인
@inject AuthenticationStateProvider AuthProvider

@code {
    protected override async Task OnInitializedAsync()
    {
        var state = await AuthProvider.GetAuthenticationStateAsync();
        var user = state.User;
        if (user.Identity?.IsAuthenticated == true)
            userName = user.Identity.Name;
    }
}
📌 Blazor 실전 개발 핵심 포인트
🏗️
컴포넌트
.razor 단위. @page·@inject·@code 세 영역으로 구성
🔄
생명주기
Init(1회)→ParametersSet(매번)→AfterRender→Dispose 순서
🔗
바인딩
@bind 양방향, EventCallback으로 자식→부모 전달
🛣️
라우팅
@page + 타입 제약, NavigationManager 프로그래매틱 이동
💉
의존성 주입
@inject로 주입. Singleton/Scoped/Transient 수명 선택
🗂️
상태 관리
CascadingValue 또는 Scoped AppState + OnChange 이벤트
📋
폼 처리
EditForm + DataAnnotations + InputXxx 컴포넌트
🌐
HTTP 통신
HttpClient + GetFromJsonAsync / PostAsJsonAsync
JS Interop
IJSRuntime.InvokeAsync + [JSInvokable] 양방향
🔒
인증/인가
[Authorize] + AuthorizeView로 UI 수준 접근 제어

실습: 완성형 할 일(Todo) 관리 앱

지금까지 배운 핵심 개념을 하나의 완성형 앱으로 통합합니다. 의존성 주입(DI), 생명주기, 바인딩, 이벤트, 폼 처리가 모두 포함되어 있습니다.

① 모델 및 서비스 정의
// Models/TodoItem.cs
public class TodoItem
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public bool IsCompleted { get; set; }
}

// Models/NewTodoModel.cs
public class NewTodoModel
{
    [Required(ErrorMessage = "할 일 내용을 입력하세요")]
    [StringLength(200, MinimumLength = 1)]
    public string Title { get; set; } = "";
}

// Services/ITodoService.cs
public interface ITodoService
{
    Task<List<TodoItem>> GetAllAsync();
    Task<TodoItem> AddAsync(string title);
    Task ToggleAsync(int id);
    Task DeleteAsync(int id);
}

// Services/TodoService.cs (인메모리 구현)
public class TodoService : ITodoService
{
    private readonly List<TodoItem> _store = new();
    private int _nextId = 1;

    public Task<List<TodoItem>> GetAllAsync()
        => Task.FromResult(_store.ToList());

    public Task<TodoItem> AddAsync(string title)
    {
        var item = new TodoItem { Id = _nextId++, Title = title };
        _store.Add(item);
        return Task.FromResult(item);
    }

    public Task ToggleAsync(int id)
    {
        var item = _store.FirstOrDefault(x => x.Id == id);
        if (item is not null) item.IsCompleted = !item.IsCompleted;
        return Task.CompletedTask;
    }

    public Task DeleteAsync(int id)
    {
        _store.RemoveAll(x => x.Id == id);
        return Task.CompletedTask;
    }
}

// Program.cs
builder.Services.AddScoped<ITodoService, TodoService>();
② 할 일 목록 컴포넌트 (Pages/TodoList.razor)
@page "/todo"
@inject ITodoService TodoService

<h2>할 일 관리</h2>

<EditForm Model="newTodo" OnValidSubmit="AddTodo">
    <DataAnnotationsValidator />
    <div style="display:flex; gap:8px;">
        <InputText @bind-Value="newTodo.Title" placeholder="새 할 일을 입력하세요" />
        <button type="submit">추가</button>
    </div>
    <ValidationMessage For="() => newTodo.Title" />
</EditForm>

@if (isLoading)
{
    <p>불러오는 중...</p>
}
else if (!todos.Any())
{
    <p>등록된 할 일이 없습니다.</p>
}
else
{
    <p>전체 @todos.Count건 / 완료 @todos.Count(x => x.IsCompleted)건</p>
    <ul>
    @foreach (var item in todos)
    {
        <li>
            <input type="checkbox" checked="@item.IsCompleted"
                   @onchange="() => ToggleTodo(item.Id)" />
            <span style="text-decoration:@(item.IsCompleted ? "line-through" : "none")">
                @item.Title
            </span>
            <button @onclick="() => DeleteTodo(item.Id)">삭제</button>
        </li>
    }
    </ul>
}

@code {
    private List<TodoItem> todos = new();
    private NewTodoModel newTodo = new();
    private bool isLoading = true;

    // 생명주기: 최초 1회 데이터 로드
    protected override async Task OnInitializedAsync()
    {
        todos = await TodoService.GetAllAsync();
        isLoading = false;
    }

    private async Task AddTodo()
    {
        var item = await TodoService.AddAsync(newTodo.Title);
        todos.Add(item);
        newTodo = new();          // 폼 초기화
    }

    private async Task ToggleTodo(int id)
    {
        await TodoService.ToggleAsync(id);
        var item = todos.FirstOrDefault(x => x.Id == id);
        if (item is not null) item.IsCompleted = !item.IsCompleted;
    }

    private async Task DeleteTodo(int id)
    {
        await TodoService.DeleteAsync(id);
        todos.RemoveAll(x => x.Id == id);
    }
}

자주 발생하는 실수와 주의사항

  1. StateHasChanged() 빠뜨리기 — 외부 이벤트(타이머, 웹소켓 콜백 등)로 상태가 바뀔 때 UI가 갱신되지 않습니다. 비동기 콜백 안에서는 반드시 await InvokeAsync(StateHasChanged)를 호출하십시오.
  2. JavaScript Interop을 OnInitializedAsync에서 호출 — 이 시점에는 DOM이 아직 렌더링되지 않아 JS에서 요소를 찾지 못합니다. JS 초기화는 반드시 OnAfterRenderAsync(firstRender) 안에서 수행하십시오.
  3. IDisposable 구현 생략AppState.OnChange 등 이벤트를 구독하고 Dispose에서 해제하지 않으면 컴포넌트가 소멸된 뒤에도 핸들러가 호출되어 메모리 누수와 예외가 발생합니다.
  4. @bind와 단방향 바인딩 혼용 오류@bind="Value"를 사용하면서 동시에 value="@Value"를 쓰면 컴파일 에러가 발생합니다. 양방향은 @bind만, 단방향은 @Value만 사용하십시오.
  5. Blazor WebAssembly에서 직접 DB 접근 시도 — WASM은 브라우저 안에서 실행되므로 EF Core나 직접 SQL 연결이 불가능합니다. 반드시 Web API를 통해 서버와 통신하십시오.
  6. Scoped 서비스를 Singleton에서 참조 — Singleton 서비스 생성자에 Scoped 서비스를 주입하면 수명 불일치(Captive Dependency)가 발생합니다. IServiceScopeFactory를 사용하여 명시적으로 스코프를 생성하십시오.
  7. 매개변수 변경 감지 누락 — 부모에서 같은 참조 타입 객체의 속성만 변경하면 Blazor가 변경을 감지하지 못합니다. 새 객체로 교체하거나 자식에서 StateHasChanged()를 직접 호출하십시오.

마무리·실전 활용 팁

프로젝트 구조 권장 패턴

MyBlazorApp/
├── Pages/           ← @page 라우트 컴포넌트
├── Components/      ← 재사용 UI 컴포넌트 (라우트 없음)
├── Layouts/         ← MainLayout.razor 등
├── Services/        ← 비즈니스 로직 인터페이스·구현
├── Models/          ← 데이터 모델, DTO, 폼 모델
├── Shared/          ← 전역 공유 컴포넌트
└── wwwroot/
    ├── css/
    └── js/          ← JS Interop용 스크립트

성능 최적화 포인트

  • ShouldRender() 오버라이드: 불필요한 리렌더링을 차단합니다. 변경이 없을 때 return false를 반환하십시오.
  • Virtualize 컴포넌트: 대량 목록을 렌더링할 때 <Virtualize Items="items" Context="item">를 사용하면 가시 영역만 렌더링하여 성능이 크게 향상됩니다.
  • lazy loading: 큰 어셈블리는 필요한 시점에만 로드하여 WASM 초기 로드 시간을 줄입니다.
  • ErrorBoundary 컴포넌트: 자식 컴포넌트 예외가 전체 앱 크래시로 이어지지 않도록 오류 경계를 설정합니다.
<!-- 성능: 대량 목록 -->
<Virtualize Items="products" Context="p">
    <ProductCard Product="p" />
</Virtualize>

<!-- 안정성: 오류 경계 -->
<ErrorBoundary>
    <ChildContent><RiskyComponent /></ChildContent>
    <ErrorContent Context="ex">
        <p>일시적인 오류가 발생했습니다: @ex.Message</p>
    </ErrorContent>
</ErrorBoundary>

<!-- 성능: 조건부 리렌더링 -->
@code {
    private string lastValue = "";

    protected override bool ShouldRender()
        => currentValue != lastValue; // 값이 바뀔 때만 렌더링
}

배포 체크리스트

  • Blazor Server: IIS / Nginx 리버스 프록시 + SignalR WebSocket 포트 허용
  • Blazor WASM: 정적 파일 서버 배포 가능. _framework/ 폴더 포함 확인
  • 환경별 설정: appsettings.Production.json에 API 주소·연결 문자열 분리
  • HTTPS: 반드시 활성화. Blazor WASM은 HTTPS 없이 일부 브라우저 API 사용 불가
✅ 핵심 정리
  • 호스팅 모델: 실시간·내부 시스템 → Blazor Server, 공개 SPA → WebAssembly, 네이티브 앱 → Hybrid
  • 컴포넌트 생명주기 순서: OnInitializedAsync(1회) → OnParametersSet(매번) → OnAfterRender → Dispose
  • 데이터 바인딩: @Property(단방향) / @bind(양방향) / EventCallback(자식→부모 전달)
  • 라우팅: @page 디렉티브 + 타입 제약, 프로그래매틱 이동은 NavigationManager
  • 의존성 주입: Program.cs에서 서비스 등록 → @inject 또는 [Inject]로 주입
  • 상태 관리: CascadingValue(계층 전달) 또는 Scoped AppState + OnChange 이벤트
  • 폼 처리: EditForm + DataAnnotationsValidator + InputXxx 컴포넌트 조합
  • HTTP 통신: HttpClient + System.Net.Http.Json 확장 메서드(GetFromJsonAsync 등)
  • JavaScript Interop: C#→JS는 InvokeAsync, JS→C#은 [JSInvokable] + DotNet.invokeMethodAsync
  • 인증/인가: [Authorize] 어트리뷰트로 페이지 보호, AuthorizeView로 UI 조건부 렌더링
  • 핵심 주의사항: JS Interop은 OnAfterRenderAsync에서 / 이벤트 구독은 Dispose에서 반드시 해제

댓글 남기기

Wordpress Social Share Plugin powered by Ultimatelysocial
Copy link
URL has been copied successfully!
THREADS
RSS
error: 저작권 콘텐츠보호를 부탁드립니다.