Ver3.0 ๐ Blazor WebAssembly์ Server๋ก ๋ง๋๋ ์ธํฐ๋ํฐ๋ธ ์น UI ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋

๐ Blazor WebAssembly์ Server๋ก ๋ง๋๋ ์ธํฐ๋ํฐ๋ธ ์น UI ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋
C#์ผ๋ก ์น ํ๋ก ํธ์๋๋ฅผ ์ ๋ณตํ๋ ํ์ ์ ์ธ ๋ฐฉ๋ฒ
๐ก ์น๊ตฌ์ผ, ํน์ JavaScript ์์ด๋ ๋ฉ์ง ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ์ ์๋ค๋ฉด ์ด๋จ๊น?
C# ๊ฐ๋ฐ์๋ผ๋ฉด ๋๊ตฌ๋ ํ ๋ฒ์ฏค ๊ฟ๊ฟ๋ดค์ ๊ฑฐ์ผ. "๋ด๊ฐ ์ข์ํ๋ C#์ผ๋ก ํ๋ก ํธ์๋๊น์ง ๋ค ํ ์ ์๋ค๋ฉด ์ผ๋ง๋ ์ข์๊น?" ๋ฐ๋ก ๊ทธ ๊ฟ์ ํ์ค๋ก ๋ง๋ค์ด์ฃผ๋ ๊ฒ Blazor์ผ! ๐ฏ
Blazor๋ ๋ง์ดํฌ๋ก์ํํธ๊ฐ ์ผ์ฌ์ฐจ๊ฒ ๋ด๋์ ํ๋ ์์ํฌ๋ก, C#๊ณผ .NET์ ์ฌ์ฉํด์ ์ธํฐ๋ํฐ๋ธํ ์น UI๋ฅผ ๋ง๋ค ์ ์๊ฒ ํด์ค. ๊ทธ๊ฒ๋ ๋ ๊ฐ์ง ๋ฐฉ์์ผ๋ก ๋ง์ด์ผ - WebAssembly์ Server ๋ชจ๋! ์ค๋์ ์ด ๋ ๊ฐ์ง๋ฅผ ๊น์ด ํํค์ณ๋ณผ ๊ฑฐ์ผ. ๐
๐จ Blazor๊ฐ ๋ญ๊ธธ๋ ์ด๋ ๊ฒ ๋๋ฆฌ์ผ?
์, ๋ณธ๊ฒฉ์ ์ผ๋ก ๋ค์ด๊ฐ๊ธฐ ์ ์ Blazor๊ฐ ์ ํํ ๋ญ์ง๋ถํฐ ์์๋ณด์. Blazor๋ Browser + Razor์ ํฉ์ฑ์ด์ผ. Razor๋ ASP.NET์์ ์ฌ์ฉํ๋ ํ
ํ๋ฆฟ ์์ง์ธ๋ฐ, ์ด๊ฑธ ๋ธ๋ผ์ฐ์ ์์๋ ์ธ ์ ์๊ฒ ๋ง๋ ๊ฑฐ์ง! ๐
์ ํต์ ์ผ๋ก ์น ๊ฐ๋ฐ์ ํ๋ ค๋ฉด ํ๋ก ํธ์๋๋ JavaScript(React, Vue, Angular ๋ฑ), ๋ฐฑ์๋๋ C#์ด๋ Java ๊ฐ์ ์ธ์ด๋ฅผ ์จ์ผ ํ์ด. ๊ทธ๋ฌ๋ค ๋ณด๋ ๊ฐ๋ฐ์๋ค์ ์ฌ๋ฌ ์ธ์ด๋ฅผ ๋ฐฐ์์ผ ํ๊ณ , ํ๋ก ํธ์๋์ ๋ฐฑ์๋ ์ฌ์ด์ ์ฝ๋ ๊ณต์ ๋ ์ด๋ ค์ ์ง.
ํ์ง๋ง Blazor๋ ์ด ๋ชจ๋ ๊ฑธ ๋ฐ๊ฟ๋จ์ด! C# ํ๋๋ก ์ ์ฒด ์คํ์ ๊ฐ๋ฐํ ์ ์๊ฒ ๋ ๊ฑฐ์ผ. ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ๋ ๋์์ง๊ณ , ํ์
์์ ์ฑ๋ ๋ณด์ฅ๋๊ณ , ๋๋ฒ๊น
๋ ํจ์ฌ ํธํด์ก์ด. ๐
โ C#๊ณผ .NET ์ํ๊ณ ํ์ฉ ๊ฐ๋ฅ
โ ๊ฐ๋ ฅํ ํ์ ์์คํ ์ผ๋ก ๋ฐํ์ ์๋ฌ ๊ฐ์
โ Visual Studio์ ๊ฐ๋ ฅํ IntelliSense ์ง์
โ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ๋ก ์ฌ์ฌ์ฉ์ฑ ๊ทน๋ํ
โ NuGet ํจํค์ง ์ํ๊ณ ํ์ฉ ๊ฐ๋ฅ
๐ Blazor WebAssembly: ๋ธ๋ผ์ฐ์ ๊ฐ ๊ณง ๋ฐํ์!
๋จผ์ Blazor WebAssembly(์ค์ฌ์ WASM)๋ถํฐ ์ดํด๋ณผ๊ฒ. ์ด ๋
์์ ์ ๋ง ํ์ ์ ์ด์ผ! ๐
WebAssembly๋ ๋ญ๋๊ณ ? ๊ฐ๋จํ ๋งํ๋ฉด, ๋ธ๋ผ์ฐ์ ์์ ๋ค์ดํฐ๋ธ์ ๊ฐ๊น์ด ์๋๋ก ์คํ๋๋ ๋ฐ์ด๋๋ฆฌ ์ฝ๋ ํ์์ด์ผ. ์๋๋ C, C++, Rust ๊ฐ์ ์ธ์ด๋ก ์์ฑ๋ ์ฝ๋๋ฅผ ์น์์ ๋๋ฆฌ๊ธฐ ์ํด ๋ง๋ค์ด์ก๋๋ฐ, .NET ํ์ด ์ฌ๊ธฐ์ C#์ ์น์ ๊ฑฐ์ง!
Blazor WASM์ ์๋ ์๋ฆฌ๋ฅผ ์ฝ๊ฒ ์ค๋ช
ํ๋ฉด ์ด๋:
C# ์ฝ๋๊ฐ .NET IL(Intermediate Language)๋ก ์ปดํ์ผ๋ผ. ์ด๊ฑด ํ์ .NET ๊ฐ๋ฐํ ๋๋ ๋๊ฐ์.
.NET ๋ฐํ์ ์์ฒด๊ฐ WebAssembly๋ก ์ปดํ์ผ๋๊ณ , ๋์ ์ ํ๋ฆฌ์ผ์ด์ DLL๋ค๊ณผ ํจ๊ป ํจํค์ง๋ผ.
์ฌ์ฉ์๊ฐ ์น์ฌ์ดํธ์ ์ ์ํ๋ฉด, ๋ธ๋ผ์ฐ์ ๊ฐ .NET ๋ฐํ์(WASM ํํ)๊ณผ ์ ํ๋ฆฌ์ผ์ด์ DLL๋ค์ ๋ค์ด๋ก๋ํด.
๋ธ๋ผ์ฐ์ ์์์ .NET ๋ฐํ์์ด ์์๋๊ณ , ๋์ C# ์ฝ๋๊ฐ ์คํ๋ผ. JavaScript ์์ด ๋ง์ด์ผ!
๐ป Blazor WASM ํ๋ก์ ํธ ์์ํ๊ธฐ
์, ์ด์ ์ค์ ๋ก ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! Visual Studio๋ VS Code๋ฅผ ์ด๊ณ ํฐ๋ฏธ๋์ ์ด๋ ๊ฒ ์ ๋ ฅํด:
dotnet new blazorwasm -o MyBlazorApp
cd MyBlazorApp
dotnet run
์ง ! ๐ ์ด๊ฒ ๋์ด์ผ. ๋ธ๋ผ์ฐ์ ์์ https://localhost:5001 ๋ก ์ ์ํ๋ฉด ๋์ ์ฒซ Blazor ์ฑ์ด ์คํ๋๊ณ ์์ ๊ฑฐ์ผ.
ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด ์ด๋ฐ ์์ด์ผ:
MyBlazorApp/
โโโ Pages/ # ํ์ด์ง ์ปดํฌ๋ํธ๋ค
โ โโโ Index.razor # ํ ํ์ด์ง
โ โโโ Counter.razor # ์นด์ดํฐ ์์
โ โโโ FetchData.razor # ๋ฐ์ดํฐ ํ์นญ ์์
โโโ Shared/ # ๊ณต์ ์ปดํฌ๋ํธ
โ โโโ MainLayout.razor
โ โโโ NavMenu.razor
โโโ wwwroot/ # ์ ์ ํ์ผ๋ค
โ โโโ css/
โ โโโ index.html # ์ง์
์ HTML
โ โโโ favicon.ico
โโโ App.razor # ๋ฃจํธ ์ปดํฌ๋ํธ
โโโ Program.cs # ์ ํ๋ฆฌ์ผ์ด์
์ง์
์
โโโ _Imports.razor # ์ ์ญ using ์ ์ธ
๊ฐ๋จํ ํ ์ผ ๋ชฉ๋ก ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณผ๊น? Pages ํด๋์ TodoList.razor ํ์ผ์ ๋ง๋ค๊ณ ์ด๋ ๊ฒ ์์ฑํด๋ด:
@page "/todo"
<h3>๋์ ํ ์ผ ๋ชฉ๋ก ๐</h3>
<div>
<input @bind="newTodo" placeholder="์๋ก์ด ํ ์ผ ์
๋ ฅ" />
<button @onclick="AddTodo">์ถ๊ฐ</button>
</div>
<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
<span style="@(todo.IsDone ? "text-decoration: line-through;" : "")">
@todo.Title
</span>
<button @onclick="() => RemoveTodo(todo)">์ญ์ </button>
</li>
}
</ul>
@code {
private List<TodoItem> todos = new();
private string newTodo = "";
private void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = "";
}
}
private void RemoveTodo(TodoItem todo)
{
todos.Remove(todo);
}
private class TodoItem
{
public string Title { get; set; } = "";
public bool IsDone { get; set; }
}
}
๋ณด์ด์ง? Razor ๋ฌธ๋ฒ์ผ๋ก HTML๊ณผ C# ์ฝ๋๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ์์ด ์ธ ์ ์์ด! @bind๋ ์๋ฐฉํฅ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์, @onclick์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ฐ๊ฒฐํด์ค. ์์ ์ง๊ด์ ์ด์ง? ๐
2. SEO ๋ฌธ์ : ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง์ด๋ผ ๊ฒ์ ์์ง ์ต์ ํ๊ฐ ์ด๋ ค์ธ ์ ์์ด. ์ด๋ด ๋ Blazor Server๋ ์ฌ์ ๋ ๋๋ง์ ๊ณ ๋ คํด๋ด.
3. ๋ธ๋ผ์ฐ์ ํธํ์ฑ: WebAssembly๋ฅผ ์ง์ํ๋ ๋ชจ๋ ๋ธ๋ผ์ฐ์ ๊ฐ ํ์ํด. IE๋ ์ ๋ผ! (๋ญ, ์ด์ IE๋ ์ญ์ฌ ์์ผ๋ก ์ฌ๋ผ์ก์ง๋ง ๐ )
๐จ Blazor WASM์ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค
๊ธฐ๋ณธ์ ๋์ด์ ์ข ๋ ๊น์ด ๋ค์ด๊ฐ๋ณผ๊น? Blazor WASM์๋ ์ ๋ง ๋ฉ์ง ๊ธฐ๋ฅ๋ค์ด ๋ง์!
1. JavaScript Interop (JS ์ํธ์ด์ฉ)
์๋ฌด๋ฆฌ C#์ด ์ข์๋, ๊ฐ๋์ JavaScript ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์จ์ผ ํ ๋๊ฐ ์์ง. ๊ฑฑ์ ๋ง! Blazor๋ JS์ ์๋ฒฝํ๊ฒ ํต์ ํ ์ ์์ด:
@inject IJSRuntime JS
<button @onclick="ShowAlert">์๋ฆผ ๋์ฐ๊ธฐ</button>
@code {
private async Task ShowAlert()
{
await JS.InvokeVoidAsync("alert", "์๋
ํ์ธ์! Blazor์์ ์์ด์! ๐");
}
// JavaScript ํจ์ ํธ์ถํ๊ณ ๊ฒฐ๊ณผ ๋ฐ๊ธฐ
private async Task<string> GetUserAgent()
{
return await JS.InvokeAsync<string>("eval", "navigator.userAgent");
}
}
๋ฐ๋๋ก JavaScript์์ C# ๋ฉ์๋๋ฅผ ํธ์ถํ ์๋ ์์ด! wwwroot/index.html์ ์ด๋ฐ ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํ๊ณ :
<script>
window.callDotNetMethod = (dotNetHelper) => {
dotNetHelper.invokeMethodAsync('UpdateMessage', '์๋ฐ์คํฌ๋ฆฝํธ์์ ์๋
!');
};
</script>
C# ์ฝ๋์์๋ ์ด๋ ๊ฒ ๋ฐ์:
@implements IDisposable
<p>@message</p>
@code {
private string message = "์ด๊ธฐ ๋ฉ์์ง";
private DotNetObjectReference<MyComponent>? objRef;
protected override void OnInitialized()
{
objRef = DotNetObjectReference.Create(this);
}
[JSInvokable]
public void UpdateMessage(string newMessage)
{
message = newMessage;
StateHasChanged(); // UI ์
๋ฐ์ดํธ
}
public void Dispose()
{
objRef?.Dispose();
}
}
Blazor WASM ์ฑ์ ์ฑ๋ฅ์ ๋์ด์ฌ๋ฆฌ๋ ๋ช ๊ฐ์ง ๋น๋ฒ์ ์๋ ค์ค๊ฒ:
1. Lazy Loading (์ง์ฐ ๋ก๋ฉ)
๋ชจ๋ DLL์ ํ ๋ฒ์ ๋ก๋ํ์ง ๋ง๊ณ , ํ์ํ ๋๋ง ๋ก๋ํ๋๋ก ์ค์ ํ ์ ์์ด:
// App.razor
<Router AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="@lazyLoadedAssemblies"
OnNavigateAsync="@OnNavigateAsync">
...
</Router>
@code {
private List<Assembly> lazyLoadedAssemblies = new();
private async Task OnNavigateAsync(NavigationContext context)
{
if (context.Path == "/admin")
{
var assemblies = await LazyLoadAssembly(new[] { "AdminModule.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
}
}
2. AOT (Ahead-of-Time) ์ปดํ์ผ.NET 6๋ถํฐ๋ AOT ์ปดํ์ผ์ ์ง์ํด์ ๋ฐํ์ ์ฑ๋ฅ์ด ํฌ๊ฒ ํฅ์๋ผ:
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
๋จ, ๋น๋ ์๊ฐ์ด ๊ธธ์ด์ง๊ณ ํ์ผ ํฌ๊ธฐ๊ฐ ์ปค์ง๋ค๋ ๋จ์ ์ด ์์ด. ํธ๋ ์ด๋์คํ๋ฅผ ์ ๊ณ ๋ คํด์ผ ํด!3. Virtualization (๊ฐ์ํ)
๊ธด ๋ฆฌ์คํธ๋ฅผ ๋ ๋๋งํ ๋๋ Virtualize ์ปดํฌ๋ํธ๋ฅผ ์จ:
<Virtualize Items="@allItems" Context="item">
<div>@item.Name</div>
</Virtualize>
์ด๋ ๊ฒ ํ๋ฉด ํ๋ฉด์ ๋ณด์ด๋ ํญ๋ชฉ๋ค๋ง ๋ ๋๋งํด์ ์ฑ๋ฅ์ด ์์ฒญ ์ข์์ ธ! ๐
โก Blazor Server: ์ค์๊ฐ ์ฐ๊ฒฐ์ ๋ง๋ฒ
์, ์ด์ Blazor Server๋ฅผ ์์๋ณผ ์ฐจ๋ก์ผ! ์ด ๋
์์ WebAssembly์๋ ์์ ํ ๋ค๋ฅธ ์ ๊ทผ ๋ฐฉ์์ ์ทจํด.
Blazor Server์ ํต์ฌ ์์ด๋์ด๋ ๊ฐ๋จํด: ๋ชจ๋ UI ๋ก์ง๊ณผ ์ํ๋ฅผ ์๋ฒ์์ ๊ด๋ฆฌํ๊ณ , ๋ธ๋ผ์ฐ์ ๋ ๊ทธ๋ฅ "ํ๋ฉด"๋ง ๋ณด์ฌ์ฃผ๋ ๊ฑฐ์ผ. ๋ธ๋ผ์ฐ์ ์ ์๋ฒ๋ SignalR์ด๋ผ๋ ์ค์๊ฐ ํต์ ๊ธฐ์ ๋ก ์ฐ๊ฒฐ๋ผ ์์ด.
์๋ ๋ฐฉ์์ ์ดํด๋ณด์:
์ฌ์ฉ์๊ฐ ํ์ด์ง์ ์ ์ํ๋ฉด, ์๋ฒ๊ฐ ์ด๊ธฐ HTML์ ๋ ๋๋งํด์ ๋ณด๋ด์ค. ๋์์ SignalR WebSocket ์ฐ๊ฒฐ์ด ์๋ฆฝ๋ผ.
์ฌ์ฉ์๊ฐ ๋ฒํผ์ ํด๋ฆญํ๊ฑฐ๋ ์ ๋ ฅ์ ํ๋ฉด, ๊ทธ ์ด๋ฒคํธ๊ฐ SignalR์ ํตํด ์๋ฒ๋ก ์ ์ก๋ผ.
์๋ฒ์์ C# ์ฝ๋๊ฐ ์คํ๋๊ณ , UI ์ํ๊ฐ ์ ๋ฐ์ดํธ๋ผ. ๊ทธ๋ฆฌ๊ณ ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ๊ณ์ฐํด.
๋ณ๊ฒฝ๋ UI ๋ถ๋ถ(diff)๋ง SignalR์ ํตํด ๋ธ๋ผ์ฐ์ ๋ก ์ ์ก๋๊ณ , ๋ธ๋ผ์ฐ์ ๊ฐ DOM์ ์ ๋ฐ์ดํธํด.
๐ Blazor Server ํ๋ก์ ํธ ๋ง๋ค๊ธฐ
Blazor Server ํ๋ก์ ํธ๋ฅผ ์์ํ๋ ๊ฑด ์ ๋ง ์ฌ์:
dotnet new blazorserver -o MyBlazorServerApp
cd MyBlazorServerApp
dotnet run
ํ๋ก์ ํธ ๊ตฌ์กฐ๋ WASM๊ณผ ๋น์ทํ์ง๋ง, ๋ช ๊ฐ์ง ์ฐจ์ด์ ์ด ์์ด:
MyBlazorServerApp/
โโโ Pages/ # ํ์ด์ง ์ปดํฌ๋ํธ๋ค
โโโ Shared/ # ๊ณต์ ์ปดํฌ๋ํธ
โโโ Data/ # ๋ฐ์ดํฐ ์๋น์ค๋ค (Server๋ง์ ํน์ง!)
โ โโโ WeatherForecastService.cs
โโโ wwwroot/ # ์ ์ ํ์ผ๋ค
โโโ App.razor # ๋ฃจํธ ์ปดํฌ๋ํธ
โโโ Program.cs # ์ ํ๋ฆฌ์ผ์ด์
์ค์
โโโ _Imports.razor # ์ ์ญ using
Program.cs๋ฅผ ๋ณด๋ฉด SignalR ์ค์ ์ด ๋ค์ด๊ฐ ์์ด:
var builder = WebApplication.CreateBuilder(args);
// Blazor Server ์๋น์ค ์ถ๊ฐ
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// ์ฑ๊ธํค ์๋น์ค ๋ฑ๋ก (๋ชจ๋ ์ฌ์ฉ์๊ฐ ๊ณต์ )
builder.Services.AddSingleton<WeatherForecastService>();
// ์ค์ฝํ ์๋น์ค ๋ฑ๋ก (๊ฐ ์ฌ์ฉ์ ์ธ์
๋ง๋ค ์ธ์คํด์ค)
builder.Services.AddScoped<UserService>();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub(); // SignalR ํ๋ธ ๋งคํ!
app.MapFallbackToPage("/_Host");
app.Run();
1. ๋น ๋ฅธ ์ด๊ธฐ ๋ก๋ฉ โก
.NET ๋ฐํ์์ ๋ค์ด๋ก๋ํ ํ์๊ฐ ์์ผ๋๊น ํ์ด์ง๊ฐ ์์๊ฐ์ ๋ฌ๋ค! ์ด๊ธฐ ํ์ด๋ก๋๊ฐ WASM๋ณด๋ค ํจ์ฌ ์์.
2. ์์ ํ .NET API ์ ๊ทผ ๐
์๋ฒ์์ ์คํ๋๋๊น ํ์ผ ์์คํ , ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ชจ๋ .NET ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ์ฝ ์์ด ์ฌ์ฉํ ์ ์์ด. WASM์์๋ ๋ธ๋ผ์ฐ์ ์๋๋ฐ์ค ๋๋ฌธ์ ์ ํ์ด ์๊ฑฐ๋ .
3. ์์ ํด๋ผ์ด์ธํธ ์๊ตฌ์ฌํญ ๐ฑ
์ค๋๋ ๊ธฐ๊ธฐ๋ ์ ์ฌ์ ๊ธฐ๊ธฐ์์๋ ์ ๋์๊ฐ. ๋ฌด๊ฑฐ์ด ์์ ์ ๋ค ์๋ฒ๊ฐ ํ๋๊น!
4. ์ฝ๋ ๋ณด์ ๐
๋น์ฆ๋์ค ๋ก์ง์ด ์๋ฒ์ ์์ผ๋๊น ํด๋ผ์ด์ธํธ์์ ๋์ปดํ์ผํ ๊ฑฑ์ ์ด ์์ด. ์ค์ํ ์๊ณ ๋ฆฌ์ฆ์ด๋ ๋ผ์ด์ ์ค ํค ๊ฐ์ ๊ฑธ ์์ ํ๊ฒ ๋ณดํธํ ์ ์์ง.
๐ฏ ์ค์ ์์ : ์ค์๊ฐ ์ฑํ ์ฑ ๋ง๋ค๊ธฐ
Blazor Server์ ์ง๊ฐ๋ ์ค์๊ฐ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ๋ ๋๋ฌ๋! ๊ฐ๋จํ ์ฑํ
์ฑ์ ๋ง๋ค์ด๋ณผ๊น?
๋จผ์ ์ฑํ
์๋น์ค๋ฅผ ๋ง๋ค์ด:
// Services/ChatService.cs
public class ChatService
{
private readonly List<ChatMessage> messages = new();
public event Action? OnMessagesChanged;
public List<ChatMessage> GetMessages() => messages;
public async Task SendMessage(string user, string message)
{
messages.Add(new ChatMessage
{
User = user,
Message = message,
Timestamp = DateTime.Now
});
// ๋ชจ๋ ๊ตฌ๋
์์๊ฒ ์๋ฆผ
OnMessagesChanged?.Invoke();
await Task.CompletedTask;
}
}
public class ChatMessage
{
public string User { get; set; } = "";
public string Message { get; set; } = "";
public DateTime Timestamp { get; set; }
}
Program.cs์ ์ฑ๊ธํค์ผ๋ก ๋ฑ๋ก:
builder.Services.AddSingleton<ChatService>();
์ด์ ์ฑํ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด:
@page "/chat"
@inject ChatService ChatService
@implements IDisposable
<h3>์ค์๊ฐ ์ฑํ
๋ฐฉ ๐ฌ</h3>
<div style="border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: auto;">
@foreach (var msg in messages)
{
<div style="margin-bottom: 10px;">
<strong>@msg.User</strong>
<span style="color: #999; font-size: 0.8em;">
@msg.Timestamp.ToString("HH:mm:ss")
</span>
<br/>
@msg.Message
</div>
}
</div>
<div style="margin-top: 10px;">
<input @bind="username" placeholder="์ด๋ฆ" style="width: 100px;" />
<input @bind="newMessage" placeholder="๋ฉ์์ง ์
๋ ฅ"
@onkeyup="HandleKeyUp" style="width: 300px;" />
<button @onclick="SendMessage">์ ์ก</button>
</div>
@code {
private List<ChatMessage> messages = new();
private string username = "์ต๋ช
";
private string newMessage = "";
protected override void OnInitialized()
{
messages = ChatService.GetMessages();
ChatService.OnMessagesChanged += UpdateMessages;
}
private async Task SendMessage()
{
if (!string.IsNullOrWhiteSpace(newMessage))
{
await ChatService.SendMessage(username, newMessage);
newMessage = "";
}
}
private async Task HandleKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await SendMessage();
}
}
private void UpdateMessages()
{
messages = ChatService.GetMessages();
InvokeAsync(StateHasChanged); // UI ์
๋ฐ์ดํธ!
}
public void Dispose()
{
ChatService.OnMessagesChanged -= UpdateMessages;
}
}
์ง ! ๐ ์ด์ ์ฌ๋ฌ ๋ธ๋ผ์ฐ์ ํญ์ ์ด์ด์ ํ ์คํธํด๋ด. ํ ํญ์์ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ฉด ๋ค๋ฅธ ๋ชจ๋ ํญ์ ์ค์๊ฐ์ผ๋ก ๋ํ๋ ๊ฑฐ์ผ! SignalR์ด ์๋์ผ๋ก ๋ชจ๋ ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฐ์ดํธ๋ฅผ ํธ์ํ๊ฑฐ๋ . ์์ ๋ง๋ฒ ๊ฐ์ง? โจ
1. ์๋ฒ ๋ฆฌ์์ค ์๋น: ๊ฐ ์ฌ์ฉ์๋ง๋ค ์๋ฒ์ ์ฐ๊ฒฐ์ด ์ ์ง๋๊ณ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฐจ์งํด. ๋์ ์ฌ์ฉ์๊ฐ ๋ง์ผ๋ฉด ์๋ฒ ํ์ฅ์ด ํ์ํ ์ ์์ด.
2. ๋คํธ์ํฌ ์ง์ฐ: ๋ชจ๋ ์ํธ์์ฉ์ด ์๋ฒ๋ฅผ ๊ฑฐ์ณ์ผ ํ๋๊น ๋คํธ์ํฌ ์ง์ฐ์ด UX์ ์ํฅ์ ์ค ์ ์์ด. ํนํ ํด์ธ ์ฌ์ฉ์๊ฐ ์๋ค๋ฉด CDN์ด๋ ์ง์ญ๋ณ ์๋ฒ ๋ฐฐํฌ๋ฅผ ๊ณ ๋ คํด์ผ ํด.
3. ์ฐ๊ฒฐ ๋๊น ์ฒ๋ฆฌ: WiFi๊ฐ ๋๊ธฐ๊ฑฐ๋ ๋คํธ์ํฌ๊ฐ ๋ถ์์ ํ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ง ์ ์์ด. ์ฌ์ฐ๊ฒฐ ๋ก์ง์ ์ ๊ตฌํํด์ผ ํด:
// _Host.cshtml์ ์ถ๊ฐ
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({
reconnectionOptions: {
maxRetries: 10,
retryIntervalMilliseconds: 2000
}
});
</script>
4. ํ์ฅ์ฑ ๊ณ ๋ ค: ๋ก๋ ๋ฐธ๋ฐ์๋ฅผ ์ฌ์ฉํ๋ค๋ฉด Sticky Session์ด ํ์ํด. SignalR ์ฐ๊ฒฐ์ด ํน์ ์๋ฒ์ ๊ณ ์ ๋์ด์ผ ํ๊ฑฐ๋ . Azure SignalR Service ๊ฐ์ ๊ฑธ ์ฐ๋ฉด ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ด!
๐ WebAssembly vs Server: ์ด๋ค ๊ฑธ ์ ํํด์ผ ํ ๊น?
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ์ง๋ฌธ์ด์ผ. "๋ด ํ๋ก์ ํธ์๋ ๋ญ ์จ์ผ ํ์ง?" ๐ค
์ ๋ต์... "์ํฉ์ ๋ฐ๋ผ ๋ค๋ฅด๋ค"์ผ! (๊ฐ๋ฐ์๋ค์ด ์ ์ผ ์ซ์ดํ๋ ๋ต๋ณ์ด์ง? ใ
ใ
) ํ์ง๋ง ๊ฑฑ์ ๋ง, ๋ช
ํํ ๊ฐ์ด๋๋ผ์ธ์ ์ค๊ฒ!
๐ Blazor WASM์ ์ ํํด์ผ ํ ๋
โ
์ด๋ฐ ๊ฒฝ์ฐ์ ๋ฑ์ด์ผ:
โข ์คํ๋ผ์ธ ๊ธฐ๋ฅ์ด ํ์ํ ๋
PWA๋ก ๋ง๋ค์ด์ ์คํ๋ผ์ธ์์๋ ์๋ํ๋ ์ฑ์ ๋ง๋ค ์ ์์ด.
โข ์๋ฒ ๋ถํ๋ฅผ ์ค์ด๊ณ ์ถ์ ๋
๋ชจ๋ ์ฒ๋ฆฌ๊ฐ ํด๋ผ์ด์ธํธ์์ ์ผ์ด๋๋๊น ์๋ฒ๋ API๋ง ์ ๊ณตํ๋ฉด ๋ผ.
โข ์ ์ ํธ์คํ
์ ์ํ ๋
GitHub Pages, Netlify, Vercel ๊ฐ์ ๊ณณ์ ๋ฌด๋ฃ๋ก ํธ์คํ
๊ฐ๋ฅ!
โข ๊ธ๋ก๋ฒ ์ฌ์ฉ์๊ฐ ๋ง์ ๋
CDN์ผ๋ก ๋ฐฐํฌํ๋ฉด ์ ์ธ๊ณ ์ด๋์๋ ๋น ๋ฅด๊ฒ ๋ก๋๋ผ.
โข SPA(Single Page Application) ๊ฒฝํ์ ์ํ ๋
React๋ Vue์ฒ๋ผ ๋ถ๋๋ฌ์ด ํ์ด์ง ์ ํ์ด ๊ฐ๋ฅํด.
โก Blazor Server๋ฅผ ์ ํํด์ผ ํ ๋
โ
์ด๋ฐ ๊ฒฝ์ฐ์ ๋ฑ์ด์ผ:
โข ๋น ๋ฅธ ์ด๊ธฐ ๋ก๋ฉ์ด ์ค์ํ ๋
ํนํ ๋ชจ๋ฐ์ผ ์ฌ์ฉ์๊ฐ ๋ง๋ค๋ฉด Server๊ฐ ์ ๋ฆฌํด.
โข ์ค์๊ฐ ๊ธฐ๋ฅ์ด ํ์ํ ๋
์ฑํ
, ๋์๋ณด๋, ํ์
๋๊ตฌ ๋ฑ์ ์ต์ ํ๋์ด ์์ด.
โข ๋ณด์์ด ์ค์ํ ๋
๋น์ฆ๋์ค ๋ก์ง์ ํด๋ผ์ด์ธํธ์ ๋
ธ์ถํ๊ณ ์ถ์ง ์๋ค๋ฉด Server!
โข ๋ ๊ฑฐ์ .NET ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์จ์ผ ํ ๋
WASM์์ ์ง์ ์ ๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ Server์์๋ ๋ฌธ์ ์์ด.
โข ์ ์ฌ์ ํด๋ผ์ด์ธํธ๋ฅผ ์ง์ํด์ผ ํ ๋
์ค๋๋ ๊ธฐ๊ธฐ๋ ์ ์ฌ์ ์ค๋งํธํฐ์์๋ ์ ๋์๊ฐ.
๋ ๋์๊ฐ์, Blazor United (๋๋ Blazor Web)๋ผ๋ ๊ฐ๋ ๋ ๋์์ด. ์ด๊ฑด Server์ WASM์ ์ฅ์ ์ ๊ฒฐํฉํ ๊ฑฐ์ผ:
โข ์ด๊ธฐ ๋ก๋ฉ์ Server ๋ชจ๋๋ก ๋น ๋ฅด๊ฒ
โข ์ดํ ์ํธ์์ฉ์ WASM์ผ๋ก ์ ํ
โข ํ์์ ๋ฐ๋ผ ๋์ ์ผ๋ก ๋ ๋๋ง ๋ชจ๋ ๋ณ๊ฒฝ
.NET 8์์ ์ด ๊ธฐ๋ฅ์ด ์ ์์ผ๋ก ๋ค์ด์์ผ๋, ์ต์ ๋ฒ์ ์ ์ด๋ค๋ฉด ๊ผญ ์ฒดํฌํด๋ด! ๐
| ๋น๊ต ํญ๋ชฉ | Blazor WebAssembly | Blazor Server |
|---|---|---|
| ์คํ ์์น | ๋ธ๋ผ์ฐ์ (ํด๋ผ์ด์ธํธ) | ์๋ฒ |
| ์ด๊ธฐ ๋ก๋ฉ | ๋๋ฆผ (2-3MB) | ๋น ๋ฆ (์๋ฐฑ KB) |
| ๋ฐํ์ ์ฑ๋ฅ | ๋น ๋ฆ (๋ก์ปฌ ์คํ) | ๋คํธ์ํฌ ์ง์ฐ ์กด์ฌ |
| ์๋ฒ ๋ถํ | ๋ฎ์ (API๋ง) | ๋์ (์ฐ๊ฒฐ ์ ์ง) |
| ์คํ๋ผ์ธ ์ง์ | ๊ฐ๋ฅ (PWA) | ๋ถ๊ฐ๋ฅ |
| SEO | ์ด๋ ค์ (์ฌ์ ๋ ๋๋ง ํ์) | ์ฌ์ (์๋ฒ ๋ ๋๋ง) |
| ํ์ฅ์ฑ | ์ฐ์ (CDN ํ์ฉ) | ์ ํ์ (์๋ฒ ํ์ฅ ํ์) |
| ๋ณด์ | ์ฝ๋ ๋ ธ์ถ ๊ฐ๋ฅ | ์๋ฒ์์ ๋ณดํธ |
| ๋๋ฒ๊น | ๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ | Visual Studio ์๋ฒฝ ์ง์ |
| ํธ์คํ ๋น์ฉ | ์ ๋ ด (์ ์ ํธ์คํ ) | ๋น์ (์๋ฒ ํ์) |
๐จ ์ปดํฌ๋ํธ ๋์์ธ ํจํด๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ด์ ์ค์ ์์ ์ ๋ง ์ค์ํ ๋ถ๋ถ์ผ๋ก ๋์ด๊ฐ๋ณผ๊ฒ! Blazor ์ปดํฌ๋ํธ๋ฅผ ์ด๋ป๊ฒ ์ค๊ณํ๊ณ ๊ตฌ์กฐํํด์ผ ์ ์ง๋ณด์๊ฐ ์ฝ๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ฝ๋๋ฅผ ๋ง๋ค ์ ์์๊น? ๐ค
1. ์ปดํฌ๋ํธ ํ๋ผ๋ฏธํฐ์ ์ด๋ฒคํธ ์ฝ๋ฐฑ
์ปดํฌ๋ํธ ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋ ๊ฑด ์ ๋ง ์ค์ํด. Blazor์์๋ ์ด๋ ๊ฒ ํด:
// ChildComponent.razor
<div>
<h4>@Title</h4>
<button @onclick="HandleClick">ํด๋ฆญ!</button>
</div>
@code {
// ๋ถ๋ชจ๋ก๋ถํฐ ๋ฐ๋ ํ๋ผ๋ฏธํฐ
[Parameter]
public string Title { get; set; } = "";
// ๋ถ๋ชจ์๊ฒ ์ด๋ฒคํธ ์ ๋ฌ
[Parameter]
public EventCallback<string> OnButtonClicked { get; set; }
private async Task HandleClick()
{
await OnButtonClicked.InvokeAsync($"{Title} ๋ฒํผ์ด ํด๋ฆญ๋จ!");
}
}
๋ถ๋ชจ ์ปดํฌ๋ํธ์์๋ ์ด๋ ๊ฒ ์ฌ์ฉํด:
// ParentComponent.razor
<ChildComponent Title="์ฒซ ๋ฒ์งธ" OnButtonClicked="HandleChildClick" />
<ChildComponent Title="๋ ๋ฒ์งธ" OnButtonClicked="HandleChildClick" />
<p>๋ฉ์์ง: @message</p>
@code {
private string message = "";
private void HandleChildClick(string msg)
{
message = msg;
}
}
์ปดํฌ๋ํธ ํธ๋ฆฌ๊ฐ ๊น์ด์ง๋ฉด ํ๋ผ๋ฏธํฐ๋ฅผ ๊ณ์ ์ ๋ฌํ๊ธฐ ๊ท์ฐฎ์์ง์ง? ์ด๋ด ๋ CascadingParameter๋ฅผ ์จ!
// ์ต์์ ์ปดํฌ๋ํธ
<CascadingValue Value="currentUser">
<MainLayout>
@Body
</MainLayout>
</CascadingValue>
@code {
private User currentUser = new User { Name = "ํ๊ธธ๋" };
}
// ๊น์ ๊ณณ์ ์์ ์ปดํฌ๋ํธ
@code {
[CascadingParameter]
private User CurrentUser { get; set; } = null!;
// ์ด์ CurrentUser๋ฅผ ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ!
}
์ด๋ ๊ฒ ํ๋ฉด ์ค๊ฐ ์ปดํฌ๋ํธ๋ค์ ๊ฑฐ์น์ง ์๊ณ ๋ฐ๋ก ์ ๋ฌํ ์ ์์ด! ํ
๋ง ์ค์ , ์ฌ์ฉ์ ์ ๋ณด, ๊ถํ ๊ฐ์ ์ ์ญ ๋ฐ์ดํฐ์ ์๋ฒฝํด. ๐
2. ์ํ ๊ด๋ฆฌ ํจํด
์ฑ์ด ์ปค์ง๋ฉด ์ํ ๊ด๋ฆฌ๊ฐ ๋ณต์กํด์ ธ. ์ฌ๋ฌ ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํด์ผ ํ ๋๊ฐ ๋ง๊ฑฐ๋ . ์ด๋ด ๋ ์ํ ๊ด๋ฆฌ ์๋น์ค๋ฅผ ๋ง๋ค์ด:
// Services/AppState.cs
public class AppState
{
private string currentUser = "";
private int notificationCount = 0;
public string CurrentUser
{
get => currentUser;
set
{
currentUser = value;
NotifyStateChanged();
}
}
public int NotificationCount
{
get => notificationCount;
set
{
notificationCount = value;
NotifyStateChanged();
}
}
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
Program.cs์ ๋ฑ๋ก:
builder.Services.AddScoped<AppState>(); // WASM
// ๋๋
builder.Services.AddSingleton<AppState>(); // Server
์ปดํฌ๋ํธ์์ ์ฌ์ฉ:
@inject AppState AppState
@implements IDisposable
<p>ํ์ฌ ์ฌ์ฉ์: @AppState.CurrentUser</p>
<p>์๋ฆผ: @AppState.NotificationCount๊ฐ</p>
@code {
protected override void OnInitialized()
{
AppState.OnChange += StateHasChanged;
}
public void Dispose()
{
AppState.OnChange -= StateHasChanged;
}
}
๋ ๋ณต์กํ ์ํ ๊ด๋ฆฌ๊ฐ ํ์ํ๋ค๋ฉด Fluxor ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ์ฒํด! Redux ํจํด์ Blazor์ ๋ง๊ฒ ๊ตฌํํ ๊ฑฐ์ผ.
์ค์น:
dotnet add package Fluxor.Blazor.Web
State ์ ์:
// Store/CounterState.cs
public record CounterState
{
public int Count { get; init; }
}
public class CounterFeature : Feature<CounterState>
{
public override string GetName() => "Counter";
protected override CounterState GetInitialState() => new() { Count = 0 };
}
Action๊ณผ Reducer:
// Store/CounterActions.cs
public class IncrementCounterAction { }
public class DecrementCounterAction { }
// Store/CounterReducers.cs
public static class CounterReducers
{
[ReducerMethod]
public static CounterState OnIncrement(CounterState state, IncrementCounterAction action)
=> state with { Count = state.Count + 1 };
[ReducerMethod]
public static CounterState OnDecrement(CounterState state, DecrementCounterAction action)
=> state with { Count = state.Count - 1 };
}
์ปดํฌ๋ํธ์์ ์ฌ์ฉ:
@inherits FluxorComponent
@inject IState<CounterState> CounterState
@inject IDispatcher Dispatcher
<p>์นด์ดํธ: @CounterState.Value.Count</p>
<button @onclick="() => Dispatcher.Dispatch(new IncrementCounterAction())">+</button>
<button @onclick="() => Dispatcher.Dispatch(new DecrementCounterAction())">-</button>
์ด๋ ๊ฒ ํ๋ฉด ์ํ ๋ณ๊ฒฝ์ด ์์ธก ๊ฐ๋ฅํ๊ณ ์ถ์ ํ๊ธฐ ์ฌ์์ ธ! ๋๋ฒ๊น
๋ ํจ์ฌ ํธํ๊ณ . ๐ฏ
3. ํผ ์ฒ๋ฆฌ์ ์ ํจ์ฑ ๊ฒ์ฌ
Blazor์ ํผ ์ฒ๋ฆฌ๋ ์ ๋ง ๊ฐ๋ ฅํด! EditForm ์ปดํฌ๋ํธ์ ๋ฐ์ดํฐ ์ด๋
ธํ
์ด์
์ ํ์ฉํ๋ฉด ์ฝ๊ฒ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๊ตฌํํ ์ ์์ด:
// Models/UserRegistration.cs
using System.ComponentModel.DataAnnotations;
public class UserRegistration
{
[Required(ErrorMessage = "์ด๋ฆ์ ํ์์
๋๋ค.")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "์ด๋ฆ์ 2-50์์ฌ์ผ ํฉ๋๋ค.")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "์ด๋ฉ์ผ์ ํ์์
๋๋ค.")]
[EmailAddress(ErrorMessage = "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.")]
public string Email { get; set; } = "";
[Required(ErrorMessage = "๋น๋ฐ๋ฒํธ๋ ํ์์
๋๋ค.")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.")]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$",
ErrorMessage = "๋น๋ฐ๋ฒํธ๋ ๋์๋ฌธ์์ ์ซ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.")]
public string Password { get; set; } = "";
[Compare(nameof(Password), ErrorMessage = "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.")]
public string ConfirmPassword { get; set; } = "";
[Range(18, 100, ErrorMessage = "๋์ด๋ 18-100 ์ฌ์ด์ฌ์ผ ํฉ๋๋ค.")]
public int Age { get; set; }
}
ํผ ์ปดํฌ๋ํธ:
@page "/register"
<h3>ํ์๊ฐ์
๐</h3>
<EditForm Model="registration" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label>์ด๋ฆ:</label>
<InputText @bind-Value="registration.Name" />
<ValidationMessage For="() => registration.Name" />
</div>
<div>
<label>์ด๋ฉ์ผ:</label>
<InputText @bind-Value="registration.Email" type="email" />
<ValidationMessage For="() => registration.Email" />
</div>
<div>
<label>๋น๋ฐ๋ฒํธ:</label>
<InputText @bind-Value="registration.Password" type="password" />
<ValidationMessage For="() => registration.Password" />
</div>
<div>
<label>๋น๋ฐ๋ฒํธ ํ์ธ:</label>
<InputText @bind-Value="registration.ConfirmPassword" type="password" />
<ValidationMessage For="() => registration.ConfirmPassword" />
</div>
<div>
<label>๋์ด:</label>
<InputNumber @bind-Value="registration.Age" />
<ValidationMessage For="() => registration.Age" />
</div>
<button type="submit">๊ฐ์
ํ๊ธฐ</button>
</EditForm>
@if (isSuccess)
{
<div style="color: green; margin-top: 20px;">
โ
ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค!
</div>
}
@code {
private UserRegistration registration = new();
private bool isSuccess = false;
private async Task HandleValidSubmit()
{
// ์ฌ๊ธฐ์ API ํธ์ถ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ
await Task.Delay(1000); // ์๋ฎฌ๋ ์ด์
isSuccess = true;
}
}
์ด๋ ๊ฒ ํ๋ฉด ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ๋ ์ฆ์ ์ ํจ์ฑ ๊ฒ์ฌ๊ฐ ์คํ๋๊ณ , ์๋ฌ ๋ฉ์์ง๊ฐ ํ์๋ผ. ์์ ํธํ์ง? ๐
๐ API ํต์ ๊ณผ HTTP ํด๋ผ์ด์ธํธ
์ค์ ์ ํ๋ฆฌ์ผ์ด์
์์๋ ๋ฐฑ์๋ API์ ํต์ ํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋๋ถ๋ถ์ด์ง. Blazor์์ HTTP ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
HttpClient ์ค์
๋จผ์ Program.cs์์ HttpClient๋ฅผ ๋ฑ๋กํด์ผ ํด:
// Blazor WASM
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// ๋๋ Named HttpClient (์ฌ๋ฌ API ์ฌ์ฉ์)
builder.Services.AddHttpClient("MyAPI", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
// Blazor Server
builder.Services.AddHttpClient();
API ์๋น์ค ๋ง๋ค๊ธฐ
API ํธ์ถ ๋ก์ง์ ์๋น์ค๋ก ๋ถ๋ฆฌํ๋ ๊ฒ ์ข์. ์ฌ์ฌ์ฉ์ฑ๋ ๋์์ง๊ณ ํ
์คํธํ๊ธฐ๋ ์ฌ์์ง๊ฑฐ๋ :
// Services/WeatherService.cs
public class WeatherService
{
private readonly HttpClient http;
private readonly ILogger<WeatherService> logger;
public WeatherService(HttpClient http, ILogger<WeatherService> logger)
{
this.http = http;
this.logger = logger;
}
public async Task<List<WeatherForecast>?> GetForecastAsync()
{
try
{
var response = await http.GetFromJsonAsync<List<WeatherForecast>>(
"api/weather/forecast");
return response;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "๋ ์จ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋๋ฐ ์คํจํ์ต๋๋ค.");
return null;
}
}
public async Task<bool> CreateForecastAsync(WeatherForecast forecast)
{
try
{
var response = await http.PostAsJsonAsync("api/weather/forecast", forecast);
return response.IsSuccessStatusCode;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "๋ ์จ ๋ฐ์ดํฐ ์์ฑ์ ์คํจํ์ต๋๋ค.");
return false;
}
}
public async Task<bool> UpdateForecastAsync(int id, WeatherForecast forecast)
{
try
{
var response = await http.PutAsJsonAsync($"api/weather/forecast/{id}", forecast);
return response.IsSuccessStatusCode;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "๋ ์จ ๋ฐ์ดํฐ ์์ ์ ์คํจํ์ต๋๋ค.");
return false;
}
}
public async Task<bool> DeleteForecastAsync(int id)
{
try
{
var response = await http.DeleteAsync($"api/weather/forecast/{id}");
return response.IsSuccessStatusCode;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "๋ ์จ ๋ฐ์ดํฐ ์ญ์ ์ ์คํจํ์ต๋๋ค.");
return false;
}
}
}
public class WeatherForecast
{
public int Id { get; set; }
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
Program.cs์ ๋ฑ๋ก:
builder.Services.AddScoped<WeatherService>();
์ปดํฌ๋ํธ์์ ์ฌ์ฉ:
@page "/weather"
@inject WeatherService WeatherService
<h3>๋ ์จ ์๋ณด ๐ค๏ธ</h3>
@if (isLoading)
{
<p>๋ก๋ฉ ์ค... โณ</p>
}
else if (forecasts == null)
{
<p>๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค. ๐ข</p>
}
else
{
<table>
<thead>
<tr>
<th>๋ ์ง</th>
<th>์จ๋ (ยฐC)</th>
<th>์์ฝ</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<WeatherForecast>? forecasts;
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
forecasts = await WeatherService.GetForecastAsync();
isLoading = false;
}
}
๋คํธ์ํฌ๋ ๋ถ์์ ํ ์ ์์ด. API ํธ์ถ์ด ์คํจํ์ ๋ ์๋์ผ๋ก ์ฌ์๋ํ๋ ๋ก์ง์ ์ถ๊ฐํ๋ฉด ์ข์. Polly ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด!
์ค์น:
dotnet add package Microsoft.Extensions.Http.Polly
์ค์ :
builder.Services.AddHttpClient<WeatherService>()
.AddTransientHttpErrorPolicy(policy =>
policy.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
.AddTransientHttpErrorPolicy(policy =>
policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
์ด๋ ๊ฒ ํ๋ฉด:โข ์คํจ์ ์ต๋ 3๋ฒ๊น์ง ์ฌ์๋ (์ง์ ๋ฐฑ์คํ ๋ฐฉ์)
โข 5๋ฒ ์ฐ์ ์คํจํ๋ฉด 30์ด ๋์ ํ๋ก ์ฐจ๋จ (์๋ฒ ๋ณดํธ)
์์ ํ๋กํ์ ๋ํ์ง? ๐
์ธ์ฆ๊ณผ ๊ถํ ๋ถ์ฌ
์ค์ ์ฑ์์๋ ์ธ์ฆ์ด ํ์์ง. Blazor๋ ASP.NET Core์ ์ธ์ฆ ์์คํ
์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ด:
// Program.cs์ ์ธ์ฆ ์ถ๊ฐ
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
// CustomAuthStateProvider.cs
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly HttpClient http;
private readonly ILocalStorageService localStorage;
public CustomAuthStateProvider(HttpClient http, ILocalStorageService localStorage)
{
this.http = http;
this.localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrEmpty(token))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var claims = ParseClaimsFromJwt(token);
var identity = new ClaimsIdentity(claims, "jwt");
var user = new ClaimsPrincipal(identity);
return new AuthenticationState(user);
}
public void NotifyUserAuthentication(string token)
{
var claims = ParseClaimsFromJwt(token);
var identity = new ClaimsIdentity(claims, "jwt");
var user = new ClaimsPrincipal(identity);
NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(user)));
}
public void NotifyUserLogout()
{
var identity = new ClaimsIdentity();
var user = new ClaimsPrincipal(identity);
NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(user)));
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
return keyValuePairs!.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()!));
}
private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
์ปดํฌ๋ํธ์์ ์ธ์ฆ ์ํ ์ฌ์ฉ:
@page "/profile"
@attribute [Authorize] // ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅ!
<AuthorizeView>
<Authorized>
<h3>์๋
ํ์ธ์, @context.User.Identity?.Name๋! ๐</h3>
<p>์ด๋ฉ์ผ: @context.User.FindFirst("email")?.Value</p>
</Authorized>
<NotAuthorized>
<p>๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.</p>
</NotAuthorized>
</AuthorizeView>
<AuthorizeView Roles="Admin">
<Authorized>
<button>๊ด๋ฆฌ์ ๊ธฐ๋ฅ</button>
</Authorized>
</AuthorizeView>
๐ ์ฌ๋ฅ๋ท์์ Blazor ์ ๋ฌธ๊ฐ๋ฅผ ๋ง๋๋ณด์ธ์!
Blazor ๊ฐ๋ฐ์ด ์ฒ์์ด๋ผ ๋ง๋งํ์ ๊ฐ์? ์๋๋ฉด ํ๋ก์ ํธ์ ๋งํ์ ๋์์ด ํ์ํ์ ๊ฐ์?
์ฌ๋ฅ๋ท์์๋ ๋ค์ํ Blazor ์ ๋ฌธ๊ฐ๋ค์ด ์ฌ๋ฌ๋ถ์ ๊ธฐ๋ค๋ฆฌ๊ณ ์์ด์! 1:1 ์ฝ์นญ๋ถํฐ ํ๋ก์ ํธ ์ธ์ฃผ๊น์ง, ํ์ํ ๋ชจ๋ ๋์์ ๋ฐ์ ์ ์๋ต๋๋ค. ๐
C# ๊ฐ๋ฐ์๋ผ๋ฉด ๋๊ตฌ๋ Blazor๋ก ๋ฉ์ง ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ์ ์์ด์. ์ฌ๋ฅ๋ท์์ ์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ฌ์ ์ ํจ๊ปํ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด์ธ์! ๐
๐จ UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ปดํฌ๋ํธ ์ํ๊ณ
Blazor์ ํฐ ์ฅ์ ์ค ํ๋๋ ํ๋ถํ UI ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ํ๊ณ์ผ! ์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ๋ง๋ค ํ์ ์์ด, ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ํ์ฉํ ์ ์์ด. ๐
์ธ๊ธฐ ์๋ Blazor UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค:
1. MudBlazor ๐จ
Material Design ๊ธฐ๋ฐ์ ๋ฌด๋ฃ ์คํ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ์ ๋ง ์์๊ณ ์ฌ์ฉํ๊ธฐ ์ฌ์!
์ค์น:
dotnet add package MudBlazor
์ค์ (Program.cs):
builder.Services.AddMudServices();
_Imports.razor์ ์ถ๊ฐ:
@using MudBlazor
์ฌ์ฉ ์์:
<MudButton Variant="Variant.Filled" Color="Color.Primary">
ํด๋ฆญํ์ธ์!
</MudButton>
<MudTextField @bind-Value="name" Label="์ด๋ฆ" Variant="Variant.Outlined" />
<MudDataGrid Items="@items" Filterable="true" SortMode="SortMode.Multiple">
<Columns>
<PropertyColumn Property="x => x.Name" Title="์ด๋ฆ" />
<PropertyColumn Property="x => x.Age" Title="๋์ด" />
</Columns>
</MudDataGrid>
MudBlazor๋ ๋ฒํผ, ์
๋ ฅ ํ๋, ํ
์ด๋ธ, ๋ค์ด์ผ๋ก๊ทธ, ์ค๋ต๋ฐ ๋ฑ ๊ฑฐ์ ๋ชจ๋ UI ์ปดํฌ๋ํธ๋ฅผ ์ ๊ณตํด. ๋ฌธ์๋ ์ ๋ง ์ ๋์ด ์์ด์ ๋ฐฐ์ฐ๊ธฐ ์ฌ์! ๐
2. Radzen Blazor Components โก
70๊ฐ ์ด์์ ๋ฌด๋ฃ ์ปดํฌ๋ํธ๋ฅผ ์ ๊ณตํ๋ ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ํนํ ๋ฐ์ดํฐ ๊ทธ๋ฆฌ๋์ ์ฐจํธ๊ฐ ํ๋ฅญํด!
์ค์น:
dotnet add package Radzen.Blazor
์ฌ์ฉ ์์:
<RadzenButton Text="ํด๋ฆญ" ButtonStyle="ButtonStyle.Primary" />
<RadzenDataGrid Data="@orders" TItem="Order">
<Columns>
<RadzenDataGridColumn TItem="Order" Property="OrderID" Title="์ฃผ๋ฌธ๋ฒํธ" />
<RadzenDataGridColumn TItem="Order" Property="CustomerName" Title="๊ณ ๊ฐ๋ช
" />
</Columns>
</RadzenDataGrid>
<RadzenChart>
<RadzenLineSeries Data="@revenue" CategoryProperty="Month" ValueProperty="Amount" />
</RadzenChart>
Radzen์ ํนํ ์ํฐํ๋ผ์ด์ฆ๊ธ ์ ํ๋ฆฌ์ผ์ด์
์ ์ ํฉํด. ๋ณต์กํ ๋ฐ์ดํฐ ์๊ฐํ๋ ๋์๋ณด๋๋ฅผ ๋ง๋ค ๋ ์ ๋ง ์ ์ฉํ์ง! ๐
3. Blazorise ๐ฏ
Bootstrap, Material, Bulma ๋ฑ ์ฌ๋ฌ CSS ํ๋ ์์ํฌ๋ฅผ ์ง์ํ๋ ์ ์ฐํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
์ค์น (Bootstrap ๋ฒ์ ):
dotnet add package Blazorise.Bootstrap5
dotnet add package Blazorise.Icons.FontAwesome
์ค์ :
builder.Services
.AddBlazorise(options => { options.Immediate = true; })
.AddBootstrap5Providers()
.AddFontAwesomeIcons();
์ฌ์ฉ ์์:
<Button Color="Color.Primary">๋ฒํผ</Button>
<Field>
<FieldLabel>์ด๋ฉ์ผ</FieldLabel>
<TextEdit @bind-Text="@email" Role="TextRole.Email" />
</Field>
<Modal @ref="modalRef">
<ModalContent Centered>
<ModalHeader>
<ModalTitle>์ ๋ชฉ</ModalTitle>
</ModalHeader>
<ModalBody>
๋ด์ฉ์ด ์ฌ๊ธฐ์...
</ModalBody>
</ModalContent>
</Modal>
Blazorise์ ์ฅ์ ์ ์ด๋ฏธ ์ต์ํ CSS ํ๋ ์์ํฌ๋ฅผ ๊ณ์ ์ฌ์ฉํ ์ ์๋ค๋ ๊ฑฐ์ผ. ๊ธฐ์กด ํ๋ก์ ํธ๋ฅผ ๋ง์ด๊ทธ๋ ์ด์
ํ ๋ ํนํ ์ข์! ๐
ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด ์์ฃผ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ๋ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ ์ข์. ์ด๋ ๊ฒ ํ๋ฉด ์ฌ๋ฌ ํ๋ก์ ํธ์์ ์ฌ์ฌ์ฉํ ์ ์๊ฑฐ๋ !
Razor Class Library ํ๋ก์ ํธ ์์ฑ:
dotnet new razorclasslib -o MyComponents
๊ณต์ ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ:
// MyComponents/LoadingSpinner.razor
<div class="spinner-container">
<div class="spinner"></div>
@if (!string.IsNullOrEmpty(Message))
{
<p>@Message</p>
}
</div>
@code {
[Parameter]
public string Message { get; set; } = "๋ก๋ฉ ์ค...";
}
๋ค๋ฅธ ํ๋ก์ ํธ์์ ์ฌ์ฉ:
// ํ๋ก์ ํธ ์ฐธ์กฐ ์ถ๊ฐ
dotnet add reference ../MyComponents/MyComponents.csproj
// _Imports.razor์ ์ถ๊ฐ
@using MyComponents
// ์ปดํฌ๋ํธ ์ฌ์ฉ
<LoadingSpinner Message="๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค..." />
์ด๋ ๊ฒ ํ๋ฉด ํ ์ ์ฒด๊ฐ ์ผ๊ด๋ UI ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ ์ ์์ด! ๐จ
๐ ์ฑ๋ฅ ์ต์ ํ์ ํ๋ก๋์ ๋ฐฐํฌ
๊ฐ๋ฐ์ ๋๋ฌ์ด! ์ด์ ์ค์ ์ฌ์ฉ์๋ค์๊ฒ ๋ฐฐํฌํ ์ฐจ๋ก์ผ. ํ์ง๋ง ๊ทธ ์ ์ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ํด์ผ ํด. ๐๏ธ
1. ๋ ๋๋ง ์ต์ ํ
Blazor๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋ํ๊ฒ ๋ ๋๋งํ์ง๋ง, ์ฐ๋ฆฌ๊ฐ ๋ ์ต์ ํํ ์ ์์ด:
// ShouldRender ์ค๋ฒ๋ผ์ด๋
@code {
private int count = 0;
private string message = "";
protected override bool ShouldRender()
{
// ํน์ ์กฐ๊ฑด์์๋ง ๋ ๋๋ง
return count % 2 == 0;
}
// ๋๋ @key ๋๋ ํฐ๋ธ ์ฌ์ฉ
}
// ๋ฆฌ์คํธ ๋ ๋๋ง์ @key ์ฌ์ฉ
@foreach (var item in items)
{
<div @key="item.Id">
@item.Name
</div>
}
2. ์ฝ๋ ๋ถํ ๊ณผ ์ง์ฐ ๋ก๋ฉ
ํฐ ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ๋ก๋ํ์ง ๋ง๊ณ , ํ์ํ ๋ ๋ก๋ํ๋๋ก ํด:
// App.razor
<Router AppAssembly="@typeof(App).Assembly"
OnNavigateAsync="@OnNavigateAsync">
<Navigating>
<p>ํ์ด์ง๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...</p>
</Navigating>
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
</Router>
@code {
private List<Assembly> lazyLoadedAssemblies = new();
private async Task OnNavigateAsync(NavigationContext context)
{
if (context.Path.Contains("admin") &&
!lazyLoadedAssemblies.Any(a => a.GetName().Name == "AdminModule"))
{
var assemblies = await AssemblyLoader.LoadAssembliesAsync(
new[] { "AdminModule.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
}
}
3. ์ด๋ฏธ์ง ์ต์ ํ
์ด๋ฏธ์ง๋ ์น ์ฑ์์ ๊ฐ์ฅ ํฐ ์ฉ๋์ ์ฐจ์งํด. ์ต์ ํ๊ฐ ํ์์ผ:
// ๋ฐ์ํ ์ด๋ฏธ์ง ์ฌ์ฉ
<img src="image-small.jpg"
srcset="image-small.jpg 480w,
image-medium.jpg 800w,
image-large.jpg 1200w"
sizes="(max-width: 600px) 480px,
(max-width: 1000px) 800px,
1200px"
alt="์ค๋ช
" />
// ์ง์ฐ ๋ก๋ฉ
<img src="image.jpg" loading="lazy" alt="์ค๋ช
" />
๐ Blazor WASM ๋ฐฐํฌํ๊ธฐ
GitHub Pages์ ๋ฐฐํฌ:
1. ํ๋ก์ ํธ ๋น๋:
dotnet publish -c Release
2. wwwroot/index.html์ base href ์์ :
<base href="/repository-name/" />
3. GitHub Actions ์ํฌํ๋ก์ฐ ์์ฑ (.github/workflows/deploy.yml):
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.0.x'
- name: Publish
run: dotnet publish -c Release -o release
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: release/wwwroot
Azure Static Web Apps์ ๋ฐฐํฌ:Azure๋ Blazor WASM์ ์ํ ์๋ฒฝํ ํธ์คํ ํ๊ฒฝ์ ์ ๊ณตํด:
# Azure CLI๋ก ๋ฐฐํฌ
az staticwebapp create \
--name my-blazor-app \
--resource-group my-resource-group \
--source https://github.com/username/repo \
--location "East Asia" \
--branch main \
--app-location "/" \
--output-location "wwwroot"
๋๋ Azure Portal์์ ํด๋ฆญ ๋ช ๋ฒ์ผ๋ก ๋ฐฐํฌํ ์ ์์ด! ์ ๋ง ๊ฐ๋จํ์ง? ๐
โก Blazor Server ๋ฐฐํฌํ๊ธฐ
Docker ์ปจํ
์ด๋๋ก ๋ฐฐํฌ:
Dockerfile ์์ฑ:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyBlazorServer.csproj", "./"]
RUN dotnet restore "MyBlazorServer.csproj"
COPY . .
RUN dotnet build "MyBlazorServer.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyBlazorServer.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyBlazorServer.dll"]
๋น๋ ๋ฐ ์คํ:
docker build -t my-blazor-server .
docker run -d -p 8080:80 --name blazor-app my-blazor-server
Azure App Service์ ๋ฐฐํฌ:Visual Studio์์ ์ฐํด๋ฆญ โ Publish โ Azure โ App Service ์ ํํ๋ฉด ๋! ์ ๋ง ์ฝ์ง? ๐
๋๋ Azure CLI๋ก:
az webapp up --name my-blazor-server --resource-group my-rg --runtime "DOTNETCORE:8.0"
โ HTTPS ์ค์ : ๋ณด์์ ์ํด ํ์!
โ ํ๊ฒฝ ๋ณ์: API ํค, ์ฐ๊ฒฐ ๋ฌธ์์ด ๋ฑ์ ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌ
โ ์๋ฌ ์ฒ๋ฆฌ: ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ ๊ตฌํ
โ ๋ก๊น : Application Insights๋ Serilog ์ค์
โ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง: ์๋ต ์๊ฐ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ถ์
โ ๋ณด์ ํค๋: CSP, HSTS ๋ฑ ์ค์
โ ์์ถ: Gzip/Brotli ์์ถ ํ์ฑํ
โ ์บ์ฑ: ์ ์ ํ์ผ ์บ์ฑ ์ ๋ต ์๋ฆฝ
โ ๋ฐฑ์ : ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์ ๊ณํ
โ ๋ชจ๋ํฐ๋ง: ํฌ์ค ์ฒดํฌ ์๋ํฌ์ธํธ ๊ตฌํ
๐ฎ Blazor์ ๋ฏธ๋์ ์ต์ ํธ๋ ๋
Blazor๋ ๊ณ์ ์งํํ๊ณ ์์ด! ์ต์ ํธ๋ ๋์ ์์ผ๋ก์ ๋ฐฉํฅ์ ์ดํด๋ณด์. ๐
.NET 8๊ณผ Blazor์ ์๋ก์ด ๊ธฐ๋ฅ๋ค:
.NET 8์์ ๊ฐ์ฅ ํฐ ๋ณํ์ผ! Server์ WASM์ ํ๋์ ํ๋ก์ ํธ์์ ์ ์ฐํ๊ฒ ์ฌ์ฉํ ์ ์์ด:
// ํ์ด์ง๋ณ๋ก ๋ ๋๋ง ๋ชจ๋ ์ง์ ๊ฐ๋ฅ!
@page "/counter"
@rendermode InteractiveServer // ๋๋ InteractiveWebAssembly, InteractiveAuto
<h3>์นด์ดํฐ</h3>
<p>ํ์ฌ ์นด์ดํธ: @currentCount</p>
<button @onclick="IncrementCount">ํด๋ฆญ</button>
@code {
private int currentCount = 0;
private void IncrementCount() => currentCount++;
}
InteractiveAuto๋ ์ ๋ง ํ์ ์ ์ด์ผ! ์ฒ์์ Server ๋ชจ๋๋ก ๋น ๋ฅด๊ฒ ๋ก๋ํ๊ณ , ๋ฐฑ๊ทธ๋ผ์ด๋์์ WASM์ ๋ค์ด๋ก๋ํด. ๊ทธ ๋ค์๋ถํฐ๋ WASM์ผ๋ก ์ ํ๋ผ์ ์๋ฒ ๋ถํ๋ ์ค์ด๊ณ ์ฑ๋ฅ๋ ์ข์์ ธ! ๐ฏ
๊ธด ๋ฐ์ดํฐ ๋ก๋ฉ ์๊ฐ์ด ์์ ๋, ํ์ด์ง๋ฅผ ๋ถ๋ถ์ ์ผ๋ก ๋จผ์ ๋ณด์ฌ์ค ์ ์์ด:
@page "/products"
@attribute [StreamRendering]
<h3>์ํ ๋ชฉ๋ก</h3>
@if (products == null)
{
<p>๋ก๋ฉ ์ค...</p>
}
else
{
@foreach (var product in products)
{
<div>@product.Name - @product.Price์</div>
}
}
@code {
private List<Product>? products;
protected override async Task OnInitializedAsync()
{
// ์ด ๋ถ๋ถ์ด ์๋ฃ๋๊ธฐ ์ ์ ํ์ด์ง์ ๋๋จธ์ง ๋ถ๋ถ์ด ๋จผ์ ๋ ๋๋ง๋ผ!
await Task.Delay(2000); // ์๋ฎฌ๋ ์ด์
products = await GetProductsAsync();
}
}
์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์ข์์ง์ง! ๐
.NET 8์ ์๋ก์ด ํผ ๊ธฐ๋ฅ๋ค:
@page "/enhanced-form"
@using Microsoft.AspNetCore.Components.Forms
<EditForm Model="@model" FormName="MyForm" OnValidSubmit="HandleSubmit" Enhance>
<DataAnnotationsValidator />
<InputText @bind-Value="model.Name" />
<ValidationMessage For="() => model.Name" />
<button type="submit">์ ์ถ</button>
</EditForm>
@code {
[SupplyParameterFromForm]
private FormModel model { get; set; } = new();
private void HandleSubmit()
{
// ํผ ์ ์ถ ์ฒ๋ฆฌ
}
}
Enhance ์์ฑ์ ์ถ๊ฐํ๋ฉด JavaScript ์์ด๋ ํผ์ด ์๋ํด! Progressive Enhancement์ ์๋ฒฝํ ์์์ผ. ๐จ
๊ณต์ ๋ฆฌ์์ค:
โข Microsoft Learn: ๊ณต์ ํํ ๋ฆฌ์ผ๊ณผ ๋ฌธ์
โข Blazor GitHub: ์์ค ์ฝ๋์ ์ด์ ํธ๋์ปค
โข .NET Blog: ์ต์ ์ ๋ฐ์ดํธ์ ๋ฐํ
์ปค๋ฎค๋ํฐ:
โข Blazor Discord: ์ค์๊ฐ ์ฑํ ๊ณผ ์ง๋ฌธ
โข Stack Overflow: [blazor] ํ๊ทธ๋ก ๊ฒ์
โข Reddit r/Blazor: ํ ๋ก ๊ณผ ๋ด์ค
โข Awesome Blazor: ํ๋ ์ด์ ๋ ๋ฆฌ์์ค ๋ชฉ๋ก
์ ํ๋ธ ์ฑ๋:
โข dotnet: ๊ณต์ ์ฑ๋
โข IAmTimCorey: ์ค์ ํํ ๋ฆฌ์ผ
โข Nick Chapsas: ๊ณ ๊ธ ํ๊ณผ ํธ๋ฆญ
ํผ์ ๊ณ ๋ฏผํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ๋์์ ๋ฐ์! ๋ค๋ค ์ ๋ง ์น์ ํ๊ฑฐ๋ . ๐
Blazor์ ๋ฏธ๋ ์ ๋ง
๋ง์ดํฌ๋ก์ํํธ๋ Blazor์ ์์ฒญ๋ ํฌ์๋ฅผ ํ๊ณ ์์ด. ์์ผ๋ก ๊ธฐ๋๋๋ ๊ฒ๋ค:
โข ๋ ๋์ ์ฑ๋ฅ: AOT ์ปดํ์ผ ๊ฐ์ , ๋ ์์ ๋ฒ๋ค ํฌ๊ธฐ
โข ๋ชจ๋ฐ์ผ ์ง์ ๊ฐํ: MAUI์์ ๋ ๊น์ ํตํฉ
โข AI ํตํฉ: Semantic Kernel๊ณผ์ ์ฐ๋
โข ๊ฐ๋ฐ์ ๊ฒฝํ ํฅ์: Hot Reload ๊ฐ์ , ๋ ๋์ ๋๋ฒ๊น
๋๊ตฌ
โข ์ํฐํ๋ผ์ด์ฆ ๊ธฐ๋ฅ: ๋ ๋ง์ ๋ณด์ ์ต์
, ํ์ฅ์ฑ ๊ฐ์
Blazor๋ฅผ ๋ฐฐ์ฐ๋ ๊ฑด ์ ๋ง ์ข์ ํฌ์์ผ! C# ๊ฐ๋ฐ์๋ผ๋ฉด ์น ๊ฐ๋ฐ์ ์๋ก์ด ๊ฐ๋ฅ์ฑ์ ์ด์ด์ค ๊ฑฐ์ผ. ๐
๐ก ์ค์ ํ๋ก์ ํธ ์์ด๋์ด
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ด! ์ด์ ์ค์ ๋ก ๋ญ๊ฐ ๋ง๋ค์ด๋ณด์. ์ฌ๊ธฐ Blazor๋ก ๋ง๋ค๊ธฐ ์ข์ ํ๋ก์ ํธ ์์ด๋์ด๋ค์ด์ผ: ๐ฏ
์ด๊ธ ํ๋ก์ ํธ:
1. ํ ์ผ ๊ด๋ฆฌ ์ฑ - CRUD ๊ธฐ๋ณธ๊ธฐ ์ตํ๊ธฐ
2. ๋ ์จ ๋์๋ณด๋ - API ํต์ ์ฐ์ต
3. ๊ฐ๋จํ ๋ธ๋ก๊ทธ - ๋ผ์ฐํ
๊ณผ ๋ ์ด์์ ํ์ต
4. ๊ณ์ฐ๊ธฐ ์ฑ - ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ฐ์ต
5. ํ์ด๋จธ/์คํฑ์์น - ์ํ ๊ด๋ฆฌ ํ์ต
์ค๊ธ ํ๋ก์ ํธ:
1. ์ค์๊ฐ ์ฑํ
์ฑ - SignalR ํ์ฉ
2. ์ ์์๊ฑฐ๋ ์ฌ์ดํธ - ๋ณต์กํ ์ํ ๊ด๋ฆฌ
3. ํ๋ก์ ํธ ๊ด๋ฆฌ ๋๊ตฌ - ๋๋๊ทธ ์ค ๋๋กญ, ์นธ๋ฐ ๋ณด๋
4. ์์
๋ฏธ๋์ด ํผ๋ - ๋ฌดํ ์คํฌ๋กค, ์ด๋ฏธ์ง ์
๋ก๋
5. ์จ๋ผ์ธ ํด์ฆ ํ๋ซํผ - ์ค์๊ฐ ์ ์, ํ์ด๋จธ
๊ณ ๊ธ ํ๋ก์ ํธ:
1. ํ์
๋ฌธ์ ํธ์ง๊ธฐ - ์ค์๊ฐ ๋๊ธฐํ
2. ๋น๋์ค ์คํธ๋ฆฌ๋ฐ ํ๋ซํผ - ๋ฏธ๋์ด ์ฒ๋ฆฌ
3. ์ฃผ์ ๊ฑฐ๋ ๋์๋ณด๋ - ์ค์๊ฐ ๋ฐ์ดํฐ, ์ฐจํธ
4. ๊ฒ์ (์ฒด์ค, ์ค๋ชฉ ๋ฑ) - ๋ณต์กํ ๋ก์ง, ๋ฉํฐํ๋ ์ด์ด
5. CRM ์์คํ
- ์ํฐํ๋ผ์ด์ฆ๊ธ ๊ธฐ๋ฅ
์ฌ๋ฅ๋ท์์ ์ด๋ฐ ํ๋ก์ ํธ๋ค์ ๋ฉํ ๋ง์ ๋ฐ์ ์๋ ์๊ณ , ์ง์ ํ๋ฆฌ๋์๋ก ํ๋ก์ ํธ๋ฅผ ์์ฃผํ ์๋ ์์ด! ๐ผ
๐ ํ์ต ๋ก๋๋งต๊ณผ ํ
Blazor๋ฅผ ๋ง์คํฐํ๊ธฐ ์ํ ๋จ๊ณ๋ณ ๋ก๋๋งต์ ์ ๋ฆฌํด์ค๊ฒ! ๐
1๋จ๊ณ: ๊ธฐ์ด ๋ค์ง๊ธฐ (1-2์ฃผ)
โข C# ๊ธฐ๋ณธ ๋ฌธ๋ฒ ๋ณต์ต
โข Razor ๋ฌธ๋ฒ ์ตํ๊ธฐ
โข ์ปดํฌ๋ํธ ๊ธฐ๋ณธ ๊ฐ๋
โข ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ
โข ์ด๋ฒคํธ ์ฒ๋ฆฌ
2๋จ๊ณ: ํต์ฌ ๊ธฐ๋ฅ (2-3์ฃผ)
โข ๋ผ์ฐํ
๊ณผ ๋ค๋น๊ฒ์ด์
โข ํผ๊ณผ ์ ํจ์ฑ ๊ฒ์ฌ
โข HTTP ํต์
โข ์์กด์ฑ ์ฃผ์
โข ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋
3๋จ๊ณ: ๊ณ ๊ธ ๊ธฐ๋ฅ (3-4์ฃผ)
โข JavaScript Interop
โข ์ํ ๊ด๋ฆฌ
โข ์ธ์ฆ๊ณผ ๊ถํ
โข SignalR ์ค์๊ฐ ํต์
โข ์ฑ๋ฅ ์ต์ ํ
4๋จ๊ณ: ์ค์ ํ๋ก์ ํธ (4์ฃผ ์ด์)
โข ์ค์ ํ๋ก์ ํธ ๊ตฌํ
โข ๋ฐฐํฌ์ ์ด์
โข ํ
์คํธ ์์ฑ
โข CI/CD ํ์ดํ๋ผ์ธ
โข ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
2. ์์ ํ๋ก์ ํธ๋ถํฐ: ์์ฑ ๊ฐ๋ฅํ ํฌ๊ธฐ๋ก ์์
3. ๊ณต์ ๋ฌธ์ ์ฝ๊ธฐ: ๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด
4. ์ปค๋ฎค๋ํฐ ์ฐธ์ฌ: ์ง๋ฌธํ๊ณ ๋ต๋ณํ๋ฉด์ ๋ฐฐ์ฐ๊ธฐ
5. ์ฝ๋ ๋ฆฌ๋ทฐ: ๋ค๋ฅธ ์ฌ๋์ ์ฝ๋ ์ฝ์ด๋ณด๊ธฐ
6. ๋ธ๋ก๊ทธ ์์ฑ: ๋ฐฐ์ด ๋ด์ฉ ์ ๋ฆฌํ๋ฉด ๋ ์ ์ดํด๋ผ
7. ์คํ์์ค ๊ธฐ์ฌ: ์ค์ ๊ฒฝํ ์๊ธฐ
8. ํ์ด ํ๋ก๊ทธ๋๋ฐ: ํจ๊ป ์ฝ๋ฉํ๋ฉด ๋ ๋นจ๋ฆฌ ๋ฐฐ์
๊ฐ์ฅ ์ค์ํ ๊ฑด ํฌ๊ธฐํ์ง ์๋ ๊ฒ! ์ฒ์์ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ง๋ง, ๊ณ์ํ๋ค ๋ณด๋ฉด ์ด๋์ ์์ฐ์ค๋ฝ๊ฒ ์ฝ๋๊ฐ ๋์ฌ ๊ฑฐ์ผ. ๐ช
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด! ๐
์ฐ๋ฆฌ๋ Blazor WebAssembly์ Server์ ๊ธฐ๋ณธ๋ถํฐ ๊ณ ๊ธ ๊ธฐ๋ฅ๊น์ง, ์ค์ ์์ ์ ๋ฒ ์คํธ ํ๋ํฐ์ค, ๊ทธ๋ฆฌ๊ณ ๋ฐฐํฌ์ ์ต์ ํ๊น์ง ๋ชจ๋ ๊ฑธ ๋ค๋ค์ด. ์ด์ ๋๋ Blazor๋ก ๋ฉ์ง ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ์ค๋น๊ฐ ๋์ด! ๐
ํต์ฌ ํฌ์ธํธ ์ ๋ฆฌ:
โข Blazor WASM์ ํด๋ผ์ด์ธํธ์์ ์คํ๋๊ณ , ์คํ๋ผ์ธ ์ง์๊ณผ ํ์ฅ์ฑ์ด ๋ฐ์ด๋
โข Blazor Server๋ ๋น ๋ฅธ ์ด๊ธฐ ๋ก๋ฉ๊ณผ ์ค์๊ฐ ๊ธฐ๋ฅ์ ์ต์ ํ๋์ด ์์ด
โข ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์ํคํ
์ฒ๋ก ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ UI๋ฅผ ๋ง๋ค ์ ์์ด
โข C# ํ๋๋ก ํ๋ก ํธ์๋์ ๋ฐฑ์๋๋ฅผ ๋ชจ๋ ๊ฐ๋ฐํ ์ ์์ด
โข ํ๋ถํ ์ํ๊ณ์ ํ๋ฐํ ์ปค๋ฎค๋ํฐ๊ฐ ์์ด
โข .NET 8์ ์๋ก์ด ๊ธฐ๋ฅ๋ค๋ก ๋์ฑ ๊ฐ๋ ฅํด์ก์ด
Blazor๋ ๊ณ์ ์งํํ๊ณ ์๊ณ , ๋ง์ดํฌ๋ก์ํํธ์ ๊ฐ๋ ฅํ ์ง์์ ๋ฐ๊ณ ์์ด. C# ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ๋ฐฐ์์ผ ํ ๊ธฐ์ ์ด์ผ! ๐
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋ ๊น์ด ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์๋ด. 1:1 ๋ฉํ ๋ง๋ถํฐ ํ๋ก์ ํธ ์ฝ์นญ๊น์ง, ํ์ํ ๋ชจ๋ ์ง์์ ๋ฐ์ ์ ์์ด! ๐
์, ์ด์ ์๋ํฐ๋ฅผ ์ด๊ณ ์ฒซ Blazor ํ๋ก์ ํธ๋ฅผ ์์ํด๋ณผ๊น? dotnet new blazor ๋ช
๋ น์ด ํ๋๋ฉด ์ถฉ๋ถํด! ๐จ
ํ๋ณตํ ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! Happy Blazor Coding! ๐โจ
๐ ์ด ๊ธ์ด ๋์์ด ๋์๋์?
Blazor ๊ฐ๋ฐ์ ๋ํด ๋ ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋, ํ๋ก์ ํธ์ ๋์์ด ํ์ํ๋ค๋ฉด
์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ๋ค๊ณผ ํจ๊ปํ์ธ์!
์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ฌ์ ์ ์์ํฉ๋๋ค! ๐ชโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.