์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Blazor WebAssembly์™€ Server๋กœ ๋งŒ๋“œ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์›น UI ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿš€ Blazor WebAssembly์™€ Server๋กœ ๋งŒ๋“œ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์›น UI ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

C#์œผ๋กœ ์›น ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ์ •๋ณตํ•˜๋Š” ํ˜์‹ ์ ์ธ ๋ฐฉ๋ฒ•

๐Ÿ’ก ์นœ๊ตฌ์•ผ, ํ˜น์‹œ JavaScript ์—†์ด๋„ ๋ฉ‹์ง„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ด๋–จ๊นŒ? C# ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ํ•œ ๋ฒˆ์ฏค ๊ฟˆ๊ฟ”๋ดค์„ ๊ฑฐ์•ผ. "๋‚ด๊ฐ€ ์ข‹์•„ํ•˜๋Š” C#์œผ๋กœ ํ”„๋ก ํŠธ์—”๋“œ๊นŒ์ง€ ๋‹ค ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ผ๋งˆ๋‚˜ ์ข‹์„๊นŒ?" ๋ฐ”๋กœ ๊ทธ ๊ฟˆ์„ ํ˜„์‹ค๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฒŒ Blazor์•ผ! ๐ŸŽฏ

Blazor๋Š” ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ๊ฐ€ ์•ผ์‹ฌ์ฐจ๊ฒŒ ๋‚ด๋†“์€ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, C#๊ณผ .NET์„ ์‚ฌ์šฉํ•ด์„œ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ์›น UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜. ๊ทธ๊ฒƒ๋„ ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ๋ง์ด์•ผ - WebAssembly์™€ Server ๋ชจ๋“œ! ์˜ค๋Š˜์€ ์ด ๋‘ ๊ฐ€์ง€๋ฅผ ๊นŠ์ด ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ. ๐Ÿ˜Š

Blazor ์•„ํ‚คํ…์ฒ˜ ๋น„๊ต Blazor WebAssembly ๐ŸŒ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋ถˆํ•„์š” ์˜คํ”„๋ผ์ธ ์ž‘๋™ ๊ฐ€๋Šฅ Blazor Server โšก ์„œ๋ฒ„์—์„œ ์‹คํ–‰ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋น ๋ฅธ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹ค์‹œ๊ฐ„ SignalR ์—ฐ๊ฒฐ VS

๐ŸŽจ Blazor๊ฐ€ ๋ญ๊ธธ๋ž˜ ์ด๋ ‡๊ฒŒ ๋‚œ๋ฆฌ์•ผ?

์ž, ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋“ค์–ด๊ฐ€๊ธฐ ์ „์— Blazor๊ฐ€ ์ •ํ™•ํžˆ ๋ญ”์ง€๋ถ€ํ„ฐ ์•Œ์•„๋ณด์ž. Blazor๋Š” Browser + Razor์˜ ํ•ฉ์„ฑ์–ด์•ผ. Razor๋Š” ASP.NET์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ…œํ”Œ๋ฆฟ ์—”์ง„์ธ๋ฐ, ์ด๊ฑธ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“  ๊ฑฐ์ง€! ๐ŸŽ‰

์ „ํ†ต์ ์œผ๋กœ ์›น ๊ฐœ๋ฐœ์„ ํ•˜๋ ค๋ฉด ํ”„๋ก ํŠธ์—”๋“œ๋Š” JavaScript(React, Vue, Angular ๋“ฑ), ๋ฐฑ์—”๋“œ๋Š” C#์ด๋‚˜ Java ๊ฐ™์€ ์–ธ์–ด๋ฅผ ์จ์•ผ ํ–ˆ์–ด. ๊ทธ๋Ÿฌ๋‹ค ๋ณด๋‹ˆ ๊ฐœ๋ฐœ์ž๋“ค์€ ์—ฌ๋Ÿฌ ์–ธ์–ด๋ฅผ ๋ฐฐ์›Œ์•ผ ํ–ˆ๊ณ , ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ ์‚ฌ์ด์˜ ์ฝ”๋“œ ๊ณต์œ ๋„ ์–ด๋ ค์› ์ง€.

ํ•˜์ง€๋งŒ Blazor๋Š” ์ด ๋ชจ๋“  ๊ฑธ ๋ฐ”๊ฟ”๋†จ์–ด! C# ํ•˜๋‚˜๋กœ ์ „์ฒด ์Šคํƒ์„ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฑฐ์•ผ. ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ๋„ ๋†’์•„์ง€๊ณ , ํƒ€์ž… ์•ˆ์ •์„ฑ๋„ ๋ณด์žฅ๋˜๊ณ , ๋””๋ฒ„๊น…๋„ ํ›จ์”ฌ ํŽธํ•ด์กŒ์–ด. ๐Ÿ˜

๐Ÿ”ฅ Blazor์˜ ํ•ต์‹ฌ ์žฅ์ 

โœ… C#๊ณผ .NET ์ƒํƒœ๊ณ„ ํ™œ์šฉ ๊ฐ€๋Šฅ
โœ… ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ์œผ๋กœ ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ ๊ฐ์†Œ
โœ… Visual Studio์˜ ๊ฐ•๋ ฅํ•œ IntelliSense ์ง€์›
โœ… ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋กœ ์žฌ์‚ฌ์šฉ์„ฑ ๊ทน๋Œ€ํ™”
โœ… NuGet ํŒจํ‚ค์ง€ ์ƒํƒœ๊ณ„ ํ™œ์šฉ ๊ฐ€๋Šฅ

๐ŸŒŠ Blazor WebAssembly: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ณง ๋Ÿฐํƒ€์ž„!

๋จผ์ € Blazor WebAssembly(์ค„์—ฌ์„œ WASM)๋ถ€ํ„ฐ ์‚ดํŽด๋ณผ๊ฒŒ. ์ด ๋…€์„์€ ์ •๋ง ํ˜์‹ ์ ์ด์•ผ! ๐Ÿš€

WebAssembly๋Š” ๋ญ๋ƒ๊ณ ? ๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด, ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ์— ๊ฐ€๊นŒ์šด ์†๋„๋กœ ์‹คํ–‰๋˜๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ ์ฝ”๋“œ ํ˜•์‹์ด์•ผ. ์›๋ž˜๋Š” C, C++, Rust ๊ฐ™์€ ์–ธ์–ด๋กœ ์ž‘์„ฑ๋œ ์ฝ”๋“œ๋ฅผ ์›น์—์„œ ๋Œ๋ฆฌ๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์กŒ๋Š”๋ฐ, .NET ํŒ€์ด ์—ฌ๊ธฐ์— C#์„ ์–น์€ ๊ฑฐ์ง€!

Blazor WASM์˜ ์ž‘๋™ ์›๋ฆฌ๋ฅผ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜๋ฉด ์ด๋ž˜:

1๋‹จ๊ณ„: ๋นŒ๋“œ ํƒ€์ž„ ๐Ÿ”จ
C# ์ฝ”๋“œ๊ฐ€ .NET IL(Intermediate Language)๋กœ ์ปดํŒŒ์ผ๋ผ. ์ด๊ฑด ํ‰์†Œ .NET ๊ฐœ๋ฐœํ•  ๋•Œ๋ž‘ ๋˜‘๊ฐ™์•„.
2๋‹จ๊ณ„: ๋ฐฐํฌ ์ค€๋น„ ๐Ÿ“ฆ
.NET ๋Ÿฐํƒ€์ž„ ์ž์ฒด๊ฐ€ WebAssembly๋กœ ์ปดํŒŒ์ผ๋˜๊ณ , ๋„ˆ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ DLL๋“ค๊ณผ ํ•จ๊ป˜ ํŒจํ‚ค์ง•๋ผ.
3๋‹จ๊ณ„: ๋ธŒ๋ผ์šฐ์ € ๋กœ๋”ฉ ๐ŸŒ
์‚ฌ์šฉ์ž๊ฐ€ ์›น์‚ฌ์ดํŠธ์— ์ ‘์†ํ•˜๋ฉด, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ .NET ๋Ÿฐํƒ€์ž„(WASM ํ˜•ํƒœ)๊ณผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ DLL๋“ค์„ ๋‹ค์šด๋กœ๋“œํ•ด.
4๋‹จ๊ณ„: ์‹คํ–‰! โšก
๋ธŒ๋ผ์šฐ์ € ์•ˆ์—์„œ .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์€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•ด์ค˜. ์™„์ „ ์ง๊ด€์ ์ด์ง€? ๐Ÿ˜Š

โš ๏ธ Blazor WASM์˜ ์ฃผ์˜์‚ฌํ•ญ 1. ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ๊ฐ„: .NET ๋Ÿฐํƒ€์ž„๊ณผ DLL๋“ค์„ ๋‹ค์šด๋กœ๋“œํ•ด์•ผ ํ•˜๋‹ˆ๊นŒ ์ฒซ ๋กœ๋”ฉ์ด ์ข€ ๋А๋ฆด ์ˆ˜ ์žˆ์–ด. ๋ณดํ†ต 2-3MB ์ •๋„ ๋˜๊ฑฐ๋“ . ํ•˜์ง€๋งŒ PWA(Progressive Web App)๋กœ ๋งŒ๋“ค๋ฉด ์บ์‹ฑ๋˜๋‹ˆ๊นŒ ๋‘ ๋ฒˆ์งธ๋ถ€ํ„ฐ๋Š” ๋น ๋ฅด์ง€!

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์ด๋ผ๋Š” ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๊ธฐ์ˆ ๋กœ ์—ฐ๊ฒฐ๋ผ ์žˆ์–ด.

์ž‘๋™ ๋ฐฉ์‹์„ ์‚ดํŽด๋ณด์ž:

1๋‹จ๊ณ„: ์ดˆ๊ธฐ ์—ฐ๊ฒฐ ๐Ÿ”Œ
์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€์— ์ ‘์†ํ•˜๋ฉด, ์„œ๋ฒ„๊ฐ€ ์ดˆ๊ธฐ HTML์„ ๋ Œ๋”๋งํ•ด์„œ ๋ณด๋‚ด์ค˜. ๋™์‹œ์— SignalR WebSocket ์—ฐ๊ฒฐ์ด ์ˆ˜๋ฆฝ๋ผ.
2๋‹จ๊ณ„: ์ด๋ฒคํŠธ ๋ฐœ์ƒ ๐Ÿ–ฑ๏ธ
์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ฑฐ๋‚˜ ์ž…๋ ฅ์„ ํ•˜๋ฉด, ๊ทธ ์ด๋ฒคํŠธ๊ฐ€ SignalR์„ ํ†ตํ•ด ์„œ๋ฒ„๋กœ ์ „์†ก๋ผ.
3๋‹จ๊ณ„: ์„œ๋ฒ„ ์ฒ˜๋ฆฌ โš™๏ธ
์„œ๋ฒ„์—์„œ C# ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ณ , UI ์ƒํƒœ๊ฐ€ ์—…๋ฐ์ดํŠธ๋ผ. ๊ทธ๋ฆฌ๊ณ  ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ๊ณ„์‚ฐํ•ด.
4๋‹จ๊ณ„: 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();
๐ŸŒŸ Blazor Server์˜ ๊ฐ•๋ ฅํ•œ ์žฅ์ ๋“ค

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์ด ์ž๋™์œผ๋กœ ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์—…๋ฐ์ดํŠธ๋ฅผ ํ‘ธ์‹œํ•˜๊ฑฐ๋“ . ์™„์ „ ๋งˆ๋ฒ• ๊ฐ™์ง€? โœจ

โš ๏ธ Blazor Server์˜ ์ฃผ์˜์‚ฌํ•ญ

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 ๊ฐ™์€ ๊ฑธ ์“ฐ๋ฉด ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด!
Blazor ์ปดํฌ๋„ŒํŠธ ์ƒ๋ช…์ฃผ๊ธฐ SetParametersAsync ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ • OnInitialized / OnInitializedAsync ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™” (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) OnParametersSet / OnParametersSetAsync ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€๊ฒฝ์‹œ๋งˆ๋‹ค ์‹คํ–‰ OnAfterRender / OnAfterRenderAsync ๋ Œ๋”๋ง ์™„๋ฃŒ ํ›„ (JS Interop ์—ฌ๊ธฐ์„œ!) Dispose ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ์‹œ ์ •๋ฆฌ ์ž‘์—…

๐Ÿ”„ 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์—์„œ๋Š” ๋ฌธ์ œ์—†์–ด.

โ€ข ์ €์‚ฌ์–‘ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ง€์›ํ•ด์•ผ ํ•  ๋•Œ
์˜ค๋ž˜๋œ ๊ธฐ๊ธฐ๋‚˜ ์ €์‚ฌ์–‘ ์Šค๋งˆํŠธํฐ์—์„œ๋„ ์ž˜ ๋Œ์•„๊ฐ€.

๐Ÿ’ก ํ”„๋กœ ํŒ: ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ๋ฒ•! ์‚ฌ์‹ค ๊ผญ ํ•˜๋‚˜๋งŒ ์„ ํƒํ•  ํ•„์š”๋Š” ์—†์–ด! .NET 6๋ถ€ํ„ฐ๋Š” Blazor Hybrid๋ผ๋Š” ๊ฒŒ ์ƒ๊ฒผ๊ฑฐ๋“ . ์ด๊ฑด Blazor๋ฅผ MAUI(Multi-platform App UI)์™€ ๊ฒฐํ•ฉํ•ด์„œ ๋ฐ์Šคํฌํ†ฑ๊ณผ ๋ชจ๋ฐ”์ผ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜.

๋” ๋‚˜์•„๊ฐ€์„œ, 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๋กœ ๊นŠ์€ ๊ณ„์ธต ์ „๋‹ฌ

์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ๊ฐ€ ๊นŠ์–ด์ง€๋ฉด ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ณ„์† ์ „๋‹ฌํ•˜๊ธฐ ๊ท€์ฐฎ์•„์ง€์ง€? ์ด๋Ÿด ๋• 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 ํŒจํ„ด ๊ตฌํ˜„

๋” ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด 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;
    }
}
๐ŸŽฏ ํ”„๋กœ ํŒ: Polly๋กœ ์žฌ์‹œ๋„ ์ •์ฑ… ๊ตฌํ˜„

๋„คํŠธ์›Œํฌ๋Š” ๋ถˆ์•ˆ์ •ํ•  ์ˆ˜ ์žˆ์–ด. 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์˜ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค:

1. Blazor United (Blazor Web) ๐ŸŒŸ

.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์œผ๋กœ ์ „ํ™˜๋ผ์„œ ์„œ๋ฒ„ ๋ถ€ํ•˜๋„ ์ค„์ด๊ณ  ์„ฑ๋Šฅ๋„ ์ข‹์•„์ ธ! ๐ŸŽฏ
2. ์ŠคํŠธ๋ฆฌ๋ฐ ๋ Œ๋”๋ง ๐Ÿ“ก

๊ธด ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ๊ฐ„์ด ์žˆ์„ ๋•Œ, ํŽ˜์ด์ง€๋ฅผ ๋ถ€๋ถ„์ ์œผ๋กœ ๋จผ์ € ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์–ด:
@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();
    }
}
์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ง€์ง€! ๐Ÿ˜Š
3. ํ–ฅ์ƒ๋œ ํผ ์ฒ˜๋ฆฌ ๐Ÿ“

.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์˜ ์™„๋ฒฝํ•œ ์˜ˆ์‹œ์•ผ. ๐ŸŽจ
โœจ ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์ƒํƒœ๊ณ„ Blazor ์ปค๋ฎค๋‹ˆํ‹ฐ๋Š” ์ •๋ง ํ™œ๋ฐœํ•ด! ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๊ณณ๋“ค:

๊ณต์‹ ๋ฆฌ์†Œ์Šค:
โ€ข 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 ํŒŒ์ดํ”„๋ผ์ธ
โ€ข ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…

๐ŸŽฏ ํšจ๊ณผ์ ์ธ ํ•™์Šต ํŒ 1. ๋งค์ผ ์ฝ”๋”ฉํ•˜๊ธฐ: ํ•˜๋ฃจ 30๋ถ„์ด๋ผ๋„ ๊พธ์ค€ํžˆ!
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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.