<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Emiliano Montesdeoca</title><link>https://emimontesdeoca.github.io/ko/</link><description>Microsoft MVP in Developer Technologies. Cloud Solutions Team Lead. Speaker, blogger, and community advocate.</description><language>ko</language><managingEditor>emimontesdeoca@outlook.es (Emiliano Montesdeoca)</managingEditor><webMaster>emimontesdeoca@outlook.es (Emiliano Montesdeoca)</webMaster><lastBuildDate>Mon, 01 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://emimontesdeoca.github.io/ko/index.xml" rel="self" type="application/rss+xml"/><item><title>Blazor 처음부터: 3장 — 확장 가능한 컴포넌트</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-chapter-3/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-chapter-3/</guid><description>Blazor 처음부터 시리즈 3장. 파라미터, 컴포지션, RenderFragment, 그리고 앱이 커져도 UI를 깔끔하게 유지하는 폴더 구조를 깊이 있게 다룹니다.</description><content:encoded>&lt;p>&lt;strong>Blazor 처음부터&lt;/strong> 3장에 오신 것을 환영합니다. 아직 &lt;a href="../blazor-from-scratch-chapter-2">2장&lt;/a>을 읽지 않았다면 먼저 읽고 기본 프로젝트를 준비해 주세요.&lt;/p>
&lt;p>2장에서는 앱을 실행했습니다. 3장에서는 앱을 &lt;strong>유지보수 가능한 구조&lt;/strong> 로 바꿉니다.&lt;/p>
&lt;p>Blazor 프로젝트는 페이지마다 거대한 &lt;code>.razor&lt;/code> 파일이 되면 금방 복잡해집니다. 컴포넌트는 이를 막아줍니다. 일관성, 재사용성, 그리고 UI 책임 분리가 좋아집니다.&lt;/p>
&lt;hr>
&lt;h2 id="blazor-컴포넌트란-무엇인가">Blazor 컴포넌트란 무엇인가&lt;/h2>
&lt;p>컴포넌트는 다음을 할 수 있는 &lt;code>.razor&lt;/code> 파일입니다.&lt;/p>
&lt;ul>
&lt;li>마크업 렌더링&lt;/li>
&lt;li>로컬 상태 보관&lt;/li>
&lt;li>파라미터로 입력 받기&lt;/li>
&lt;li>부모 컴포넌트로 이벤트 전달&lt;/li>
&lt;li>자식 콘텐츠 렌더링&lt;/li>
&lt;/ul>
&lt;p>런타임에서 Blazor는 각 컴포넌트를 작은 상태 머신처럼 다룹니다. 상태가 바뀌면 다시 렌더링하고 DOM diff를 적용합니다.&lt;/p>
&lt;p>즉, 명확한 입력과 예측 가능한 동작을 설계하는 것이 핵심입니다.&lt;/p>
&lt;hr>
&lt;h2 id="1단계-목적이-분명한-컴포넌트부터-시작">1단계: 목적이 분명한 컴포넌트부터 시작&lt;/h2>
&lt;p>&lt;code>Components/Common/SectionHeader.razor&lt;/code>를 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;header class=&amp;#34;section-header&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;@Title&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @if (!string.IsNullOrWhiteSpace(Subtitle))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;@Subtitle&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Title { get; set; } = string.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string? Subtitle { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Components/Pages/Home.razor&lt;/code>에서 사용:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Blazor 처음부터&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;SectionHeader
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title=&amp;#34;Blazor 처음부터&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Subtitle=&amp;#34;3장은 컴포넌트를 다룹니다.&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>작은 예제지만 핵심은 분명합니다. 컴포넌트는 파라미터만 봐도 의도를 이해할 수 있어야 합니다.&lt;/p>
&lt;hr>
&lt;h2 id="2단계-파라미터로-동작을-명시적으로-표현">2단계: 파라미터로 동작을 명시적으로 표현&lt;/h2>
&lt;p>재사용 가능한 버튼 컴포넌트를 만들어 봅니다.&lt;/p>
&lt;p>&lt;code>Components/Common/AppButton.razor&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;app-button @VariantCssClass&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;OnClick&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>Text
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Text { get; set; } &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Button&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Variant { get; set; } &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;primary&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public EventCallback OnClick { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string VariantCssClass &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> Variant&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToLowerInvariant() &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;secondary&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--secondary&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;danger&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--danger&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--primary&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>사용 예:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int _savedCount;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void Save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _savedCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;AppButton Text=&amp;#34;Save&amp;#34; Variant=&amp;#34;primary&amp;#34; OnClick=&amp;#34;Save&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;저장 횟수: @_savedCount&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>핵심은 계약(Contract) 설계입니다.&lt;/p>
&lt;ul>
&lt;li>적고 명확한 파라미터&lt;/li>
&lt;li>암묵적 동작 대신 명시적 이름&lt;/li>
&lt;li>안전한 기본값&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="3단계-renderfragment로-레이아웃-컴포지션">3단계: &lt;code>RenderFragment&lt;/code>로 레이아웃 컴포지션&lt;/h2>
&lt;p>&lt;code>RenderFragment&lt;/code>를 사용하면 부모가 자식에게 UI 블록을 전달할 수 있습니다.&lt;/p>
&lt;p>&lt;code>Components/Common/Card.razor&lt;/code> 생성:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;article class=&amp;#34;card&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;header class=&amp;#34;card__header&amp;#34;&amp;gt;@Title&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section class=&amp;#34;card__body&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @ChildContent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/article&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Title { get; set; } = string.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public RenderFragment? ChildContent { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>사용 예:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;Card Title=&amp;#34;Roadmap&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;컴포넌트&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;Data binding&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;Routing&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/Card&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 패턴은 래퍼 마크업 반복 없이 일관된 페이지 구조를 만드는 데 매우 유용합니다.&lt;/p>
&lt;hr>
&lt;h2 id="4단계-거대한-페이지보다-컴포지션">4단계: 거대한 페이지보다 컴포지션&lt;/h2>
&lt;p>페이지가 커지면 책임 기준으로 분리하세요.&lt;/p>
&lt;ul>
&lt;li>&lt;code>ProfileSummary&lt;/code> (상단 요약)&lt;/li>
&lt;li>&lt;code>ProfileStats&lt;/code> (지표)&lt;/li>
&lt;li>&lt;code>ProfileActivityList&lt;/code> (최근 활동)&lt;/li>
&lt;/ul>
&lt;p>그 결과 페이지는 구현 상세가 아니라 조합/오케스트레이션 역할을 하게 됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="5단계-마크업과-로직의-균형">5단계: 마크업과 로직의 균형&lt;/h2>
&lt;p>단순한 컴포넌트는 inline &lt;code>@code&lt;/code>로 충분합니다.&lt;/p>
&lt;p>커지기 시작하면 code-behind로 분리하세요.&lt;/p>
&lt;ul>
&lt;li>&lt;code>UserCard.razor&lt;/code>&lt;/li>
&lt;li>&lt;code>UserCard.razor.cs&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>마크업과 C# 로직 모두 읽기 쉬워집니다.&lt;/p>
&lt;hr>
&lt;h2 id="6단계-실용적인-폴더-구조">6단계: 실용적인 폴더 구조&lt;/h2>
&lt;p>확장에 유리한 구조 예시:&lt;/p>
&lt;ul>
&lt;li>&lt;code>Components/Pages/&lt;/code> -&amp;gt; 라우팅 가능한 페이지&lt;/li>
&lt;li>&lt;code>Components/Layout/&lt;/code> -&amp;gt; 앱 셸/내비게이션&lt;/li>
&lt;li>&lt;code>Components/Common/&lt;/code> -&amp;gt; 공용 범용 컴포넌트&lt;/li>
&lt;li>&lt;code>Components/Features/&amp;lt;FeatureName&amp;gt;/&lt;/code> -&amp;gt; 기능별 컴포넌트&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="초기에-자주-하는-실수">초기에 자주 하는 실수&lt;/h2>
&lt;ul>
&lt;li>전용 모델 없이 파라미터를 너무 많이 전달&lt;/li>
&lt;li>페이지 컴포넌트에 비즈니스 규칙을 직접 작성&lt;/li>
&lt;li>수백 줄짜리 &amp;ldquo;God component&amp;rdquo; 생성&lt;/li>
&lt;li>반복되는 마크업을 추출하지 않음&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="다음-장">다음 장&lt;/h2>
&lt;p>4장에서는 &lt;strong>데이터 바인딩과 이벤트&lt;/strong>를 다룹니다. &lt;code>@bind&lt;/code>, 이벤트 처리, 양방향 바인딩의 트레이드오프, 예측 가능한 상태 관리 패턴을 살펴봅니다.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazor 기초부터: 챕터 2 — 첫 번째 Blazor 앱</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-chapter-2/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-chapter-2/</guid><description>Blazor 기초부터 시리즈 2장입니다. 첫 앱을 만들고 로컬에서 실행한 뒤, 프로젝트 구조를 빠르게 이해할 수 있도록 핵심 파일을 살펴봅니다.</description><content:encoded>&lt;p>&lt;strong>Blazor 기초부터&lt;/strong> 챕터 2에 오신 것을 환영합니다. &lt;a href="../blazor-from-scratch-chapter-1">챕터 1&lt;/a>을 아직 읽지 않았다면 먼저 확인하세요.&lt;/p>
&lt;p>이번 챕터에서는 새 Blazor 앱을 만들고, 로컬에서 실행하고, 중요한 파일들의 역할을 이해합니다.&lt;/p>
&lt;hr>
&lt;h2 id="1단계-프로젝트-생성">1단계: 프로젝트 생성&lt;/h2>
&lt;p>터미널에서 실행:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new blazor -o BlazorFromScratch
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd BlazorFromScratch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 명령은 .NET 9 Blazor Web App을 기본 구조로 생성합니다.&lt;/p>
&lt;p>도구가 상호작용 모델을 물어보면 &lt;strong>Interactive Server&lt;/strong>로 시작하세요.&lt;/p>
&lt;hr>
&lt;h2 id="2단계-로컬-실행">2단계: 로컬 실행&lt;/h2>
&lt;p>앱 실행:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet watch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>터미널에 표시된 URL(보통 &lt;code>https://localhost:5xxx&lt;/code>)을 엽니다. Home, Counter, Weather 페이지가 보이면 정상입니다.&lt;/p>
&lt;p>여기까지 되면 개발 환경은 준비 완료입니다.&lt;/p>
&lt;hr>
&lt;h2 id="3단계-프로젝트-구조-이해">3단계: 프로젝트 구조 이해&lt;/h2>
&lt;p>처음에는 아래 파일/폴더만 알면 충분합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>Program.cs&lt;/code> — 서비스 등록 및 HTTP 파이프라인 설정.&lt;/li>
&lt;li>&lt;code>Components/App.razor&lt;/code> — 앱 루트 컴포넌트.&lt;/li>
&lt;li>&lt;code>Components/Routes.razor&lt;/code> — 페이지 라우트 매핑.&lt;/li>
&lt;li>&lt;code>Components/Pages/&lt;/code> — Home, Counter 같은 라우팅 페이지.&lt;/li>
&lt;li>&lt;code>Components/Layout/&lt;/code> — 공통 레이아웃(&lt;code>MainLayout.razor&lt;/code>, 메뉴).&lt;/li>
&lt;li>&lt;code>wwwroot/&lt;/code> — 정적 리소스(CSS, 이미지, favicon).&lt;/li>
&lt;li>&lt;code>appsettings.json&lt;/code> — 설정 값.&lt;/li>
&lt;/ul>
&lt;p>지금은 모든 파일을 깊게 알 필요 없습니다. UI 위치와 시작 설정 위치를 파악하는 것이 핵심입니다.&lt;/p>
&lt;hr>
&lt;h2 id="4단계-첫-변경-만들기">4단계: 첫 변경 만들기&lt;/h2>
&lt;p>&lt;code>Components/Pages/Home.razor&lt;/code>를 열고 내용을 아래로 바꾸세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Blazor 기초부터&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Blazor 기초부터&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;챕터 2가 로컬에서 실행 중입니다.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>저장 후 브라우저를 새로고침합니다. &lt;code>dotnet watch&lt;/code>를 사용 중이면 즉시 반영됩니다.&lt;/p>
&lt;p>이 작은 수정으로 전체 흐름(edit -&amp;gt; build -&amp;gt; render)을 확인할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="5단계-programcs-한-번-읽기">5단계: Program.cs 한 번 읽기&lt;/h2>
&lt;p>외울 필요는 없지만, 다음 줄은 알아두세요.&lt;/p>
&lt;ul>
&lt;li>&lt;code>AddRazorComponents()&lt;/code> Razor 컴포넌트 활성화.&lt;/li>
&lt;li>&lt;code>AddInteractiveServerComponents()&lt;/code> 서버 상호작용 컴포넌트 활성화.&lt;/li>
&lt;li>&lt;code>MapRazorComponents&amp;lt;App&amp;gt;()&lt;/code> 루트 컴포넌트를 엔드포인트에 매핑.&lt;/li>
&lt;/ul>
&lt;p>이 줄들이 앱 부팅 방식과 활성화된 렌더링 기능을 설명합니다.&lt;/p>
&lt;hr>
&lt;h2 id="자주-발생하는-문제">자주 발생하는 문제&lt;/h2>
&lt;p>문제가 생기면 보통 다음 중 하나입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SDK 버전 문제&lt;/strong>: &lt;code>dotnet --version&lt;/code>으로 .NET 9 확인.&lt;/li>
&lt;li>&lt;strong>HTTPS 인증서 경고&lt;/strong>: &lt;code>dotnet dev-certs https --trust&lt;/code> 실행.&lt;/li>
&lt;li>&lt;strong>포트 충돌&lt;/strong>: 기존 &lt;code>dotnet&lt;/code> 프로세스를 종료 후 재실행.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="다음-챕터">다음 챕터&lt;/h2>
&lt;p>챕터 3에서는 &lt;strong>컴포넌트&lt;/strong>를 깊이 다룹니다: 파라미터, 조합, 재사용 가능한 UI 구조.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazor 처음부터: 1장 — Blazor란 무엇인가?</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-chapter-1/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-chapter-1/</guid><description>Blazor 처음부터 시리즈의 1장입니다. Blazor가 실제로 무엇인지, 어디서 왔는지, 오늘날 사용 가능한 다양한 렌더링 모델, 그리고 JavaScript 프레임워크와 어떻게 비교되는지 살펴봅니다.</description><content:encoded>&lt;p>&lt;strong>Blazor 처음부터&lt;/strong> 시리즈의 1장에 오신 것을 환영합니다. 이 시리즈의 목적과 대상 독자를 설명한 &lt;a href="../blazor-from-scratch-intro">소개 포스트&lt;/a>를 놓치셨다면 먼저 읽어보세요 — 짧습니다.&lt;/p>
&lt;p>이 챕터에서는 근본적인 질문에 답할 것입니다: &lt;em>Blazor란 무엇인가?&lt;/em> 단순해 보이지만, 특히 &amp;ldquo;Blazor&amp;quot;가 수년에 걸쳐 약간씩 다른 의미를 갖게 되었기 때문에 답변에는 여러 층이 있습니다. 이 포스트가 끝날 때쯤 여러분은 Blazor가 무엇인지, .NET 생태계에서 어떻게 자리 잡는지, 다양한 렌더링 모델이 무엇인지, 그리고 왜 JavaScript 프레임워크 대신 이것을 선택할 수도 있는지 알게 될 것입니다.&lt;/p>
&lt;hr>
&lt;h2 id="간략한-역사">간략한 역사&lt;/h2>
&lt;p>Blazor는 2017년경 Microsoft의 Steve Sanderson이 진행한 실험적 프로젝트로 시작되었습니다. 아이디어는 도발적이었습니다: WebAssembly를 사용하여 브라우저에서 C#을 실행하여 JavaScript의 필요성을 완전히 없애는 것이었습니다. 개념 증명이었으며, 이름은 &lt;strong>Bla&lt;/strong>zer와 Ra&lt;strong>zor&lt;/strong>를 의도적으로 합친 것이었습니다 — Razor는 Blazor가 결국 위에 구축될 템플릿 엔진입니다.&lt;/p>
&lt;p>실험은 Microsoft가 진지하게 받아들일 만큼 충분한 관심을 불러일으켰습니다. Blazor는 2019년 9월 ASP.NET Core 3.0의 일부로 출시되었으며, 처음에는 &lt;strong>Blazor Server&lt;/strong> — 서버에서 C# 코드를 실행하고 실시간 SignalR 연결을 사용하여 브라우저에 UI 업데이트를 전송하는 모델이었습니다. &lt;strong>Blazor WebAssembly&lt;/strong>는 ASP.NET Core 3.1의 일부로 2020년 5월에 뒤따라, 프레임워크에 진정한 클라이언트 측 실행을 가져왔습니다.&lt;/p>
&lt;p>.NET 6과 7은 개발자 경험을 다듬었습니다. 그런 다음 2023년 11월에 출시된 .NET 8은 Microsoft가 &lt;em>풀스택 웹 UI&lt;/em>라고 부르는 것으로 렌더링 모델을 근본적으로 재고했습니다. 정적 서버 사이드 렌더링, 스트리밍 렌더링, 인터랙티브 서버, 인터랙티브 WebAssembly, 그리고 새로운 Auto 모드가 단일 프로젝트의 동일한 지붕 아래 공존하게 되었습니다. .NET 9는 이 기반 위에 구축하고 거친 부분을 다듬었습니다.&lt;/p>
&lt;p>오늘날 우리가 있는 곳이 바로 여기입니다.&lt;/p>
&lt;hr>
&lt;h2 id="blazor가-실제로-무엇인가">Blazor가 실제로 무엇인가&lt;/h2>
&lt;p>핵심적으로, Blazor는 &lt;strong>.NET을 위한 컴포넌트 기반 UI 프레임워크&lt;/strong>입니다. 컴포넌트로 UI를 구축합니다 — 상태를 유지하고, 이벤트에 반응하며, 더 큰 구조로 구성될 수 있는 C#과 HTML 마크업의 자기 완결적 조각들입니다. React나 Vue로 작업해본 적이 있다면 이 멘탈 모델이 익숙하게 느껴질 것입니다. 핵심 차이점은 JavaScript 대신 C#을 작성한다는 것입니다.&lt;/p>
&lt;p>Blazor의 컴포넌트는 &lt;code>.razor&lt;/code> 파일로 작성됩니다. &lt;code>.razor&lt;/code> 파일은 ASP.NET MVC 뷰에서 이미 알고 있을 수 있는 Razor 구문을 사용하여 HTML 마크업과 C# 코드를 혼합합니다. 간단한 컴포넌트는 다음과 같습니다:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/counter&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;카운터&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;현재 카운트: @currentCount&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button @onclick=&amp;#34;IncrementCount&amp;#34;&amp;gt;클릭하세요&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int currentCount = 0;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void IncrementCount()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>JavaScript는 보이지 않습니다. &lt;code>@onclick&lt;/code> 지시자는 버튼 클릭을 C# 메서드에 연결합니다. &lt;code>@currentCount&lt;/code> 표현식은 현재 값을 렌더링합니다. 상태가 변경되면 Blazor는 DOM에서 무엇을 업데이트할지 파악합니다.&lt;/p>
&lt;p>그것이 핵심입니다. 그 외 모든 것 — 라우팅, 의존성 주입, 폼, HTTP 호출 — 이 컴포넌트 모델 위에 구축됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="렌더링-모델">렌더링 모델&lt;/h2>
&lt;p>여기에 많은 혼란이 존재하므로 정확히 살펴보겠습니다. 오늘날 &amp;ldquo;Blazor&amp;quot;는 단일 배포 모델이 아닌 렌더링 모드의 계열을 의미합니다. 차이를 이해하는 것이 중요한 이유는 성능, 인프라 요구 사항, 그리고 컴포넌트가 할 수 있는 것과 없는 것에 영향을 미치기 때문입니다.&lt;/p>
&lt;h3 id="정적-ssr">정적 SSR&lt;/h3>
&lt;p>가장 간단한 모드입니다. Razor 컴포넌트가 서버에서 HTML로 렌더링되고 그 HTML이 브라우저로 전송됩니다. 영구적인 연결도, WebAssembly도, 기본적으로 클라이언트 측 인터랙티비티도 없습니다. 이것은 본질적으로 Razor Pages가 하는 일이지만, 컴포넌트 모델을 사용합니다.&lt;/p>
&lt;p>콘텐츠가 많은 페이지, 랜딩 페이지, 실시간 인터랙티비티가 필요 없는 모든 것에 사용하세요.&lt;/p>
&lt;h3 id="인터랙티브-서버">인터랙티브 서버&lt;/h3>
&lt;p>컴포넌트 코드가 서버에서 실행됩니다. 브라우저와 서버 사이에 SignalR WebSocket 연결이 설정됩니다. 버튼을 클릭하거나 입력 필드에 타이핑하면 이벤트가 WebSocket을 통해 서버로 전달되고, C#이 실행되며, 차이가 계산되고, DOM 업데이트가 브라우저로 다시 전송됩니다.&lt;/p>
&lt;p>&lt;strong>장점:&lt;/strong> 서버 리소스(데이터베이스, 파일 시스템, 시크릿)에 대한 완전한 접근. 빠른 초기 로드. 작은 다운로드 크기. WebAssembly 지원 없이도 브라우저에서 작동합니다.&lt;/p>
&lt;p>&lt;strong>단점:&lt;/strong> 모든 사용자 인터랙션이 서버 왕복을 필요로 하여 지연이 추가됩니다. 서버 리소스는 활성 연결 수에 따라 확장됩니다. 연결이 끊기면 앱이 저하됩니다.&lt;/p>
&lt;p>비즈니스 앱, 내부 도구, 오프라인 기능보다 서버 측 데이터 접근이 더 중요한 앱에 사용하세요.&lt;/p>
&lt;h3 id="인터랙티브-webassembly">인터랙티브 WebAssembly&lt;/h3>
&lt;p>.NET 런타임이 브라우저에 다운로드되고 앱이 완전히 클라이언트에서 실행됩니다. 초기 다운로드 후, 앱은 완전히 오프라인으로 작동하고 모든 인터랙션이 즉각적입니다 — UI 로직을 위한 서버 왕복이 없습니다.&lt;/p>
&lt;p>&lt;strong>장점:&lt;/strong> 진정한 클라이언트 측 실행. 오프라인 작동. 앱 로드 후 서버 부하 감소.&lt;/p>
&lt;p>&lt;strong>단점:&lt;/strong> 더 큰 초기 다운로드(.NET 런타임 + 앱). 더 느린 첫 번째 로드. 서버 측 데이터에 접근하려면 API가 필요합니다.&lt;/p>
&lt;p>오프라인 기능이 있는 앱, 즉각적인 반응성이 필요한 도구, UI에 서버 처리가 필요하지 않은 시나리오에 사용하세요.&lt;/p>
&lt;h3 id="auto-모드">Auto 모드&lt;/h3>
&lt;p>.NET 8에서 도입되었습니다. 앱은 인터랙티브 서버 모드로 시작합니다(빠른 초기 로드, WebAssembly 다운로드를 기다리지 않음). WebAssembly 파일이 백그라운드에서 다운로드되면 이후 방문은 자동으로 WebAssembly 모드로 전환됩니다.&lt;/p>
&lt;p>이것은 서버 모드의 빠른 첫 번째 로드와 결국 WebAssembly의 완전한 클라이언트 측 실행을 모두 제공합니다. 이해하기 가장 복잡한 모델이지만 많은 앱에 실용적인 기본값입니다.&lt;/p>
&lt;h3 id="동일한-앱에서-모드-혼합">동일한 앱에서 모드 혼합&lt;/h3>
&lt;p>.NET 8 이상에서는 전체 앱에 대해 단일 렌더링 모드에 고정되지 않습니다. 마케팅 랜딩 페이지는 정적 SSR일 수 있고, 인증된 대시보드는 인터랙티브 서버일 수 있으며, 데이터 시각화 위젯은 인터랙티브 WebAssembly일 수 있습니다. 프레임워크가 전환을 처리합니다.&lt;/p>
&lt;p>이 시리즈에서는 인터랙티브 서버로 시작할 것입니다. 가장 간단하게 작동시킬 수 있고 가장 직관적인 멘탈 모델을 가지고 있기 때문입니다. 다른 모드는 진행하면서 탐색하겠습니다.&lt;/p>
&lt;hr>
&lt;h2 id="blazor와-javascript-프레임워크-비교">Blazor와 JavaScript 프레임워크 비교&lt;/h2>
&lt;p>React, Angular, 또는 Vue로 개발해본 적이 있다면 아마도 이 비교가 궁금할 것입니다.&lt;/p>
&lt;p>&lt;strong>유사점은 실재합니다.&lt;/strong> Blazor의 컴포넌트 모델은 의도적으로 React와 유사합니다. props(Blazor에서는 Parameters), 로컬 상태(&lt;code>@code&lt;/code> 블록의 필드), 라이프사이클 훅, 그리고 동일한 단방향 데이터 흐름 / 이벤트 업 통신 패턴이 있습니다. React를 안다면 몇 시간 안에 익숙해질 것입니다.&lt;/p>
&lt;p>&lt;strong>핵심 차이점은 언어입니다.&lt;/strong> Blazor에서는 C#을 작성합니다. 비즈니스 로직, 유효성 검사 규칙, 데이터 모델을 Blazor 프론트엔드와 ASP.NET Core 백엔드 사이에 모두 공유할 수 있습니다. 이미 C#에 있는 &lt;code>User&lt;/code> 클래스를 TypeScript에서 중복 작성할 필요가 없습니다.&lt;/p>
&lt;p>&lt;strong>생태계 격차는 실재하지만 좁혀지고 있습니다.&lt;/strong> npm 생태계는 방대합니다. UI 컴포넌트를 위한 NuGet 생태계는 더 작지만 상당히 성장했습니다. 특정 차트 라이브러리나 드래그 앤 드롭 위젯이 필요하다면 JavaScript에 더 많은 옵션이 있습니다. 하지만 대부분의 비즈니스 애플리케이션의 경우 .NET 세계에서 사용 가능한 것이 충분합니다.&lt;/p>
&lt;p>&lt;strong>JavaScript 상호 운용성은 필요할 때 존재합니다.&lt;/strong> Blazor는 C#에서 JavaScript를 호출하고 JavaScript에서 C#을 호출할 수 있게 합니다. .NET 래퍼가 없는 브라우저 API나 기존 JS 라이브러리를 사용하고 싶을 때 상호 운용이 가능합니다. 레이어를 추가하지만 고통스럽지는 않습니다.&lt;/p>
&lt;p>&lt;strong>솔직한 답변:&lt;/strong> 팀 전체가 JavaScript 개발자이고 SEO, 성능, npm 생태계가 중요한 공개 제품을 만들고 있다면 JavaScript를 고수하세요. 팀이 .NET 개발자이고, 내부 또는 비즈니스 애플리케이션을 만들거나, 프론트엔드와 백엔드 사이의 코드 공유가 중요하다면 Blazor는 설득력 있는 선택입니다.&lt;/p>
&lt;hr>
&lt;h2 id="따라하기-위해-필요한-것">따라하기 위해 필요한 것&lt;/h2>
&lt;p>이 시리즈의 나머지 부분을 위해 필요한 것:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>.NET 9 SDK&lt;/strong> — &lt;a href="https://dotnet.microsoft.com/download">dot.net&lt;/a>에서 다운로드&lt;/li>
&lt;li>&lt;strong>IDE&lt;/strong> — Visual Studio 2022(Community 에디션 무료), C# Dev Kit 확장이 있는 VS Code, 또는 JetBrains Rider&lt;/li>
&lt;li>C# 기초 지식 — 클래스, 속성, 인터페이스, async/await&lt;/li>
&lt;/ul>
&lt;p>그것뿐입니다. Node, npm, webpack 없음.&lt;/p>
&lt;hr>
&lt;h2 id="다음-편">다음 편&lt;/h2>
&lt;p>2장에서는 첫 번째 Blazor 앱을 스캐폴딩하고, 프로젝트 구조를 살펴보며, 로컬에서 실행할 수 있는지 확인할 것입니다. 끝에는 작동하는 앱과 각 파일이 무엇을 하는지에 대한 명확한 그림을 갖게 될 것입니다.&lt;/p>
&lt;p>거기서 뵙겠습니다.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazor 처음부터: 새 시리즈</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-intro/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-from-scratch-intro/</guid><description>Blazor를 기초부터 차근차근 쌓아가는 장편 시리즈 시작 — 지름길도, 얼버무리기도 없이, 명확한 설명과 실제 코드만으로.</description><content:encoded>&lt;p>저는 한동안 Blazor에 대해 글을 써왔습니다 — 컴포넌트 생명 주기, 스코프 CSS, 인터랙티비티 모델, 인증. 각각의 글은 나름 유용했지만, 항상 기반이 빠져 있다는 느낌이 들었습니다. Blazor가 무엇인지, 왜 존재하는지, .NET 생태계에 어떻게 맞는지를 이미 알고 있다고 가정하고 있었죠. 모두가 그렇지는 않고, 그건 완전히 괜찮습니다.&lt;/p>
&lt;p>그래서 새로운 것을 시작합니다: &lt;strong>Blazor 처음부터&lt;/strong>. 제대로 된 시리즈로, 기초부터 쌓아올려, 자신이 무엇을 만들고 있는지 정말로 이해하고 싶은 개발자를 위해 — 그냥 작동할 때까지 복사-붙여넣기만 하는 것이 아니라.&lt;/p>
&lt;h2 id="이-시리즈는-누구를-위한-것인가">이 시리즈는 누구를 위한 것인가&lt;/h2>
&lt;p>이 시리즈는 다음에 해당하는 분을 위한 것입니다:&lt;/p>
&lt;ul>
&lt;li>Blazor에 대해 들어봤지만 깊이 파고들 시간이나 적절한 시작점을 찾지 못한 .NET 개발자&lt;/li>
&lt;li>Blazor를 시도해보고 돌아가게는 했지만, &lt;em>왜&lt;/em> 작동하는지 짐작만 하고 있는 것 같은 분&lt;/li>
&lt;li>JavaScript/React/Angular 세계에서 와서 Microsoft의 모던 프론트엔드에 대한 답이 어떤 모습인지 이해하고 싶은 분&lt;/li>
&lt;li>흩어진 문서와 블로그 글들 대신 하나의 일관된 리소스를 원하는 분&lt;/li>
&lt;/ul>
&lt;p>시니어 개발자일 필요는 없습니다. 다만 C# 기본기 — 클래스, 인터페이스, async/await — 는 익숙해야 합니다. ASP.NET Core에서 간단한 CRUD API를 작성할 수 있다면 준비된 것입니다.&lt;/p>
&lt;h2 id="무엇을-다룰-것인가">무엇을 다룰 것인가&lt;/h2>
&lt;p>다음은 제가 계획한 대략적인 로드맵입니다. 필요에 따라 일부 주제는 여러 글로 확장될 수 있습니다:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Blazor란 무엇인가?&lt;/strong> — 호스팅 모델, 역사, 전통적인 웹 개발과의 비교&lt;/li>
&lt;li>&lt;strong>첫 번째 Blazor 앱&lt;/strong> — 스캐폴딩, 프로젝트 구조, 로컬 실행&lt;/li>
&lt;li>&lt;strong>컴포넌트&lt;/strong> — 모든 Blazor UI의 기본 블록&lt;/li>
&lt;li>&lt;strong>데이터 바인딩과 이벤트&lt;/strong> — UI를 반응형으로 만들기&lt;/li>
&lt;li>&lt;strong>컴포넌트 간 통신&lt;/strong> — 파라미터, EventCallbacks, 캐스케이딩 값&lt;/li>
&lt;li>&lt;strong>라우팅과 네비게이션&lt;/strong> — Blazor가 URL과 페이지 전환을 처리하는 방법&lt;/li>
&lt;li>&lt;strong>의존성 주입&lt;/strong> — 서비스, 스코프, Blazor의 DI 컨테이너&lt;/li>
&lt;li>&lt;strong>폼과 유효성 검사&lt;/strong> — EditForm, DataAnnotations, 커스텀 유효성 검사기&lt;/li>
&lt;li>&lt;strong>HTTP와 외부 데이터&lt;/strong> — Blazor 앱에서 API 호출하기&lt;/li>
&lt;li>&lt;strong>인증과 인가&lt;/strong> — 앱을 올바르게 보호하기&lt;/li>
&lt;li>&lt;strong>JavaScript 인터롭&lt;/strong> — 브라우저에 직접 접근해야 할 때&lt;/li>
&lt;li>&lt;strong>성능과 최적화&lt;/strong> — 가상화, 지연 로딩, 렌더 전략&lt;/li>
&lt;li>&lt;strong>Blazor 컴포넌트 테스트&lt;/strong> — bUnit과 탄탄한 테스트의 모습&lt;/li>
&lt;li>&lt;strong>배포&lt;/strong> — Azure, IIS, 정적 호스트에 게시하기&lt;/li>
&lt;/ol>
&lt;p>이 목록은 발전할 것입니다. 일부 주제는 여러 글로 나뉘고, 다른 것들은 합쳐질 수 있습니다. 시리즈가 진행되면서 이 글을 업데이트하고 각 항목이 게시될 때 링크를 추가하겠습니다.&lt;/p>
&lt;h2 id="왜-시리즈를-왜-지금">왜 시리즈를, 왜 지금&lt;/h2>
&lt;p>Blazor는 많이 성숙했습니다. .NET 8과 9에서 렌더링 모델이 크게 개편되었습니다 — 정적 SSR, 스트리밍 렌더링, 인터랙티브 서버, 인터랙티브 WebAssembly, 오토 모드가 이제 한 지붕 아래 공존합니다. 진정으로 흥미롭고 유능한 프레임워크이지만, 복잡성의 증가로 인해 시작 경험이 혼란스러울 수 있습니다.&lt;/p>
&lt;p>여러분이 있는 곳에서 만나 전체를 체계적으로 안내하는 리소스를 만들고 싶습니다. 공식 문서의 대체물이 아닙니다 — 그것은 좋고 읽어야 합니다 — 하지만 &lt;em>무엇&lt;/em> 뒤에 있는 &lt;em>왜&lt;/em>를 설명하는 동반자입니다.&lt;/p>
&lt;h2 id="시리즈를-따라가는-방법">시리즈를 따라가는 방법&lt;/h2>
&lt;p>시리즈의 각 글은 단독으로 읽기에 충분히 독립적이지만, 서로 쌓아올리는 방식으로 구성됩니다. 처음부터 시작한다면 순서대로 읽을 것을 권장합니다. 특정 부분을 채우러 들어오는 경우에도 괜찮습니다 — 중요한 경우 선행 글에 대한 링크를 달겠습니다.&lt;/p>
&lt;p>각 글의 코드는 GitHub에서 제공됩니다. 진행하면서 링크를 공유하겠습니다.&lt;/p>
&lt;p>다음 글에서 만나요.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>C# Union 유형: 차별된 Union이 마침내 등장합니다.</title><link>https://emimontesdeoca.github.io/ko/posts/csharp-union-types/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/csharp-union-types/</guid><description>곧 출시될 C# 구별 통합 유형에 대해 자세히 알아보세요. 정의, 작동 방식, 도메인 모델링 방식을 변경하는 이유 등이 있습니다.</description><content:encoded>&lt;p>상당한 시간 동안 C#을 작성해 왔다면 &amp;ldquo;여러 가지 중 하나&amp;quot;가 될 수 있는 것을 모델링하려고 할 때 벽에 부딪혔을 가능성이 높습니다. 어쩌면 성공 값이나 오류를 반환하는 메서드가 필요할 수도 있습니다. 어쩌면 각각 완전히 다른 데이터를 사용하는 신용카드, 은행 송금, 디지털 지갑을 처리하는 결제 시스템을 구축하고 있었을 수도 있습니다. 아니면 그냥 F#이나 Rust를 보고 &amp;ldquo;왜 C#에서는 그걸 가질 수 없지?&amp;ldquo;라고 생각했을 수도 있습니다.&lt;/p>
&lt;p>기다림이 거의 끝났습니다. &lt;strong>차별화된 노동조합이 C#으로 출시됩니다.&lt;/strong>&lt;/p>
&lt;p>이는 수년간 가장 많이 요청된 언어 기능 중 하나였으며 커뮤니티 토론은 2017년 이전부터 이어졌습니다. C# 언어 디자인 팀은 철저한 패턴 일치를 통해 폐쇄형 계층 구조를 정의하기 위해 &lt;code>union&lt;/code> 키워드를 도입하는 제안을 작업해 왔습니다. 이 게시물에서는 차별적 공용체(discriminated Union)가 무엇인지, 왜 그렇게 중요한지, 지금까지 우리가 이를 어떻게 속였는지, 그리고 제안된 구문이 실제로 어떤 모습인지에 대해 각각에 대한 실제 코드 예제를 통해 안내하고 싶습니다.&lt;/p>
&lt;p>시작하기 전에 간단히 알아두어야 할 점은 이 글을 쓰는 시점에서 통합 유형 기능은 아직 제안 및 미리 보기 단계에 있다는 것입니다. 여기서 설명하는 구문과 동작은 공개적으로 사용 가능한 최신 디자인 문서와 C# 언어 디자인 팀의 토론을 기반으로 합니다. 최종 출시 이전에는 상황이 변경될 수 있습니다. 확정된 사항과 아직 논의 중인 사항에 대해 명확히 말씀드리겠습니다.&lt;/p>
&lt;h2 id="차별적-노동조합이란-무엇입니까">차별적 노동조합이란 무엇입니까?&lt;/h2>
&lt;p>본질적으로 구별된 공용체(때때로 &amp;ldquo;태그된 공용체&amp;rdquo; 또는 &amp;ldquo;합계 유형&amp;quot;이라고도 함)는 각 변형이 서로 다른 데이터를 전달할 수 있는 고정된 가능한 값 세트 중 하나를 보유할 수 있는 유형입니다. &amp;ldquo;구별된&amp;rdquo; 부분은 런타임이 항상 어떤 변형인지 알고 있음을 의미합니다. 이를 식별하는 태그가 있습니다.&lt;/p>
&lt;p>&lt;code>enum&lt;/code>처럼 생각하면 되지만, 각 구성원은 자신만의 데이터 페이로드를 운반할 수 있습니다.&lt;/p>
&lt;p>다른 언어를 사용해 본 적이 있다면 아마도 이전에 이 개념을 본 적이 있을 것입니다.&lt;/p>
&lt;p>**F#**는 첫날부터 차별적인 노동조합을 운영해 왔습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fsharp" data-lang="fsharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">type&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Shape&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> Circle &lt;span style="color:#ff7b72">of&lt;/span> radius&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> Rectangle &lt;span style="color:#ff7b72">of&lt;/span> width&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">*&lt;/span> height&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> Triangle &lt;span style="color:#ff7b72">of&lt;/span> &lt;span style="color:#ff7b72">base&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">*&lt;/span> height&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Rust&lt;/strong>는 동일한 아이디어로 &lt;code>enum&lt;/code>를 사용합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">enum&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Shape&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>Circle&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681"> &lt;/span>radius: &lt;span style="color:#ff7b72">f64&lt;/span> },&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>Rectangle&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681"> &lt;/span>width: &lt;span style="color:#ff7b72">f64&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>height: &lt;span style="color:#ff7b72">f64&lt;/span> },&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>Triangle&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681"> &lt;/span>base: &lt;span style="color:#ff7b72">f64&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>height: &lt;span style="color:#ff7b72">f64&lt;/span> },&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>TypeScript&lt;/strong>는 태그된 공용체와 비슷한 결과를 얻습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-typescript" data-lang="typescript">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">type&lt;/span> Shape &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;circle&amp;#34;&lt;/span>; radius: &lt;span style="color:#ff7b72">number&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;rectangle&amp;#34;&lt;/span>; width: &lt;span style="color:#ff7b72">number&lt;/span>; height: &lt;span style="color:#ff7b72">number&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;triangle&amp;#34;&lt;/span>; base: &lt;span style="color:#ff7b72">number&lt;/span>; height: &lt;span style="color:#ff7b72">number&lt;/span> };
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이러한 모든 경우에 컴파일러는 가능한 모든 변형을 알고 있으며 사용자가 이를 모두 처리하도록 강제할 수 있습니다. 이것이 초능력입니다: &lt;strong>완전성 검사&lt;/strong>. 새로운 변형을 추가하면 컴파일러는 이를 처리하는 것을 잊어버린 모든 곳에서 알려줍니다.&lt;/p>
&lt;p>C#에서는 이를 표현하는 최고의 방법이 없었습니다. 지금까지.&lt;/p>
&lt;h2 id="오늘날-노동조합을-시뮬레이션하는-방법">오늘날 노동조합을 시뮬레이션하는 방법&lt;/h2>
&lt;p>수년에 걸쳐 C# 커뮤니티에서는 각각 고유한 장단점이 있는 여러 가지 해결 방법을 생각해 냈습니다. 가장 일반적인 접근 방식을 살펴보겠습니다.&lt;/p>
&lt;h3 id="상속이-포함된-추상-레코드">상속이 포함된 추상 레코드&lt;/h3>
&lt;p>최신 C#에서 가장 관용적인 해결 방법은 봉인된 파생 유형이 있는 추상 레코드를 사용하는 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Shape&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Circle&lt;/span>(&lt;span style="color:#ff7b72">double&lt;/span> Radius) : Shape;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Rectangle&lt;/span>(&lt;span style="color:#ff7b72">double&lt;/span> Width, &lt;span style="color:#ff7b72">double&lt;/span> Height) : Shape;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Triangle&lt;/span>(&lt;span style="color:#ff7b72">double&lt;/span> Base, &lt;span style="color:#ff7b72">double&lt;/span> Height) : Shape;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> Shape() { } &lt;span style="color:#8b949e;font-style:italic">// Prevent external inheritance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것은 합리적으로 잘 작동합니다. 불변성, 값 평등을 얻고 패턴 일치를 사용할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">double&lt;/span> Area(Shape shape) =&amp;gt; shape &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Shape.Circle c =&amp;gt; Math.PI * c.Radius * c.Radius,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Shape.Rectangle r =&amp;gt; r.Width * r.Height,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Shape.Triangle t =&amp;gt; &lt;span style="color:#a5d6ff">0.5&lt;/span> * t.Base * t.Height,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> InvalidOperationException(&lt;span style="color:#a5d6ff">&amp;#34;Unknown shape&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```그러나&lt;/span> &lt;span style="color:#f85149">중요한&lt;/span> &lt;span style="color:#f85149">단점이&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>. &lt;span style="color:#f85149">컴파일러는&lt;/span> &lt;span style="color:#f85149">계층&lt;/span> &lt;span style="color:#f85149">구조가&lt;/span> &lt;span style="color:#f85149">닫혀&lt;/span> &lt;span style="color:#f85149">있다는&lt;/span> &lt;span style="color:#f85149">것을&lt;/span> &lt;span style="color:#f85149">모르므로&lt;/span> &lt;span style="color:#f85149">항상&lt;/span> &lt;span style="color:#f85149">`&lt;/span>_&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">폐기&lt;/span> &lt;span style="color:#f85149">팔이&lt;/span> &lt;span style="color:#f85149">필요합니다&lt;/span>. &lt;span style="color:#f85149">그렇지&lt;/span> &lt;span style="color:#f85149">않으면&lt;/span> &lt;span style="color:#f85149">경고가&lt;/span> &lt;span style="color:#f85149">표시됩니다&lt;/span>. &lt;span style="color:#f85149">새로운&lt;/span> &lt;span style="color:#f85149">변형을&lt;/span> &lt;span style="color:#f85149">추가하면&lt;/span> &lt;span style="color:#f85149">컴파일러는&lt;/span> &lt;span style="color:#f85149">이를&lt;/span> &lt;span style="color:#f85149">처리하는&lt;/span> &lt;span style="color:#f85149">것을&lt;/span> &lt;span style="color:#f85149">잊은&lt;/span> &lt;span style="color:#f85149">모든&lt;/span> &lt;span style="color:#f85149">위치에&lt;/span> &lt;span style="color:#f85149">대해&lt;/span> &lt;span style="color:#f85149">알려주지&lt;/span> &lt;span style="color:#f85149">않습니다&lt;/span>. &lt;span style="color:#f85149">폐기&lt;/span> &lt;span style="color:#f85149">시&lt;/span> &lt;span style="color:#f85149">자동으로&lt;/span> &lt;span style="color:#f85149">이를&lt;/span> &lt;span style="color:#f85149">삼켜버립니다&lt;/span>. &lt;span style="color:#f85149">그것은&lt;/span> &lt;span style="color:#f85149">목적을&lt;/span> &lt;span style="color:#f85149">완전히&lt;/span> &lt;span style="color:#f85149">무너뜨립니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">원오브&lt;/span> &lt;span style="color:#f85149">도서관&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">또&lt;/span> &lt;span style="color:#f85149">다른&lt;/span> &lt;span style="color:#f85149">인기&lt;/span> &lt;span style="color:#f85149">있는&lt;/span> &lt;span style="color:#f85149">접근&lt;/span> &lt;span style="color:#f85149">방식은&lt;/span> [OneOf](https:&lt;span style="color:#8b949e;font-style:italic">//github.com/mcintyre321/OneOf) NuGet 패키지입니다.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> OneOf&amp;lt;Success&amp;lt;Order&amp;gt;, NotFound, ValidationError&amp;gt; ProcessOrder(OrderRequest request)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// ...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Usage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = ProcessOrder(request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>result.Switch(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> success =&amp;gt; Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Order {success.Value.Id} processed&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> notFound =&amp;gt; Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Order not found&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error =&amp;gt; Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Validation failed: {error.Message}&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>OneOf는 일반 유형 매개변수를 통해 컴파일 타임에 완전성 검사를 제공합니다. 그러나 위치 일치(첫 번째 유형, 두 번째 유형 등)에 의존하고 일반 시그니처는 다루기 힘들 정도로 빠르며 언어의 패턴 일치와 통합되지 않습니다. 영리한 해킹이지만 여전히 해킹입니다.&lt;/p>
&lt;h3 id="수동-열거형--데이터-패턴">수동 열거형 + 데이터 패턴&lt;/h3>
&lt;p>일부 개발자는 enum 태그와 데이터 컨테이너를 사용하여 클래식 경로를 사용합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">enum&lt;/span> PaymentType { CreditCard, BankTransfer, DigitalWallet }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Payment&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> PaymentType Type { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CreditCardInfo? CreditCard { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> BankTransferInfo? BankTransfer { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DigitalWalletInfo? DigitalWallet { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것은 깨지기 쉽습니다. &lt;code>Type&lt;/code>를 &lt;code>CreditCard&lt;/code>로 설정하는 것 외에는 &lt;code>BankTransfer&lt;/code> 속성을 채우는 것을 막을 수 없습니다. 컴파일러는 도움을 줄 수 없으며 모든 곳에서 런타임 오류와 null 검사가 발생하게 됩니다. 이는 유형 모델링에 대한 &amp;ldquo;문자열 유형&amp;rdquo; 접근 방식이며 확장되지 않습니다.&lt;/p>
&lt;p>이러한 접근 방식은 모두 근본적인 문제를 공유합니다. &lt;strong>언어를 사용하는 대신 언어와 싸우고 있습니다.&lt;/strong> 컴파일러는 닫힌 가능성 집합에 대해 추론할 수 없으므로 차별 공용체의 가장 귀중한 속성인 철저한 검사를 잃게 됩니다.&lt;/p>
&lt;h2 id="c-제안-union-키워드">C# 제안: &lt;code>union&lt;/code> 키워드&lt;/h2>
&lt;p>C# 언어 팀의 제안에는 각각 선택적으로 데이터를 전달하는 명명된 멤버의 닫힌 집합을 정의하는 전용 &lt;code>union&lt;/code> 키워드가 도입되었습니다. 제안된 기본 구문은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Shape
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">double&lt;/span> Radius),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle(&lt;span style="color:#ff7b72">double&lt;/span> Width, &lt;span style="color:#ff7b72">double&lt;/span> Height),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Triangle(&lt;span style="color:#ff7b72">double&lt;/span> Base, &lt;span style="color:#ff7b72">double&lt;/span> Height)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그게 다야. 깔끔하고 간결하며 즉시 읽을 수 있습니다. 공용체 내부의 각 구성원은 자체 데이터를 사용하여 고유한 변형을 정의합니다. 컴파일러는 &lt;code>Shape&lt;/code>가 이 세 가지 중 하나만 될 수 있다는 것을 알고 있습니다.&lt;/p>
&lt;p>내부적으로 컴파일러는 추상 레코드를 사용하여 직접 작성하는 것과 유사하지만 컴파일러가 유형의 폐쇄적 특성을 완전히 인식하는 봉인된 유형 계층 구조를 생성합니다. 이는 컴파일러가 패턴 일치에 철저함을 적용할 수 있다는 것을 의미하며, 이것이 주요 이점입니다.&lt;/p>
&lt;h3 id="가치-전용-회원">가치 전용 회원&lt;/h3>
&lt;p>조합원은 데이터를 가지고 다닐 필요가 없습니다. 데이터를 전달하는 멤버와 단순 값 멤버를 혼합할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Option&amp;lt;T&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Some(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> None
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 기능적 프로그래머가 수년간 C#에서 요구해 왔던 고전적인 &lt;code>Option&lt;/code>/&lt;code>Maybe&lt;/code> 유형입니다. &lt;code>None&lt;/code>에는 데이터가 없습니다. 단지 태그일 뿐입니다.&lt;/p>
&lt;h3 id="일반-공용체">일반 공용체&lt;/h3>
&lt;p>위의 &lt;code>Option&amp;lt;T&amp;gt;&lt;/code> 예제에서 볼 수 있듯이 공용체는 제네릭을 지원합니다. 다음은 좀 더 관련된 예입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Result&amp;lt;T, E&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(E Err)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 예상되는 실패 사례에 대한 예외에 의존하지 않는 전체 오류 처리 스타일을 열어줍니다. 이는 수년간 Rust 및 함수형 언어의 표준 관행이었습니다.&lt;/p>
&lt;h3 id="메소드가-있는-공용체">메소드가 있는 공용체&lt;/h3>
&lt;p>또한 이 제안은 공용체가 다른 유형과 마찬가지로 메소드, 계산된 속성 및 구현 인터페이스를 가질 수 있도록 허용합니다.```csharp
union Shape
{
Circle(double Radius),
Rectangle(double Width, double Height),
Triangle(double Base, double Height);&lt;/p>
&lt;pre>&lt;code>public double Area =&amp;gt; this switch
{
Circle(var r) =&amp;gt; Math.PI * r * r,
Rectangle(var w, var h) =&amp;gt; w * h,
Triangle(var b, var h) =&amp;gt; 0.5 * b * h
};
public double Perimeter =&amp;gt; this switch
{
Circle(var r) =&amp;gt; 2 * Math.PI * r,
Rectangle(var w, var h) =&amp;gt; 2 * (w + h),
Triangle(var b, var h) =&amp;gt; b + h + Math.Sqrt(b * b + h * h)
};
&lt;/code>&lt;/pre>
&lt;p>}&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-mysql" data-lang="mysql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>switch&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">내부의&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>Area&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">및&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>Perimeter&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">표현식에는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">기본&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>arm이&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">필요하지&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">않습니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">컴파일러는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">통합이&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">철저하다는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">것을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">알고&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있습니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">즉&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">세&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">가지&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">변형만&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있고&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">세&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">가지&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모두&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">처리됩니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">나중에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">네&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">번째&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">변형을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">추가하면&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">컴파일러는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">이를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">처리하지&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">않는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모든&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>switch&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#f85149">에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">플래그를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">지정합니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">## 패턴 매칭 통합
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">패턴&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">일치는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">버전&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">7&lt;/span>.&lt;span style="color:#a5d6ff">0&lt;/span>&lt;span style="color:#f85149">부터&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>C&lt;span style="color:#8b949e;font-style:italic">#에서 발전해 왔으며 공용체 형식은 해당 시스템의 일급 시민으로 설계되었습니다.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### 철저한 스위치 표현
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">가장&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">영향력&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">기능은&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">철저한&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">스위치&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">확인입니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">공용체를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">사용하면&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">컴파일러는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">가능한&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모든&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경우를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">알고&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있습니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>csharp&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>string&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">Describe&lt;/span>(Shape&lt;span style="color:#6e7681"> &lt;/span>shape)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>shape&lt;span style="color:#6e7681"> &lt;/span>switch&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">{&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Circle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>r)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A circle with radius {r}&amp;#34;&lt;/span>,&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Rectangle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>w,&lt;span style="color:#6e7681"> &lt;/span>var&lt;span style="color:#6e7681"> &lt;/span>h)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A {w}x{h} rectangle&amp;#34;&lt;/span>,&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Triangle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>b,&lt;span style="color:#6e7681"> &lt;/span>var&lt;span style="color:#6e7681"> &lt;/span>h)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A triangle with base {b} and height {h}&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">}&lt;/span>;&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>폐기 팔이 없습니다. 아니요 &lt;code>_ =&amp;gt; throw new NotImplementedException()&lt;/code>. 대소문자를 잊어버린 경우 컴파일러는 경고가 아닌 오류를 내보냅니다. 이는 안전성이 근본적으로 개선된 것입니다.&lt;/p>
&lt;h3 id="중첩-패턴-일치">중첩 패턴 일치&lt;/h3>
&lt;p>공용체는 중첩된 패턴으로 자연스럽게 구성됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Expr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Literal(&lt;span style="color:#ff7b72">double&lt;/span> Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Add(Expr Left, Expr Right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Multiply(Expr Left, Expr Right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Negate(Expr Inner)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">double&lt;/span> Evaluate(Expr expr) =&amp;gt; expr &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Literal(&lt;span style="color:#ff7b72">var&lt;/span> v) =&amp;gt; v,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Add(&lt;span style="color:#ff7b72">var&lt;/span> left, &lt;span style="color:#ff7b72">var&lt;/span> right) =&amp;gt; Evaluate(left) + Evaluate(right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Multiply(&lt;span style="color:#ff7b72">var&lt;/span> left, &lt;span style="color:#ff7b72">var&lt;/span> right) =&amp;gt; Evaluate(left) * Evaluate(right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Negate(&lt;span style="color:#ff7b72">var&lt;/span> inner) =&amp;gt; -Evaluate(inner)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이러한 종류의 재귀적 데이터 구조는 컴파일러, 인터프리터, 규칙 엔진 및 수학적 모델링에서 매우 일반적입니다. 현재 C#에서는 깊은 클래스 계층 구조와 방문자 패턴이 필요합니다. 공용체를 사용하면 코드가 훨씬 더 간단해집니다.&lt;/p>
&lt;h3 id="보호-조항">보호 조항&lt;/h3>
&lt;p>공용체와의 패턴 일치는 예상한 대로 &lt;code>when&lt;/code> 가드를 지원합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> Classify(Shape shape) =&amp;gt; shape &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">var&lt;/span> r) when r &amp;gt; &lt;span style="color:#a5d6ff">100&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Large circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">var&lt;/span> r) when r &amp;gt; &lt;span style="color:#a5d6ff">10&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Medium circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(_) =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Small circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle(&lt;span style="color:#ff7b72">var&lt;/span> w, &lt;span style="color:#ff7b72">var&lt;/span> h) when w == h =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Square&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle _ =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Rectangle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Triangle _ =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Triangle&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="실제-예">실제 예&lt;/h2>
&lt;p>공용체 유형이 코드를 극적으로 향상시키는 몇 가지 실제 시나리오를 살펴보겠습니다.&lt;/p>
&lt;h3 id="결과-패턴-예상되는-오류에-대한-예외-교체">결과 패턴: 예상되는 오류에 대한 예외 교체&lt;/h3>
&lt;p>최신 애플리케이션 개발에서 가장 일반적인 패턴 중 하나는 제어 흐름에 대한 예외를 사용하지 않고 성공하거나 실패할 수 있는 작업을 나타내는 것입니다. 예외는 예외적이어야 합니다(예: 네트워크 오류 또는 메모리 부족 상태 등). 유효성 검사 오류 또는 &amp;ldquo;찾을 수 없음&amp;rdquo; 결과는 예상된 결과이며 예외는 아닙니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Result&amp;lt;T, E&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(E Err)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>union OrderError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotFound(Guid OrderId),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> InsufficientStock(&lt;span style="color:#ff7b72">string&lt;/span> ProductId, &lt;span style="color:#ff7b72">int&lt;/span> Requested, &lt;span style="color:#ff7b72">int&lt;/span> Available),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PaymentDeclined(&lt;span style="color:#ff7b72">string&lt;/span> Reason),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ValidationFailed(IReadOnlyList&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Errors)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Result&amp;lt;Order, OrderError&amp;gt; ProcessOrder(OrderRequest request)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Validate(request, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> errors))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationFailed(errors);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> product = catalog.Find(request.ProductId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (product &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> NotFound(request.OrderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (product.Stock &amp;lt; request.Quantity)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> InsufficientStock(request.ProductId, request.Quantity, product.Stock);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> paymentResult = paymentGateway.Charge(request.Payment);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!paymentResult.Success)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> PaymentDeclined(paymentResult.Message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> order = CreateOrder(request, product);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Ok(order);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 호출자는 가능한 모든 결과를 처리해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = ProcessOrder(request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> response = result &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(&lt;span style="color:#ff7b72">var&lt;/span> order) =&amp;gt; Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/orders/{order.Id}&amp;#34;&lt;/span>, order),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(NotFound(&lt;span style="color:#ff7b72">var&lt;/span> id)) =&amp;gt; Results.NotFound(&lt;span style="color:#a5d6ff">$&amp;#34;Order {id} not found&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(InsufficientStock(&lt;span style="color:#ff7b72">var&lt;/span> pid, &lt;span style="color:#ff7b72">var&lt;/span> req, &lt;span style="color:#ff7b72">var&lt;/span> avail)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.Conflict(&lt;span style="color:#a5d6ff">$&amp;#34;Product {pid}: requested {req}, only {avail} available&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(PaymentDeclined(&lt;span style="color:#ff7b72">var&lt;/span> reason)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.UnprocessableEntity(&lt;span style="color:#a5d6ff">$&amp;#34;Payment declined: {reason}&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(ValidationFailed(&lt;span style="color:#ff7b72">var&lt;/span> errors)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Errors = errors })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>try-catch 블록도 없고, 잊어버린 예외 유형도 없고, 런타임에 예상치 못한 일도 없습니다. 모든 실패 모드는 유형 서명에 표시되며 컴파일러에 의해 적용됩니다. 이는 API 안정성이 크게 향상되었습니다.&lt;/p>
&lt;h3 id="도메인-모델링-결제-유형">도메인 모델링: 결제 유형&lt;/h3>
&lt;p>다음은 전자상거래 시스템에서 다양한 결제 방법을 처리하는 실제 도메인 모델링 예입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union PaymentMethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard(&lt;span style="color:#ff7b72">string&lt;/span> CardNumber, &lt;span style="color:#ff7b72">string&lt;/span> Expiry, &lt;span style="color:#ff7b72">string&lt;/span> Cvv),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer(&lt;span style="color:#ff7b72">string&lt;/span> Iban, &lt;span style="color:#ff7b72">string&lt;/span> Bic),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(&lt;span style="color:#ff7b72">string&lt;/span> Provider, &lt;span style="color:#ff7b72">string&lt;/span> Token),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">decimal&lt;/span> CalculateProcessingFee(PaymentMethod method, &lt;span style="color:#ff7b72">decimal&lt;/span> amount) =&amp;gt; method &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard _ =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.029&lt;/span>m + &lt;span style="color:#a5d6ff">0.30&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer _ =&amp;gt; &lt;span style="color:#a5d6ff">1.50&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(provider: &lt;span style="color:#a5d6ff">&amp;#34;PayPal&amp;#34;&lt;/span>, _) =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.034&lt;/span>m + &lt;span style="color:#a5d6ff">0.35&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet _ =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.025&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery =&amp;gt; &lt;span style="color:#a5d6ff">4.99&lt;/span>m
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> FormatForReceipt(PaymentMethod method) =&amp;gt; method &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard(&lt;span style="color:#ff7b72">var&lt;/span> num, _, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Credit Card ending in {num[^4..]}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer(&lt;span style="color:#ff7b72">var&lt;/span> iban, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Bank Transfer ({iban[..4]}****)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(&lt;span style="color:#ff7b72">var&lt;/span> provider, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Digital Wallet ({provider})&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Cash on Delivery&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이를 5개의 파일에 분산된 5개의 서로 다른 구현이 있는 인터페이스 또는 추상 클래스가 있고 방문자 패턴이 맨 위에 있을 수 있는 현재 접근 방식과 비교해 보세요. 통합 접근 방식은 데이터 정의와 작업을 함께 유지하고 읽기 가능하며 철저하게 검사합니다.&lt;/p>
&lt;h3 id="상태-머신">상태 머신&lt;/h3>
&lt;p>상태 머신은 주문 처리, 워크플로 엔진, 연결 관리, UI 상태 등 소프트웨어의 모든 곳에 있습니다. 노동조합은 이를 명시적이고 안전하게 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union ConnectionState
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Disconnected,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Connecting(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, DateTime StartedAt),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Connected(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, TcpClient Client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Reconnecting(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, &lt;span style="color:#ff7b72">int&lt;/span> Attempt, TimeSpan Delay),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Failed(&lt;span style="color:#ff7b72">string&lt;/span> Host, Exception Error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ConnectionState HandleEvent(ConnectionState state, ConnectionEvent evt) =&amp;gt; (state, evt) &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Disconnected, Connect(&lt;span style="color:#ff7b72">var&lt;/span> host, &lt;span style="color:#ff7b72">var&lt;/span> port)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connecting(host, port, DateTime.UtcNow),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, _), ConnectionSucceeded(&lt;span style="color:#ff7b72">var&lt;/span> client)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connected(h, p, client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, _), ConnectionFailed(&lt;span style="color:#ff7b72">var&lt;/span> ex)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Reconnecting(h, p, Attempt: &lt;span style="color:#a5d6ff">1&lt;/span>, Delay: TimeSpan.FromSeconds(&lt;span style="color:#a5d6ff">1&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionSucceeded(&lt;span style="color:#ff7b72">var&lt;/span> client)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connected(h, p, client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionFailed(&lt;span style="color:#ff7b72">var&lt;/span> ex)) when attempt &amp;gt;= &lt;span style="color:#a5d6ff">5&lt;/span> =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Failed(h, ex),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionFailed(_)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Reconnecting(h, p, attempt + &lt;span style="color:#a5d6ff">1&lt;/span>, TimeSpan.FromSeconds(Math.Pow(&lt;span style="color:#a5d6ff">2&lt;/span>, attempt))),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connected(_, _, &lt;span style="color:#ff7b72">var&lt;/span> client), Disconnect) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Disconnected,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; state &lt;span style="color:#8b949e;font-style:italic">// Ignore events that don&amp;#39;t apply to current state&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>각 상태는 해당 상태와 관련된 데이터를 정확하게 전달합니다. 해당 변형에 존재하지 않기 때문에 &lt;code>Connecting&lt;/code> 상태에 있을 때 실수로 &lt;code>TcpClient&lt;/code>에 액세스할 수 없습니다. 유형 시스템은 상태 시스템의 불변성을 적용합니다.&lt;/p>
&lt;h2 id="직렬화-및-상호-운용성-고려-사항공용체-유형과-관련하여-즉시-떠오르는-실용적인-질문-중-하나는-다음과-같습니다-이를-어떻게-직렬화합니까-api를-구축하거나-데이터를-저장하는-경우-올바르게-작동하려면-json-직렬화가-필요합니다">직렬화 및 상호 운용성 고려 사항공용체 유형과 관련하여 즉시 떠오르는 실용적인 질문 중 하나는 다음과 같습니다. 이를 어떻게 직렬화합니까? API를 구축하거나 데이터를 저장하는 경우 올바르게 작동하려면 JSON 직렬화가 필요합니다.&lt;/h2>
&lt;p>디자인 팀은 공용체 유형에 대한 기본 제공 &lt;code>System.Text.Json&lt;/code> 지원을 논의해 왔습니다. 예상되는 접근 방식에는 판별자 속성을 사용한 직렬화가 포함됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;$type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;radius&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">5.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;$type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Rectangle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;width&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">10.0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;height&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">20.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 &lt;code>JsonDerivedType&lt;/code> 특성을 사용하여 .NET 7에 도입된 기존 다형성 직렬화 지원과 일치합니다. 기본적으로 변형 이름을 유형 판별자로 사용하여 공용체는 기본적으로 &lt;code>System.Text.Json&lt;/code>와 함께 작동할 것으로 예상됩니다.&lt;/p>
&lt;p>Entity Framework Core의 경우 TPH(계층별 테이블) 상속 매핑이 이미 작동하는 방식과 유사하게 판별자 열을 사용하여 공용체 값을 저장하는 접근 방식이 있을 가능성이 높습니다. 정확한 EF Core 통합은 아직 설계 중이지만 폐쇄형 계층 구조를 처리하기 위한 인프라는 이미 존재합니다.&lt;/p>
&lt;p>공용체가 내부적으로 표준 IL 클래스 계층 구조로 컴파일되기 때문에 다른 .NET 언어와의 상호 운용이 원활해야 한다는 점은 주목할 가치가 있습니다. C# 공용체를 사용하는 F# 코드는 이를 표준 형식 계층 구조로 간주하고 그 반대의 경우도 마찬가지입니다.&lt;/p>
&lt;h2 id="사용-방법">사용 방법&lt;/h2>
&lt;p>작성 시점을 기준으로 공용체 유형은 최신 .NET SDK 미리 보기에서 미리 보기 기능으로 사용할 수 있습니다. 제안된 구문을 실험하려면 다음을 수행해야 합니다.&lt;/p>
&lt;ol>
&lt;li>최신 .NET 미리보기 SDK 설치&lt;/li>
&lt;li>프로젝트 파일에서 미리보기 언어 버전을 활성화합니다.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;Project&lt;/span> Sdk=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.NET.Sdk&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;TargetFramework&amp;gt;&lt;/span>net10.0&lt;span style="color:#7ee787">&amp;lt;/TargetFramework&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;LangVersion&amp;gt;&lt;/span>preview&lt;span style="color:#7ee787">&amp;lt;/LangVersion&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/Project&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>미리보기 기능은 변경될 수 있다는 점에 유의하세요. 구문, 동작 및 컴파일러 진단은 최종 릴리스 이전에 크게 발전할 수 있습니다. 미리 보기 언어 기능에 의존하는 프로덕션 코드를 출시하지 마세요. 하지만 반드시 이를 실험하고 피드백을 제공하세요. C# 팀은 &lt;a href="https://github.com/dotnet/csharplang">csharplang&lt;/a> 저장소 토론을 적극적으로 모니터링합니다.&lt;/p>
&lt;p>제안의 진행 상황을 확인하고 싶다면 주요 시청 장소는 다음과 같습니다.&lt;/p>
&lt;ul>
&lt;li>언어 디자인 토론을 위한 &lt;a href="https://github.com/dotnet/csharplang">dotnet/csharplang&lt;/a> 저장소&lt;/li>
&lt;li>컴파일러 구현 진행 상황을 위한 &lt;a href="https://github.com/dotnet/roslyn">dotnet/roslyn&lt;/a> 저장소&lt;/li>
&lt;li>공식 공지를 위한 .NET 블로그&lt;/li>
&lt;/ul>
&lt;h2 id="기존-접근방식과의-비교">기존 접근방식과의 비교&lt;/h2>
&lt;p>다양한 접근 방식이 어떻게 결합되어 있는지 확인할 수 있도록 간단한 비교를 해보겠습니다.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기능&lt;/th>
&lt;th>초록기록&lt;/th>
&lt;th>OneOf&amp;lt;T1,T2&amp;gt;&lt;/th>
&lt;th>열거형 + 데이터&lt;/th>
&lt;th>노동조합 유형&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>완전성 검사&lt;/td>
&lt;td>❌ 아니요&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;td>❌ 아니요&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>패턴 매칭&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;td>❌ 한정&lt;/td>
&lt;td>❌ 매뉴얼&lt;/td>
&lt;td>✅ 네이티브&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>컴파일러 강제 종료&lt;/td>
&lt;td>❌ 아니요&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;td>❌ 아니요&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>변형별 데이터&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;td>⚠️ 깨지기 쉬움&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>가독성&lt;/td>
&lt;td>⚠️ 자세한 내용&lt;/td>
&lt;td>⚠️ 위치&lt;/td>
&lt;td>❌ 가난&lt;/td>
&lt;td>✅ 우수&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>직렬화&lt;/td>
&lt;td>✅ 매뉴얼&lt;/td>
&lt;td>⚠️ 복합&lt;/td>
&lt;td>✅ 매뉴얼&lt;/td>
&lt;td>✅ 내장&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>상용구&lt;/td>
&lt;td>⚠️ 보통&lt;/td>
&lt;td>✅ 낮음&lt;/td>
&lt;td>⚠️ 높음&lt;/td>
&lt;td>✅ 최소&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>외부 종속성 없음&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;td>❌ 누겟&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;td>✅ 예&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="이것이-net-생태계에-미치는-영향">이것이 .NET 생태계에 미치는 영향&lt;/h2>
&lt;p>차별적인 공용체의 도입은 전체 .NET 생태계에 파급력을 미칠 것입니다. 내가 기대하는 내용은 다음과 같습니다.&lt;/p>
&lt;p>&lt;strong>라이브러리 디자인은 개선될 것입니다.&lt;/strong> 현재 &amp;ldquo;찾을 수 없음&amp;quot;을 나타내거나 유효성 검사 실패에 대한 예외를 발생시키기 위해 &lt;code>null&lt;/code>를 반환하는 API는 대신 &lt;code>Result&amp;lt;T, E&amp;gt;&lt;/code> 유형을 반환할 수 있습니다. 이는 유형 서명에서 오류 모드를 명시적으로 만듭니다. 문서나 소스 코드를 읽는 것이 아니라 메서드 서명을 보면 무엇이 잘못될 수 있는지 알 수 있습니다.&lt;/p>
&lt;p>&lt;strong>도메인 모델링의 표현력이 더욱 풍부해졌습니다.&lt;/strong> 문제 도메인과 코드 표현 사이의 격차가 극적으로 줄어듭니다. 도메인 전문가가 &amp;ldquo;지불은 신용 카드, 은행 송금 또는 대금 상환이 될 수 있습니다&amp;quot;라고 말하면 이를 상속 계층 구조로 변환하는 대신 통합으로 직접 모델링할 수 있습니다.&lt;/p>
&lt;p>&lt;strong>C# 개발자는 F# 아이디어에 접근할 수 있습니다.&lt;/strong> 많은 C# 개발자는 멀리서 F#의 형식 시스템을 존경했지만 조직에서 F#을 채택할 수 없었습니다. Union 형식은 F#의 가장 강력한 기능 중 하나를 C#에 가져오며 이는 전체 .NET 생태계에 도움이 됩니다.&lt;/p>
&lt;p>&lt;strong>런타임 오류가 줄어듭니다.&lt;/strong> 완전성 검사만으로도 전체 버그 범주를 방지할 수 있습니다. 새로운 변형을 공용체에 추가할 때마다 컴파일러는 업데이트가 필요한 코드베이스의 모든 위치를 안내합니다. 더 이상 잊어버린 &lt;code>switch&lt;/code> 사례가 없으며, 프로덕션에서만 나타나는 기본 분기에 더 이상 &lt;code>NotImplementedException&lt;/code>가 없습니다.&lt;/p>
&lt;h2 id="결론">결론&lt;/h2>
&lt;p>차별적인 공용체는 거의 10년 동안 C# 커뮤니티의 희망 목록 1위에 올랐으며 그럴 만한 이유가 있습니다. 이는 유형 시스템의 근본적인 격차, 즉 컴파일러 강제 안전을 통해 &amp;ldquo;여러 가지 중 하나&amp;quot;가 될 수 있는 데이터를 모델링하는 기능을 해결합니다.&lt;/p>
&lt;p>제안된 &lt;code>union&lt;/code> 키워드는 C#의 패턴 일치와 긴밀하게 통합되고, 제네릭과 함께 작동하고, 메서드 및 인터페이스를 지원하고, 런타임이 아닌 컴파일 타임에 버그를 잡는 철저한 검사를 가능하게 하는 깔끔하고 간결한 구문을 제공합니다.&lt;/p>
&lt;p>도메인 모델을 구축하든, 명시적 오류 유형으로 API를 설계하든, 상태 머신을 구현하든, 아니면 어색한 상속 계층 구조를 좀 더 자연스러운 것으로 대체하려고 하든 공용체 유형은 C# 작성 방식을 바꿀 것입니다.&lt;/p>
&lt;p>우리는 이것을 오랫동안 기다려왔습니다. 구문은 우아하고 기존 언어 기능과의 통합은 사려 깊으며 전체 .NET 생태계에 실질적인 영향을 미칠 것입니다.&lt;/p>
&lt;p>미리 보기 릴리스를 주시하고, 기능을 실험하고, 언어 디자인 팀에 피드백을 제공하세요. 이것은 일단 사용해 보면 이 기능 없이 어떻게 살았는지 궁금해하게 되는 기능 중 하나입니다.&lt;/p></content:encoded><category>.NET</category><category>C#</category></item><item><title>시맨틱 커널을 사용하여 C#으로 RAG 시스템 구축</title><link>https://emimontesdeoca.github.io/ko/posts/rag-csharp-semantic-kernel/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/rag-csharp-semantic-kernel/</guid><description>의미론적 커널, 임베딩 및 벡터 검색을 사용하여 C#에서 검색 증강 생성을 구현합니다.</description><content:encoded>&lt;h2 id="소개">소개&lt;/h2>
&lt;p>회사 문서, 제품 사양, 내부 지식 기반 등 자신의 데이터에 대한 질문에 답하기 위해 LLM을 사용해 본 적이 있다면 아마도 환각을 느끼거나 &amp;ldquo;그 내용에 대한 정보가 없습니다.&amp;ldquo;라고만 말하는 것을 눈치챘을 것입니다. 모델은 자신이 훈련받은 내용만 알고 있기 때문입니다.&lt;/p>
&lt;p>RAG(Retrieval-Augmented Generation)가 이 문제를 해결합니다. 데이터에 대한 모델을 미세 조정하는 대신 쿼리 시 문서의 관련 청크를 검색하여 LLM에 컨텍스트로 전달합니다. 그런 다음 모델은 실제 데이터를 기반으로 답변을 생성합니다.&lt;/p>
&lt;p>이 게시물에서는 Semantic Kernel을 사용하여 C#으로 완전한 RAG 파이프라인을 구축하는 과정을 안내하겠습니다.&lt;/p>
&lt;h2 id="rag-작동-방식">RAG 작동 방식&lt;/h2>
&lt;p>흐름은 간단합니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>수집&lt;/strong>: 문서를 청크로 분할하고, 각 청크에 대한 임베딩을 생성하고, 이를 벡터 데이터베이스에 저장합니다.&lt;/li>
&lt;li>&lt;strong>쿼리&lt;/strong>: 사용자가 질문을 하면 쿼리에 대한 임베딩을 생성하고 벡터 데이터베이스에서 유사한 청크를 검색합니다.&lt;/li>
&lt;li>&lt;strong>생성&lt;/strong>: 검색된 청크를 사용자의 질문과 함께 LLM에 컨텍스트로 전달합니다.&lt;/li>
&lt;/ol>
&lt;p>그게 다야. 마법은 임베딩에 있습니다. 임베딩은 텍스트의 의미론적 의미를 벡터로 캡처하므로 정확한 단어가 일치하지 않는 경우에도 관련 콘텐츠를 찾을 수 있습니다.&lt;/p>
&lt;h2 id="전제-조건">전제 조건&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Extensions.VectorData.Abstractions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.InMemory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>프로덕션의 경우 Azure AI Search, Qdrant, Pinecone 또는 기타 지원되는 벡터 데이터베이스에 대한 메모리 내 저장소를 교체합니다. 하지만 인메모리는 학습과 프로토타입 제작에 적합합니다.&lt;/p>
&lt;h2 id="커널-설정">커널 설정&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.AzureOpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.VectorData&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.InMemory&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Embeddings&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAITextEmbeddingGeneration(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;text-embedding-3-small&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>두 가지 모델이 필요합니다. 하나는 채팅 완료(질문에 답변)용이고 다른 하나는 임베딩 생성(텍스트를 벡터로 변환)용입니다.&lt;/p>
&lt;h2 id="데이터-모델-정의">데이터 모델 정의&lt;/h2>
&lt;p>벡터 저장소에서 문서 청크를 나타내려면 클래스가 필요합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.VectorData&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DocumentChunk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordKey]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = Guid.NewGuid().ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordData]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Content { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordData]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Source { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordData]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> ChunkIndex { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordVector(1536)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ReadOnlyMemory&amp;lt;&lt;span style="color:#ff7b72">float&lt;/span>&amp;gt; Embedding { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>VectorStoreRecordVector(1536)&lt;/code> 속성은 벡터 저장소에 임베딩의 차원을 알려줍니다. &lt;code>text-embedding-3-small&lt;/code> 모델은 1536차원 벡터를 생성합니다.&lt;/p>
&lt;h2 id="문서-청킹">문서 청킹&lt;/h2>
&lt;p>임베딩을 생성하기 전에 문서를 관리 가능한 덩어리로 분할해야 합니다. 다음은 간단한 텍스트 분할기입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TextChunker&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SplitText(&lt;span style="color:#ff7b72">string&lt;/span> text, &lt;span style="color:#ff7b72">int&lt;/span> maxChunkSize = &lt;span style="color:#a5d6ff">500&lt;/span>, &lt;span style="color:#ff7b72">int&lt;/span> overlap = &lt;span style="color:#a5d6ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chunks = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> paragraphs = text.Split(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>, StringSplitOptions.RemoveEmptyEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> currentChunk = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> paragraph &lt;span style="color:#ff7b72">in&lt;/span> paragraphs)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (currentChunk.Length + paragraph.Length &amp;gt; maxChunkSize &amp;amp;&amp;amp; currentChunk.Length &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chunks.Add(currentChunk.ToString().Trim());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Keep overlap from the end of the previous chunk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> overlapText = currentChunk.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (overlapText.Length &amp;gt; overlap)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(overlapText[^overlap..]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(paragraph);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (currentChunk.Length &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chunks.Add(currentChunk.ToString().Trim());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> chunks;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>겹치는 부분은 중요합니다. 이는 청크 사이의 경계에 있는 컨텍스트가 손실되지 않도록 보장합니다. 관련 문장이 두 개의 덩어리로 분할되는 경우 중복은 그 중 적어도 하나에 완전히 표시된다는 것을 의미합니다.&lt;/p>
&lt;h2 id="문서-수집-중">문서 수집 중&lt;/h2>
&lt;p>이제 벡터 저장소에 문서를 수집하기 위해 모든 것을 하나로 묶어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> vectorStore = &lt;span style="color:#ff7b72">new&lt;/span> InMemoryVectorStore();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> collection = vectorStore.GetCollection&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, DocumentChunk&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;documents&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> collection.CreateCollectionIfNotExistsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddingService = kernel.GetRequiredService&amp;lt;ITextEmbeddingGenerationService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task IngestDocument(&lt;span style="color:#ff7b72">string&lt;/span> content, &lt;span style="color:#ff7b72">string&lt;/span> source)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chunks = TextChunker.SplitText(content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> (&lt;span style="color:#ff7b72">int&lt;/span> i = &lt;span style="color:#a5d6ff">0&lt;/span>; i &amp;lt; chunks.Count; i++)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> embedding = &lt;span style="color:#ff7b72">await&lt;/span> embeddingService.GenerateEmbeddingAsync(chunks[i]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chunk = &lt;span style="color:#ff7b72">new&lt;/span> DocumentChunk
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Content = chunks[i],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Source = source,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChunkIndex = i,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Embedding = embedding
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> collection.UpsertAsync(chunk);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ Ingested {chunks.Count} chunks from {source}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Ingest some documents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doc1 = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(&lt;span style="color:#a5d6ff">&amp;#34;docs/product-guide.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doc2 = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(&lt;span style="color:#a5d6ff">&amp;#34;docs/faq.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doc3 = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(&lt;span style="color:#a5d6ff">&amp;#34;docs/troubleshooting.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> IngestDocument(doc1, &lt;span style="color:#a5d6ff">&amp;#34;product-guide.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> IngestDocument(doc2, &lt;span style="color:#a5d6ff">&amp;#34;faq.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> IngestDocument(doc3, &lt;span style="color:#a5d6ff">&amp;#34;troubleshooting.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="관련-청크-검색">관련 청크 검색&lt;/h2>
&lt;p>사용자가 질문을 하면 해당 쿼리에 대한 임베딩을 생성하고 유사한 청크를 검색합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;List&amp;lt;DocumentChunk&amp;gt;&amp;gt; SearchAsync(&lt;span style="color:#ff7b72">string&lt;/span> query, &lt;span style="color:#ff7b72">int&lt;/span> topK = &lt;span style="color:#a5d6ff">3&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> queryEmbedding = &lt;span style="color:#ff7b72">await&lt;/span> embeddingService.GenerateEmbeddingAsync(query);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> searchResults = &lt;span style="color:#ff7b72">await&lt;/span> collection.VectorizedSearchAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queryEmbedding,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> VectorSearchOptions { Top = topK });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;DocumentChunk&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72">in&lt;/span> searchResults.Results)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> results.Add(result.Record);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> results;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="상황에-맞는-답변-생성">상황에 맞는 답변 생성&lt;/h2>
&lt;p>이제 RAG 부분입니다. 검색된 청크를 가져와서 프롬프트에 컨텍스트로 포함시킵니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; AskAsync(&lt;span style="color:#ff7b72">string&lt;/span> question)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 1: Retrieve relevant chunks&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> relevantChunks = &lt;span style="color:#ff7b72">await&lt;/span> SearchAsync(question);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 2: Build context from chunks&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> context = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n\n---\n\n&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> relevantChunks.Select(c =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;[Source: {c.Source}]\n{c.Content}&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 3: Generate answer with context&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddSystemMessage(&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> You are a helpful assistant that answers questions based on the provided context.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Use ONLY the information from the context to answer. If the context doesn&amp;#39;t contain
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> enough information to answer the question, say &amp;#34;&lt;/span>I don&lt;span style="color:#f85149">&amp;#39;&lt;/span>t have enough information
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> to answer that question.&lt;span style="color:#a5d6ff">&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Do not make up information. Always cite the source document when possible.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Context:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{context}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(question);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;No response generated.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="사용하기">사용하기&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Ask questions about your documents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> answer1 = &lt;span style="color:#ff7b72">await&lt;/span> AskAsync(&lt;span style="color:#a5d6ff">&amp;#34;How do I reset my password?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Q: How do I reset my password?\nA: {answer1}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> answer2 = &lt;span style="color:#ff7b72">await&lt;/span> AskAsync(&lt;span style="color:#a5d6ff">&amp;#34;What are the system requirements?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Q: What are the system requirements?\nA: {answer2}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> answer3 = &lt;span style="color:#ff7b72">await&lt;/span> AskAsync(&lt;span style="color:#a5d6ff">&amp;#34;What&amp;#39;s the capital of France?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Q: What&amp;#39;s the capital of France?\nA: {answer3}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Should respond with &amp;#34;I don&amp;#39;t have enough information&amp;#34; since it&amp;#39;s not in the docs&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="프로덕션으로-이동">프로덕션으로 이동&lt;/h2>
&lt;p>인메모리 벡터 저장소는 프로토타입 제작에 적합하지만 프로덕션에는 영구 벡터 데이터베이스가 필요합니다. Semantic Kernel에는 여러 옵션에 대한 커넥터가 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic"># Azure AI Search&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.AzureAISearch
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic"># Qdrant&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.Qdrant
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic"># Redis&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.Redis
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>모두 동일한 &lt;code>IVectorStore&lt;/code> 인터페이스를 구현하므로 스와핑은 간단합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Instead of InMemoryVectorStore, use:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.AzureAISearch&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> vectorStore = &lt;span style="color:#ff7b72">new&lt;/span> AzureAISearchVectorStore(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Azure.Search.Documents.Indexes.SearchIndexClient(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Uri(config[&lt;span style="color:#a5d6ff">&amp;#34;AzureAISearch:Endpoint&amp;#34;&lt;/span>]),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> AzureKeyCredential(config[&lt;span style="color:#a5d6ff">&amp;#34;AzureAISearch:ApiKey&amp;#34;&lt;/span>])));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```다른&lt;/span> &lt;span style="color:#f85149">모든&lt;/span> &lt;span style="color:#f85149">것은&lt;/span> &lt;span style="color:#f85149">동일하게&lt;/span> &lt;span style="color:#f85149">유지됩니다&lt;/span>. &lt;span style="color:#f85149">그것이&lt;/span> &lt;span style="color:#f85149">바로&lt;/span> &lt;span style="color:#f85149">추상화의&lt;/span> &lt;span style="color:#f85149">아름다움이다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> RAG &lt;span style="color:#f85149">시스템&lt;/span> &lt;span style="color:#f85149">구축&lt;/span> &lt;span style="color:#f85149">팁&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">제가&lt;/span> &lt;span style="color:#f85149">힘들게&lt;/span> &lt;span style="color:#f85149">배운&lt;/span> &lt;span style="color:#f85149">몇&lt;/span> &lt;span style="color:#f85149">가지&lt;/span> &lt;span style="color:#f85149">사항은&lt;/span> &lt;span style="color:#f85149">다음과&lt;/span> &lt;span style="color:#f85149">같습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">청크&lt;/span> &lt;span style="color:#f85149">크기가&lt;/span> &lt;span style="color:#f85149">매우&lt;/span> &lt;span style="color:#f85149">중요합니다&lt;/span>.** &lt;span style="color:#f85149">너무&lt;/span> &lt;span style="color:#f85149">작으면&lt;/span> &lt;span style="color:#f85149">맥락을&lt;/span> &lt;span style="color:#f85149">잃게&lt;/span> &lt;span style="color:#f85149">됩니다&lt;/span>. &lt;span style="color:#f85149">너무&lt;/span> &lt;span style="color:#f85149">크면&lt;/span> &lt;span style="color:#f85149">관련&lt;/span> &lt;span style="color:#f85149">없는&lt;/span> &lt;span style="color:#f85149">콘텐츠에&lt;/span> &lt;span style="color:#f85149">토큰을&lt;/span> &lt;span style="color:#f85149">낭비하게&lt;/span> &lt;span style="color:#f85149">됩니다&lt;/span>. &lt;span style="color:#a5d6ff">500&lt;/span>-&lt;span style="color:#a5d6ff">800&lt;/span>&lt;span style="color:#f85149">개의&lt;/span> &lt;span style="color:#f85149">토큰으로&lt;/span> &lt;span style="color:#f85149">시작하고&lt;/span> &lt;span style="color:#f85149">데이터에&lt;/span> &lt;span style="color:#f85149">따라&lt;/span> &lt;span style="color:#f85149">조정하세요&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">겹침은&lt;/span> &lt;span style="color:#f85149">경계&lt;/span> &lt;span style="color:#f85149">문제를&lt;/span> &lt;span style="color:#f85149">방지합니다&lt;/span>.** &lt;span style="color:#f85149">일반적으로&lt;/span> &lt;span style="color:#f85149">청크&lt;/span> &lt;span style="color:#f85149">사이에&lt;/span> &lt;span style="color:#a5d6ff">50&lt;/span>-&lt;span style="color:#a5d6ff">100&lt;/span>&lt;span style="color:#f85149">개의&lt;/span> &lt;span style="color:#f85149">토큰이&lt;/span> &lt;span style="color:#f85149">겹치는&lt;/span> &lt;span style="color:#f85149">것으로&lt;/span> &lt;span style="color:#f85149">충분합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">생각보다&lt;/span> &lt;span style="color:#f85149">더&lt;/span> &lt;span style="color:#f85149">많은&lt;/span> &lt;span style="color:#f85149">것을&lt;/span> &lt;span style="color:#f85149">검색합니다&lt;/span>.** &lt;span style="color:#f85149">`&lt;/span>topK = &lt;span style="color:#a5d6ff">5&lt;/span>&lt;span style="color:#f85149">`로&lt;/span> &lt;span style="color:#f85149">시작하고&lt;/span> &lt;span style="color:#f85149">노이즈가&lt;/span> &lt;span style="color:#f85149">너무&lt;/span> &lt;span style="color:#f85149">많으면&lt;/span> &lt;span style="color:#f85149">줄이세요&lt;/span>. &lt;span style="color:#f85149">관련&lt;/span> &lt;span style="color:#f85149">부분을&lt;/span> &lt;span style="color:#f85149">놓치는&lt;/span> &lt;span style="color:#f85149">것보다&lt;/span> &lt;span style="color:#f85149">추가적인&lt;/span> &lt;span style="color:#f85149">맥락을&lt;/span> &lt;span style="color:#f85149">갖는&lt;/span> &lt;span style="color:#f85149">것이&lt;/span> &lt;span style="color:#f85149">더&lt;/span> &lt;span style="color:#f85149">낫습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">시스템&lt;/span> &lt;span style="color:#f85149">프롬프트가&lt;/span> &lt;span style="color:#f85149">중요합니다&lt;/span>.** &lt;span style="color:#f85149">제공된&lt;/span> &lt;span style="color:#f85149">컨텍스트만&lt;/span> &lt;span style="color:#f85149">사용하는&lt;/span> &lt;span style="color:#f85149">것에&lt;/span> &lt;span style="color:#f85149">대해&lt;/span> &lt;span style="color:#f85149">매우&lt;/span> &lt;span style="color:#f85149">명시적으로&lt;/span> &lt;span style="color:#f85149">설명하세요&lt;/span>. &lt;span style="color:#f85149">해당&lt;/span> &lt;span style="color:#f85149">지침이&lt;/span> &lt;span style="color:#f85149">없으면&lt;/span> &lt;span style="color:#f85149">모델은&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;훈련 데이터를 기반으로&amp;#34;&lt;/span> &lt;span style="color:#f85149">즐거운&lt;/span> &lt;span style="color:#f85149">환각을&lt;/span> &lt;span style="color:#f85149">느낄&lt;/span> &lt;span style="color:#f85149">것입니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">소스를&lt;/span> &lt;span style="color:#f85149">추적하세요&lt;/span>.** &lt;span style="color:#f85149">답변의&lt;/span> &lt;span style="color:#f85149">출처를&lt;/span> &lt;span style="color:#f85149">인용할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있도록&lt;/span> &lt;span style="color:#f85149">항상&lt;/span> &lt;span style="color:#f85149">청크와&lt;/span> &lt;span style="color:#f85149">함께&lt;/span> &lt;span style="color:#f85149">메타데이터를&lt;/span> &lt;span style="color:#f85149">저장하세요&lt;/span>. &lt;span style="color:#f85149">사용자는&lt;/span> &lt;span style="color:#f85149">출처를&lt;/span> &lt;span style="color:#f85149">확인할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있을&lt;/span> &lt;span style="color:#f85149">때&lt;/span> &lt;span style="color:#f85149">답변을&lt;/span> &lt;span style="color:#f85149">더&lt;/span> &lt;span style="color:#f85149">신뢰합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">필요한&lt;/span> &lt;span style="color:#f85149">경우&lt;/span> &lt;span style="color:#f85149">순위를&lt;/span> &lt;span style="color:#f85149">다시&lt;/span> &lt;span style="color:#f85149">지정하세요&lt;/span>.** &lt;span style="color:#f85149">벡터&lt;/span> &lt;span style="color:#f85149">유사성이&lt;/span> &lt;span style="color:#f85149">완벽하지&lt;/span> &lt;span style="color:#f85149">않습니다&lt;/span>. &lt;span style="color:#f85149">중요한&lt;/span> &lt;span style="color:#f85149">애플리케이션의&lt;/span> &lt;span style="color:#f85149">경우&lt;/span> &lt;span style="color:#f85149">크로스&lt;/span> &lt;span style="color:#f85149">인코더&lt;/span> &lt;span style="color:#f85149">모델을&lt;/span> &lt;span style="color:#f85149">사용하여&lt;/span> &lt;span style="color:#f85149">순위&lt;/span> &lt;span style="color:#f85149">재지정&lt;/span> &lt;span style="color:#f85149">단계를&lt;/span> &lt;span style="color:#f85149">추가하면&lt;/span> &lt;span style="color:#f85149">정밀도가&lt;/span> &lt;span style="color:#f85149">향상됩니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">결론&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RAG는 &lt;span style="color:#f85149">현재&lt;/span> AI에서 &lt;span style="color:#f85149">가장&lt;/span> &lt;span style="color:#f85149">실용적인&lt;/span> &lt;span style="color:#f85149">패턴&lt;/span> &lt;span style="color:#f85149">중&lt;/span> &lt;span style="color:#f85149">하나입니다&lt;/span>. &lt;span style="color:#f85149">미세&lt;/span> &lt;span style="color:#f85149">조정&lt;/span> &lt;span style="color:#f85149">없이&lt;/span> &lt;span style="color:#f85149">자체&lt;/span> &lt;span style="color:#f85149">데이터에&lt;/span> &lt;span style="color:#f85149">대해&lt;/span> AI &lt;span style="color:#f85149">기반&lt;/span> Q&amp;amp;A &lt;span style="color:#f85149">시스템을&lt;/span> &lt;span style="color:#f85149">구축할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있으며&lt;/span>, Semantic Kernel을 &lt;span style="color:#f85149">사용하면&lt;/span> C&lt;span style="color:#f85149">#에서&lt;/span> &lt;span style="color:#f85149">놀라울&lt;/span> &lt;span style="color:#f85149">정도로&lt;/span> &lt;span style="color:#f85149">깔끔하게&lt;/span> &lt;span style="color:#f85149">정리됩니다&lt;/span>. &lt;span style="color:#f85149">인메모리&lt;/span> &lt;span style="color:#f85149">저장소로&lt;/span> &lt;span style="color:#f85149">시작하여&lt;/span> &lt;span style="color:#f85149">청크와&lt;/span> &lt;span style="color:#f85149">프롬프트를&lt;/span> &lt;span style="color:#f85149">올바르게&lt;/span> &lt;span style="color:#f85149">얻은&lt;/span> &lt;span style="color:#f85149">다음&lt;/span>, &lt;span style="color:#f85149">제작&lt;/span> &lt;span style="color:#f85149">준비가&lt;/span> &lt;span style="color:#f85149">되면&lt;/span> &lt;span style="color:#f85149">실제&lt;/span> &lt;span style="color:#f85149">벡터&lt;/span> &lt;span style="color:#f85149">데이터베이스로&lt;/span> &lt;span style="color:#f85149">교체하세요&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">즐거운&lt;/span> &lt;span style="color:#f85149">코딩하세요&lt;/span>!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">리소스&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- [&lt;span style="color:#f85149">의미론적&lt;/span> &lt;span style="color:#f85149">커널&lt;/span> &lt;span style="color:#f85149">벡터&lt;/span> &lt;span style="color:#f85149">저장소&lt;/span> &lt;span style="color:#f85149">문서&lt;/span>](https:&lt;span style="color:#8b949e;font-style:italic">//learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- [Azure AI Search를 &lt;span style="color:#f85149">사용한&lt;/span> RAG &lt;span style="color:#f85149">패턴&lt;/span>](https:&lt;span style="color:#8b949e;font-style:italic">//learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- [&lt;span style="color:#f85149">텍스트&lt;/span> &lt;span style="color:#f85149">임베딩&lt;/span> &lt;span style="color:#f85149">모델&lt;/span>](https:&lt;span style="color:#8b949e;font-style:italic">//learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#embeddings)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded><category>.NET</category><category>AI</category><category>Semantic Kernel</category><category>C#</category><category>Azure</category></item><item><title>Microsoft Agent Framework를 사용하여 에이전트 워크플로 구축</title><link>https://emimontesdeoca.github.io/ko/posts/agent-framework-workflows/</link><pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/agent-framework-workflows/</guid><description>.NET의 Microsoft Agent Framework를 사용하여 순차 및 병렬 에이전트 워크플로를 설계하고 조정합니다.</description><content:encoded>&lt;h2 id="소개">소개&lt;/h2>
&lt;p>Microsoft의 에이전트 프레임워크에 대한 이전 게시물을 읽어보셨다면 개별 에이전트는 물론 다중 에이전트 그룹 채팅을 만드는 방법도 아실 것입니다. 그러나 실제 시나리오에서는 에이전트가 특정 순서로 실행하고, 결과를 서로 전달하고, 결과에 따라 분기 논리를 처리하는 워크플로와 같이 좀 더 구조화된 작업이 필요한 경우가 많습니다.&lt;/p>
&lt;p>이것이 바로 Agent Framework의 워크플로 기능이 제공하는 것입니다. 에이전트를 그룹 채팅에 투입하고 에이전트가 해결하기를 바라는 대신 에이전트 간의 명시적인 단계, 종속성 및 데이터 흐름을 정의합니다.&lt;/p>
&lt;h2 id="상담사-워크플로란-무엇인가요">상담사 워크플로란 무엇인가요?&lt;/h2>
&lt;p>에이전트 워크플로를 파이프라인처럼 생각하세요. 각 단계는 전문 에이전트에 의해 처리되며 한 단계의 출력이 다음 단계로 전달됩니다. 단계를 순차적으로, 병렬로 실행하거나 결과에 따라 조건부로 실행할 수 있습니다.&lt;/p>
&lt;p>몇 가지 예:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>콘텐츠 파이프라인&lt;/strong>: 연구 → 초안 → 검토 → 게시&lt;/li>
&lt;li>&lt;strong>데이터 처리&lt;/strong>: 추출 → 변환 → 검증 → 로드&lt;/li>
&lt;li>&lt;strong>고객지원&lt;/strong> : 분류 → 전달 → 대응 → 후속 조치&lt;/li>
&lt;/ul>
&lt;h2 id="전제-조건">전제 조건&lt;/h2>
&lt;p>최신 Agent Framework 패키지가 있는지 확인하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Agents.Core
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 Azure OpenAI 또는 OpenAI 엔드포인트가 구성되었습니다.&lt;/p>
&lt;h2 id="순차-워크플로-구축">순차 워크플로 구축&lt;/h2>
&lt;p>연구원, 작가, 편집자라는 세 명의 에이전트로 콘텐츠 제작 워크플로를 구축해 보겠습니다.&lt;/p>
&lt;h3 id="에이전트-정의">에이전트 정의&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = Kernel.CreateBuilder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> researcher = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a research specialist. Given a topic, find key facts, statistics,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and talking points. Return a structured research brief with bullet points.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Be thorough but concise.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> writer = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical blog writer. Given a research brief, write a clear
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and engaging blog post. Use a conversational tone, include code examples
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">where&lt;/span> relevant, and structure the post with clear headings.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> editor = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Editor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior editor. Review the blog post &lt;span style="color:#ff7b72">for&lt;/span> clarity, accuracy,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> grammar, and flow. Return the corrected version with a summary of
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> changes made at the end.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="순차적으로-실행-중">순차적으로 실행 중&lt;/h3>
&lt;p>가장 간단한 워크플로는 순차적입니다. 각 에이전트는 이전 에이전트의 출력을 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents.Chat&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; RunSequentialWorkflow(&lt;span style="color:#ff7b72">string&lt;/span> topic)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> currentInput = &lt;span style="color:#a5d6ff">$&amp;#34;Research the following topic: {topic}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agents = &lt;span style="color:#ff7b72">new&lt;/span>[] { researcher, writer, editor };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> agent &lt;span style="color:#ff7b72">in&lt;/span> agents)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(currentInput);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.Append(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentInput = response.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ {agent.Name} completed&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentInput;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> RunSequentialWorkflow(&lt;span style="color:#a5d6ff">&amp;#34;Blazor render modes in .NET 9&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>각 에이전트는 축적된 컨텍스트를 가져와 처리하고 그 결과는 다음 단계로 이동합니다.&lt;/p>
&lt;h2 id="병렬-실행">병렬 실행&lt;/h2>
&lt;p>때로는 상담원이 독립적으로 작업할 수도 있습니다. 예를 들어, 여러 하위 주제를 동시에 조사하고 싶을 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;&amp;gt; RunParallelResearch(&lt;span style="color:#ff7b72">string&lt;/span>[] topics)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> tasks = topics.Select(&lt;span style="color:#ff7b72">async&lt;/span> topic =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Research: {topic}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> researcher.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.Append(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ Research completed: {topic}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> Task.WhenAll(tasks);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> results.ToList();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> topics = &lt;span style="color:#ff7b72">new&lt;/span>[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Blazor SSR streaming&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Enhanced navigation in .NET 9&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Render mode boundaries&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> briefs = &lt;span style="color:#ff7b72">await&lt;/span> RunParallelResearch(topics);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 모든 연구 개요를 단일 작가 에이전트에 공급하여 하나의 응집력 있는 게시물을 생성할 수 있습니다.&lt;/p>
&lt;h2 id="조건부-분기">조건부 분기&lt;/h2>
&lt;p>실제 워크플로에는 결정이 필요합니다. 게시물이 충분하지 않은 경우 작성자에게 다시 전달하는 품질 확인 단계를 원할 수도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> qualityChecker = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;QualityChecker&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a quality assurance reviewer. Evaluate the blog post &lt;span style="color:#ff7b72">on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">1.&lt;/span> Technical accuracy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2.&lt;/span> Clarity and readability
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3.&lt;/span> Completeness
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Respond with either:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#a5d6ff">&amp;#34;APPROVED&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> the post meets all criteria
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#a5d6ff">&amp;#34;REVISION NEEDED: [specific feedback]&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> changes are required
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Be strict. Only approve posts that are truly ready to publish.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; RunWithQualityLoop(&lt;span style="color:#ff7b72">string&lt;/span> topic, &lt;span style="color:#ff7b72">int&lt;/span> maxRevisions = &lt;span style="color:#a5d6ff">3&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 1: Research&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Research: {topic}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> research = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(researcher, history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 2: Write&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(research);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> draft = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(writer, history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 3: Quality loop&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> (&lt;span style="color:#ff7b72">int&lt;/span> i = &lt;span style="color:#a5d6ff">0&lt;/span>; i &amp;lt; maxRevisions; i++)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> qaHistory = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> qaHistory.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Review this post:\n\n{draft}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> qaResult = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(qualityChecker, qaHistory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (qaResult.Contains(&lt;span style="color:#a5d6ff">&amp;#34;APPROVED&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ Approved after {i + 1} review(s)&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> draft;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;🔄 Revision {i + 1}: {qaResult[..Math.Min(100, qaResult.Length)]}...&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Send back to writer with feedback&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Please revise based on this feedback:\n{qaResult}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> draft = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(writer, history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;⚠️ Max revisions reached, returning latest draft&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> draft;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; InvokeAgent(ChatCompletionAgent agent, ChatHistory history)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.Append(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 패턴은 매우 유용합니다. 품질 검사기는 게이트 역할을 하며, 출력이 충분하거나 최대 개정 제한에 도달할 때까지 작업 흐름이 반복됩니다.&lt;/p>
&lt;h2 id="워크플로-에이전트에-플러그인-추가">워크플로 에이전트에 플러그인 추가&lt;/h2>
&lt;p>워크플로의 에이전트는 독립형 에이전트처럼 플러그인을 사용할 수 있습니다. 에이전트가 워크플로 단계의 일부로 API를 호출하거나, 데이터베이스를 쿼리하거나, 파일 작업을 수행할 수 있는 것은 매우 강력합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> researcherWithTools = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;Research the topic using available tools. Summarize findings.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add a web search plugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromType&amp;lt;WebSearchPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add a database plugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromObject(&lt;span style="color:#ff7b72">new&lt;/span> DatabasePlugin(connectionString), &lt;span style="color:#a5d6ff">&amp;#34;Database&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WebSearchPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction, Description(&amp;#34;Search the web for information&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SearchAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The search query&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> query)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Your search implementation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetStringAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;https://api.search.example.com?q={Uri.EscapeDataString(query)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="워크플로의-오류-처리">워크플로의 오류 처리&lt;/h2>
&lt;p>여러 에이전트를 연결하는 경우 오류 처리가 중요해집니다. 하나의 실패한 단계로 인해 전체 워크플로가 자동으로 중단되는 것을 원하지 않습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;WorkflowResult&amp;gt; RunResilientWorkflow(&lt;span style="color:#ff7b72">string&lt;/span> input)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">new&lt;/span> WorkflowResult();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Research = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(researcher, &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory(input));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Errors.Add(&lt;span style="color:#a5d6ff">$&amp;#34;Research failed: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> result;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> writeHistory = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory(result.Research);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Draft = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(writer, writeHistory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Errors.Add(&lt;span style="color:#a5d6ff">$&amp;#34;Writing failed: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Draft = result.Research; &lt;span style="color:#8b949e;font-style:italic">// Fallback to research output&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Success = result.Errors.Count == &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> result;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WorkflowResult&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Research { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Draft { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Errors { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> Success { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="모범-사례">모범 사례&lt;/h2>
&lt;p>여러 에이전트 워크플로를 구축한 후 배운 내용은 다음과 같습니다.- &lt;strong>상담원 지시에 집중하세요&lt;/strong> — 각 상담원은 한 가지 일을 잘 수행해야 합니다. 스위스 군용 칼 요원을 만들려고 하지 마세요.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>컨텍스트 제한&lt;/strong> - 전체 대화 기록을 모든 상담원에게 전달하지 마세요. 각 단계에 필요한 것만 제공하십시오.&lt;/li>
&lt;li>&lt;strong>수정 제한 설정&lt;/strong> — 품질 루프는 훌륭하지만 주의하지 않으면 영원히 실행될 수 있습니다. 항상 최대 반복 횟수 제한을 유지하세요.&lt;/li>
&lt;li>&lt;strong>모든 것을 기록&lt;/strong> — 에이전트 출력은 예측할 수 없습니다. 디버깅을 위해 모든 단계의 입력 및 출력을 기록합니다.&lt;/li>
&lt;li>&lt;strong>병렬 실행을 현명하게 사용하세요&lt;/strong> — 속도는 빨라지지만 API 속도 제한과 토큰 비용을 주의하세요.&lt;/li>
&lt;li>&lt;strong>더 작은 모델로 먼저 테스트&lt;/strong> — 프로덕션을 위해 GPT-4o로 전환하기 전에 GPT-3.5로 워크플로 논리를 개발하고 테스트하세요.&lt;/li>
&lt;/ul>
&lt;h2 id="결론">결론&lt;/h2>
&lt;p>에이전트 워크플로를 사용하면 각 에이전트가 전문가인 복잡한 다단계 AI 파이프라인을 구축할 수 있습니다. 순차 워크플로로 시작하고, 단계가 독립적인 병렬 실행을 추가하고, 품질 게이트에 조건부 분기를 사용하세요. 패턴은 구성 가능합니다. 일단 익숙해지면 매우 정교한 자동화를 구축할 수 있습니다.&lt;/p>
&lt;p>즐거운 코딩하세요!&lt;/p>
&lt;h2 id="리소스">리소스&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent">Microsoft Agent Framework 설명서&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithAgents">의미론적 커널 에이전트 샘플&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>AI</category><category>Agent Framework</category><category>Semantic Kernel</category></item><item><title>Blazor 구성 요소 수명 주기: 전체 가이드</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-component-lifecycle/</link><pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-component-lifecycle/</guid><description>초기화부터 폐기까지 모든 Blazor 구성 요소 수명 주기 방법과 각 방법을 언제 사용해야 하는지 이해하세요.</description><content:encoded>&lt;p>저는 한동안 Blazor를 사용해 왔는데 솔직히 처음에는 수명 주기 방법이 저를 혼란스럽게 했습니다. &lt;code>OnInitialized&lt;/code> 대 &lt;code>OnInitializedAsync&lt;/code>? &lt;code>OnParametersSet&lt;/code> 대 &lt;code>OnAfterRender&lt;/code>? &lt;code>StateHasChanged&lt;/code>는 실제로 언제 다시 렌더링을 실행합니까? 수많은 시행착오 끝에 마침내 나는 이 모든 것에 대한 견고한 정신 모델을 갖게 되었습니다.&lt;/p>
&lt;h1 id="라이프사이클을-한눈에">라이프사이클을 한눈에&lt;/h1>
&lt;p>Blazor 구성 요소가 렌더링되면 다음 메서드를 순서대로 진행합니다.&lt;/p>
&lt;ol>
&lt;li>&lt;code>SetParametersAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>OnInitialized&lt;/code> / &lt;code>OnInitializedAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>OnParametersSet&lt;/code> / &lt;code>OnParametersSetAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>OnAfterRender&lt;/code> / &lt;code>OnAfterRenderAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>Dispose&lt;/code> / &lt;code>DisposeAsync&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>각각을 살펴보겠습니다.&lt;/p>
&lt;h1 id="setparametersasync">SetParametersAsync&lt;/h1>
&lt;p>이것이 가장 먼저 호출되는 메소드입니다. 상위 구성 요소로부터 원시 &lt;code>ParameterView&lt;/code>를 받습니다. 대부분의 경우 이를 재정의하지 않습니다. Blazor는 매핑 매개 변수를 자동으로 처리합니다. 하지만 할당하기 전에 맞춤 매개변수 처리 또는 유효성 검사가 필요한 경우:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task SetParametersAsync(ParameterView parameters)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Custom logic before parameters are set&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (parameters.TryGetValue&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Title is being set to: {title}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">base&lt;/span>.SetParametersAsync(parameters);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>나는 이것을 실제 프로젝트에서 정확히 한 번 사용했습니다. 대부분의 경우 건너뛰게 됩니다.&lt;/p>
&lt;h1 id="oninitialized--oninitializedasync">OnInitialized / OnInitializedAsync&lt;/h1>
&lt;p>여기에서 데이터 로드, 서비스 초기화, 기본값 설정 등의 설정 작업을 수행할 수 있습니다. 구성요소가 처음 생성될 때 &lt;strong>한 번&lt;/strong> 실행됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;Product&amp;gt; products = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>나를 당황하게 만든 한 가지: Blazor Server에서는 사전 렌더링 중에 &lt;code>OnInitializedAsync&lt;/code>가 &lt;strong>두 번&lt;/strong> 호출됩니다. 서버 측 사전 렌더링 중 처음이고 SignalR 연결이 설정될 때 두 번째입니다. API 호출 비용이 많이 드는 경우 이를 처리하는 것이 좋습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> isPrerendering = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> isPrerendering = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>또는 이중 호출을 완전히 피하려면 &lt;code>PersistentComponentState&lt;/code>를 사용하는 것이 더 좋습니다.&lt;/p>
&lt;h1 id="onparametersset--onparameterssetasync">OnParametersSet / OnParametersSetAsync&lt;/h1>
&lt;p>이는 상위 구성요소가 다시 렌더링되고 새 매개변수 값을 전달할 때마다 실행됩니다. 또한 &lt;code>OnInitialized&lt;/code> 이후에도 실행됩니다. 매개변수 변경에 반응하는 적절한 위치는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> CategoryId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> previousCategoryId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;Product&amp;gt; products = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnParametersSetAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (CategoryId != previousCategoryId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> previousCategoryId = CategoryId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;api/products?category={CategoryId}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>CategoryId != previousCategoryId&lt;/code>에 대한 확인이 중요합니다. 이것이 없으면 카테고리가 변경되지 않았더라도 상위 항목이 다시 렌더링될 때마다 데이터를 다시 로드하게 됩니다.&lt;/p>
&lt;h1 id="onafterrender--onafterrenderasync">OnAfterRender / OnAfterRenderAsync&lt;/h1>
&lt;p>이는 구성 요소가 DOM에 렌더링된 후에 실행됩니다. &lt;code>firstRender&lt;/code> 매개변수는 초기 렌더링인지 여부를 알려줍니다. 이 시점에 DOM 요소가 존재하므로 JS Interop 호출을 위한 위치는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> IJSRuntime JS { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;initializeChart&amp;#34;&lt;/span>, chartElement);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>나는 다시 렌더링할 때마다 JavaScript 라이브러리를 다시 초기화하는 것을 피하기 위해 &lt;code>firstRender&lt;/code>를 많이 사용합니다. 이벤트 리스너, 차트 라이브러리 또는 DOM에 직접 닿는 모든 것을 설정하는 경우 여기로 이동합니다.&lt;/p>
&lt;p>#렌더링해야함&lt;/p>
&lt;p>이것은 덜 알려져 있지만 매우 유용합니다. &lt;code>StateHasChanged&lt;/code>가 호출될 때 구성 요소를 다시 렌더링할지 여부를 제어합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> shouldRender = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> ShouldRender() =&amp;gt; shouldRender;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> HeavyOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> shouldRender = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Do a bunch of state changes without triggering renders&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> (&lt;span style="color:#ff7b72">int&lt;/span> i = &lt;span style="color:#a5d6ff">0&lt;/span>; i &amp;lt; &lt;span style="color:#a5d6ff">1000&lt;/span>; i++)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> items[i].Process();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> shouldRender = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged(); &lt;span style="color:#8b949e;font-style:italic">// Now render once with all changes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>나는 이것을 큰 목록을 처리하는 구성 요소를 최적화하는 데 사용했습니다. 항목이 변경될 때마다 다시 렌더링하는 대신 업데이트를 일괄 처리하고 마지막에 한 번 렌더링합니다.&lt;/p>
&lt;h1 id="폐기--disposeasync">폐기 / DisposeAsync&lt;/h1>
&lt;p>UI에서 구성요소가 제거되면 모든 리소스를 정리해야 합니다. &lt;code>IDisposable&lt;/code> 또는 &lt;code>IAsyncDisposable&lt;/code> 구현:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@implements IAsyncDisposable
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> Timer? timer;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IJSObjectReference? jsModule;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> timer = &lt;span style="color:#ff7b72">new&lt;/span> Timer(OnTick, &lt;span style="color:#79c0ff">null&lt;/span>, &lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">1000&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jsModule = &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeAsync&amp;lt;IJSObjectReference&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;import&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;./timer.js&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask DisposeAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> timer?.Dispose();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (jsModule &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> jsModule.DisposeAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>일반적으로 폐기해야 할 항목: 타이머, 이벤트 구독, JS 모듈 참조, &lt;code>CancellationTokenSource&lt;/code> 및 생성한 모든 &lt;code>IDisposable&lt;/code> 서비스.&lt;/p>
&lt;h1 id="statehaschanged이는-수명주기-방법은-아니지만-밀접하게-관련되어-있습니다-blazor에게-안녕하세요-내-상태가-변경되었습니다-다시-렌더링해-주세요라고-알려줍니다-blazor는-이벤트-핸들러-후에-자동으로-호출하지만-수동으로-호출해야-하는-경우도-있습니다-일반적으로-일반-blazor-이벤트-흐름-외부에서-상태가-변경되는-경우입니다">StateHasChanged이는 수명주기 방법은 아니지만 밀접하게 관련되어 있습니다. Blazor에게 &amp;ldquo;안녕하세요, 내 상태가 변경되었습니다. 다시 렌더링해 주세요.&amp;ldquo;라고 알려줍니다. Blazor는 이벤트 핸들러 후에 자동으로 호출하지만 수동으로 호출해야 하는 경우도 있습니다. 일반적으로 일반 Blazor 이벤트 흐름 외부에서 상태가 변경되는 경우입니다.&lt;/h1>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task StartPolling()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">while&lt;/span> (!cts.Token.IsCancellationRequested)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;Data&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/data&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged(); &lt;span style="color:#8b949e;font-style:italic">// Manual call needed since this isn&amp;#39;t a Blazor event&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">5000&lt;/span>, cts.Token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>한 가지 중요한 참고 사항: Blazor Server의 백그라운드 스레드에서 업데이트하는 경우 &lt;code>InvokeAsync&lt;/code>를 사용하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> InvokeAsync(() =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data = newData;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="전체-사진">전체 사진&lt;/h1>
&lt;p>모든 일이 일어나는 순서는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>Component created
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ SetParametersAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnInitialized / OnInitializedAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnParametersSet / OnParametersSetAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Render
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnAfterRender(firstRender: true)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Parameter change from parent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ SetParametersAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnParametersSet / OnParametersSetAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ ShouldRender?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Render
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnAfterRender(firstRender: false)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Component removed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Dispose / DisposeAsync
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 흐름이 머릿속에 있으면 수명주기 문제 디버깅이 훨씬 쉬워집니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! &lt;strong>@emimontesdeoca&lt;/strong>로 소셜 미디어를 통해 언제든지 저에게 연락해주세요.&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle">ASP.NET Core Blazor 구성 요소 수명 주기&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle#component-disposal-with-idisposable-and-iasyncdisposable">IDisposable 및 IAsyncDisposable을 사용한 구성 요소 처리&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>크리스마스를 저장하는 Microsoft의 에이전트 프레임워크</title><link>https://emimontesdeoca.github.io/ko/posts/agent-framework-christmas-presents/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/agent-framework-christmas-presents/</guid><description>.NET이 포함된 Microsoft의 Agent Framework를 사용하여 다중 에이전트 크리스마스 선물 쇼핑 시스템을 구축하세요.</description><content:encoded>&lt;h2 id="소개">소개&lt;/h2>
&lt;p>완벽한 크리스마스 선물을 찾는 것은 스트레스가 될 수 있습니다. 선물 아이디어를 브레인스토밍하고, 매장 간 가격을 비교하고, 모든 것이 제 시간에 도착하는지 확인하는 과정에서 연말연시 쇼핑은 순식간에 부담스러워집니다. 함께 일하는 전문 AI 에이전트에게 이러한 작업을 위임할 수 있다면 어떨까요? 이 게시물에서는 Microsoft의 에이전트 프레임워크를 사용하여 각 에이전트가 선물 아이디어 생성부터 가격 비교까지 워크플로를 통해 조정되는 특정 작업을 전문으로 하는 다중 에이전트 시스템을 구축하는 방법을 살펴보겠습니다.&lt;/p>
&lt;h2 id="축제-기술-달력-2025">축제 기술 달력 2025&lt;/h2>
&lt;p 정렬="중앙">
&lt;img src="https://sessionize.com/image/49aa-1140o400o3-sdJUGhdR3FCmm1KuPRM3D3.png"/>
&lt;/p>
&lt;p>이 프로젝트는 연휴 기간 동안 기술을 기념하는 멋진 커뮤니티 이벤트인 &lt;strong>Festive Tech Calendar 2025&lt;/strong> 세션의 일부입니다. &lt;a href="https://sessionize.com/festive-tech-calendar-2025/">Sessionize&lt;/a>에서 이벤트에 대한 자세한 내용을 확인할 수 있습니다.&lt;/p>
&lt;h2 id="microsoft의-에이전트-프레임워크란-무엇입니까">Microsoft의 에이전트 프레임워크란 무엇입니까?&lt;/h2>
&lt;p>에이전트 프레임워크는 AI 에이전트 및 다중 에이전트 시스템을 구축, 조정 및 배포하기 위한 Microsoft의 솔루션입니다. 이는 순차적으로, 동시에 또는 핸드오프 패턴을 통해 작업할 수 있는 에이전트를 생성하기 위한 유연한 기반을 제공합니다. 이 프레임워크는 OpenAI, Azure OpenAI 및 Microsoft Foundry 모델을 지원하므로 매우 다양합니다.&lt;/p>
&lt;p>주요 기능은 다음과 같습니다:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>다중 에이전트 오케스트레이션&lt;/strong>: 그룹 채팅, 순차, 동시 및 핸드오프 패턴&lt;/li>
&lt;li>&lt;strong>플러그인 생태계&lt;/strong>: 기본 기능, OpenAPI 및 MCP(모델 컨텍스트 프로토콜)로 확장&lt;/li>
&lt;li>&lt;strong>워크플로 지원&lt;/strong>: 실행기와 에지를 사용하여 복잡한 에이전트 파이프라인 구축&lt;/li>
&lt;/ul>
&lt;h2 id="전제-조건">전제 조건&lt;/h2>
&lt;p>코드를 살펴보기 전에 다음 사항을 확인하세요.&lt;/p>
&lt;ul>
&lt;li>.NET 9&lt;/li>
&lt;li>Azure OpenAI 액세스(또는 OpenAI API 키)&lt;/li>
&lt;li>비주얼 스튜디오 또는 비주얼 스튜디오 코드&lt;/li>
&lt;/ul>
&lt;p>필수 패키지를 설치합니다(미리보기에서는 &lt;code>--prerelease&lt;/code> 플래그가 필요합니다).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Agents.AI.Workflows --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.AI.OpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.Identity
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.AI.Agents.Persistent --prerelease
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="선물-쇼핑-에이전트">선물 쇼핑 에이전트&lt;/h2>
&lt;p>우리의 크리스마스 선물 찾기는 함께 일하는 세 명의 전문 에이전트로 구성됩니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>선물 아이디어 에이전트&lt;/strong> - 받는 사람의 프로필을 기반으로 창의적인 선물 제안 생성&lt;/li>
&lt;li>&lt;strong>가격 비교 에이전트&lt;/strong> - 여러 매장에서 가장 좋은 가격을 찾아줍니다.&lt;/li>
&lt;li>&lt;strong>요약 에이전트&lt;/strong> - 최종 권장 사항을 컴파일합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="모델">모델&lt;/h2>
&lt;p>에이전트 파이프라인을 통해 흐를 데이터 모델을 정의하는 것부터 시작해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftRecipient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Age { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Interests { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = [];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Budget { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftIdea&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Description { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Category { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> EstimatedPrice { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PriceResult&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GiftName { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Store { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Url { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftRecommendation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> GiftIdea Gift { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;PriceResult&amp;gt; Prices { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = [];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> PriceResult? BestDeal { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="에이전트-구축">에이전트 구축&lt;/h2>
&lt;p>에이전트 프레임워크의 장점은 전문 에이전트를 생성하는 것이 얼마나 쉬운가입니다. 각 에이전트는 전문 지식을 정의하는 특정 시스템 프롬프트가 있는 단순한 &lt;code>ChatClientAgent&lt;/code>입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI.Workflows&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ChristmasAgentFactory&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> AIAgent CreateGiftIdeaAgent(IChatClient chatClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">@&amp;#34;You are a creative Christmas gift advisor. When given information about a person
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> (age, interests, budget), you suggest thoughtful and personalized gift ideas.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> For each suggestion, provide:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Gift name
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Brief description of why it&amp;#39;s a good fit
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Category (Electronics, Books, Fashion, Home, Experience, etc.)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Estimated price range
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Always suggest 3-5 gift options within the specified budget.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Format your response as a structured list.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> AIAgent CreatePriceComparisonAgent(IChatClient chatClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">@&amp;#34;You are a price comparison specialist. Given a list of gift ideas,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> you research and compare prices from different online retailers.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> For each gift, provide:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Store name (Amazon, Best Buy, Target, Walmart, etc.)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Current price
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Any available discounts or deals
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Always highlight the best deal for each item.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Consider shipping costs and delivery times for Christmas.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> AIAgent CreateSummaryAgent(IChatClient chatClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">@&amp;#34;You are a gift recommendation summarizer. Take the gift ideas and price
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> comparisons and create a final recommendation report.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Your summary should:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Rank gifts by value (quality vs price)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Highlight the top pick with reasoning
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Include total cost estimate
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Add any tips for holiday shopping
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Make the summary cheerful and festive!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="워크플로-만들기">워크플로 만들기&lt;/h2>
&lt;p>이제 에이전트를 순차적인 워크플로에 연결하는 재미있는 부분이 나옵니다. 에이전트 프레임워크는 에이전트를 다양한 패턴으로 구성하기 위해 &lt;code>WorkflowBuilder&lt;/code> 및 &lt;code>AgentWorkflowBuilder&lt;/code>를 제공합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ChristmasGiftWorkflow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task RunAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Set up the Azure OpenAI client&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> endpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ?? &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> InvalidOperationException(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT is not set.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> deploymentName = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT_NAME&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o-mini&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chatClient = &lt;span style="color:#ff7b72">new&lt;/span> AzureOpenAIClient(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Uri(endpoint),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetChatClient(deploymentName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsIChatClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create our specialized agents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AIAgent giftIdeaAgent = ChristmasAgentFactory.CreateGiftIdeaAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AIAgent priceAgent = ChristmasAgentFactory.CreatePriceComparisonAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AIAgent summaryAgent = ChristmasAgentFactory.CreateSummaryAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Build a sequential workflow: Ideas -&amp;gt; Prices -&amp;gt; Summary&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> workflow = AgentWorkflowBuilder.BuildSequential(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;ChristmasGiftFinder&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [giftIdeaAgent, priceAgent, summaryAgent]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Define our gift recipient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> recipient = &lt;span style="color:#ff7b72">new&lt;/span> GiftRecipient
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Mom&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Age = &lt;span style="color:#a5d6ff">55&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Interests = [&lt;span style="color:#a5d6ff">&amp;#34;gardening&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;cooking&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;reading&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;yoga&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Budget = &lt;span style="color:#a5d6ff">100&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$@&amp;#34;Find Christmas gifts for {recipient.Name},
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> age {recipient.Age}, who enjoys {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, recipient.Interests)}.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Budget: &lt;span style="color:#f85149">$&lt;/span>{recipient.Budget}&lt;span style="color:#a5d6ff">&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Execute the workflow with streaming&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">StreamingRun&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> InProcessExecution.StreamAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> workflow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessage(ChatRole.User, prompt));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Send the turn token to start processing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> run.TrySendMessageAsync(&lt;span style="color:#ff7b72">new&lt;/span> TurnToken(emitEvents: &lt;span style="color:#79c0ff">true&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Watch for workflow events&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (WorkflowEvent evt &lt;span style="color:#ff7b72">in&lt;/span> run.WatchStreamAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (evt &lt;span style="color:#ff7b72">is&lt;/span> AgentRunUpdateEvent agentUpdate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(agentUpdate.Data);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (evt &lt;span style="color:#ff7b72">is&lt;/span> WorkflowOutputEvent outputEvent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🎄 Final Recommendations:&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(outputEvent.Data);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="동시에-에이전트-실행">동시에 에이전트 실행&lt;/h2>
&lt;p>여러 사람에게 줄 선물을 동시에 검색하고 싶다면 어떻게 해야 할까요? 에이전트 프레임워크는 동시 실행을 지원하며 이는 다음 시나리오에 적합합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task FindGiftsForEveryoneAsync(IChatClient chatClient, List&amp;lt;GiftRecipient&amp;gt; recipients)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create an agent for each recipient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agents = recipients.Select(r =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$@&amp;#34;Find the perfect Christmas gift for {r.Name} (age {r.Age}),
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> who loves {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, r.Interests)}. Budget: ${r.Budget}.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Provide one well-researched recommendation with price.&lt;span style="color:#a5d6ff">&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> )).ToList();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Build a concurrent workflow - all agents run in parallel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> workflow = AgentWorkflowBuilder.BuildConcurrent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;FamilyGiftFinder&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> agents);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">StreamingRun&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> InProcessExecution.StreamAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> workflow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessage(ChatRole.User, &lt;span style="color:#a5d6ff">&amp;#34;Find gifts now!&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> run.TrySendMessageAsync(&lt;span style="color:#ff7b72">new&lt;/span> TurnToken(emitEvents: &lt;span style="color:#79c0ff">true&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (WorkflowEvent evt &lt;span style="color:#ff7b72">in&lt;/span> run.WatchStreamAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (evt &lt;span style="color:#ff7b72">is&lt;/span> WorkflowOutputEvent output)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🎁 All gift recommendations ready!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(output.Data);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="더-많은-제어를-위한-맞춤형-실행기보다-복잡한-시나리오의-경우-워크플로를-세밀하게-제어할-수-있는-사용자-지정-실행기를-만들-수-있습니다">더 많은 제어를 위한 맞춤형 실행기보다 복잡한 시나리오의 경우 워크플로를 세밀하게 제어할 수 있는 사용자 지정 실행기를 만들 수 있습니다.&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftValidatorExecutor&lt;/span> : Executor&amp;lt;List&amp;lt;ChatMessage&amp;gt;, List&amp;lt;ChatMessage&amp;gt;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> GiftValidatorExecutor() : &lt;span style="color:#ff7b72">base&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#34;GiftValidator&amp;#34;&lt;/span>) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask&amp;lt;List&amp;lt;ChatMessage&amp;gt;&amp;gt; HandleAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;ChatMessage&amp;gt; messages,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IWorkflowContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CancellationToken cancellationToken = &lt;span style="color:#ff7b72">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> lastMessage = messages.LastOrDefault()?.Text ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Validate that suggestions are within budget&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (lastMessage.Contains(&lt;span style="color:#a5d6ff">&amp;#34;over budget&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;⚠️ Some suggestions exceeded budget, filtering...&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Add validation logic here&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;✅ Gift suggestions validated!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> messages;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 이 실행자를 워크플로에 삽입할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> validator = &lt;span style="color:#ff7b72">new&lt;/span> GiftValidatorExecutor();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> workflow = &lt;span style="color:#ff7b72">new&lt;/span> WorkflowBuilder(giftIdeaAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEdge(giftIdeaAgent, validator)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEdge(validator, priceAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEdge(priceAgent, summaryAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithOutputFrom(summaryAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="bing-grounding으로-실제-웹-검색-추가">Bing Grounding으로 실제 웹 검색 추가&lt;/h2>
&lt;p>지금까지 에이전트는 AI 모델의 지식을 기반으로 응답을 생성합니다. 하지만 실제 제품 가격과 재고 여부를 웹에서 검색하고 싶다면 어떻게 해야 할까요? &lt;strong>Bing Search를 통한 접지&lt;/strong>가 필요한 곳입니다. 이는 에이전트가 응답을 생성할 때 실시간 공개 웹 데이터를 통합할 수 있도록 해주는 Microsoft Foundry(이전의 Azure AI Foundry)에서 사용할 수 있는 도구입니다.&lt;/p>
&lt;p>먼저 &lt;a href="https://portal.azure.com/#create/Microsoft.BingGroundingSearch">Azure Portal&lt;/a>에서 &lt;strong>Bing Search를 통한 접지&lt;/strong> 리소스를 만들어야 합니다. AI 프로젝트와 동일한 리소스 그룹에 만들어야 합니다.&lt;/p>
&lt;h3 id="bing-search를-사용하여-접지-설정">Bing Search를 사용하여 접지 설정&lt;/h3>
&lt;p>Bing Search를 통한 Grounding의 장점은 Azure AI 에이전트와 직접 통합된다는 것입니다. 에이전트는 사용자의 쿼리를 기반으로 검색 도구를 언제 사용할지 결정하고, 웹을 검색하고, 그 결과를 바탕으로 근거 있는 응답을 생성합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.Agents.Persistent&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">BingGroundingSetup&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;PersistentAgent&amp;gt; CreateAgentWithBingGroundingAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> projectEndpoint,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> modelDeploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> bingConnectionId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create the Persistent Agents client&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agentClient = &lt;span style="color:#ff7b72">new&lt;/span> PersistentAgentsClient(projectEndpoint, &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Configure the Bing Grounding tool&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingGroundingTool = &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingToolDefinition(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingSearchToolParameters(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [new BingGroundingSearchConfiguration(bingConnectionId)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create the agent with Bing Grounding enabled&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">await&lt;/span> agentClient.Administration.CreateAgentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model: modelDeploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: &lt;span style="color:#a5d6ff">&amp;#34;ChristmasPriceHunter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> instructions: &lt;span style="color:#a5d6ff">@&amp;#34;You are a Christmas gift price comparison specialist.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> When given gift ideas, use Bing to search for:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Current prices at major retailers (Amazon, Best Buy, Target, Walmart)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Available discounts and holiday deals
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Shipping times to ensure delivery before Christmas
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Always provide URLs to the products you find.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Highlight the best deals and recommend where to buy.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tools: [bingGroundingTool]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> agent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="실제-검색으로-가격비교-에이전트-만들기">실제 검색으로 가격비교 에이전트 만들기&lt;/h3>
&lt;p>이제 웹에서 실제 제품 정보를 검색하는 완전한 가격 비교 에이전트를 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.Agents.Persistent&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ChristmasPriceAgent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> PersistentAgentsClient _client;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _modelDeployment;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _bingConnectionId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ChristmasPriceAgent(&lt;span style="color:#ff7b72">string&lt;/span> projectEndpoint, &lt;span style="color:#ff7b72">string&lt;/span> modelDeployment, &lt;span style="color:#ff7b72">string&lt;/span> bingConnectionId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _client = &lt;span style="color:#ff7b72">new&lt;/span> PersistentAgentsClient(projectEndpoint, &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _modelDeployment = modelDeployment;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _bingConnectionId = bingConnectionId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; FindGiftPricesAsync(&lt;span style="color:#ff7b72">string&lt;/span> giftIdeas)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create agent with Bing Grounding&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingTool = &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingToolDefinition(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingSearchToolParameters(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [new BingGroundingSearchConfiguration(_bingConnectionId)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">await&lt;/span> _client.Administration.CreateAgentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model: _modelDeployment,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: &lt;span style="color:#a5d6ff">&amp;#34;PriceHunter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> instructions: &lt;span style="color:#a5d6ff">@&amp;#34;Search the web for current prices on the given gift ideas.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> For each gift, find prices from at least 2-3 different stores.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Include direct links to the products.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Note any Christmas sales or discounts available.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tools: [bingTool]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create a thread for the conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> thread = &lt;span style="color:#ff7b72">await&lt;/span> _client.Threads.CreateThreadAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Add the gift ideas as a message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Messages.CreateMessageAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> thread.Id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MessageRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;Find current prices for these gift ideas: {giftIdeas}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Run the agent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> _client.Runs.CreateRunAsync(thread.Id, agent.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Wait for completion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">500&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> run = &lt;span style="color:#ff7b72">await&lt;/span> _client.Runs.GetRunAsync(thread.Id, run.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">while&lt;/span> (run.Status == RunStatus.Queued || run.Status == RunStatus.InProgress);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get the response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> messages = _client.Messages.GetMessages(thread.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = messages
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(m =&amp;gt; m.Role == MessageRole.Agent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SelectMany(m =&amp;gt; m.ContentItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OfType&amp;lt;MessageTextContent&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Clean up&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Threads.DeleteThreadAsync(thread.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response?.Text ?? &lt;span style="color:#a5d6ff">&amp;#34;No results found.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">finally&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Clean up the agent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Administration.DeleteAgentAsync(agent.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="bing-grounding을-작업-흐름에-통합">Bing Grounding을 작업 흐름에 통합&lt;/h3>
&lt;p>전체 선물 찾기 워크플로에서 Bing 기반 가격 에이전트를 사용하는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task RunWithBingGroundingAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> projectEndpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;PROJECT_ENDPOINT&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> modelDeployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;MODEL_DEPLOYMENT_NAME&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingConnectionId = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;BING_CONNECTION_ID&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Connection ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{account}/projects/{project}/connections/{connection}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🔍 Searching the web for real prices with Bing Grounding...\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceAgent = &lt;span style="color:#ff7b72">new&lt;/span> ChristmasPriceAgent(projectEndpoint, modelDeployment, bingConnectionId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// First, generate gift ideas (could come from another agent)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> giftIdeas = &lt;span style="color:#a5d6ff">&amp;#34;1. Milwaukee cordless drill set, 2. Weber portable grill, 3. Carhartt beanie&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Search for real prices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceResults = &lt;span style="color:#ff7b72">await&lt;/span> priceAgent.FindGiftPricesAsync(giftIdeas);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;📊 Price Comparison Results:&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(priceResults);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🎁 Happy Shopping! 🎁&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="bing-결과에서-인용-처리">Bing 결과에서 인용 처리&lt;/h3>
&lt;p>Bing Search를 통한 접지의 중요한 측면 중 하나는 응답에 소스 웹 사이트에 대한 링크가 포함된 인용이 포함된다는 것입니다. 이를 추출하고 표시하는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ProcessBingGroundingResponse(IEnumerable&amp;lt;PersistentThreadMessage&amp;gt; messages)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> messages.Where(m =&amp;gt; m.Role == MessageRole.Agent))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> content &lt;span style="color:#ff7b72">in&lt;/span> message.ContentItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (content &lt;span style="color:#ff7b72">is&lt;/span> MessageTextContent textContent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = textContent.Text;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Process URL citations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (textContent.Annotations != &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> annotation &lt;span style="color:#ff7b72">in&lt;/span> textContent.Annotations)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (annotation &lt;span style="color:#ff7b72">is&lt;/span> MessageTextUriCitationAnnotation uriAnnotation)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Replace citation placeholder with markdown link&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response = response.Replace(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uriAnnotation.Text,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34; [{uriAnnotation.UriCitation.Title}]({uriAnnotation.UriCitation.Uri})&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(response);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 가격 비교 에이전트가 실행되면 &lt;strong>Bing Search를 통한 접지&lt;/strong>를 사용하여 실제 제품 목록, 현재 가격 및 라이브 웹에서 사용 가능한 거래를 찾습니다. 에이전트는 쿼리를 기반으로 언제 검색할지 자동으로 결정하고 적절한 인용과 함께 근거 있는 응답을 반환합니다.&lt;/p>
&lt;h2 id="완전한-프로그램">완전한 프로그램&lt;/h2>
&lt;p>&lt;code>Program.cs&lt;/code>에서 모든 것이 하나로 합쳐지는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI.Workflows&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🎄 Christmas Gift Finder - Powered by AI Agents 🎄\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> endpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> deployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT_NAME&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o-mini&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatClient = &lt;span style="color:#ff7b72">new&lt;/span> AzureOpenAIClient(&lt;span style="color:#ff7b72">new&lt;/span> Uri(endpoint), &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetChatClient(deployment)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsIChatClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Create the agent team&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> ideaAgent = ChristmasAgentFactory.CreateGiftIdeaAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> priceAgent = ChristmasAgentFactory.CreatePriceComparisonAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> summaryAgent = ChristmasAgentFactory.CreateSummaryAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the workflow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> workflow = AgentWorkflowBuilder.BuildSequential(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;ChristmasGiftWorkflow&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ideaAgent, priceAgent, summaryAgent]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;Who are you shopping for? &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> recipientName = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;Someone special&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;What are their interests? &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> interests = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;general&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;What&amp;#39;s your budget? $&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> budget = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;50&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$&amp;#34;Find Christmas gifts for {recipientName} who enjoys {interests}. Budget: ${budget}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🔍 Searching for the perfect gifts...\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> InProcessExecution.StreamAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> workflow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessage(ChatRole.User, prompt));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> run.TrySendMessageAsync(&lt;span style="color:#ff7b72">new&lt;/span> TurnToken(emitEvents: &lt;span style="color:#79c0ff">true&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> evt &lt;span style="color:#ff7b72">in&lt;/span> run.WatchStreamAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (evt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> AgentRunUpdateEvent update:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(update.Update.Text);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> WorkflowOutputEvent output:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n\n🎁 Happy Shopping! 🎁&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="결론">결론&lt;/h2>
&lt;p>Microsoft의 에이전트 프레임워크를 사용하면 각 에이전트가 특정 작업을 전문으로 하는 다중 에이전트 시스템을 놀라울 정도로 쉽게 구축할 수 있습니다. 워크플로를 통해 이러한 에이전트를 조정함으로써 구조적이고 효율적인 방식으로 크리스마스 쇼핑과 같은 복잡한 문제를 해결할 수 있습니다.&lt;/p>
&lt;p>이를 더욱 강력하게 만드는 것은 도구를 통해 실제 기능을 추가할 수 있는 능력입니다. Microsoft Foundry의 &lt;strong>Grounding with Bing Search&lt;/strong>를 통합함으로써 당사의 가격 비교 에이전트는 실제로 웹에서 현재 가격, 거래 및 가용성을 검색할 수 있습니다. 이를 통해 간단한 AI 챗봇을 적절한 인용을 통해 진정으로 유용한 쇼핑 도우미로 탈바꿈시킬 수 있습니다.&lt;/p>
&lt;p>순차, 동시 및 핸드오프 패턴에 대한 프레임워크의 지원은 사용자의 정확한 요구 사항에 맞는 에이전트 시스템을 설계할 수 있음을 의미합니다. 선물 찾기, 여행 계획 또는 기타 다단계 작업 등 무엇이든 에이전트 프레임워크는 이를 실현하는 데 필요한 구성 요소를 제공합니다.이번 휴가철에는 AI 에이전트에게 연구를 맡기고 선물 포장에 집중하고 가족과 함께 즐거운 시간을 보내세요!&lt;/p>
&lt;h2 id="소스-코드">소스 코드&lt;/h2>
&lt;p>이 게시물에 표시된 개념은 공식 Agent Framework 샘플을 기반으로 합니다. &lt;a href="https://github.com/microsoft/agent-framework">Microsoft Agent Framework GitHub 리포지토리&lt;/a>에서 더 많은 예제를 탐색할 수 있습니다.&lt;/p>
&lt;p>즐거운 휴일 보내시고 즐거운 코딩하세요! 🎄&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>AI</category><category>Agent Framework</category></item><item><title>AI 기반 RSS 피드 수집기 구축</title><link>https://emimontesdeoca.github.io/ko/posts/ai-agent-socials/</link><pubDate>Fri, 12 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/ai-agent-socials/</guid><description>Semantic Kernel 및 Azure OpenAI를 사용하여 RSS 피드 모니터링 및 소셜 미디어 게시물 생성을 자동화합니다.</description><content:encoded>&lt;p>Microsoft MVP이자 기술 애호가로서 저는 Microsoft DevBlog에 게시된 놀라운 콘텐츠의 바다에 끊임없이 빠져들고 있습니다. .NET 발표부터 Visual Studio 업데이트, Azure 혁신부터 의미 체계 커널 심층 분석까지 Microsoft 생태계에는 항상 새롭고 흥미로운 일이 일어나고 있습니다.&lt;/p>
&lt;p>문제? &lt;strong>모든 것을 따라잡는 것은 거의 불가능합니다.&lt;/strong>&lt;/p>
&lt;p>최신 공지 사항을 파악하고 이를 내 네트워크와 공유하고 싶었지만 7개의 서로 다른 RSS 피드를 수동으로 확인하고, 기사를 읽고, 흥미로운 소셜 미디어 게시물을 작성하고, 이미 공유한 내용을 추적하는 것이 그 자체로 정규직이 되었습니다. 매일 아침 나는 여러 개의 브라우저 탭을 열고, 수십 개의 기사를 훑어보고, 내가 이미 공유한 기사를 기억하려고 노력한 다음, 내 관심을 끌었던 기사에 대한 게시물을 작성하는 데 소중한 시간을 보냈습니다.&lt;/p>
&lt;p>그래서 나는 개발자라면 누구나 할 법한 일을 했습니다. &lt;strong>자동화했습니다.&lt;/strong>&lt;/p>
&lt;p>이 종합 가이드에서는 새 콘텐츠에 대한 여러 Microsoft DevBlogs RSS 피드를 모니터링하고, Azure OpenAI 및 Semantic Kernel을 사용하여 기사를 분석하고 매력적인 게시물을 생성하고, 분석된 각 기사에 대한 자세한 마크다운 문서를 생성하고, 콘텐츠를 검토 및 공유할 수 있도록 Telegram을 통해 알림을 보내고, 중복 게시물을 피하기 위해 모든 것을 추적하고, GitHub Actions를 통해 자동으로 실행하는 AI 기반 RSS 피드 수집기를 구축하는 방법을 안내합니다.&lt;/p>
&lt;p>이 솔루션의 모든 측면을 자세히 살펴보겠습니다.&lt;/p>
&lt;h2 id="이-프로젝트의-비하인드-스토리">이 프로젝트의 비하인드 스토리&lt;/h2>
&lt;h3 id="정보-과잉-속에서-살아가기">정보 과잉 속에서 살아가기&lt;/h3>
&lt;p>이 도구를 만들기 전의 전형적인 아침 모습을 그려보겠습니다. 저는 일어나서 커피를 들고 노트북을 열어 Microsoft 개발자 생태계의 새로운 소식을 확인했습니다. 먼저 주요 DevBlogs 사이트로 이동하여 주요 공지 사항이 있는지 확인했습니다. 그런 다음 .NET 블로그가 나의 주요 기술 스택이기 때문에 구체적으로 확인하겠습니다. 그 후에는 AI가 점점 더 중요해지고 있기 때문에 Semantic Kernel 블로그를 방문하겠습니다. IDE 업데이트가 일상적인 워크플로에 큰 영향을 미칠 수 있기 때문에 Visual Studio 블로그가 그 다음 목록에 포함되었습니다. 그런 다음 CI/CD 및 GitHub 관련 뉴스를 위한 DevOps 블로그가 나왔고, 클라우드 인프라 업데이트를 위한 All Things Azure 블로그, 마지막으로 데이터베이스 혁신을 위한 Azure SQL 블로그가 나왔습니다.&lt;/p>
&lt;p>확인해야 할 7가지 피드가 있습니다. 이러한 각 블로그는 매주 여러 개의 기사를 게시하며 때로는 .NET Conf 또는 Build와 같은 주요 발표 기간 동안 하루에 여러 개를 게시합니다. 추적하고 읽고 공유할 수 있는 기사가 수십 개에 달할 수 있습니다. 그리고 여기에 문제가 있습니다. 커뮤니티와의 지식 공유를 중요하게 생각하는 사람으로서 저는 이 기사를 그냥 읽고 싶지 않았습니다. 저는 가장 가치 있는 정보를 LinkedIn의 네트워크와 공유하여 다른 개발자에게도 정보를 제공하고 싶었습니다.하지만 좋은 LinkedIn 게시물을 작성하려면 시간이 걸립니다. 기사를 철저하게 읽고, 핵심 사항을 이해하고, 기사가 청중에게 왜 중요한지 생각하고, 흥미로운 내용을 작성하고, 모든 내용을 멋지게 구성해야 합니다. 여기에 일주일에 여러 기사를 곱하면 작업 시간이 표시됩니다.&lt;/p>
&lt;h3 id="내가-정말-원했던-것">내가 정말 원했던 것&lt;/h3>
&lt;p>몇 달 동안 이 문제를 처리한 후 저는 이상적인 솔루션이 어떤 모습일지 생각해 보았습니다. 무엇보다도 중요한 공지를 다시는 놓치고 싶지 않았습니다. 시스템은 새 기사가 게시되는 즉시 자동으로 이를 포착해야 합니다. 또한 AI가 매력적인 게시물을 작성하는 데 도움을 주어 콘텐츠 제작 시간을 절약하고 싶었습니다. 이는 제 목소리를 완전히 대체하는 것이 아니라 맞춤 설정할 수 있는 확실한 출발점을 제공하기 위함이었습니다.&lt;/p>
&lt;p>일관성은 또 다른 큰 요소였습니다. 매일 수동으로 수행할 필요 없이 정기적으로 콘텐츠를 공유하고 싶었습니다. 추적 측면도 매우 중요했습니다. 중복된 게시물을 게시하고 팔로어를 짜증나게 하는 것을 피하기 위해 이미 공유한 내용을 알 수 있는 방법이 필요했습니다. 마지막으로, 내가 처리한 모든 내용을 영구적으로 기록하여 체계적으로 정리하여 내가 다룬 주제가 무엇인지 되돌아보고 확인할 수 있기를 원했습니다.&lt;/p>
&lt;h3 id="솔루션의-형태가-바뀌다">솔루션의 형태가 바뀌다&lt;/h3>
&lt;p>제가 구상한 솔루션은 완전히 핸즈프리인 GitHub Actions를 사용하여 일정에 따라 실행됩니다. 단일 브라우저 탭을 열지 않고도 자동으로 7개의 피드를 모두 가져올 수 있습니다. AI 구성 요소는 실제로 콘텐츠를 읽고 이해한 다음 청중에게 유용한 방식으로 요약합니다. 처음부터 게시물을 작성하는 대신, 필요할 경우 수정할 수 있는 즉시 공유 가능한 소셜 미디어 콘텐츠가 생성됩니다. 검토를 위해 모든 것이 내 텔레그램으로 전송되므로 신속하게 휴대폰을 살펴보고 무엇을 공유할지 결정할 수 있었습니다. 그리고 물론, 나중에 참고할 수 있도록 모든 것을 영구적으로 기록해 둘 것입니다.&lt;/p>
&lt;h2 id="구축을-시작하기-전에">구축을 시작하기 전에&lt;/h2>
&lt;h3 id="컴퓨터에-필요한-것">컴퓨터에 필요한 것&lt;/h3>
&lt;p>이 튜토리얼을 진행하려면 개발 컴퓨터에 몇 가지 항목을 설치해야 합니다. 가장 중요한 것은 .NET SDK 버전 9.0 이상입니다. 이것은 우리의 런타임이며 필요한 모든 빌드 도구를 제공합니다. 아직 설치되어 있지 않다면 dot.net으로 가서 최신 버전을 다운로드하세요. 설치는 Windows, macOS 또는 Linux에서 간단합니다.&lt;/p>
&lt;p>또한 버전 제어를 위해 Git을 설치해야 합니다. 우리는 코드를 GitHub에 푸시하고 자동화를 위해 GitHub Actions를 사용할 것이므로 Git을 로컬로 설정하는 것이 필수적입니다. 최신 버전이면 모두 잘 작동합니다.&lt;/p>
&lt;p>개발 환경으로는 Visual Studio 또는 VS Code를 권장합니다. 개인적으로 저는 요즘 대부분의 작업에 VS Code를 사용합니다. VS Code는 가볍고 C# Dev Kit 확장을 통해 뛰어난 C# 지원을 제공하기 때문입니다. 그러나 전체 Visual Studio에 더 익숙하다면 그것도 완벽하게 작동합니다.&lt;/p>
&lt;h3 id="필요한-서비스-및-계정로컬-도구-외에도-몇-가지-서비스가-포함된-계정이-필요합니다-가장-중요한-것은-ai-분석을-지원하는-azure-openai입니다-이는-종량제-서비스이지만-이-사용-사례에서는-비용이-최소화됩니다-분석된-기사당-센트를-말하는-것입니다-azure-계정이-없으면-시작할-수-있는-크레딧이-포함된-무료-평가판에-등록할-수-있습니다">필요한 서비스 및 계정로컬 도구 외에도 몇 가지 서비스가 포함된 계정이 필요합니다. 가장 중요한 것은 AI 분석을 지원하는 Azure OpenAI입니다. 이는 종량제 서비스이지만 이 사용 사례에서는 비용이 최소화됩니다. 분석된 기사당 센트를 말하는 것입니다. Azure 계정이 없으면 시작할 수 있는 크레딧이 포함된 무료 평가판에 등록할 수 있습니다.&lt;/h3>
&lt;p>알림을 위해 Telegram Bot을 사용합니다. Telegram의 가장 큰 장점은 봇 API를 완전히 무료로 사용할 수 있다는 것입니다. 원하는 만큼 봇을 만들고 메시지를 무제한으로 보낼 수 있습니다. 이 가이드 뒷부분에서 설정 과정을 안내해 드리겠습니다.&lt;/p>
&lt;p>마지막으로, 코드를 호스팅하고 GitHub Actions를 실행하려면 GitHub 계정이 필요합니다. 이 프로젝트에는 무료 등급이면 충분합니다. GitHub는 개인 저장소에서 매월 2,000분의 Actions 런타임을 제공하고 공용 저장소에서는 무제한(분)을 제공합니다.&lt;/p>
&lt;h3 id="이것을-가능하게-하는-라이브러리">이것을 가능하게 하는 라이브러리&lt;/h3>
&lt;p>우리 프로젝트는 각각 특정 목적을 수행하는 세 가지 주요 NuGet 패키지를 사용합니다.&lt;/p>
&lt;p>첫 번째는 .NET의 HTML 구문 분석에 대한 표준인 HtmlAgilityPack입니다. 블로그에서 기사를 가져올 때 탐색 메뉴, 바닥글, 광고 및 관심 없는 모든 종류의 요소를 포함하여 페이지의 전체 HTML을 다시 가져옵니다. HtmlAgilityPack을 사용하면 해당 HTML을 구문 분석하고 필요한 기사 콘텐츠만 추출할 수 있습니다.&lt;/p>
&lt;p>두 번째 패키지는 AI 모델을 애플리케이션에 통합하기 위한 Microsoft의 SDK인 Microsoft.SemanticKernel입니다. 이를 .NET 코드와 GPT-4와 같은 대규모 언어 모델 사이의 다리로 생각하십시오. API 호출, 토큰 관리 및 응답 구문 분석의 모든 복잡성을 처리하므로 AI가 실제로 수행하기를 원하는 작업에 집중할 수 있습니다.&lt;/p>
&lt;p>세 번째 패키지는 RSS 및 Atom 피드 구문 분석을 기본적으로 지원하는 System.ServiceModel.Syndication입니다. RSS는 오래된 기술처럼 보일 수도 있지만 여전히 블로그와 뉴스 사이트에서 구조화된 업데이트를 얻는 가장 좋은 방법입니다. 이 패키지는 원시 XML 피드를 작업하기 쉬운 강력한 형식의 C# 개체로 변환합니다.&lt;/p>
&lt;h2 id="아키텍처-이해">아키텍처 이해&lt;/h2>
&lt;h3 id="조각이-서로-맞는-방법">조각이 서로 맞는 방법&lt;/h3>
&lt;p>코드를 살펴보기 전에 모든 구성 요소가 어떻게 함께 작동하는지 설명하겠습니다. 큰 그림을 이해하면 구현 세부 사항이 훨씬 더 명확해집니다.&lt;/p>
&lt;p>최고 수준에는 오케스트레이터 역할을 하는 기본 Program.cs 파일이 있습니다. 이는 애플리케이션의 진입점이며 다른 모든 구성요소를 조정합니다. 애플리케이션이 실행되면 먼저 API 키 및 Telegram 자격 증명과 같은 환경 변수에서 구성을 로드합니다. 그런 다음 나가서 7개의 Microsoft DevBlogs 소스 모두에서 RSS 피드를 가져옵니다. 이러한 피드를 처리하면서 동일한 기사가 여러 피드에 나타나는 경우를 처리하기 위해 기사의 중복을 제거합니다. 추적 파일과 비교하여 각 기사를 확인하여 이미 처리되었는지 확인합니다. 새로운 기사의 경우 처리를 위해 AI 분석기에 전달합니다.ArticleAnalyzer 클래스는 AI 마법이 일어나는 곳입니다. 이 구성 요소는 기사를 수신하고 기사로 여러 가지 작업을 수행합니다. 먼저 기사의 URL에서 전체 HTML 콘텐츠를 가져옵니다. 그런 다음 해당 HTML에서 깨끗한 텍스트를 추출하여 필요하지 않은 모든 탐색 요소, 스크립트 및 스타일을 제거합니다. 깨끗한 텍스트가 있으면 신중하게 제작된 프롬프트와 함께 Semantic Kernel을 통해 이를 Azure OpenAI로 보냅니다. AI는 기사를 분석하고 요약, 주요 주제, 관련성 설명, 가장 중요하게는 즉시 사용 가능한 LinkedIn 게시물을 포함하는 구조화된 응답을 반환합니다. 분석기는 이 응답을 구문 분석하고 이 모든 정보가 포함된 ArticleAnalytic 개체를 반환합니다.&lt;/p>
&lt;p>MarkdownGenerator 클래스는 해당 ArticleAnalytic 개체를 가져와 이에 대한 영구 기록을 만듭니다. 모든 기사 메타데이터, AI 분석 및 생성된 게시물을 포함하는 적절한 형식의 마크다운 파일을 생성합니다. 이러한 파일은 생성된 게시물 디렉터리에 저장되어 처리한 모든 내용에 대한 검색 가능한 아카이브를 제공합니다.&lt;/p>
&lt;p>마지막으로 Telegram 통합은 생성된 게시물 콘텐츠를 휴대폰으로 보냅니다. 이것은 인간으로서 AI의 작업을 검토하고 공유할지 여부를 결정하는 지점입니다. 봇은 게시물 내용이 포함된 메시지를 보내며, 게시물 내용을 LinkedIn에 직접 복사하거나 먼저 수정할 수 있습니다.&lt;/p>
&lt;h3 id="데이터의-흐름">데이터의 흐름&lt;/h3>
&lt;p>.NET 블로그에 새 기사가 게시되면 어떤 일이 발생하는지 안내해 드리겠습니다. GitHub Actions가 일정에 따라 애플리케이션을 트리거할 때 워크플로가 시작됩니다(예: 6시간마다). 애플리케이션이 깨어나서 7개의 RSS 피드를 모두 가져오기 시작합니다. 각 피드는 해당 블로그의 최신 기사가 포함된 XML 문서를 반환합니다.&lt;/p>
&lt;p>각 피드를 구문 분석하면서 개별 기사를 추출하여 목록에 저장합니다. 하지만 여기에 까다로운 부분이 있습니다. 기본 DevBlogs 피드에는 개별 카테고리 피드에도 나타나는 기사가 포함되는 경우가 많습니다. 따라서 &amp;ldquo;.NET 10&amp;quot;에 대한 기사가 기본 피드와 .NET 관련 피드 모두에 표시될 수 있습니다. 우리는 중복을 자동으로 방지하는 HashSet의 URL을 추적하여 이를 처리합니다.&lt;/p>
&lt;p>중복 제거된 기사 목록이 있으면 최근 기사(일반적으로 마지막 날 정도에 게시된 기사)로만 필터링합니다. 이전 실행에서 이미 처리한 오래된 기사를 처리하고 싶지 않습니다. 그런 다음 추적 파일과 비교하여 각 최근 기사를 확인합니다. 기사에 대해 이미 처리하고 게시한 경우 해당 기사를 건너뜁니다.&lt;/p>
&lt;p>새로운 기사가 ​​나올 때마다 AI 분석 파이프라인이 시작됩니다. 분석기는 전체 기사 HTML을 가져와 정리한 후 프롬프트와 함께 GPT-4로 보냅니다. AI는 기사를 읽고 LinkedIn 게시물과 함께 포괄적인 분석을 생성합니다. 우리는 기록을 위해 이 분석을 마크다운 파일에 저장합니다.분석이 완료되면 메시지 형식을 지정하고 Telegram을 통해 보냅니다. 메시지에는 URL과 해시태그가 추가된 생성된 게시물 콘텐츠가 포함됩니다. 휴대폰으로 알림을 받고 게시물을 검토한 후 마음에 들면 몇 번의 탭만으로 복사하여 LinkedIn에 공유할 수 있습니다.&lt;/p>
&lt;p>마지막으로 추적 파일을 업데이트하여 이 기사를 처리된 것으로 표시하므로 향후 실행에서는 다시 처리하지 않습니다. 파일이 생성되거나 수정된 ​​경우 GitHub Actions는 이러한 변경 사항을 저장소에 다시 커밋하여 모든 것을 동기화합니다.&lt;/p>
&lt;h2 id="처음부터-프로젝트-설정하기">처음부터 프로젝트 설정하기&lt;/h2>
&lt;h3 id="솔루션-구조-만들기">솔루션 구조 만들기&lt;/h3>
&lt;p>건축을 시작합시다. 터미널을 열고 프로젝트를 생성하려는 위치로 이동합니다. 나는 개발 폴더에 프로젝트를 정리하는 것을 좋아하지만, 원하는 곳에 배치할 수 있습니다.&lt;/p>
&lt;p>먼저 새 솔루션 파일을 만듭니다. .NET에서 솔루션은 여러 프로젝트를 보유할 수 있는 컨테이너입니다. 지금은 프로젝트가 하나뿐이지만 솔루션부터 시작하면 나중에 필요한 경우 프로젝트를 더 쉽게 추가할 수 있습니다. &lt;code>dotnet new sln -n vs-feed-linkedin&lt;/code> 명령을 실행하여 vs-feed-linkedin이라는 솔루션을 생성합니다.&lt;/p>
&lt;p>다음으로 콘솔 애플리케이션 프로젝트를 생성해야 합니다. 정리된 상태를 유지하기 위해 이것을 src 하위 디렉터리에 넣을 것입니다. &lt;code>dotnet new console -n VsFeedLinkedin -o src&lt;/code>를 실행하여 src 폴더에 VsFeedLinkedin이라는 콘솔 프로젝트를 만듭니다. 그런 다음 &lt;code>dotnet sln add src/VsFeedLinkedin.csproj&lt;/code>를 사용하여 이 프로젝트를 솔루션에 추가합니다.&lt;/p>
&lt;p>이제 &lt;code>cd src&lt;/code>를 사용하여 src 디렉터리로 이동합니다. 여기에서 NuGet 패키지를 추가하고 대부분의 개발을 수행합니다.&lt;/p>
&lt;h3 id="필수-패키지-추가">필수 패키지 추가&lt;/h3>
&lt;p>프로젝트가 생성되면 앞서 언급한 세 가지 NuGet 패키지를 추가해야 합니다. 다음 명령을 각각 순서대로 실행하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package System.ServiceModel.Syndication --version 9.0.9
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel --version 1.30.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package HtmlAgilityPack --version 1.11.72
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 명령을 실행한 후 프로젝트 파일은 다음과 같아야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;Project&lt;/span> Sdk=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.NET.Sdk&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;OutputType&amp;gt;&lt;/span>Exe&lt;span style="color:#7ee787">&amp;lt;/OutputType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;TargetFramework&amp;gt;&lt;/span>net9.0&lt;span style="color:#7ee787">&amp;lt;/TargetFramework&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;ImplicitUsings&amp;gt;&lt;/span>enable&lt;span style="color:#7ee787">&amp;lt;/ImplicitUsings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;Nullable&amp;gt;&lt;/span>enable&lt;span style="color:#7ee787">&amp;lt;/Nullable&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;ItemGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;HtmlAgilityPack&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;1.11.72&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.SemanticKernel&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;1.30.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;System.ServiceModel.Syndication&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.9&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/ItemGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/Project&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>프로젝트 파일은 .NET 9.0을 대상으로 실행 파일(OutputType은 Exe)을 빌드하고 암시적 using 및 null 허용 참조 유형과 같은 최신 C# 기능을 사용하고 있음을 .NET에 알려줍니다. ItemGroup 섹션에는 정확한 버전과 함께 세 가지 패키지 종속성이 나열되어 있습니다.&lt;/p>
&lt;h2 id="rss-피드-심층-분석">RSS 피드 심층 분석&lt;/h2>
&lt;h3 id="rss란-정확히-무엇인가요">RSS란 정확히 무엇인가요?&lt;/h3>
&lt;p>피드를 가져오는 코드 작성을 시작하기 전에 우리가 작업 중인 내용을 확실히 이해하도록 합시다. RSS는 Really Simple Syndication의 약어이며 콘텐츠 업데이트 배포를 위한 표준화된 XML 형식입니다. 아이디어는 간단합니다. 사용자에게 새 콘텐츠가 있는지 확인하기 위해 웹사이트를 방문하도록 요구하는 대신 최신 콘텐츠를 나열하는 기계 판독 가능 파일을 게시합니다. 그런 다음 애플리케이션은 이 파일을 주기적으로 폴링하여 새 기사를 검색할 수 있습니다.&lt;/p>
&lt;p>RSS는 1990년대 후반과 2000년대 초반부터 존재해왔습니다. 오래된 기술이라고 생각할 수도 있지만 실제로는 특히 블로그, 뉴스 사이트, 팟캐스트에서 여전히 널리 사용되고 있습니다. RSS의 장점은 단순성에 있습니다. 이는 정의된 구조를 가진 XML일 뿐이며 모든 애플리케이션에서 이를 구문 분석할 수 있습니다.&lt;/p>
&lt;h3 id="devblogs-피드의-구조microsoft-devblogs에서-rss-피드를-가져오면-특정-구조를-따르는-xml-문서를-다시-가져옵니다-최상위-수준에는-단일-채널-요소를-포함하는-rss-요소가-있습니다-채널은-블로그-자체를-나타내며-블로그-제목-url-설명과-같은-메타데이터를-포함합니다">DevBlogs 피드의 구조Microsoft DevBlogs에서 RSS 피드를 가져오면 특정 구조를 따르는 XML 문서를 다시 가져옵니다. 최상위 수준에는 단일 채널 요소를 포함하는 RSS 요소가 있습니다. 채널은 블로그 자체를 나타내며 블로그 제목, URL, 설명과 같은 메타데이터를 포함합니다.&lt;/h3>
&lt;p>채널 내에는 각각 개별 블로그 게시물을 나타내는 여러 항목 요소가 있습니다. 각 항목에는 제목(기사 헤드라인), 링크(전체 기사를 읽을 수 있는 URL), pubDate(기사가 게시된 시기), dc:creator 요소(작성자 이름), 하나 이상의 카테고리 요소(기사 태그) 및 설명(일반적으로 기사 요약 또는 발췌)이 포함됩니다.&lt;/p>
&lt;p>다음은 이것이 어떻게 보이는지에 대한 간단한 예입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;rss&lt;/span> version=&lt;span style="color:#a5d6ff">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;channel&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;title&amp;gt;&lt;/span>.NET Blog&lt;span style="color:#7ee787">&amp;lt;/title&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;link&amp;gt;&lt;/span>https://devblogs.microsoft.com/dotnet&lt;span style="color:#7ee787">&amp;lt;/link&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;description&amp;gt;&lt;/span>The latest news about .NET&lt;span style="color:#7ee787">&amp;lt;/description&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;item&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;title&amp;gt;&lt;/span>Announcing .NET 10&lt;span style="color:#7ee787">&amp;lt;/title&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;link&amp;gt;&lt;/span>https://devblogs.microsoft.com/dotnet/announcing-dotnet-10&lt;span style="color:#7ee787">&amp;lt;/link&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;pubDate&amp;gt;&lt;/span>Mon, 10 Dec 2025 12:00:00 GMT&lt;span style="color:#7ee787">&amp;lt;/pubDate&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;dc:creator&amp;gt;&lt;/span>Microsoft&lt;span style="color:#7ee787">&amp;lt;/dc:creator&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;category&amp;gt;&lt;/span>Announcements&lt;span style="color:#7ee787">&amp;lt;/category&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;category&amp;gt;&lt;/span>.NET&lt;span style="color:#7ee787">&amp;lt;/category&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;description&amp;gt;&lt;/span>Article summary...&lt;span style="color:#7ee787">&amp;lt;/description&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/item&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/channel&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/rss&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>.NET System.ServiceModel.Syndication 패키지의 가장 큰 장점은 이 패키지가 이 모든 것을 구문 분석한다는 것입니다. XML 노드를 수동으로 탐색하거나 다른 RSS 버전에 대해 걱정할 필요가 없습니다. 우리는 피드를 로드하고 강력한 유형의 개체를 다시 가져옵니다.&lt;/p>
&lt;h3 id="우리가-모니터링하는-7가지-피드">우리가 모니터링하는 7가지 피드&lt;/h3>
&lt;p>저는 구현 시 7개의 서로 다른 Microsoft DevBlogs 피드를 모니터링합니다. devblogs.microsoft.com/feed의 기본 DevBlogs 피드에서는 Microsoft가 모든 개발자 블로그에 게시하는 모든 내용을 폭넓게 볼 수 있습니다. devblogs.microsoft.com/dotnet/feed의 .NET 관련 피드는 특히 .NET 릴리스, 기능 및 모범 사례에 중점을 두고 있습니다. devblogs.microsoft.com/semantic-kernel/feed의 Semantic Kernel 피드에서는 AI 오케스트레이션 및 통합을 다룹니다. AI가 현대 개발의 중심이 되면서 점점 더 중요해지고 있습니다.&lt;/p>
&lt;p>devblogs.microsoft.com/visualstudio/feed의 Visual Studio 피드에서는 IDE 개선 사항 및 생산성 기능에 대한 최신 정보를 계속해서 받아볼 수 있습니다. devblogs.microsoft.com/devops/feed의 DevOps 피드에서는 Azure DevOps, GitHub 및 CI/CD 주제를 다룹니다. devblogs.microsoft.com/all-things-azure/feed의 All Things Azure 피드는 클라우드 서비스 및 아키텍처 패턴에 중점을 둡니다. 마지막으로 devblogs.microsoft.com/azure-sql/feed의 Azure SQL 피드에서는 데이터베이스 혁신과 기능을 다룹니다.&lt;/p>
&lt;p>왜 제가 메인피드와 개별 ​​카테고리 피드를 모두 확인하는지 궁금하실 겁니다. 기본 피드를 통해 폭넓은 정보를 얻을 수 있습니다. 내가 알지 못하는 내용을 포함하여 모든 Microsoft 개발자 블로그의 기사를 볼 수 있습니다. 카테고리 피드는 나에게 깊이를 줍니다. 새로운 콘텐츠로 인해 해당 기사가 기본 피드에서 밀려나더라도 핵심 관심 분야에서 중요한 내용을 놓치지 않도록 해줍니다.&lt;/p>
&lt;h2 id="rss-가져오기-논리-구축">RSS 가져오기 논리 구축&lt;/h2>
&lt;h3 id="코어-가져오기-기능">코어 가져오기 기능&lt;/h3>
&lt;p>이제 몇 가지 코드를 작성해 보겠습니다. 우리 애플리케이션의 기초는 RSS 피드를 가져오고 구문 분석하는 기능입니다. 이를 처리하는 함수는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;SyndicationFeed?&amp;gt; FetchRssFeedAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpClient.DefaultRequestHeaders.Add(&lt;span style="color:#a5d6ff">&amp;#34;User-Agent&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;VsFeedLinkedin/1.0&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetStringAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> stringReader = &lt;span style="color:#ff7b72">new&lt;/span> StringReader(response);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> XmlReaderSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DtdProcessing = DtdProcessing.Parse,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaxCharactersFromEntities = &lt;span style="color:#a5d6ff">1024&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> xmlReader = XmlReader.Create(stringReader, settings);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> SyndicationFeed.Load(xmlReader);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 코드의 기능을 살펴보겠습니다. HTTP 요청을 만들기 위한 .NET의 기본 제공 클래스인 HttpClient를 만드는 것부터 시작합니다. 일부 서버는 자신을 식별하지 않는 요청을 차단하기 때문에 User-Agent 헤더를 설정합니다. 서버에서 필요하지 않은 경우에도 이를 설정하는 것이 좋습니다.그런 다음 피드 URL에 대해 GET 요청을 보내고 응답을 문자열로 받습니다. 이 문자열에는 RSS 피드의 원시 XML이 포함되어 있습니다.&lt;/p>
&lt;p>이 XML을 구문 분석하기 위해 StringReader를 만들어 응답 문자열을 래핑한 다음 일부 XmlReaderSettings를 구성합니다. DtdProcessing 설정은 중요합니다. RSS 피드에는 처리해야 하는 DTD(문서 유형 정의) 선언이 포함되는 경우가 있습니다. MaxCharactersFromEntities 설정은 엔터티 확장이 발생할 수 있는 정도를 제한하여 XML 폭탄 공격을 방지하는 보안 조치입니다.&lt;/p>
&lt;p>마지막으로 이러한 설정을 사용하여 XmlReader를 만들고 SyndicationFeed.Load를 사용하여 XML을 강력한 형식의 SyndicationFeed 개체로 구문 분석합니다. 이를 통해 원시 XML 탐색 대신 멋진 C# 속성을 통해 피드의 메타데이터와 모든 항목에 액세스할 수 있습니다.&lt;/p>
&lt;h3 id="오류-처리를-통해-여러-피드-가져오기">오류 처리를 통해 여러 피드 가져오기&lt;/h3>
&lt;p>실제 세계에서는 네트워크 요청이 실패합니다. 서버가 다운되고 연결 시간이 초과되며 XML 형식이 잘못될 수 있습니다. 우리는 이러한 경우를 우아하게 처리해야 합니다. 실패에 대한 복원력을 유지하면서 모든 피드를 가져오는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> allArticles = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;(SyndicationItem item, &lt;span style="color:#ff7b72">string&lt;/span> feedUrl)&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> seenUrls = &lt;span style="color:#ff7b72">new&lt;/span> HashSet&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> feedUrl &lt;span style="color:#ff7b72">in&lt;/span> feedUrls)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34; 📡 Fetching {feedUrl}...&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> feed = &lt;span style="color:#ff7b72">await&lt;/span> FetchRssFeedAsync(feedUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (feed?.Items != &lt;span style="color:#79c0ff">null&lt;/span> &amp;amp;&amp;amp; feed.Items.Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> feed.Items)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> itemUrl = item.Links.FirstOrDefault()?.Uri.ToString() ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(itemUrl) &amp;amp;&amp;amp; seenUrls.Add(itemUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> allArticles.Add((item, feedUrl));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34; ⚠️ Failed to fetch {feedUrl}: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>우리는 여기서 두 개의 컬렉션을 유지합니다. allArticles 목록에는 우리가 찾은 모든 기사와 해당 기사의 출처가 포함됩니다. visibleUrls HashSet은 우리가 이미 본 기사 URL을 추적하여 중복을 방지하는 데 도움이 됩니다.&lt;/p>
&lt;p>각 피드 URL을 반복하고 try-catch 블록에서 가져오기 작업을 래핑합니다. 특정 피드를 가져오는 데 실패하면(서버가 일시적으로 다운되었을 수 있음) 경고를 기록하고 다음 피드를 계속 진행합니다. 이렇게 하면 하나의 피드에 문제가 있어도 다른 피드를 처리하는 데 방해가 되지 않습니다.&lt;/p>
&lt;p>성공적으로 가져온 각 피드에 대해 해당 항목을 반복합니다. 항목의 Links 컬렉션에서 기사 URL을 추출합니다. HashSet.Add 메서드는 URL이 이미 세트에 있는 경우 false를 반환하며 이는 중복 제거 논리에 적합합니다. 기사가 새로운 경우에만 목록에 기사를 추가합니다.&lt;/p>
&lt;p>이 정보는 나중에 유용할 수 있으므로 각 기사와 함께 피드 URL을 저장합니다. 예를 들어 디버깅 또는 로깅 목적으로 기사가 어떤 특정 피드에서 왔는지 알고 싶을 수 있습니다.&lt;/p>
&lt;h2 id="중복-처리-및-상태-추적">중복 처리 및 상태 추적&lt;/h2>
&lt;h3 id="중복-제거-과제">중복 제거 과제&lt;/h3>
&lt;p>앞서 언급했듯이 Microsoft DevBlogs에는 흥미로운 문제를 일으키는 계층적 피드 구조가 있습니다. .NET 팀 구성원이 .NET 10의 성능 향상에 대한 기사를 게시하면 해당 기사가 기본 DevBlogs 피드와 .NET 관련 피드 모두에 나타날 가능성이 높습니다. 때로는 IDE 기능과 관련된 경우 Visual Studio 피드에 나타날 수도 있습니다.&lt;/p>
&lt;p>모든 피드의 모든 기사를 순진하게 처리했다면 결국 동일한 기사에 대해 여러 번 분석하고 게시하게 될 것입니다. 그러면 Azure OpenAI에 대한 API 호출이 낭비되고, 텔레그램에 중복 알림이 발송되며, 중복된 알림을 게시하면 팔로어가 잠재적으로 짜증을 낼 수 있습니다.해결책은 URL 기반 중복 제거입니다. 각 기사에는 고유한 URL이 있으므로 이를 식별자로 사용할 수 있습니다. HashSet 데이터 구조는 O(1) 조회 시간을 제공하고 자동으로 중복을 방지하므로 이에 적합합니다. 이미 집합에 있는 URL을 추가하려고 하면 Add 메서드는 단순히 false를 반환하여 해당 항목을 건너뛰어야 한다는 것을 알려줍니다.&lt;/p>
&lt;h3 id="마크다운을-사용한-지속-상태">마크다운을 사용한 지속 상태&lt;/h3>
&lt;p>중복 제거는 단일 실행 내에서 중복을 처리하지만 전체 실행에서는 어떻습니까? 애플리케이션이 6시간마다 실행될 때 이미 처리한 기사를 기억하여 다시 처리하지 않도록 해야 합니다.&lt;/p>
&lt;p>나는 이 상태를 Posted-articles.md라는 마크다운 파일에 저장하기로 결정했습니다. 왜 마크다운인가? 몇 가지 이유. 첫째, 사람이 읽을 수 있습니다. 파일을 열고 내가 어떤 글을 공유했는지 즉시 확인할 수 있습니다. 둘째, 버전 관리입니다. 이 파일은 Git 저장소에 있으므로 기사가 처리된 시점에 대한 완전한 기록을 가지고 있습니다. 셋째, 문서화 역할을 합니다. 저장소를 보는 사람은 누구나 애플리케이션이 수행한 작업을 볼 수 있습니다.&lt;/p>
&lt;p>이 파일의 형식은 간단합니다. 여기에는 헤더, 애플리케이션이 마지막으로 실행된 시간을 보여주는 타임스탬프, 마크다운 링크 형식의 기사 목록이 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#79c0ff;font-weight:bold"># Posted Articles
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#79c0ff;font-weight:bold">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-style:italic">*Last run: 2025-12-10 15:30:00*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>List of articles posted to LinkedIn:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">-&lt;/span> [&lt;span style="color:#7ee787">Announcing .NET 10&lt;/span>](https://devblogs.microsoft.com/dotnet/announcing-dotnet-10?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-10 15:30:00 (Published: 2025-12-10)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">-&lt;/span> [&lt;span style="color:#7ee787">Visual Studio 2026 Preview&lt;/span>](https://devblogs.microsoft.com/visualstudio/vs-2026-preview?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-09 10:15:00 (Published: 2025-12-09)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="추적-파일-로드-및-구문-분석">추적 파일 로드 및 구문 분석&lt;/h3>
&lt;p>기사를 이미 처리했는지 확인하려면 이 파일을 로드하고 URL을 추출해야 합니다. 이 작업을 수행하는 함수는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> HashSet&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; LoadPostedArticles(&lt;span style="color:#ff7b72">string&lt;/span> filePath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> postedUrls = &lt;span style="color:#ff7b72">new&lt;/span> HashSet&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(filePath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> postedUrls;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> lines = File.ReadAllLines(filePath);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> line &lt;span style="color:#ff7b72">in&lt;/span> lines)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> match = System.Text.RegularExpressions.Regex.Match(line, &lt;span style="color:#a5d6ff">@&amp;#34;\(([^)]+)\)&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (match.Success)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> url = match.Groups[&lt;span style="color:#a5d6ff">1&lt;/span>].Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (url.Contains(&lt;span style="color:#a5d6ff">&amp;#34;?wt.mc_id=&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url = url.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, url.IndexOf(&lt;span style="color:#a5d6ff">&amp;#34;?wt.mc_id=&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (url.Contains(&lt;span style="color:#a5d6ff">&amp;#34;?&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url = url.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, url.IndexOf(&lt;span style="color:#a5d6ff">&amp;#34;?&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url = url.TrimEnd(&lt;span style="color:#a5d6ff">&amp;#39;/&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> postedUrls.Add(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> postedUrls;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 함수는 우리가 이미 처리한 모든 URL을 포함하는 HashSet을 반환합니다. 파일이 존재하는지 확인하는 것부터 시작합니다. 처음 실행하면 존재하지 않으므로 빈 세트를 반환합니다.&lt;/p>
&lt;p>파일의 각 줄에 대해 정규식을 사용하여 마크다운 링크 형식에서 URL을 추출합니다. 정규식 &lt;code>\(([^)]+)\)&lt;/code>은 마크다운 링크가 URL을 저장하는 괄호 안의 모든 항목과 일치합니다.&lt;/p>
&lt;p>그런 다음 중요한 단계인 URL 정규화를 수행합니다. 동일한 기사에 대한 URL의 형식은 다를 수 있습니다. RSS 피드는 &lt;code>https://devblogs.microsoft.com/dotnet/article&lt;/code>를 제공할 수 있지만 저장된 버전에는 추적 매개변수 &lt;code>https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972&lt;/code>가 추가되어 있습니다. 일부 URL에는 뒤에 슬래시가 있지만 다른 URL에는 없습니다.&lt;/p>
&lt;p>이를 처리하기 위해 모든 쿼리 매개변수(&lt;code>?&lt;/code> 뒤의 모든 항목)를 제거하고 후행 슬래시를 제거합니다. 이러한 정규화를 통해 기사의 URL이 표면적으로 다르더라도 기사를 중복으로 인식할 수 있습니다.&lt;/p>
&lt;h3 id="새-기사-저장">새 기사 저장&lt;/h3>
&lt;p>기사를 성공적으로 처리하면 이를 추적 파일에 추가해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> SavePostedArticle(&lt;span style="color:#ff7b72">string&lt;/span> filePath, &lt;span style="color:#ff7b72">string&lt;/span> url, &lt;span style="color:#ff7b72">string&lt;/span> title, DateTimeOffset publishDate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> markdownEntry = &lt;span style="color:#a5d6ff">$&amp;#34;- [{title}]({url}) - Posted on {DateTime.Now:yyyy-MM-dd HH:mm:ss} (Published: {publishDate:yyyy-MM-dd})\n&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(filePath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.WriteAllText(filePath, &lt;span style="color:#a5d6ff">&amp;#34;# Posted Articles\n\n*Last run: {DateTime.Now:yyyy-MM-dd HH:mm:ss}*\n\nList of articles posted:\n\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.AppendAllText(filePath, markdownEntry);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 기능은 기사 제목을 링크로 사용하고 그 뒤에 기사를 게시한 시기와 원래 게시된 시기를 보여주는 타임스탬프가 포함된 마크다운 형식의 항목을 생성합니다. 파일이 아직 존재하지 않으면 먼저 헤더를 사용하여 파일을 만듭니다.&lt;/p>
&lt;h2 id="ai-분석-엔진">AI 분석 엔진&lt;/h2>
&lt;h3 id="시맨틱-커널-이해하기이제-우리-애플리케이션의-가장-흥미로운-부분인-ai-분석에-들어갑니다-semantic-kernel은-대규모-언어-모델을-애플리케이션에-통합하기-위한-microsoft의-오픈-소스-sdk입니다-이는-api-호출을-둘러싼-래퍼-그-이상입니다-플러그인-플래너-메모리와-같은-기능을-갖춘-정교한-ai-애플리케이션을-구축하기-위한-프레임워크를-제공합니다">시맨틱 커널 이해하기이제 우리 애플리케이션의 가장 흥미로운 부분인 AI 분석에 들어갑니다. Semantic Kernel은 대규모 언어 모델을 애플리케이션에 통합하기 위한 Microsoft의 오픈 소스 SDK입니다. 이는 API 호출을 둘러싼 래퍼 그 이상입니다. 플러그인, 플래너, 메모리와 같은 기능을 갖춘 정교한 AI 애플리케이션을 구축하기 위한 프레임워크를 제공합니다.&lt;/h3>
&lt;p>사용 사례에서는 Semantic Kernel의 채팅 완료 기능을 사용하고 있습니다. Azure OpenAI에 메시지를 보내면 모델이 기사를 분석하고 응답을 생성합니다. 시맨틱 커널은 API 인증, 요청 형식화 및 응답 구문 분석의 모든 복잡성을 처리합니다.&lt;/p>
&lt;h3 id="기사-분석기-설정">기사 분석기 설정&lt;/h3>
&lt;p>분석기 클래스를 설정하는 방법을 살펴보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HtmlAgilityPack&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">VsFeedLinkedin.Services&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ArticleAnalyzer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> Kernel _kernel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IChatCompletionService _chatService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ArticleAnalyzer(&lt;span style="color:#ff7b72">string&lt;/span> endpoint, &lt;span style="color:#ff7b72">string&lt;/span> apiKey, &lt;span style="color:#ff7b72">string&lt;/span> deploymentName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: deploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: endpoint,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: apiKey
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _chatService = _kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>시맨틱 커널은 구성을 위해 빌더 패턴을 사용합니다. KernelBuilder를 만들고 필요한 자격 증명을 사용하여 Azure OpenAI 채팅 완료 서비스를 추가한 다음 커널을 빌드합니다. 빌드된 커널에서 프롬프트를 보내고 응답을 받는 데 사용할 IChatCompletionService 인터페이스를 검색합니다.&lt;/p>
&lt;p>생성자는 Azure OpenAI 엔드포인트(예: &lt;code>https://your-resource.openai.azure.com/&lt;/code>), API 키 및 배포 이름(예: &lt;code>gpt-4o&lt;/code>)의 세 가지 매개 변수를 사용합니다. 이는 환경 변수에서 전달되어 자격 증명을 안전하게 유지합니다.&lt;/p>
&lt;h3 id="완벽한-프롬프트-만들기">완벽한 프롬프트 만들기&lt;/h3>
&lt;p>우리가 AI에 보내는 메시지는 매우 중요합니다. 잘 만들어진 프롬프트는 일관된 고품질 출력을 생성합니다. 모호하거나 잘못 구성된 프롬프트는 일관성이 없고 평범한 결과를 낳습니다. 나는 만족스러운 결과를 얻기 위해 이 프롬프트를 반복하는 데 상당한 시간을 보냈습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> You are a professional tech content analyst and LinkedIn content creator.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Analyze the following Microsoft DevBlogs article and create an engaging LinkedIn post.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Article Title: {title}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Author: {author}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> URL: {url}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Tags: {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, tags)}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Article Content:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {cleanContent}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Please provide:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">1.&lt;/span> A brief summary (&lt;span style="color:#a5d6ff">2&lt;/span>-&lt;span style="color:#a5d6ff">3&lt;/span> sentences) of the key points
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2.&lt;/span> The main technologies or topics covered
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3.&lt;/span> Why &lt;span style="color:#ff7b72">this&lt;/span> &lt;span style="color:#ff7b72">is&lt;/span> relevant &lt;span style="color:#ff7b72">for&lt;/span> developers/tech professionals
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">4.&lt;/span> An engaging LinkedIn post (max &lt;span style="color:#a5d6ff">1300&lt;/span> characters) that:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Starts with a hook or attention-grabbing statement
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Highlights the key &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">for&lt;/span> readers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Includes a call to action
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Uses appropriate emojis (but not too many)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Maintains a professional yet approachable tone
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - DO NOT include hashtags &lt;span style="color:#ff7b72">in&lt;/span> the post (they will be added separately)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - DO NOT include the URL &lt;span style="color:#ff7b72">in&lt;/span> the post (it will be added separately)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Format your response &lt;span style="color:#ff7b72">as&lt;/span> follows:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Summary
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Your summary here]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Key Topics
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [List of main topics/technologies]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Relevance
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Why this matters]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> LinkedIn Post
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Your engaging LinkedIn post here]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>여기서 디자인 결정을 설명하겠습니다. 우리는 AI에게 &amp;ldquo;당신은 전문 기술 콘텐츠 분석가이자 LinkedIn 콘텐츠 제작자입니다.&amp;ldquo;라는 명확한 역할을 부여하는 것부터 시작합니다. 이는 모델이 적절한 스타일과 목소리로 반응하도록 준비시킵니다.&lt;/p>
&lt;p>기사 제목, 작성자, URL, RSS 피드의 태그, 전체 기사 콘텐츠 등 AI에 필요한 모든 컨텍스트를 제공합니다. 더 많은 맥락을 제공할수록 분석이 더 좋아질 것입니다.&lt;/p>
&lt;p>그런 다음 우리가 원하는 것을 정확히 지정합니다. 요약, 핵심 주제, 관련성 설명, LinkedIn 게시물의 네 가지를 요청합니다. 특히 LinkedIn 게시물의 경우 좋은 게시물을 만드는 방법에 대한 자세한 지침을 제공합니다. 즉, 관심을 끌 수 있어야 하고, 가치를 강조해야 하며, 클릭 유도 문구를 포함하고, 이모티콘을 적절하게 사용하고, 전문적인 분위기를 유지해야 합니다.&lt;/p>
&lt;p>부정적인 지시도 마찬가지로 중요합니다. 나는 AI에게 게시물에 해시태그나 URL을 포함하지 말라고 명시적으로 지시합니다. 왜? 왜냐하면 저는 이것을 별도로 추가하고 AI가 이를 포함했다면 중복된 내용을 갖게 되기 때문입니다. 이런 종류의 명시적인 지시는 흔히 발생하는 실수를 방지합니다.&lt;/p>
&lt;p>마지막으로 정확한 출력 형식을 지정합니다. ## 헤더로 표시된 섹션을 요청함으로써 프로그래밍 방식으로 응답을 쉽게 구문 분석할 수 있습니다. AI는 형식 지정 지침을 매우 잘 따르며 이러한 일관성으로 인해 구문 분석 코드가 더 간단하고 안정적이게 됩니다.&lt;/p>
&lt;h3 id="분석-실행">분석 실행&lt;/h3>
&lt;p>모든 것을 종합하는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ArticleAnalysis&amp;gt; AnalyzeArticleAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> htmlContent,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; tags)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> cleanContent = ExtractTextFromHtml(htmlContent);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (cleanContent.Length &amp;gt; &lt;span style="color:#a5d6ff">8000&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cleanContent = cleanContent.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">8000&lt;/span>) + &lt;span style="color:#a5d6ff">&amp;#34;...&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chatHistory = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatHistory.AddUserMessage(prompt);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _chatService.GetChatMessageContentAsync(chatHistory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> responseText = response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ParseAnalysisResponse(responseText, title, url, author, tags);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```먼저&lt;/span> HTML &lt;span style="color:#f85149">콘텐츠에서&lt;/span> &lt;span style="color:#f85149">깨끗한&lt;/span> &lt;span style="color:#f85149">텍스트를&lt;/span> &lt;span style="color:#f85149">추출합니다&lt;/span>(&lt;span style="color:#f85149">다음&lt;/span> &lt;span style="color:#f85149">섹션에서&lt;/span> &lt;span style="color:#f85149">이에&lt;/span> &lt;span style="color:#f85149">대해&lt;/span> &lt;span style="color:#f85149">설명하겠습니다&lt;/span>). &lt;span style="color:#f85149">그런&lt;/span> &lt;span style="color:#f85149">다음&lt;/span> &lt;span style="color:#f85149">내용이&lt;/span> &lt;span style="color:#f85149">너무&lt;/span> &lt;span style="color:#f85149">길면&lt;/span> &lt;span style="color:#f85149">내용을&lt;/span> &lt;span style="color:#f85149">자릅니다&lt;/span>. &lt;span style="color:#f85149">대규모&lt;/span> &lt;span style="color:#f85149">언어&lt;/span> &lt;span style="color:#f85149">모델에는&lt;/span> &lt;span style="color:#f85149">토큰&lt;/span> &lt;span style="color:#f85149">제한이&lt;/span> &lt;span style="color:#f85149">있으며&lt;/span> &lt;span style="color:#f85149">매우&lt;/span> &lt;span style="color:#f85149">긴&lt;/span> &lt;span style="color:#f85149">기사가&lt;/span> &lt;span style="color:#f85149">이를&lt;/span> &lt;span style="color:#f85149">초과할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>. &lt;span style="color:#a5d6ff">8&lt;/span>,&lt;span style="color:#a5d6ff">000&lt;/span>&lt;span style="color:#f85149">자로&lt;/span> &lt;span style="color:#f85149">제한함으로써&lt;/span> &lt;span style="color:#f85149">상당한&lt;/span> &lt;span style="color:#f85149">맥락을&lt;/span> &lt;span style="color:#f85149">제공하면서도&lt;/span> &lt;span style="color:#f85149">한도&lt;/span> &lt;span style="color:#f85149">내에서&lt;/span> &lt;span style="color:#f85149">유지되도록&lt;/span> &lt;span style="color:#f85149">보장합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatHistory &lt;span style="color:#f85149">개체를&lt;/span> &lt;span style="color:#f85149">만들고&lt;/span> &lt;span style="color:#f85149">프롬프트를&lt;/span> &lt;span style="color:#f85149">사용자&lt;/span> &lt;span style="color:#f85149">메시지로&lt;/span> &lt;span style="color:#f85149">추가합니다&lt;/span>. &lt;span style="color:#f85149">이는&lt;/span> &lt;span style="color:#f85149">채팅&lt;/span> &lt;span style="color:#f85149">기반&lt;/span> &lt;span style="color:#f85149">상호작용을&lt;/span> &lt;span style="color:#f85149">위한&lt;/span> Semantic Kernel의 &lt;span style="color:#f85149">추상화입니다&lt;/span>. &lt;span style="color:#f85149">이를&lt;/span> &lt;span style="color:#f85149">채팅&lt;/span> &lt;span style="color:#f85149">완료&lt;/span> &lt;span style="color:#f85149">서비스로&lt;/span> &lt;span style="color:#f85149">보내고&lt;/span> &lt;span style="color:#f85149">응답을&lt;/span> &lt;span style="color:#f85149">받습니다&lt;/span>. &lt;span style="color:#f85149">마지막으로&lt;/span> &lt;span style="color:#f85149">응답을&lt;/span> &lt;span style="color:#f85149">구문&lt;/span> &lt;span style="color:#f85149">분석하여&lt;/span> &lt;span style="color:#f85149">개별&lt;/span> &lt;span style="color:#f85149">섹션을&lt;/span> &lt;span style="color:#f85149">추출합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> AI &lt;span style="color:#f85149">응답&lt;/span> &lt;span style="color:#f85149">구문&lt;/span> &lt;span style="color:#f85149">분석&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AI는 &lt;span style="color:#f85149">요청한&lt;/span> &lt;span style="color:#f85149">구조에&lt;/span> &lt;span style="color:#f85149">따라&lt;/span> &lt;span style="color:#f85149">형식이&lt;/span> &lt;span style="color:#f85149">지정된&lt;/span> &lt;span style="color:#f85149">텍스트로&lt;/span> &lt;span style="color:#f85149">응답을&lt;/span> &lt;span style="color:#f85149">반환합니다&lt;/span>. &lt;span style="color:#f85149">이를&lt;/span> &lt;span style="color:#f85149">개별&lt;/span> &lt;span style="color:#f85149">필드로&lt;/span> &lt;span style="color:#f85149">구문&lt;/span> &lt;span style="color:#f85149">분석해야&lt;/span> &lt;span style="color:#f85149">합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> ArticleAnalysis ParseAnalysisResponse(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> response,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; tags)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> analysis = &lt;span style="color:#ff7b72">new&lt;/span> ArticleAnalysis
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title = title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Url = url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Author = author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Tags = tags,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RawAnalysis = response
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sections = response.Split(&lt;span style="color:#a5d6ff">&amp;#34;##&amp;#34;&lt;/span>, StringSplitOptions.RemoveEmptyEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> section &lt;span style="color:#ff7b72">in&lt;/span> sections)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> lines = section.Trim().Split(&lt;span style="color:#a5d6ff">&amp;#39;\n&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">2&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (lines.Length &amp;lt; &lt;span style="color:#a5d6ff">2&lt;/span>) &lt;span style="color:#ff7b72">continue&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sectionTitle = lines[&lt;span style="color:#a5d6ff">0&lt;/span>].Trim().ToLower();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sectionContent = lines[&lt;span style="color:#a5d6ff">1&lt;/span>].Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (sectionTitle)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;summary&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.Summary = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;key topics&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.KeyTopics = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;relevance&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.Relevance = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;linkedin post&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.LinkedInPost = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> analysis;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>응답을 &lt;code>##&lt;/code> 마커로 분할하여 각 섹션을 제공합니다. 각 섹션에 대해 헤더와 콘텐츠를 분리하기 위해 줄바꿈으로 분할했습니다. 그런 다음 switch 문을 사용하여 각 섹션의 콘텐츠를 적절한 속성에 할당합니다.&lt;/p>
&lt;p>또한 구문 분석되지 않은 원시 응답도 저장합니다. 이는 디버깅에 유용합니다. 구문 분석에 문제가 있는 경우 AI가 실제로 반환한 내용을 확인할 수 있습니다.&lt;/p>
&lt;h2 id="html에서-콘텐츠-추출">HTML에서 콘텐츠 추출&lt;/h2>
&lt;h3 id="html을-정리해야-하는-이유">HTML을 정리해야 하는 이유&lt;/h3>
&lt;p>블로그에서 기사를 가져올 때 페이지의 전체 HTML을 얻습니다. 여기에는 기사 콘텐츠 그 이상을 포함합니다. 탐색 메뉴, 머리글, 바닥글, 사이드바, 관련 기사 위젯, 댓글 섹션, 분석 및 추적용 스크립트, 스타일시트 및 모든 종류의 기타 요소가 포함됩니다.&lt;/p>
&lt;p>이 모든 것을 AI에 보내면 몇 가지 나쁜 일이 일어날 것입니다. AI는 관련 없는 텍스트를 많이 처리해야 하고 토큰을 낭비하며 잠재적으로 분석을 혼란스럽게 해야 합니다. 탐색 및 바닥글 텍스트가 요약에 포함될 수 있습니다. 스크립트와 CSS는 콘텐츠로 취급되어 분석을 더욱 오염시킵니다.&lt;/p>
&lt;p>우리는 인간 독자가 실제로 읽을 부분인 기사 내용만 추출해야 합니다.&lt;/p>
&lt;h3 id="htmlagilitypack-사용">HtmlAgilityPack 사용&lt;/h3>
&lt;p>HtmlAgilityPack은 .NET용 강력한 HTML 구문 분석 라이브러리입니다. XML과 달리 HTML은 형식이 잘못된 경우가 많습니다. 태그가 제대로 닫히지 않거나 속성이 올바르게 인용되지 않을 수 있습니다. HtmlAgilityPack은 이 모든 것을 우아하게 처리하여 쿼리하고 조작할 수 있는 DOM과 같은 구조를 제공합니다.&lt;/p>
&lt;p>추출 기능은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ExtractTextFromHtml(&lt;span style="color:#ff7b72">string&lt;/span> html)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(html))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> doc = &lt;span style="color:#ff7b72">new&lt;/span> HtmlDocument();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> doc.LoadHtml(html);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> nodesToRemove = doc.DocumentNode.SelectNodes(&lt;span style="color:#a5d6ff">&amp;#34;//script|//style|//nav|//footer|//header&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (nodesToRemove != &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> node &lt;span style="color:#ff7b72">in&lt;/span> nodesToRemove)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> node.Remove();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> text = doc.DocumentNode.InnerText;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> text = System.Text.RegularExpressions.Regex.Replace(text, &lt;span style="color:#a5d6ff">@&amp;#34;\s+&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> text.Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>HTML을 HtmlDocument에 로드하여 트리 구조로 구문 분석합니다. 그런 다음 XPath를 사용하여 제거하려는 모든 노드를 선택합니다. XPath 표현식 &lt;code>//script|//style|//nav|//footer|//header&lt;/code>는 모든 스크립트 요소(필요하지 않은 JavaScript 코드), 스타일 요소(필요하지 않은 CSS), nav 요소(탐색 메뉴), 바닥글 요소 및 헤더 요소를 선택합니다.&lt;/p>
&lt;p>이러한 노드를 제거한 후 HTML 태그를 제거하는 동안 모든 텍스트 내용을 추출하는 InnerText 속성을 얻습니다. 이는 기사의 일반 텍스트를 제공합니다.마지막으로 공백을 정리합니다. HTML에는 서식 지정을 위해 공백, 탭, 줄 바꿈 등 많은 추가 공백이 있는 경우가 많습니다. 정규식을 사용하여 일련의 공백 문자를 단일 공백으로 바꾼 다음 결과를 자릅니다.&lt;/p>
&lt;h3 id="전체-기사-가져오기">전체 기사 가져오기&lt;/h3>
&lt;p>RSS 피드는 전체 기사 내용이 아닌 요약만 제공합니다. 전체 텍스트를 얻으려면 기사의 웹 페이지를 가져와야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; FetchArticleContentAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpClient.DefaultRequestHeaders.Add(&lt;span style="color:#a5d6ff">&amp;#34;User-Agent&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;VsFeedLinkedin/1.0&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetStringAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;⚠️ Failed to fetch article content: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 간단합니다. 기사 URL에 대해 HTTP GET 요청을 하고 HTML 응답을 반환합니다. 네트워크 요청이 실패할 수 있으므로 try-catch로 래핑하고 전체 애플리케이션을 충돌시키는 것보다 빈 문자열을 반환하는 것이 좋습니다.&lt;/p>
&lt;h2 id="영구-문서-만들기">영구 문서 만들기&lt;/h2>
&lt;h3 id="마크다운-파일을-생성하는-이유">마크다운 파일을 생성하는 이유&lt;/h3>
&lt;p>기사를 분석할 때마다 해당 분석을 문서화하는 상세한 마크다운 파일을 생성합니다. 이는 여러 가지 목적으로 사용됩니다.&lt;/p>
&lt;p>먼저 검색 가능한 아카이브를 생성합니다. 시간이 지남에 따라 분석된 기사 모음을 구축하게 됩니다. 이러한 파일을 검색하여 특정 주제에 대한 과거 콘텐츠를 찾을 수 있습니다.&lt;/p>
&lt;p>둘째, 투명성을 제공합니다. 전체 분석 및 LinkedIn 게시물을 포함하여 각 기사에 대해 AI가 생성한 내용을 정확하게 확인할 수 있습니다.&lt;/p>
&lt;p>셋째, 디버깅에 유용합니다. 게시물에 문제가 있는 경우 마크다운 파일을 보고 무슨 일이 일어났는지 이해할 수 있습니다.&lt;/p>
&lt;h3 id="마크다운-생성기-클래스">마크다운 생성기 클래스&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MarkdownGenerator&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _outputDirectory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> MarkdownGenerator(&lt;span style="color:#ff7b72">string&lt;/span> outputDirectory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _outputDirectory = outputDirectory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Directory.Exists(_outputDirectory))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Directory.CreateDirectory(_outputDirectory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateMarkdownFile(ArticleAnalysis analysis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sb = &lt;span style="color:#ff7b72">new&lt;/span> StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> safeTitle = GenerateSafeFileName(analysis.Title);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fileName = &lt;span style="color:#a5d6ff">$&amp;#34;{analysis.AnalyzedAt:yyyy-MM-dd}_{safeTitle}.md&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> filePath = Path.Combine(_outputDirectory, fileName);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;# {analysis.Title}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## Article Information&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Author:** {analysis.Author}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **URL:** [{analysis.Url}]({analysis.Url})&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Published:** {analysis.PublishDate:yyyy-MM-dd}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Analyzed:** {analysis.AnalyzedAt:yyyy-MM-dd HH:mm:ss}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Tags:** {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, analysis.Tags)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## AI Analysis&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Summary&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.Summary);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Key Topics&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.KeyTopics);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Relevance for Developers&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.Relevance);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## Generated LinkedIn Post&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;```&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#f85149">분석&lt;/span>.LinkedInPost);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;```&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;*This analysis was generated using AI (Semantic Kernel with Azure OpenAI)*&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.WriteAllText(filePath, sb.ToString());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> filePath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>생성자는 출력 디렉터리 경로를 가져와서 존재하지 않는 경우 생성합니다. generateMarkdownFile 메서드는 ArticleAnalytic 개체를 사용하여 적절한 형식의 마크다운 문서를 생성합니다.&lt;/p>
&lt;p>파일 이름에는 날짜와 제목의 삭제된 버전이 포함됩니다. 이를 통해 파일을 시간순으로 쉽게 정렬하고 한눈에 식별할 수 있습니다.&lt;/p>
&lt;h3 id="안전하지-않은-파일-이름-처리">안전하지 않은 파일 이름 처리&lt;/h3>
&lt;p>기사 제목에는 파일 이름에 허용되지 않는 문자(콜론, 슬래시, 물음표, 따옴표 등)가 포함될 수 있습니다. 우리는 다음을 소독해야 합니다:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateSafeFileName(&lt;span style="color:#ff7b72">string&lt;/span> title)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> invalidChars = Path.GetInvalidFileNameChars();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> safeTitle = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>(title
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(c =&amp;gt; !invalidChars.Contains(c))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> safeTitle = safeTitle.Replace(&lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>).Replace(&lt;span style="color:#a5d6ff">&amp;#34;--&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (safeTitle.Length &amp;gt; &lt;span style="color:#a5d6ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> safeTitle = safeTitle.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">50&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> safeTitle.TrimEnd(&lt;span style="color:#a5d6ff">&amp;#39;-&amp;#39;&lt;/span>).ToLowerInvariant();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Path.GetInvalidFileNameChars()를 사용하여 현재 운영 체제에서 파일 이름에 나타날 수 없는 문자 목록을 가져옵니다. 이를 필터링하고, 가독성을 위해 공백을 하이픈으로 바꾸고, 길이를 50자로 제한하고, 일관성을 위해 소문자로 변환합니다.&lt;/p>
&lt;h2 id="텔레그램-알림-설정">텔레그램 알림 설정&lt;/h2>
&lt;h3 id="내가-텔레그램을-선택한-이유">내가 텔레그램을 선택한 이유&lt;/h3>
&lt;p>알림 구성 요소의 경우 이메일, SMS, Slack, Discord 및 Telegram과 같은 여러 옵션을 고려했습니다. 저는 여러 가지 이유로 궁극적으로 Telegram을 선택했습니다.&lt;/p>
&lt;p>API는 합리적인 사용을 위해 속도 제한 없이 완전 무료입니다. 많은 알림 서비스에는 무료로 보낼 수 있는 메시지 수에 제한이 있지만 Telegram은 봇 메시지를 개별 사용자에게 제한하지 않습니다.&lt;/p>
&lt;p>봇 API는 놀라울 정도로 간단합니다. JSON 페이로드가 포함된 HTTP 요청일 뿐입니다. 복잡한 인증 흐름이 없으며 기본 기능에 웹후크가 필요하지 않습니다.텔레그램은 휴대폰, 데스크톱, 웹 브라우저 등 어디에서나 작동합니다. 내가 어디에 있든 알림을 받고 즉시 응답할 수 있습니다.&lt;/p>
&lt;p>메시지는 다양한 형식을 지원합니다. 굵은 텍스트, 기울임꼴, 심지어 코드 블록을 사용하여 알림을 더 읽기 쉽게 만들 수 있습니다.&lt;/p>
&lt;h3 id="텔레그램-봇-만들기">텔레그램 봇 만들기&lt;/h3>
&lt;p>텔레그램 봇을 설정하는 것은 놀라울 정도로 쉽습니다. Telegram을 열고 @BotFather를 검색하세요. 이는 봇 생성 및 관리를 위한 Telegram의 공식 봇입니다. BotFather와 대화를 시작하고 /newbot 명령을 보냅니다. BotFather는 봇의 이름(표시 이름)과 사용자 이름(고유해야 하며 &amp;ldquo;bot&amp;quot;으로 끝나야 함)을 묻습니다. 이를 제공하면 BotFather가 봇을 생성하고 API 토큰을 제공합니다. 이 토큰은 비밀번호와 같습니다. 비밀로 유지하고 공개 저장소에 커밋하지 마세요.&lt;/p>
&lt;p>봇이 메시지를 보낼 위치를 알 수 있도록 채팅 ID를 찾으려면 검색하고 시작을 눌러 새 봇과 대화를 시작하세요. 그런 다음 브라우저에서 또는 컬을 사용하여 URL &lt;code>https://api.telegram.org/bot&amp;lt;YOUR_TOKEN&amp;gt;/getUpdates&lt;/code>에 액세스합니다. 응답에서 &lt;code>chat&lt;/code> 개체를 찾으세요. &lt;code>id&lt;/code> 필드는 채팅 ID입니다.&lt;/p>
&lt;h3 id="api를-통해-메시지-보내기">API를 통해 메시지 보내기&lt;/h3>
&lt;p>텔레그램 메시지를 보내는 기능은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task SendToTelegramAsync(&lt;span style="color:#ff7b72">string&lt;/span> botToken, &lt;span style="color:#ff7b72">string&lt;/span> chatId, &lt;span style="color:#ff7b72">string&lt;/span> message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> telegramApiUrl = &lt;span style="color:#a5d6ff">$&amp;#34;https://api.telegram.org/bot{botToken}/sendMessage&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> payload = &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chat_id = chatId,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> text = message,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parse_mode = &lt;span style="color:#a5d6ff">&amp;#34;HTML&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> jsonContent = JsonSerializer.Serialize(payload);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> content = &lt;span style="color:#ff7b72">new&lt;/span> StringContent(jsonContent, Encoding.UTF8, &lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.PostAsync(telegramApiUrl, content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> errorContent = &lt;span style="color:#ff7b72">await&lt;/span> response.Content.ReadAsStringAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">$&amp;#34;Telegram API error: {response.StatusCode} - {errorContent}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Telegram Bot API는 REST 기반입니다. 채팅 ID(보내는 위치), 메시지 텍스트(보내는 내용) 및 선택적으로 구문 분석 모드(서식 지정용)가 포함된 JSON 본문을 사용하여 sendMessage 엔드포인트에 대한 POST 요청을 수행합니다.&lt;/p>
&lt;p>parse_mode를 &amp;ldquo;HTML&amp;quot;로 설정하면 메시지에 &lt;code>&amp;lt;b&amp;gt;bold&amp;lt;/b&amp;gt;&lt;/code> 및 &lt;code>&amp;lt;i&amp;gt;italic&amp;lt;/i&amp;gt;&lt;/code>와 같은 기본 HTML 태그를 사용할 수 있습니다. 이렇게 하면 알림을 더 읽기 쉽게 만들 수 있지만 현재 사용 사례에서는 일반 텍스트를 보냅니다.&lt;/p>
&lt;p>요청이 실패하면 무엇이 잘못되었는지에 대한 세부정보와 함께 예외가 발생합니다. 이는 무언가가 작동하지 않는 경우 디버깅에 도움이 됩니다.&lt;/p>
&lt;h2 id="애플리케이션-구성">애플리케이션 구성&lt;/h2>
&lt;h3 id="환경-변수">환경 변수&lt;/h3>
&lt;p>우리 애플리케이션에는 API 키, 봇 토큰, 엔드포인트 URL 등 여러 가지 민감한 정보가 필요합니다. 우리는 이것을 하드코딩하거나 버전 관리에 커밋해서는 안 됩니다. 대신 애플리케이션이 실행되는 각 환경에서 안전하게 설정할 수 있는 환경 변수를 사용합니다.&lt;/p>
&lt;p>텔레그램의 경우 TELEGRAM_BOT_TOKEN(BotFather가 제공한 토큰)과 TELEGRAM_CHAT_ID(메시지를 보내야 하는 채팅 ID)가 필요합니다.&lt;/p>
&lt;p>Azure OpenAI의 경우 AZURE_OPENAI_ENDPOINT(리소스 URL), AZURE_OPENAI_API_KEY(API 키) 및 AZURE_OPENAI_DEPLOYMENT(&amp;ldquo;gpt-4o&amp;quot;와 같은 배포된 모델 이름)가 필요합니다.&lt;/p>
&lt;h3 id="코드에서-구성-로드">코드에서 구성 로드&lt;/h3>
&lt;p>애플리케이션 시작 시 이러한 값을 로드하는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> telegramBotToken = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;TELEGRAM_BOT_TOKEN&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> telegramChatId = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;TELEGRAM_CHAT_ID&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiEndpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiKey = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_API_KEY&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiDeployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> aiAnalysisEnabled = !&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(azureOpenAiEndpoint) &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> !&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(azureOpenAiKey);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Environment.GetEnvironmentVariable을 사용하여 각 값을 읽습니다. 배포 이름에는 값이 설정되지 않은 경우 기본값인 &amp;ldquo;gpt-4o&amp;quot;가 제공됩니다.&lt;/p>
&lt;p>그런 다음 엔드포인트와 API 키가 모두 있는지 확인하여 AI 분석을 활성화해야 하는지 여부를 확인합니다. 이를 통해 Azure OpenAI가 구성되지 않은 경우 애플리케이션이 저하된 모드에서 실행될 수 있습니다. AI 분석 없이도 여전히 피드를 가져오고 기사를 추적합니다.### 우아한 저하&lt;/p>
&lt;p>이러한 우아한 저하 개념은 중요합니다. 하나의 선택적 기능이 구성되지 않았기 때문에 애플리케이션이 충돌하는 것을 원하지 않습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ArticleAnalyzer? articleAnalyzer = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MarkdownGenerator? markdownGenerator = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (aiAnalysisEnabled)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🤖 AI Analysis enabled - Using Azure OpenAI with Semantic Kernel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> articleAnalyzer = &lt;span style="color:#ff7b72">new&lt;/span> ArticleAnalyzer(azureOpenAiEndpoint!, azureOpenAiKey!, azureOpenAiDeployment);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> markdownGenerator = &lt;span style="color:#ff7b72">new&lt;/span> MarkdownGenerator(articlesOutputDir);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;ℹ️ AI Analysis disabled - Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to enable&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>AI가 활성화되면 분석기와 마크다운 생성기를 만듭니다. 그렇지 않은 경우 null로 두고 처리 중에 AI 관련 단계를 건너뜁니다. AI 향상 없이도 애플리케이션은 피드를 가져오고 기본 알림을 보내 여전히 가치를 제공합니다.&lt;/p>
&lt;h2 id="github-actions로-자동화">GitHub Actions로 자동화&lt;/h2>
&lt;h3 id="github-action을-사용해야-하는-이유">GitHub Action을 사용해야 하는 이유&lt;/h3>
&lt;p>이 솔루션의 진정한 힘은 자동화에서 나옵니다. 우리는 애플리케이션을 몇 시간마다 수동으로 실행하고 싶지 않고 백그라운드에서 자동으로 실행되기를 원합니다.&lt;/p>
&lt;p>GitHub Actions가 이에 적합합니다. GitHub에 내장되어 있으므로 추가로 설정할 서비스가 없습니다. 공용 저장소의 경우 무료이며 개인 저장소의 경우 넉넉한 무료 시간이 포함되어 있습니다. 일정에 따라 실행되어 정기적으로 애플리케이션을 트리거할 수 있습니다. API 키를 안전하게 저장하기 위한 비밀 관리 기능이 내장되어 있습니다. 그리고 변경 사항을 저장소에 다시 커밋하여 추적 파일을 최신 상태로 유지할 수 있습니다.&lt;/p>
&lt;h3 id="워크플로-파일">워크플로 파일&lt;/h3>
&lt;p>.github/workflows/fetch-and-notify.yml에 다음 내용이 포함된 파일을 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Fetch DevBlogs and Notify&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">schedule&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">cron&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;0 */6 * * *&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">workflow_dispatch&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">fetch-and-notify&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">runs-on&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">ubuntu-latest&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">steps&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Checkout repository&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/checkout@v4&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Setup .NET&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/setup-dotnet@v4&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">with&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dotnet-version&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;9.0.x&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Restore dependencies&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet restore src/VsFeedLinkedin.csproj&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Build&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet build src/VsFeedLinkedin.csproj --no-restore&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Run application&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">env&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">TELEGRAM_BOT_TOKEN&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.TELEGRAM_BOT_TOKEN }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">TELEGRAM_CHAT_ID&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.TELEGRAM_CHAT_ID }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_ENDPOINT&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_ENDPOINT }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_API_KEY&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_API_KEY }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_DEPLOYMENT&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_DEPLOYMENT }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet run --project src/VsFeedLinkedin.csproj&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Commit and push changes&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>|&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git config user.name &amp;#34;GitHub Actions Bot&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git config user.email &amp;#34;actions@github.com&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> if [[ -n $(git status --porcelain posted-articles.md generated-posts/) ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> TIMESTAMP=$(date +%Y%m%d_%H%M%S)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git add posted-articles.md generated-posts/
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git commit -m &amp;#34;chore($TIMESTAMP): processed new articles&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git push
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> echo &amp;#34;No changes to commit&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> fi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>각 부분을 설명하겠습니다. on 섹션은 워크플로가 실행되는 시기를 정의합니다. 일정 트리거는 cron 구문을 사용합니다. &lt;code>0 */6 * * *&lt;/code>는 &amp;ldquo;매 6시간의 0분에&amp;quot;를 의미합니다. 따라서 워크플로는 UTC 기준 자정, 오전 6시, 정오 및 오후 6시에 실행됩니다. Workflow_dispatch 트리거를 사용하면 GitHub UI에서 수동으로 실행할 수 있어 테스트에 유용합니다.&lt;/p>
&lt;p>작업은 Linux 가상 머신인 ubuntu-latest에서 실행됩니다. 저장소를 확인하고, .NET 9를 설정하고, NuGet 패키지를 복원하고, 프로젝트를 빌드합니다.&lt;/p>
&lt;p>애플리케이션 실행 단계에서는 마법 같은 일이 일어납니다. ${{ secrets.SECRET_NAME }} 구문을 사용하여 비밀을 환경 변수로 전달합니다. 이러한 비밀은 GitHub에 안전하게 저장되며 로그에 노출되지 않습니다.&lt;/p>
&lt;p>마지막으로 모든 변경 사항을 저장소에 다시 커밋합니다. 봇 ID로 Git을 구성하고, 추적 파일이나 생성된 게시물 디렉터리에 변경 사항이 있는지 확인하고, 그렇다면 커밋을 생성하여 푸시합니다.&lt;/p>
&lt;h3 id="비밀-설정">비밀 설정&lt;/h3>
&lt;p>GitHub 저장소에 비밀을 추가하려면 저장소의 설정, 비밀 및 변수, 작업으로 이동하세요. &amp;ldquo;새 저장소 비밀&amp;quot;을 클릭하고 각 환경 변수를 추가하십시오. 이름은 워크플로 파일에서 참조하는 이름과 정확히 일치해야 합니다.&lt;/p>
&lt;h2 id="마무리">마무리&lt;/h2>
&lt;h3 id="우리가-만든-것">우리가 만든 것&lt;/h3>
&lt;p>우리가 다룬 모든 내용을 되돌아보면, 우리는 지루한 수동 프로세스였던 것을 자동화하는 포괄적인 AI 기반 RSS 피드 수집기를 구축했습니다. 이 애플리케이션은 7개의 Microsoft DevBlog 피드를 자동으로 모니터링하여 모든 새 기사가 게시되는 즉시 이를 포착합니다. 동일한 기사가 여러 피드에 나타나는 경우를 인식하여 중복 제거의 복잡성을 처리합니다.Semantic Kernel 및 Azure OpenAI를 기반으로 하는 AI 분석은 기사 콘텐츠를 읽고 이해하고, 요약을 생성하고, 주요 주제를 식별하고, 관련성을 설명하는 작업을 모두 자동으로 수행합니다. 가장 중요한 점은 최소한의 편집만으로 공유할 수 있는 매력적인 LinkedIn 게시물을 생성한다는 것입니다.&lt;/p>
&lt;p>Telegram 통합은 검토할 새 콘텐츠가 있을 때마다 휴대폰으로 알림을 받을 수 있음을 의미합니다. 메시지를 보고 공유할지 결정하고 즉시 조치를 취할 수 있습니다.&lt;/p>
&lt;p>그리고 일정에 따라 GitHub Actions에서 실행되기 때문에 어떤 작업도 기억할 필요가 없습니다. 시스템은 백그라운드에서 작동하며 공유할 가치가 있는 내용이 있을 때만 참여합니다.&lt;/p>
&lt;h3 id="이를-가능하게-한-기술">이를 가능하게 한 기술&lt;/h3>
&lt;p>이 프로젝트는 각각 중요한 역할을 하는 여러 기술을 통합했습니다. .NET 9는 최신 언어 기능과 뛰어난 성능으로 견고한 기반을 제공했습니다. Semantic Kernel은 API 호출 및 응답 관리의 모든 복잡성을 처리하여 AI 통합을 간단하게 만들었습니다. Azure OpenAI는 기술 콘텐츠를 실제로 이해하고 분석하는 능력인 인텔리전스를 제공했습니다. HtmlAgilityPack은 웹 페이지에서 깨끗한 텍스트를 추출하는 복잡한 문제를 해결했습니다. System.ServiceModel.Syndication을 사용하면 RSS 구문 분석이 쉬워졌습니다. Telegram Bot API는 우리에게 신뢰할 수 있는 무료 알림을 제공했습니다. 그리고 GitHub Actions는 자동화된 예약 실행을 통해 이 모든 것을 하나로 묶었습니다.&lt;/p>
&lt;h3 id="비용에-대한-생각">비용에 대한 생각&lt;/h3>
&lt;p>여러분이 가질 수 있는 한 가지 질문은 다음과 같습니다. 이 작업을 실행하는 데 드는 비용은 얼마입니까? 대답은: 전혀 많지 않습니다.&lt;/p>
&lt;p>텔레그램은 완전히 무료입니다. 봇을 통해 메시지를 보내는 데 비용이 들지 않습니다.&lt;/p>
&lt;p>GitHub Actions는 공개 저장소에 무료로 제공됩니다. 프라이빗 리포지토리의 경우 무료 등급으로 월 2,000분을 이용할 수 있으며 이는 우리 사용 사례에 충분합니다.&lt;/p>
&lt;p>Azure OpenAI는 유일한 유료 구성 요소이며 비용은 최소화됩니다. GPT-4o를 사용하면 일반적인 블로그 기사를 분석하는 데 1센트에서 3센트 정도의 비용이 듭니다. 한 달에 수십 개의 기사를 처리하더라도 AI 비용은 1달러 미만으로 보고 있습니다.&lt;/p>
&lt;h3 id="다음에는-어디로-갈-수-있나요">다음에는 어디로 갈 수 있나요?&lt;/h3>
&lt;p>이 솔루션은 내 요구 사항에 매우 적합하지만 이를 확장할 수 있는 방법은 많습니다. 여러 소셜 플랫폼에 대한 지원을 추가할 수 있습니다. LinkedIn 외에도 Twitter/X, Mastodon 또는 Bluesky에 게시할 수도 있습니다. 감정 분석을 구현하여 시간 경과에 따른 기사의 분위기를 추적하고 추세를 파악할 수 있습니다. 다양한 피드에 대해 다양한 프롬프트 템플릿을 허용하여 다양한 주제에 대해 다양한 스타일의 게시물을 생성할 수 있습니다. 텔레그램을 사용하는 대신 게시물을 검토하고 관리하기 위한 웹 대시보드를 구축할 수 있습니다. 게시된 콘텐츠에 대한 참여 지표를 추적하여 어떤 주제가 청중에게 가장 큰 공감을 불러일으키는지 확인할 수 있습니다.&lt;/p>
&lt;h3 id="최종-생각이-프로젝트에서-제가-가장-좋아하는-점은-제가-강력하게-믿는-철학을-구현한다는-것입니다-자동화는-지루한-부분을-처리하고-창의적이고-의사결정적인-부분은-인간에게-맡겨야-한다는-것입니다-시스템은-가져오기-구문-분석-분석-생성-등-모든-힘든-작업을-수행하지만-공유하기-전에-모든-것을-검토합니다-ai가-생성한-게시물은-내가-맞춤화하고-개인화할-수-있는-출발점입니다">최종 생각이 프로젝트에서 제가 가장 좋아하는 점은 제가 강력하게 믿는 철학을 구현한다는 것입니다. 자동화는 지루한 부분을 처리하고 창의적이고 의사결정적인 부분은 인간에게 맡겨야 한다는 것입니다. 시스템은 가져오기, 구문 분석, 분석, 생성 등 모든 힘든 작업을 수행하지만 공유하기 전에 모든 것을 검토합니다. AI가 생성한 게시물은 내가 맞춤화하고 개인화할 수 있는 출발점입니다.&lt;/h3>
&lt;p>.NET, Semantic Kernel 및 Azure OpenAI의 강력한 기능을 결합하여 품질과 일관성을 유지하면서 매주 수동 작업 시간을 절약하는 도구를 만들었습니다. 이는 일상 생활에 실질적인 변화를 가져오는 일종의 실용적인 자동화입니다.&lt;/p>
&lt;p>비슷한 것을 만들거나 개선할 아이디어가 있다면 알려주시면 감사하겠습니다. LinkedIn에 언제든지 문의하세요!&lt;/p>
&lt;p>즐거운 코딩 보내시고, 즐거운 크리스마스 보내세요! 🎄&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>NuGet</category><category>CI/CD</category><category>Docker</category><category>AI</category></item><item><title>Microsoft의 에이전트 프레임워크를 사용하여 다중 에이전트 AI 시스템 구축</title><link>https://emimontesdeoca.github.io/ko/posts/microsoft-agent-framework-multi-agent/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/microsoft-agent-framework-multi-agent/</guid><description>.NET에서 Microsoft의 Agent Framework를 사용하여 다중 에이전트 AI 시스템을 구축, 조정 및 배포하기 위한 실무 가이드입니다.</description><content:encoded>&lt;h2 id="소개">소개&lt;/h2>
&lt;p>우리는 멀티 에이전트 AI 시스템 시대에 들어섰습니다. 모든 것을 처리하는 단일 모놀리식 AI 대신, 업계는 잘 조직된 전문가 팀처럼 복잡한 문제를 해결하기 위해 협력하는 전문 에이전트로 전환하고 있습니다. 한 에이전트는 조사하고, 다른 에이전트는 분석하고, 세 번째 에이전트는 작성하고, 코디네이터는 모든 사람의 정보를 추적합니다.&lt;/p>
&lt;p>대규모 언어 모델로 작업한 경우 단일 프롬프트로 수행할 수 있는 작업의 한계에 도달했을 가능성이 높습니다. 컨텍스트 창이 가득 차고 지침이 엉키며 품질이 저하됩니다. 다중 에이전트 아키텍처는 복잡한 작업을 각 에이전트가 한 분야의 전문가인 집중된 책임으로 분해하여 이 문제를 해결합니다.&lt;/p>
&lt;p>광범위한 Semantic Kernel 에코시스템의 일부인 Microsoft Agent Framework는 .NET 개발자에게 이러한 종류의 시스템을 구축하기 위한 최고의 도구 키트를 제공합니다. 이 게시물에서는 시작하는 데 필요한 핵심 개념, 오케스트레이션 패턴 및 실제 코드를 다루면서 완전하게 작동하는 다중 에이전트 파이프라인으로 이동합니다.&lt;/p>
&lt;h2 id="microsoft의-에이전트-프레임워크란-무엇입니까">Microsoft의 에이전트 프레임워크란 무엇입니까?&lt;/h2>
&lt;p>에이전트 프레임워크는 .NET에서 AI 에이전트 및 다중 에이전트 시스템을 구축, 조정 및 배포하기 위한 Microsoft의 답변입니다. 이는 2023년부터 Microsoft의 AI 오케스트레이션을 위한 오픈 소스 SDK인 Semantic Kernel과 나란히 위치하며 긴밀하게 통합됩니다.&lt;/p>
&lt;p>이렇게 생각해보세요. &lt;strong>의미론적 커널&lt;/strong>은 기본 요소(커널, 플러그인, 메모리, 플래너)를 제공하는 반면 &lt;strong>에이전트 프레임워크&lt;/strong>는 에이전트 간 통신 및 조정을 위해 특별히 설계된 상위 수준 추상화를 제공합니다.&lt;/p>
&lt;p>프레임워크는 Azure OpenAI, OpenAI 및 Azure AI Foundry에서 호스팅되는 모델을 포함한 여러 모델 공급자를 지원합니다. 디자인 면에서는 모델에 구애받지 않지만 Azure 에코시스템과 긴밀하게 통합되어 있어 엔터프라이즈 시나리오에 특히 매력적입니다.&lt;/p>
&lt;p>주요 기능은 다음과 같습니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>여러 에이전트 유형&lt;/strong>: 다양한 백엔드에 대한 &lt;code>ChatCompletionAgent&lt;/code>, &lt;code>OpenAIAssistantAgent&lt;/code> 및 &lt;code>AzureAIAgent&lt;/code>&lt;/li>
&lt;li>&lt;strong>오케스트레이션 패턴&lt;/strong>: 순차, 동시, 핸드오프 및 그룹 채팅 워크플로&lt;/li>
&lt;li>&lt;strong>플러그인 에코시스템&lt;/strong>: 기본 C# 기능, OpenAPI 사양 및 MCP(Model Context Protocol) 도구를 사용하여 에이전트 확장&lt;/li>
&lt;li>&lt;strong>대화 관리&lt;/strong>: 스레딩, 이력 관리, 종료 전략 내장&lt;/li>
&lt;li>&lt;strong>관찰 가능성&lt;/strong>: 에이전트 상호 작용 추적을 위해 OpenTelemetry와 통합&lt;/li>
&lt;/ul>
&lt;h2 id="주요-개념">주요 개념&lt;/h2>
&lt;p>코드를 작성하기 전에 어휘를 설정해 보겠습니다. 에이전트 프레임워크는 몇 가지 핵심 추상화를 중심으로 진행됩니다.&lt;/p>
&lt;h3 id="에이전트">에이전트&lt;/h3>
&lt;p>에이전트는 특정 지침(시스템 프롬프트), 이름 및 선택적으로 플러그인 또는 도구 세트로 구성된 AI 모델의 지원을 받는 엔터티입니다. 각 에이전트는 전문가입니다. 에이전트가 무엇을 알고 있는지, 무엇을 할 수 있는지, 어떻게 행동해야 하는지 정의할 수 있습니다.&lt;/p>
&lt;h3 id="chatcompletionagent가장-간단한-에이전트-유형입니다-채팅-완료-엔드포인트azure-openai-openai-등를-래핑하고-대화를-유지-관리합니다-호출-간에는-상태-비저장입니다-기록을-제공하면-응답합니다-이렇게-하면-가볍고-추론하기-쉽습니다">ChatCompletionAgent가장 간단한 에이전트 유형입니다. 채팅 완료 엔드포인트(Azure OpenAI, OpenAI 등)를 래핑하고 대화를 유지 관리합니다. 호출 간에는 상태 비저장입니다. 기록을 제공하면 응답합니다. 이렇게 하면 가볍고 추론하기 쉽습니다.&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ChatCompletionAgent agent = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;You are a senior code reviewer. Analyze the provided code for bugs, security issues, and style violations. Be concise and actionable.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="openaiassistantagent">OpenAIAssistantAgent&lt;/h3>
&lt;p>이 에이전트 유형은 서버 측 대화 상태, 파일 처리 및 코드 해석을 제공하는 OpenAI Assistants API를 활용합니다. 더 무겁지만 지속적인 스레드와 코드 해석기 및 파일 검색과 같은 내장 도구를 제공합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>OpenAIAssistantAgent agent = &lt;span style="color:#ff7b72">await&lt;/span> OpenAIAssistantAgent.CreateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clientProvider: clientProvider,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> definition: &lt;span style="color:#ff7b72">new&lt;/span> OpenAIAssistantDefinition(&lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;DataAnalyst&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;You analyze datasets and produce statistical summaries.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="에이전트그룹채팅">에이전트그룹채팅&lt;/h3>
&lt;p>오케스트레이터입니다. &lt;code>AgentGroupChat&lt;/code>는 여러 에이전트 간의 다단계 대화를 관리하여 다음에 말할 사람, 대화 종료 시기 및 기록 공유 방법을 제어합니다. 다중 에이전트 협업의 마법이 일어나는 곳입니다.&lt;/p>
&lt;h2 id="오케스트레이션-패턴">오케스트레이션 패턴&lt;/h2>
&lt;p>프레임워크는 각기 다른 문제에 적합한 네 가지 기본 오케스트레이션 패턴을 지원합니다.&lt;/p>
&lt;h3 id="순차">순차&lt;/h3>
&lt;p>에이전트는 정의된 순서에 따라 하나씩 실행됩니다. 에이전트 A의 출력은 에이전트 B로 공급되고, 에이전트 B의 출력은 에이전트 C로 공급됩니다. 이는 초안 → 검토 → 편집 → 게시와 같은 파이프라인에 이상적입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Conceptual flow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> draft = &lt;span style="color:#ff7b72">await&lt;/span> writerAgent.InvokeAsync(&lt;span style="color:#a5d6ff">&amp;#34;Write a blog post about .NET 9&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> reviewed = &lt;span style="color:#ff7b72">await&lt;/span> reviewerAgent.InvokeAsync(&lt;span style="color:#a5d6ff">$&amp;#34;Review this: {draft}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> edited = &lt;span style="color:#ff7b72">await&lt;/span> editorAgent.InvokeAsync(&lt;span style="color:#a5d6ff">$&amp;#34;Edit based on feedback: {reviewed}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="동시">동시&lt;/h3>
&lt;p>여러 에이전트가 동일한 입력에 대해 동시에 작업합니다. 작업을 펼치고 결과를 집계합니다. 세 명의 검토자가 동일한 끌어오기 요청을 보는 것과 같이 다양한 관점을 얻는 데 적합합니다.&lt;/p>
&lt;h3 id="핸드오프">핸드오프&lt;/h3>
&lt;p>에이전트는 대화 컨텍스트를 기반으로 다른 에이전트에게 제어권을 이전하기로 결정합니다. 이는 고객 서비스 팀의 업무 방식을 모방합니다. 일선 상담원이 기본 쿼리를 처리하고 필요할 때 전문가에게 에스컬레이션합니다.&lt;/p>
&lt;h3 id="그룹-채팅">그룹 채팅&lt;/h3>
&lt;p>여러 에이전트가 선택 전략에 따라 차례로 공개 대화에 참여합니다. &lt;code>AgentGroupChat&lt;/code> 클래스는 구성 가능한 차례 진행 및 종료 논리를 사용하여 이 패턴을 구현합니다.&lt;/p>
&lt;h2 id="첫-번째-에이전트-구축">첫 번째 에이전트 구축&lt;/h2>
&lt;p>실습해 봅시다. 첫 번째 에이전트를 단계별로 구축하는 방법은 다음과 같습니다.&lt;/p>
&lt;h3 id="전제조건">전제조건&lt;/h3>
&lt;p>다음이 필요합니다.&lt;/p>
&lt;ul>
&lt;li>.NET 9 SDK&lt;/li>
&lt;li>배포된 모델이 있는 Azure OpenAI 리소스(예: &lt;code>gpt-4o&lt;/code>)&lt;/li>
&lt;li>비주얼 스튜디오 또는 VS 코드&lt;/li>
&lt;/ul>
&lt;h3 id="프로젝트-설정">프로젝트 설정&lt;/h3>
&lt;p>새 콘솔 애플리케이션을 만들고 필요한 패키지를 설치합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new console -n AgentDemo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd AgentDemo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Agents.Core --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.AI.OpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.Identity
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="단순-에이전트-만들기">단순 에이전트 만들기&lt;/h3>
&lt;p>먼저 Azure OpenAI 구성으로 커널을 설정합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> credentials: &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Kernel kernel = builder.Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 에이전트를 생성하고 호출합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ChatCompletionAgent agent = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;TechWriter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer specializing &lt;span style="color:#ff7b72">in&lt;/span> software documentation.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Write clear, concise content aimed at experienced developers.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Use code examples when appropriate.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Always structure your output with headings and bullet points.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatHistory history = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddUserMessage(&lt;span style="color:#a5d6ff">&amp;#34;Explain dependency injection in .NET in 200 words.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent response &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(response.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그게 다야. 근무 중인 에이전트가 있습니다. 하지만 진정한 힘은 상담원들이 함께 일할 때 나옵니다.&lt;/p>
&lt;h2 id="agentgroupchat을-사용한-다중-에이전트-오케스트레이션">AgentGroupChat을 사용한 다중 에이전트 오케스트레이션&lt;/h2>
&lt;p>여러 에이전트가 협업하는 그룹 채팅이라는 좀 더 흥미로운 것을 만들어 보겠습니다. &lt;code>AgentGroupChat&lt;/code> 클래스는 다음에 말할 사람과 중지할 시간을 포함하여 대화 흐름을 관리합니다.&lt;/p>
&lt;h3 id="에이전트-정의">에이전트 정의&lt;/h3>
&lt;p>작가, 리뷰어, 편집자의 세 가지 에이전트를 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ChatCompletionAgent writer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content writer. When given a topic, produce a well-structured
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> draft. Focus &lt;span style="color:#ff7b72">on&lt;/span> clarity and technical accuracy.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When you receive feedback &lt;span style="color:#ff7b72">from&lt;/span> the Reviewer, incorporate it &lt;span style="color:#ff7b72">into&lt;/span> a
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> revised draft.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent reviewer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content reviewer. Analyze drafts &lt;span style="color:#ff7b72">for&lt;/span> technical accuracy,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clarity, and completeness. Provide specific, actionable feedback.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Do NOT rewrite the content &lt;span style="color:#f85149">—&lt;/span> only provide feedback.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When the content meets your standards, respond with: APPROVED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent editor = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Editor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are an editor. Once content &lt;span style="color:#ff7b72">is&lt;/span> approved &lt;span style="color:#ff7b72">by&lt;/span> the Reviewer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> polish it &lt;span style="color:#ff7b72">for&lt;/span> grammar, tone, and formatting.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Output only the final polished version.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When you have produced the final version, respond with: COMPLETE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="그룹-채팅-설정하기">그룹 채팅 설정하기&lt;/h3>
&lt;p>&lt;code>AgentGroupChat&lt;/code>에는 &lt;strong>선택 전략&lt;/strong>(다음에 말할 사람)과 &lt;strong>종료 전략&lt;/strong>(중지할 시기)이라는 두 가지 핵심 구성이 필요합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>AgentGroupChat chat = &lt;span style="color:#ff7b72">new&lt;/span>(writer, reviewer, editor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SelectionStrategy = &lt;span style="color:#ff7b72">new&lt;/span> SequentialSelectionStrategy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agents = [editor],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">12&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="맞춤형-종료-전략종료-전략은-대화가-끝나는-시기를-정의합니다-다음은-complete-키워드를-찾는-사용자-정의입니다">맞춤형 종료 전략종료 전략은 대화가 끝나는 시기를 정의합니다. 다음은 &amp;ldquo;COMPLETE&amp;rdquo; 키워드를 찾는 사용자 정의입니다.&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ApprovalTerminationStrategy&lt;/span> : TerminationStrategy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; ShouldAgentTerminateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agent agent,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IReadOnlyList&amp;lt;ChatMessageContent&amp;gt; history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CancellationToken cancellationToken = &lt;span style="color:#ff7b72">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">bool&lt;/span> isComplete = history
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Last()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Content?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Contains(&lt;span style="color:#a5d6ff">&amp;#34;COMPLETE&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase) ?? &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.FromResult(isComplete);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="대화-실행하기">대화 실행하기&lt;/h3>
&lt;p>사용자 메시지로 대화를 시작하고 상담원이 협업할 수 있도록 하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>chat.AddChatMessage(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(AuthorRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Write a 300-word technical overview of gRPC vs REST for microservices.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]: {message.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;---&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>흐름은 다음과 같습니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>작가&lt;/strong>가 초안을 작성합니다.&lt;/li>
&lt;li>&lt;strong>검토자&lt;/strong>가 피드백을 제공합니다.&lt;/li>
&lt;li>&lt;strong>작가&lt;/strong>는 피드백을 바탕으로 수정합니다.&lt;/li>
&lt;li>&lt;strong>검토자&lt;/strong>가 &amp;ldquo;승인됨&amp;quot;이라고 말합니다.&lt;/li>
&lt;li>&lt;strong>편집자&lt;/strong>가 다듬고 &amp;ldquo;완료&amp;quot;라고 말합니다.&lt;/li>
&lt;li>대화가 종료됩니다&lt;/li>
&lt;/ol>
&lt;p>이러한 앞뒤 이동은 종료 조건이 충족되거나 &lt;code>MaximumIterations&lt;/code>에 도달할 때까지 자동으로 계속됩니다.&lt;/p>
&lt;h2 id="플러그인-및-도구">플러그인 및 도구&lt;/h2>
&lt;p>에이전트는 외부 시스템과 상호 작용할 수 있을 때 진정으로 강력해집니다. Agent Framework는 세 가지 주요 확장 메커니즘을 지원합니다.&lt;/p>
&lt;h3 id="기본-기능커널-플러그인">기본 기능(커널 플러그인)&lt;/h3>
&lt;p>에이전트에 도구로 C# 메서드에 대한 액세스 권한을 부여할 수 있습니다. 에이전트는 필요하다고 판단되면 다음 함수를 호출합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ContentToolsPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;word_count&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Counts the number of words in the provided text.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> WordCount([Description(&lt;span style="color:#a5d6ff">&amp;#34;The text to count words in&amp;#34;&lt;/span>)] &lt;span style="color:#ff7b72">string&lt;/span> text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> text.Split(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>, StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;check_readability&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Calculates a readability score for the given text.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CheckReadability([Description(&lt;span style="color:#a5d6ff">&amp;#34;The text to analyze&amp;#34;&lt;/span>)] &lt;span style="color:#ff7b72">string&lt;/span> text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> words = text.Split(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>, StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sentences = text.Split([&lt;span style="color:#a5d6ff">&amp;#39;.&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;!&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;?&amp;#39;&lt;/span>], StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (sentences == &lt;span style="color:#a5d6ff">0&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unable to calculate — no sentences found.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">double&lt;/span> avgWordsPerSentence = (&lt;span style="color:#ff7b72">double&lt;/span>)words / sentences;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> avgWordsPerSentence &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt; &lt;span style="color:#a5d6ff">15&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Easy to read (avg {avgWordsPerSentence:F1} words/sentence)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt; &lt;span style="color:#a5d6ff">25&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Moderate difficulty (avg {avgWordsPerSentence:F1} words/sentence)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Difficult to read (avg {avgWordsPerSentence:F1} words/sentence). Consider shorter sentences.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>에이전트를 만들기 전에 커널에 플러그인을 등록하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromType&amp;lt;ContentToolsPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent analyst = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;ContentAnalyst&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;Analyze content using available tools. Report word count and readability.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> PromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="모델-컨텍스트-프로토콜mcp">모델 컨텍스트 프로토콜(MCP)&lt;/h3>
&lt;p>MCP는 AI 모델을 외부 도구 및 데이터 소스에 연결하기 위한 개방형 표준입니다. 에이전트 프레임워크는 MCP를 지원합니다. 즉, 에이전트는 모든 MCP 호환 서버에서 제공하는 도구를 사용할 수 있습니다. 이는 표준화된 인터페이스를 통해 파일 시스템, 데이터베이스, API 등에 대한 문을 열어줍니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Example: Adding an MCP server for file operations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromMcpServer(&lt;span style="color:#a5d6ff">&amp;#34;filesystem&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;http://localhost:3000/mcp&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 귀하의 에이전트가 귀하가 구축한 것에 국한되지 않고 다른 사람들이 개발하고 공유하는 MCP 도구 생태계를 활용할 수 있다는 것을 의미하기 때문에 특히 흥미롭습니다.&lt;/p>
&lt;h2 id="실제-사례-콘텐츠-검토-파이프라인">실제 사례: 콘텐츠 검토 파이프라인&lt;/h2>
&lt;p>실용적인 시나리오로 모든 것을 하나로 묶어 봅시다. 문서화 팀의 콘텐츠 검토를 자동화하는 내부 도구를 구축한다고 상상해 보십시오. 파이프라인에는 4단계가 있습니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>연구원&lt;/strong> — 관련 기술 정보 수집&lt;/li>
&lt;li>&lt;strong>작가&lt;/strong> — 연구를 바탕으로 초안을 작성합니다.&lt;/li>
&lt;li>&lt;strong>검토자&lt;/strong> — 정확성과 완전성을 확인합니다.&lt;/li>
&lt;li>&lt;strong>게시자&lt;/strong> — 최종 출력 형식을 지정하고 준비합니다.&lt;/li>
&lt;/ol>
&lt;p>요약된 구현은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the kernel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> credentials: &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Kernel kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Define specialized agents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent researcher = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical researcher. Given a topic, identify the key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> concepts, recent developments, and important details that should
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> be covered. Output a structured research brief.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent writer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer. Using the research brief provided,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> write a comprehensive, well-structured article. Include code
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> examples &lt;span style="color:#ff7b72">where&lt;/span> relevant. Target audience: experienced developers.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent reviewer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior technical reviewer. Check the article &lt;span style="color:#ff7b72">for&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Technical accuracy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Completeness relative to the research brief
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Code correctness
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Clarity and structure
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> If everything looks good, respond with APPROVED.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Otherwise, provide specific feedback &lt;span style="color:#ff7b72">for&lt;/span> revision.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent publisher = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Publisher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content publisher. Once the article &lt;span style="color:#ff7b72">is&lt;/span> approved,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> format it with proper markdown, &lt;span style="color:#ff7b72">add&lt;/span> a summary at the top,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and ensure consistent formatting throughout.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> End your response with COMPLETE.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the group chat&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AgentGroupChat chat = &lt;span style="color:#ff7b72">new&lt;/span>(researcher, writer, reviewer, publisher)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SelectionStrategy = &lt;span style="color:#ff7b72">new&lt;/span> SequentialSelectionStrategy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agents = [publisher],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">15&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Start the pipeline&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>chat.AddChatMessage(&lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthorRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Create a technical article about implementing health checks in ASP.NET Core microservices.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.ForegroundColor = message.AuthorName &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Cyan,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Green,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Yellow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Publisher&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Magenta,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; ConsoleColor.White
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;\n{&amp;#39;=&amp;#39;new string(&amp;#39;=&amp;#39;, 60)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#39;=&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">60&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.ResetColor();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 파이프라인은 에이전트 협업을 통해 완벽하게 조사하고, 작성하고, 검토하고, 형식화된 기사를 생성합니다. 각 에이전트는 자신이 가장 잘하는 일에 집중하고 &lt;code>AgentGroupChat&lt;/code>는 워크플로를 조정합니다.&lt;/p>
&lt;h2 id="모범-사례">모범 사례&lt;/h2>
&lt;p>여러 다중 에이전트 시스템을 구축한 후 가장 가치 있다고 생각한 패턴과 사례는 다음과 같습니다.&lt;/p>
&lt;h3 id="오류-처리">오류 처리&lt;/h3>
&lt;p>종료 전략에 항상 &lt;code>MaximumIterations&lt;/code>를 설정하세요. 이것이 없으면 상담원은 무한 루프에 빠질 수 있습니다. 특히 검토자가 계속 문제를 찾고 작성자가 개선 없이 계속 수정하는 경우에는 더욱 그렇습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">12&lt;/span> &lt;span style="color:#8b949e;font-style:italic">// Safety net&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>에이전트 호출을 try-catch 블록으로 래핑합니다. API 속도 제한, 네트워크 문제 및 모델 오류는 모두 프로덕션 시스템의 현실입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync(cancellationToken))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Process messages&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">catch&lt;/span> (HttpOperationException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Implement retry with exponential backoff&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(TimeSpan.FromSeconds(&lt;span style="color:#a5d6ff">30&lt;/span>), cancellationToken);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="관찰-가능성agent-framework는-opentelemetry와-통합되어-모든-에이전트-상호-작용-도구-호출-및-토큰-사용을-추적할-수-있습니다-이는-어떤-에이전트가-문제를-일으켰는지-항상-명확하지-않은-다중-에이전트-워크플로를-디버깅하는-데-필수적입니다">관찰 가능성Agent Framework는 OpenTelemetry와 통합되어 모든 에이전트 상호 작용, 도구 호출 및 토큰 사용을 추적할 수 있습니다. 이는 어떤 에이전트가 문제를 일으켰는지 항상 명확하지 않은 다중 에이전트 워크플로를 디버깅하는 데 필수적입니다.&lt;/h3>
&lt;p>Semantic Kernel 원격 분석 패키지를 추가하고 기본 내보내기 도구(Application Insights, Jaeger 등)를 구성하여 기본 원격 분석을 설정합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenTelemetry()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTracing(tracing =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddSource(&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.SemanticKernel*&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddOtlpExporter();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="비용-관리">비용 관리&lt;/h3>
&lt;p>다중 에이전트 시스템은 API 비용을 배가시킵니다. 모든 에이전트 차례는 API 호출이며 그룹 채팅은 많은 차례를 생성할 수 있습니다. 비용을 통제하기 위한 몇 가지 전략:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>간단한 에이전트를 위해 더 저렴한 모델을 사용하세요&lt;/strong>: 모든 에이전트에 GPT-4o가 필요한 것은 아닙니다. 평론가는 더 작은 모델로 작업할 수 있지만 작성자에게는 강력한 모델이 필요합니다.&lt;/li>
&lt;li>&lt;strong>기록 제한&lt;/strong>: 실행 설정에서 &lt;code>ReducedHistoryCount&lt;/code>를 사용하여 각 에이전트가 수신하는 대화 컨텍스트의 양을 제한합니다.&lt;/li>
&lt;li>&lt;strong>엄격한 반복 제한 설정&lt;/strong>: 합리적인 &lt;code>MaximumIterations&lt;/code> 값으로 폭주 대화를 방지합니다.&lt;/li>
&lt;li>&lt;strong>가능한 경우 캐시&lt;/strong>: 에이전트가 동일한 조회를 반복적으로 수행하는 경우 결과를 플러그인에 캐시합니다.&lt;/li>
&lt;/ul>
&lt;h3 id="에이전트-디자인">에이전트 디자인&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>지침에 집중&lt;/strong>: 각 상담원은 하나의 명확한 책임을 져야 합니다. 광범위한 지침은 모든 작업에서 평범한 성능으로 이어집니다.&lt;/li>
&lt;li>&lt;strong>출력 형식을 명시하세요&lt;/strong>: 상담원에게 응답 구성 방법을 정확하게 알려줍니다. 이는 다운스트림 구문 분석을 안정적으로 만듭니다.&lt;/li>
&lt;li>&lt;strong>종료 키워드 사용&lt;/strong>: 상담원이 완료되었음을 나타내기 위해 사용하는 명확한 신호(예: &amp;ldquo;승인됨&amp;rdquo; 또는 &amp;ldquo;완료&amp;rdquo;)를 정의합니다. 이는 종료 전략을 간단하고 예측 가능하게 만듭니다.&lt;/li>
&lt;/ul>
&lt;h2 id="결론">결론&lt;/h2>
&lt;p>다중 에이전트 AI 시스템은 지능형 애플리케이션 구축 방법의 근본적인 변화를 나타냅니다. 모든 것을 처리하기 위해 단일 프롬프트로 씨름하는 대신 문제를 전문적인 역할로 분해하고 상담원이 협업할 수 있도록 할 수 있습니다.&lt;/p>
&lt;p>Microsoft의 Agent Framework를 사용하면 .NET 개발자에게 이러한 기능이 실용적으로 제공됩니다. 에이전트, 그룹 채팅, 선택 및 종료 전략 등 추상화는 깨끗하며 자연스럽게 구성됩니다. Semantic Kernel의 플러그인 에코시스템과 Azure의 모델 호스팅이 결합되어 프로덕션 수준의 다중 에이전트 시스템을 구축하기 위한 전체 스택을 갖게 됩니다.&lt;/p>
&lt;p>프레임워크는 계속 발전하고 있지만(많은 패키지가 미리 보기 상태임) 핵심 패턴은 견고하고 방향은 명확합니다. .NET에서 AI 기반 애플리케이션을 구축하고 있다면 이제 다중 에이전트 아키텍처 실험을 시작할 때입니다.&lt;/p>
&lt;p>소규모로 시작하세요. 간단한 작업을 위해 협업하는 두 개의 에이전트를 구축하세요. 단일 에이전트 접근 방식에 비해 품질 향상이 확인되면 모든 것을 에이전트 팀으로 분해하고 싶을 것입니다.&lt;/p>
&lt;p>즐거운 코딩하세요!&lt;/p></content:encoded><category>AI</category><category>.NET</category><category>Agent Framework</category><category>Semantic Kernel</category></item><item><title>MarkupString을 사용하여 Blazor에서 원시 HTML 렌더링</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-markup-string-raw-html/</link><pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-markup-string-raw-html/</guid><description>이스케이프된 텍스트 대신 MarkupString을 사용하여 Blazor 구성 요소의 원시 HTML 콘텐츠를 안전하게 렌더링합니다.</description><content:encoded>&lt;p>얼마 전 저는 CMS에서 가져온 일부 HTML을 렌더링하는 데 필요한 구성 요소를 구축하고 있었습니다. 변수에 HTML 문자열이 있고 &lt;code>@myHtml&lt;/code>와 같은 템플릿에 놓았습니다. 물론 Blazor는 모든 것을 이스케이프 처리하고 실제 태그를 페이지의 텍스트로 렌더링했습니다. 내가 원하는 것이 아닙니다.&lt;/p>
&lt;h1 id="문제">문제&lt;/h1>
&lt;p>기본적으로 Blazor는 템플릿에서 렌더링하는 모든 문자열을 인코딩합니다. 따라서 다음과 같은 경우:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> content = &lt;span style="color:#a5d6ff">&amp;#34;&amp;lt;strong&amp;gt;Hello&amp;lt;/strong&amp;gt; &amp;lt;em&amp;gt;world&amp;lt;/em&amp;gt;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 당신은 이렇게 합니다:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@content&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>페이지에 &lt;strong>Hello&lt;/strong> &lt;em>world&lt;/em> 대신 리터럴 텍스트 &lt;code>&amp;lt;strong&amp;gt;Hello&amp;lt;/strong&amp;gt; &amp;lt;em&amp;gt;world&amp;lt;/em&amp;gt;&lt;/code>가 표시됩니다. Blazor는 XSS 공격을 방지하기 위해 의도적으로 이 작업을 수행하며, 이는 올바른 기본 동작입니다.&lt;/p>
&lt;h1 id="해결책-markupstring">해결책: MarkupString&lt;/h1>
&lt;p>실제로 원시 HTML을 렌더링해야 하는 경우 문자열을 &lt;code>MarkupString&lt;/code>로 래핑합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@((MarkupString)content)&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 그게 다야! 이제 Blazor는 HTML을 실제 마크업으로 렌더링합니다. 변수에 할당할 수도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> rawHtml = &lt;span style="color:#a5d6ff">&amp;#34;&amp;lt;strong&amp;gt;Hello&amp;lt;/strong&amp;gt; &amp;lt;em&amp;gt;world&amp;lt;/em&amp;gt;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> MarkupString HtmlContent =&amp;gt; (MarkupString)rawHtml;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@HtmlContent&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="실제-사례">실제 사례&lt;/h1>
&lt;p>API에서 블로그 콘텐츠를 가져오고 미리 보기 구성 요소에서 렌더링해야 했습니다. 콘텐츠에는 제목, 코드 블록, 링크, 이미지 등 모든 종류의 HTML이 포함되어 있습니다. 대략적인 모습은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@inject HttpClient Http
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@if (article &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;article&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h1&amp;gt;@article.Title&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;content&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>((MarkupString)article.HtmlBody)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/article&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> ArticleId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> ArticleDto? article;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> article = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;ArticleDto&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;api/articles/{ArticleId}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>완벽하게 작동합니다. API의 HTML은 실제 마크업으로 렌더링됩니다.&lt;/p>
&lt;p>#신뢰할 수 없는 내용에 주의하세요&lt;/p>
&lt;p>이는 중요합니다. &lt;code>MarkupString&lt;/code>는 HTML을 삭제하지 &lt;strong>않습니다&lt;/strong>. &lt;code>&amp;lt;script&amp;gt;&lt;/code> 태그를 포함하여 사용자가 제공한 모든 것을 렌더링합니다. 따라서 콘텐츠가 사용자 입력이나 신뢰할 수 없는 소스에서 나온 경우 먼저 해당 콘텐츠를 삭제해야 합니다.&lt;/p>
&lt;p>Blazor에는 내장된 HTML 새니타이저가 없지만 &lt;a href="https://github.com/mganss/HtmlSanitizer">HtmlSanitizer&lt;/a>와 같은 라이브러리를 사용할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Ganss.Xss&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> HtmlSanitizer sanitizer = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> MarkupString SafeHtml(&lt;span style="color:#ff7b72">string&lt;/span> html)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> clean = sanitizer.Sanitize(html);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> (MarkupString)clean;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@SafeHtml(untrustedContent)&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 &lt;code>&amp;lt;script&amp;gt;&lt;/code>, &lt;code>onclick&lt;/code> 핸들러와 같은 위험한 요소와 사용자 제공 콘텐츠에서 렌더링하고 싶지 않은 기타 항목을 제거합니다.&lt;/p>
&lt;h1 id="언제-사용하는가">언제 사용하는가&lt;/h1>
&lt;p>나는 다음 용도로 &lt;code>MarkupString&lt;/code>를 사용합니다.&lt;/p>
&lt;ul>
&lt;li>HTML 서버측으로 변환된 CMS 콘텐츠 또는 마크다운&lt;/li>
&lt;li>리치 텍스트 편집기 출력&lt;/li>
&lt;li>이메일 템플릿 미리보기&lt;/li>
&lt;li>신뢰할 수 있는 소스에서 사전 구축된 HTML&lt;/li>
&lt;/ul>
&lt;p>사용자 입력에서 나오는 모든 항목은 항상 먼저 삭제하세요. 후회하는 것보다 안전합니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! &lt;strong>@emimontesdeoca&lt;/strong>로 소셜 미디어를 통해 언제든지 저에게 연락해주세요.&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring">MarkupString 구조체&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/security/content-security-policy">Blazor XSS 방지&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>EF Core 9의 새로운 기능: 알아야 할 기능</title><link>https://emimontesdeoca.github.io/ko/posts/whats-new-ef-core-9/</link><pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/whats-new-ef-core-9/</guid><description>LINQ 개선 및 대량 작업부터 JSON 열 및 AOT 컴파일 지원까지 Entity Framework Core 9의 가장 영향력 있는 기능을 포괄적으로 살펴봅니다.</description><content:encoded>&lt;p>Entity Framework Core 9는 2024년 11월에 .NET 9와 함께 출시되었으며, 여러 프로젝트에서 이를 사용하여 상당한 시간을 투자한 결과, 한동안 가장 의미 있는 릴리스 중 하나라고 말할 수 있습니다. 바퀴를 재발명해서가 아니라 EF Core가 역사적으로 가장 많은 마찰을 일으켰던 영역(쿼리 변환, 성능, 최신 데이터 패턴 작업)을 개선하기 때문입니다.&lt;/p>
&lt;p>이 게시물에서는 일상 업무에 가장 큰 영향을 미친 기능을 살펴보겠습니다. 아직 EF Core 8(또는 7)을 사용하고 있다면 업그레이드의 다른 측면에서 무엇이 기다리고 있는지 명확하게 알 수 있습니다.&lt;/p>
&lt;h2 id="net-9-생태계의-ef-core-9">.NET 9 생태계의 EF Core 9&lt;/h2>
&lt;p>EF Core 9는 .NET 8 및 .NET 9를 대상으로 합니다. 즉, 이러한 기능을 대부분 활용하기 위해 전체 애플리케이션을 반드시 .NET 9로 업그레이드할 필요는 없습니다. 즉, AOT 및 성능 개선 사항 중 일부는 .NET 9 런타임 변경 사항과 긴밀하게 결합되어 있으므로 끝까지 진행하면 최대한 활용할 수 있습니다.&lt;/p>
&lt;p>릴리스는 Microsoft가 설정한 홀수/짝수 흐름을 따릅니다. 홀수 릴리스(예: .NET 9)는 18개월 지원이 포함된 STS(표준 기간 지원)이고, 짝수 릴리스(예: .NET 8)는 장기 지원(LTS)입니다. 업그레이드 일정을 계획할 때 이 점을 염두에 두십시오.&lt;/p>
&lt;h2 id="linq-번역-개선">LINQ 번역 개선&lt;/h2>
&lt;p>대부분의 개발자는 바로 이 부분에서 차이를 느낄 것입니다. EF Core 9는 LINQ 식을 실제로 의미 있는 SQL로 변환하는 데 상당한 진전을 이루었습니다.&lt;/p>
&lt;h3 id="더-나은-groupby-번역">더 나은 GroupBy 번역&lt;/h3>
&lt;p>EF Core에서 &lt;code>GroupBy&lt;/code> 쿼리를 작성했는데 클라이언트 측 평가 경고 또는 기괴한 SQL이 발생했다면 그 어려움을 아실 것입니다. EF Core 9는 훨씬 더 광범위한 &lt;code>GroupBy&lt;/code> 시나리오 세트를 SQL에서 직접 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> salesByCategory = &lt;span style="color:#ff7b72">await&lt;/span> context.Products
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(p =&amp;gt; p.Category.Name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(g =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Category = g.Key,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TotalRevenue = g.Sum(p =&amp;gt; p.Price * p.UnitsSold),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AveragePrice = g.Average(p =&amp;gt; p.Price),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ProductCount = g.Count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(x =&amp;gt; x.TotalRevenue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이전 버전에서는 &lt;code>GroupBy&lt;/code> 내부 탐색 속성에 대한 집계와 관련된 쿼리가 클라이언트 평가로 대체되는 경우가 있었습니다. EF Core 9는 이를 &lt;code>GROUP BY&lt;/code>, &lt;code>SUM&lt;/code>, &lt;code>AVG&lt;/code> 및 &lt;code>COUNT&lt;/code>를 사용하여 단일 SQL 쿼리로 명확하게 변환합니다.&lt;/p>
&lt;h3 id="복잡한-프로젝션-및-하위-쿼리">복잡한 프로젝션 및 하위 쿼리&lt;/h3>
&lt;p>중첩된 하위 쿼리와 복잡한 프로젝션도 대폭 업그레이드되었습니다. 다음과 같은 것을 고려해보세요:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> orderSummaries = &lt;span style="color:#ff7b72">await&lt;/span> context.Customers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(c =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> CustomerSummaryDto
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = c.FullName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TotalOrders = c.Orders.Count(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MostRecentOrder = c.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(o =&amp;gt; o.OrderDate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(o =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> OrderBriefDto
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Id = o.Id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = o.OrderDate,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Total = o.LineItems.Sum(li =&amp;gt; li.Quantity * li.UnitPrice)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TopCategory = c.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SelectMany(o =&amp;gt; o.LineItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(li =&amp;gt; li.Product.Category.Name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(g =&amp;gt; g.Count())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(g =&amp;gt; g.Key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 EF Core 9에서는 클라이언트 측 평가를 트리거하지 않고도 이 전체 표현식을 SQL로 변환할 수 있습니다. 생성된 쿼리는 적절한 경우 상관 하위 쿼리와 측면 조인을 사용하며 SQL 계획은 이전 버전에서 생성된 것보다 훨씬 더 효율적입니다.&lt;/p>
&lt;h3 id="매개변수화된-기본-컬렉션">매개변수화된 기본 컬렉션&lt;/h3>
&lt;p>LINQ의 뛰어난 개선 사항 중 하나는 기본 값 컬렉션을 쿼리에 직접 전달하는 기능입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> statusFilter = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; { &lt;span style="color:#a5d6ff">&amp;#34;Active&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Pending&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Review&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> filteredOrders = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(o =&amp;gt; statusFilter.Contains(o.Status))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 8에서는 인라인 값이 있는 &lt;code>IN&lt;/code> 절을 사용하여 변환되었습니다. 즉, 목록이 변경되면 쿼리 계획 캐시를 재사용할 수 없습니다. EF Core 9는 이러한 컬렉션을 적절하게 매개 변수화하여 구조화된 매개 변수로 보냅니다. 이는 SQL Server 및 PostgreSQL의 쿼리 계획 캐싱에 있어 큰 문제입니다.## 대량 작업 — ExecuteUpdate 및 ExecuteDelete&lt;/p>
&lt;p>&lt;code>ExecuteUpdate&lt;/code> 및 &lt;code>ExecuteDelete&lt;/code>는 EF Core 7에 도입되었지만 EF Core 9에서는 이를 사용하여 수행할 수 있는 작업을 의미 있는 방식으로 확장합니다.&lt;/p>
&lt;h3 id="더-복잡한-업데이트-표현식">더 복잡한 업데이트 표현식&lt;/h3>
&lt;p>이제 탐색 속성을 통한 다른 테이블에 대한 참조를 포함하여 &lt;code>ExecuteUpdate&lt;/code>에서 더 복잡한 표현식을 사용할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.Products
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(p =&amp;gt; p.Category.IsDiscontinued)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ExecuteUpdateAsync(setters =&amp;gt; setters
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.IsAvailable, &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.DiscontinuedDate, DateTimeOffset.UtcNow)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.Price, p =&amp;gt; p.Price * &lt;span style="color:#a5d6ff">0.5&lt;/span>m));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이렇게 하면 카테고리 테이블에 &lt;code>JOIN&lt;/code>가 포함된 단일 &lt;code>UPDATE&lt;/code> 문이 생성됩니다. 즉, 엔터티를 메모리에 로드할 필요가 없고 변경 내용 추적 오버헤드도 없습니다.&lt;/p>
&lt;h3 id="하위-쿼리를-사용한-조건부-대량-삭제">하위 쿼리를 사용한 조건부 대량 삭제&lt;/h3>
&lt;p>이제 하위 쿼리 필터를 사용한 대량 삭제가 완전히 지원됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.AuditLogs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(log =&amp;gt; log.CreatedAt &amp;lt; DateTime.UtcNow.AddYears(-&lt;span style="color:#a5d6ff">2&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(log =&amp;gt; !context.ProtectedRecords
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Any(pr =&amp;gt; pr.AuditLogId == log.Id))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ExecuteDeleteAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 &lt;code>NOT EXISTS&lt;/code> 하위 쿼리가 있는 &lt;code>DELETE&lt;/code>로 변환되며, 이는 정확히 직접 작성하는 것과 같습니다. 로드된 엔터티가 없고 왕복도 없습니다.&lt;/p>
&lt;h2 id="json-열-개선-사항">JSON 열 개선 사항&lt;/h2>
&lt;p>JSON 열은 최근 EF Core 릴리스에서 가장 흥미로운 기능 중 하나였으며 EF Core 9에서는 이를 더욱 발전시켰습니다.&lt;/p>
&lt;h3 id="json-내부-쿼리">JSON 내부 쿼리&lt;/h3>
&lt;p>이제 더 나은 번역 지원을 통해 JSON 열 내에서 데이터를 필터링하고 투영할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Order&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CustomerName { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ShippingAddress Address { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Stored as JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;OrderNote&amp;gt; Notes { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Stored as JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ShippingAddress&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Street { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> City { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> State { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ZipCode { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Country { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">OrderNote&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Text { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Author { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>DbContext&lt;/code>의 구성:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnModelCreating(ModelBuilder modelBuilder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelBuilder.Entity&amp;lt;Order&amp;gt;(builder =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.OwnsOne(o =&amp;gt; o.Address, ab =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ab.ToJson();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.OwnsMany(o =&amp;gt; o.Notes, nb =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nb.ToJson();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 JSON에 직접 쿼리할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> newYorkOrders = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(o =&amp;gt; o.Address.State == &lt;span style="color:#a5d6ff">&amp;#34;NY&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(o =&amp;gt; o.Notes.Count)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(o =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> o.CustomerName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> o.Address.City,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LatestNote = o.Notes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(n =&amp;gt; n.CreatedAt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(n =&amp;gt; n.Text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 9는 SQL Server(또는 다른 공급자의 경우 이에 상응하는 항목)에서 적절한 &lt;code>JSON_VALUE&lt;/code> 및 &lt;code>JSON_QUERY&lt;/code> 호출을 생성하며 변환은 이전보다 JSON 요소에 대한 훨씬 더 광범위한 LINQ 작업을 다룹니다.&lt;/p>
&lt;h3 id="json-속성-업데이트">JSON 속성 업데이트&lt;/h3>
&lt;p>EF Core 8의 문제점 중 하나는 JSON 열 내부의 단일 속성을 업데이트하면 전체 JSON 문서가 다시 작성된다는 것입니다. EF Core 9는 JSON 매핑 유형에 대한 보다 세부적인 변경 추적을 통해 이를 개선하여 가능한 경우 더 많은 대상 업데이트를 생성합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> order = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders.FindAsync(orderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>order.Address.ZipCode = &lt;span style="color:#a5d6ff">&amp;#34;10001&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>지원되는 공급자에서는 전체 Blob을 다시 작성하는 대신 보다 타겟팅된 JSON 수정을 생성할 수 있습니다.&lt;/p>
&lt;h2 id="복잡한-유형---id가-없는-값-개체">복잡한 유형 - ID가 없는 값 개체&lt;/h2>
&lt;p>복합 유형은 도메인 중심 디자인 실무자가 기다려온 기능 중 하나입니다. 소유된 유형과 달리 복합 유형에는 ID가 없습니다. 즉, 순수한 값 개체입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[ComplexType]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Money&lt;/span>(&lt;span style="color:#ff7b72">decimal&lt;/span> Amount, &lt;span style="color:#ff7b72">string&lt;/span> Currency);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[ComplexType]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DateRange&lt;/span>(DateTime Start, DateTime End);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Project&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Money Budget { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateRange Timeline { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 별도의 테이블이나 어떤 종류의 키도 필요 없이 상위 테이블(&lt;code>Budget_Amount&lt;/code>, &lt;code>Budget_Currency&lt;/code>, &lt;code>Timeline_Start&lt;/code>, &lt;code>Timeline_End&lt;/code>)에 평면화된 열로 저장됩니다.&lt;/p>
&lt;p>소유 유형과의 주요 차이점: 복합 유형은 참조가 아닌 값으로 비교됩니다. &lt;code>Amount&lt;/code> 및 &lt;code>Currency&lt;/code>가 동일한 두 개의 &lt;code>Money&lt;/code> 인스턴스는 속한 엔터티에 관계없이 동일한 것으로 간주됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> expensiveProjects = &lt;span style="color:#ff7b72">await&lt;/span> context.Projects
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(p =&amp;gt; p.Budget.Amount &amp;gt; &lt;span style="color:#a5d6ff">100_000&lt;/span>m &amp;amp;&amp;amp; p.Budget.Currency == &lt;span style="color:#a5d6ff">&amp;#34;USD&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(p =&amp;gt; p.Budget.Amount)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 평면화된 열에 대한 필터링으로 직접 변환됩니다. 즉, 깔끔하고 효율적이며 기대했던 것과 정확히 일치합니다.&lt;/p>
&lt;h2 id="sql-server에-대한-hierarchyid-지원">SQL Server에 대한 HierarchyId 지원&lt;/h2>
&lt;p>SQL Server의 계층적 데이터(조직 차트, 범주 트리, 파일 시스템)로 작업해 본 적이 있다면 &lt;code>HierarchyId&lt;/code>가 이에 대한 기본 제공 유형이라는 것을 알고 있을 것입니다. EF Core 9는 이에 대한 최고 수준의 지원을 제공합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Employee&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> HierarchyId PathFromCeo { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 계층적 관계를 직접 쿼리할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> managerId = HierarchyId.Parse(&lt;span style="color:#a5d6ff">&amp;#34;/1/3/&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// All direct and indirect reports&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> allReports = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo.IsDescendantOf(managerId))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo != managerId) &lt;span style="color:#8b949e;font-style:italic">// exclude the manager&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(e =&amp;gt; e.PathFromCeo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Direct reports only (one level down)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> directReports = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo.GetAncestor(&lt;span style="color:#a5d6ff">1&lt;/span>) == managerId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Get an employee&amp;#39;s depth in the hierarchy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> employeesWithDepth = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(e =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> e.Name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> e.Title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Level = e.PathFromCeo.GetLevel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(e =&amp;gt; e.Level)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```이들&lt;/span> &lt;span style="color:#f85149">모두는&lt;/span> SQL Server의 &lt;span style="color:#f85149">기본&lt;/span> &lt;span style="color:#f85149">`&lt;/span>HierarchyId&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">메서드로&lt;/span> &lt;span style="color:#f85149">변환됩니다&lt;/span>. &lt;span style="color:#f85149">자체&lt;/span> &lt;span style="color:#f85149">참조&lt;/span> &lt;span style="color:#f85149">외래&lt;/span> &lt;span style="color:#f85149">키와&lt;/span> &lt;span style="color:#f85149">재귀&lt;/span> CTE를 &lt;span style="color:#f85149">사용하여&lt;/span> &lt;span style="color:#f85149">트리&lt;/span> &lt;span style="color:#f85149">구조를&lt;/span> &lt;span style="color:#f85149">구현했다면&lt;/span> &lt;span style="color:#f85149">이&lt;/span> &lt;span style="color:#f85149">방법이&lt;/span> &lt;span style="color:#f85149">훨씬&lt;/span> &lt;span style="color:#f85149">더&lt;/span> &lt;span style="color:#f85149">깔끔한&lt;/span> &lt;span style="color:#f85149">접근&lt;/span> &lt;span style="color:#f85149">방식입니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">컴파일된&lt;/span> &lt;span style="color:#f85149">모델&lt;/span> &lt;span style="color:#f85149">및&lt;/span> AOT &lt;span style="color:#f85149">지원&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">성능에&lt;/span> &lt;span style="color:#f85149">민감한&lt;/span> &lt;span style="color:#f85149">개발자는&lt;/span> &lt;span style="color:#f85149">컴파일된&lt;/span> &lt;span style="color:#f85149">모델과&lt;/span> AOT(Ahead-of-Time) &lt;span style="color:#f85149">컴파일&lt;/span> &lt;span style="color:#f85149">지원에&lt;/span> &lt;span style="color:#f85149">대한&lt;/span> &lt;span style="color:#f85149">지속적인&lt;/span> &lt;span style="color:#f85149">투자를&lt;/span> &lt;span style="color:#f85149">높이&lt;/span> &lt;span style="color:#f85149">평가할&lt;/span> &lt;span style="color:#f85149">것입니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">컴파일된&lt;/span> &lt;span style="color:#f85149">모델&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">컴파일된&lt;/span> &lt;span style="color:#f85149">모델은&lt;/span> EF Core가 &lt;span style="color:#f85149">일반적으로&lt;/span> &lt;span style="color:#f85149">시작&lt;/span> &lt;span style="color:#f85149">시&lt;/span> &lt;span style="color:#f85149">빌드하는&lt;/span> &lt;span style="color:#f85149">모델&lt;/span> &lt;span style="color:#f85149">메타데이터를&lt;/span> &lt;span style="color:#f85149">미리&lt;/span> &lt;span style="color:#f85149">생성합니다&lt;/span>. &lt;span style="color:#f85149">대규모&lt;/span> &lt;span style="color:#f85149">모델&lt;/span>(&lt;span style="color:#f85149">수백&lt;/span> &lt;span style="color:#f85149">개의&lt;/span> &lt;span style="color:#f85149">엔터티&lt;/span> &lt;span style="color:#f85149">생각&lt;/span>)&lt;span style="color:#f85149">의&lt;/span> &lt;span style="color:#f85149">경우&lt;/span> &lt;span style="color:#f85149">콜드&lt;/span> &lt;span style="color:#f85149">스타트&lt;/span> &lt;span style="color:#f85149">​​시간을&lt;/span> &lt;span style="color:#f85149">크게&lt;/span> &lt;span style="color:#f85149">줄일&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet ef dbcontext optimize --output-dir CompiledModels --&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">MyApp.CompiledModels&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 연결하십시오.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddDbContext&amp;lt;AppDbContext&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .UseSqlServer(connectionString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 9에서는 컴파일된 모델이 더욱 완벽해졌습니다. 즉, 더 많은 매핑 기능을 지원하고 더 작은 출력을 생성합니다. 약 400개 엔터티가 있는 모델의 경우 시작 시간은 몇 초에서 거의 순간적으로 단축될 수 있습니다.&lt;/p>
&lt;h3 id="aot-컴파일-진행">AOT 컴파일 진행&lt;/h3>
&lt;p>EF Core에 대한 완전한 기본 AOT 지원은 아직 진행 중인 작업이지만 EF Core 9는 상당한 발전을 이루었습니다. 반사가 많은 코드 경로 중 상당수가 트리밍에 적합하도록 리팩터링되었으며 컴파일된 모델은 AOT 스토리의 핵심 부분입니다. 콜드 스타트가 중요한 Azure Functions 또는 마이크로서비스와 같은 시나리오를 대상으로 하는 경우 이러한 개선 사항은 직접적인 관련이 있습니다.&lt;/p>
&lt;h2 id="cosmos-db-공급자-업데이트">Cosmos DB 공급자 업데이트&lt;/h2>
&lt;p>Azure Cosmos DB 공급자는 EF Core 9를 통해 계속 발전하고 있습니다. 몇 가지 주목할만한 개선 사항은 다음과 같습니다.&lt;/p>
&lt;h3 id="파티션-키-처리">파티션 키 처리&lt;/h3>
&lt;p>이제 공급자는 계층적 파티션 키를 지원하고 파티션 키 필터를 보다 지능적으로 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TenantDocument&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> TenantId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Partition key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Region { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Sub-partition key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Content { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This query now correctly uses the partition key for routing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> docs = &lt;span style="color:#ff7b72">await&lt;/span> context.TenantDocuments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(d =&amp;gt; d.TenantId == &lt;span style="color:#a5d6ff">&amp;#34;tenant-42&amp;#34;&lt;/span> &amp;amp;&amp;amp; d.Region == &lt;span style="color:#a5d6ff">&amp;#34;us-east&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(d =&amp;gt; d.Content.Contains(&lt;span style="color:#a5d6ff">&amp;#34;important&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="linq에서-nosql로의-변환-개선">LINQ에서 NoSQL로의 변환 개선&lt;/h3>
&lt;p>이제 &lt;code>Contains&lt;/code>, &lt;code>Any&lt;/code>, 중첩 배열 작업 및 수학 함수에 대한 더 나은 지원을 포함하여 더 많은 LINQ 작업이 Cosmos DB의 SQL 언어로 변환됩니다. 이전에 클라이언트 평가로 대체되었던 쿼리는 이제 서버 측에서 처리됩니다.&lt;/p>
&lt;h3 id="벡터-검색-지원">벡터 검색 지원&lt;/h3>
&lt;p>EF Core 9에는 Cosmos DB를 통한 벡터 유사성 검색에 대한 초기 지원이 도입되었습니다. 이는 임베딩 또는 AI 기반 검색과 통합되는 애플리케이션을 구축하는 경우에 유용합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> context.Documents
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(d =&amp;gt; EF.Functions.VectorDistance(d.Embedding, queryVector))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(&lt;span style="color:#a5d6ff">10&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="마이그레이션-개선">마이그레이션 개선&lt;/h2>
&lt;p>마이그레이션을 통해 팀 환경에서 마이그레이션 작업의 고통을 덜 수 있도록 삶의 질이 향상되었습니다.&lt;/p>
&lt;h3 id="마이그레이션의-임시-테이블">마이그레이션의 임시 테이블&lt;/h3>
&lt;p>이제 마이그레이션은 기간 열 및 기록 테이블 이름 지정에 대한 적절한 지원을 통해 임시 테이블 구성을 보다 원활하게 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnModelCreating(ModelBuilder modelBuilder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelBuilder.Entity&amp;lt;Employee&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToTable(&lt;span style="color:#a5d6ff">&amp;#34;Employees&amp;#34;&lt;/span>, b =&amp;gt; b.IsTemporal(t =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.HasPeriodStart(&lt;span style="color:#a5d6ff">&amp;#34;ValidFrom&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.HasPeriodEnd(&lt;span style="color:#a5d6ff">&amp;#34;ValidTo&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.UseHistoryTable(&lt;span style="color:#a5d6ff">&amp;#34;EmployeeHistory&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="멱등성-스크립트">멱등성 스크립트&lt;/h3>
&lt;p>&lt;code>Script-Migration&lt;/code> 명령(및 이에 상응하는 CLI)은 기본적으로 특정 상태에 존재하는 데이터에 의존하는 스키마 변경과 관련된 엣지 케이스 처리가 개선되어 더 나은 멱등성 스크립트를 생성합니다.&lt;/p>
&lt;h3 id="마이그레이션-번들">마이그레이션 번들&lt;/h3>
&lt;p>마이그레이션을 배포용 독립 실행형 실행 파일로 패키징하는 마이그레이션 번들은 더 나은 오류 보고 및 일시적 오류에 대한 재시도 논리를 통해 EF Core 9에서 더욱 안정적입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet ef migrations bundle --self-contained -r linux-x64
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그러면 배포 대상에 .NET SDK를 설치하지 않고도 CI/CD 파이프라인에서 실행할 수 있는 바이너리가 생성됩니다.&lt;/p>
&lt;h2 id="성능-벤치마크다음은-제가-직접-테스트한-대략적인-벤치마크입니다-이는-benchmarkdotnet으로-측정된-sql-server-2022에-대해-실행되는-약-200개의-엔터티가-있는-프로젝트에서-가져온-것입니다-수치는-다양하지만-상대적인-개선-정도는-비슷할-것입니다">성능 벤치마크다음은 제가 직접 테스트한 대략적인 벤치마크입니다. 이는 BenchmarkDotNet으로 측정된 SQL Server 2022에 대해 실행되는 약 200개의 엔터티가 있는 프로젝트에서 가져온 것입니다. 수치는 다양하지만 상대적인 개선 정도는 비슷할 것입니다.&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>시나리오&lt;/th>
&lt;th>EF 코어 8&lt;/th>
&lt;th>EF 코어 9&lt;/th>
&lt;th>개선&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>모델 빌드(콜드 스타트)&lt;/td>
&lt;td>1,850ms&lt;/td>
&lt;td>320ms&lt;/td>
&lt;td>~5.8배 더 빠름(컴파일됨)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>단순 쿼리(PK별 단일 엔터티)&lt;/td>
&lt;td>0.42ms&lt;/td>
&lt;td>0.38ms&lt;/td>
&lt;td>~10% 더 빨라짐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>복잡한 쿼리(조인 + 집계)&lt;/td>
&lt;td>3.1ms&lt;/td>
&lt;td>2.4ms&lt;/td>
&lt;td>~23% 더 빨라짐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>대량 업데이트(10,000개 행)&lt;/td>
&lt;td>145ms&lt;/td>
&lt;td>118ms&lt;/td>
&lt;td>~19% 더 빨라짐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>JSON 열 쿼리&lt;/td>
&lt;td>2.8ms&lt;/td>
&lt;td>1.9ms&lt;/td>
&lt;td>~32% 더 빨라짐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SaveChanges(엔티티 100개)&lt;/td>
&lt;td>48ms&lt;/td>
&lt;td>41ms&lt;/td>
&lt;td>~15% 더 빨라짐&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>컴파일된 모델 개선은 가장 극적이지만 전반적으로 꾸준한 개선이 이루어지고 있습니다. 특히 초당 수천 개의 쿼리를 실행하는 처리량이 높은 시나리오에서는 더욱 그렇습니다.&lt;/p>
&lt;h2 id="ef-core-8에서-업그레이드">EF Core 8에서 업그레이드&lt;/h2>
&lt;p>EF Core 8을 사용하는 경우 업그레이드 경로가 비교적 원활합니다. 체크리스트는 다음과 같습니다.&lt;/p>
&lt;p>&lt;strong>1. 패키지 업데이트:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore.SqlServer&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore.Tools&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. 주요 변경 사항을 확인하세요.&lt;/strong> EF Core 9의 목록은 일부 이전 릴리스에 비해 상대적으로 짧습니다. 가장 주목할만한 것 :&lt;/p>
&lt;ul>
&lt;li>이전에 사용되지 않았던 일부 API가 제거되었습니다.&lt;/li>
&lt;li>특정 &lt;code>GroupBy&lt;/code> 쿼리가 변환되는 방식 변경(이제 서버측으로 이동하여 클라이언트 평가에 의존하는 경우 동작이 변경됨)&lt;/li>
&lt;li>마이그레이션 스캐폴딩 출력의 사소한 변경&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. 컴파일된 모델&lt;/strong>을 사용하는 경우 다시 생성하세요. 형식이 변경되었으므로 이전에 컴파일된 모델은 EF Core 9에서 작동하지 않습니다.&lt;/p>
&lt;p>&lt;strong>4. 테스트 스위트를 실행하십시오.&lt;/strong> 이전에 클라이언트에서 평가된 쿼리에 특별한 주의를 기울이십시오. 이제 서버에서 평가될 수도 있습니다. 이는 일반적으로 더 좋지만 데이터 차이가 드러날 수 있습니다.&lt;/p>
&lt;p>&lt;strong>5. 해당 공급자를 사용하는 경우 Cosmos DB 쿼리&lt;/strong>를 확인하세요. 향상된 번역은 일부 쿼리가 다르게(일반적으로 더 빠르게) 실행된다는 것을 의미하지만 결과가 동일한지 확인할 가치가 있습니다.&lt;/p>
&lt;p>일반적인 프로젝트의 최소 업그레이드는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet build
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet test
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>모든 것이 컴파일되고 테스트에 통과되면 아마도 좋은 상태일 것입니다. 문제가 발생하면 EF Core 9 주요 변경 설명서에 각 변경 사항에 대한 자세한 마이그레이션 지침이 있습니다.&lt;/p>
&lt;h2 id="마무리">마무리&lt;/h2>
&lt;p>EF Core 9는 혁명적인 릴리스가 아닙니다. 이는 진화적인 릴리스이며, 이것이 바로 필요한 것입니다. LINQ 개선만으로도 대부분의 프로젝트에 대한 업그레이드가 정당화되며 JSON 열 개선, 복합 유형 및 &lt;code>HierarchyId&lt;/code>와 같은 기능은 이전에 어색하거나 불가능했던 개방형 패턴을 지원합니다.&lt;/p>
&lt;p>내 프로젝트에 가장 큰 영향을 미친 세 가지 기능을 선택해야 한다면:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>매개변수화된 기본 컬렉션&lt;/strong> — 쿼리 계획 캐시 효율성이 규모에 따라 중요하기 때문&lt;/li>
&lt;li>&lt;strong>JSON 열 개선&lt;/strong> — 하이브리드 관계형 문서 패턴이 매우 유용하기 때문입니다.&lt;/li>
&lt;li>&lt;strong>컴파일된 모델&lt;/strong> — 시작 시간이 개발자 생산성과 배포 속도에 직접적인 영향을 미치기 때문입니다.EF Core 팀은 EF Core 5 이후 탄탄한 궤도를 유지해 왔으며 버전 9에서는 이러한 추세를 이어갑니다. 이미 EF Core 8을 사용하고 있다면 업그레이드는 위험은 낮고 보상은 높습니다. 만약 당신이 오래된 무언가를 하고 있다면, 도약하기에 이보다 더 좋은 때는 없었습니다.&lt;/li>
&lt;/ol>
&lt;p>즐거운 코딩과 행복한 쿼리.&lt;/p></content:encoded><category>.NET</category><category>Entity Framework</category><category>Database</category></item><item><title>시맨틱 커널 시작하기: C#의 AI 오케스트레이션</title><link>https://emimontesdeoca.github.io/ko/posts/getting-started-semantic-kernel/</link><pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/getting-started-semantic-kernel/</guid><description>Microsoft의 Semantic Kernel을 사용하여 플러그인 및 플래너부터 메모리 및 함수 호출에 이르기까지 C#으로 AI 기반 애플리케이션을 구축하는 방법을 알아보세요.</description><content:encoded>&lt;p>.NET 애플리케이션을 구축하고 AI 환경이 발전하는 것을 지켜봤다면 아마도 다음과 같이 궁금해했을 것입니다. &lt;em>내 코드베이스를 스파게티로 바꾸지 않고 대규모 언어 모델을 내 C# 프로젝트에 통합하는 가장 좋은 방법은 무엇입니까?&lt;/em> 이것이 바로 Microsoft의 Semantic Kernel이 해결하는 문제이며, 작년에 이를 사용하여 프로덕션 애플리케이션을 구축한 후에는 이것이 내 개발자 툴킷에서 가장 중요한 도구 중 하나가 되었다고 말할 수 있습니다.&lt;/p>
&lt;p>이 게시물에서는 핵심 개념 이해부터 실제 AI 도우미 구축에 이르기까지 Semantic Kernel을 시작하는 데 필요한 모든 것을 안내해 드리겠습니다. AI 개발에 발을 담그고 있거나 기존 .NET 애플리케이션에서 LLM 호출을 조정하는 구조화된 방법을 찾고 있다면 이 가이드에서 다룹니다.&lt;/p>
&lt;h2 id="시맨틱-커널이란-무엇입니까">시맨틱 커널이란 무엇입니까?&lt;/h2>
&lt;p>SK(의미론적 커널)는 애플리케이션 코드와 GPT-4o, Azure OpenAI 또는 기타 AI 서비스와 같은 대규모 언어 모델 사이에서 &lt;strong>오케스트레이션 레이어&lt;/strong> 역할을 하는 Microsoft의 오픈 소스 SDK입니다. 기존 C# 코드와 AI 기능을 깔끔하고 구성 가능한 방식으로 결합할 수 있는 경량 미들웨어라고 생각하세요.&lt;/p>
&lt;p>하지만 OpenAI API를 직접 호출하면 어떨까요? 물론 가능합니다. 간단한 사용 사례에서는 괜찮습니다. 하지만 다음과 같은 조치가 필요한 순간:&lt;/p>
&lt;ul>
&lt;li>AI가 사용자 입력에 따라 호출할 &lt;strong>함수&lt;/strong>를 결정하도록 함&lt;/li>
&lt;li>파이프라인에서 &lt;strong>여러 AI 호출&lt;/strong>을 기존 코드와 결합합니다.&lt;/li>
&lt;li>AI가 이전 상호 작용을 기억할 수 있도록 &lt;strong>메모리 및 컨텍스트&lt;/strong>를 추가하세요.&lt;/li>
&lt;li>복잡한 작업을 통해 추론하는 &lt;strong>다단계 에이전트&lt;/strong> 구축&lt;/li>
&lt;/ul>
&lt;p>&amp;hellip;당신은 바퀴를 재발명하는 자신을 발견하게 될 것입니다. Semantic Kernel은 최고 수준의 .NET 지원, 종속성 주입 통합, 모든 C# 개발자에게 자연스럽게 느껴지는 플러그인 아키텍처를 통해 이 모든 기능을 즉시 제공합니다.&lt;/p>
&lt;p>이 프로젝트는 GitHub의 &lt;code>microsoft/semantic-kernel&lt;/code> 저장소에 있으며 C#, Python 및 Java용 SDK가 있습니다. C# SDK는 가장 성숙한 버전이며 여기서 중점적으로 살펴보겠습니다.&lt;/p>
&lt;h2 id="핵심-개념">핵심 개념&lt;/h2>
&lt;p>코드를 작성하기 전에 구성 요소를 이해해 봅시다.&lt;/p>
&lt;h3 id="커널">커널&lt;/h3>
&lt;p>&lt;code>Kernel&lt;/code>은 Semantic Kernel의 중심 개체입니다. AI 서비스, 플러그인 및 구성을 하나로 묶는 오케스트레이터입니다. 하나를 만들고 여기에 서비스와 플러그인을 등록한 다음 이를 사용하여 프롬프트를 실행하거나 기능을 호출합니다. ASP.NET Core의 종속성 주입에 익숙하다면 커널은 매우 친숙하게 느껴질 것입니다. 즉, 본질적으로 AI 초능력을 갖춘 서비스 컨테이너입니다.&lt;/p>
&lt;h3 id="플러그인-및-기능">플러그인 및 기능&lt;/h3>
&lt;p>&lt;strong>플러그인&lt;/strong>은 커널이 호출할 수 있는 관련 &lt;strong>함수&lt;/strong>의 모음입니다. 함수에는 두 가지 형태가 있습니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>프롬프트 기능&lt;/strong> — LLM으로 전송되는 자연어 템플릿으로 정의됩니다.&lt;/li>
&lt;li>&lt;strong>네이티브 함수&lt;/strong> — 커널이 발견하고 호출할 수 있는 속성으로 장식된 일반 C# 메서드&lt;/li>
&lt;/ul>
&lt;p>예를 들어 기본 함수 &lt;code>GetCurrentWeather(string city)&lt;/code>와 날씨 데이터를 친숙한 방식으로 요약하는 프롬프트 함수가 있는 &lt;code>WeatherPlugin&lt;/code>가 있을 수 있습니다.### AI 커넥터&lt;/p>
&lt;p>커넥터는 Semantic Kernel이 AI 서비스와 통신하는 방식입니다. 가장 일반적인 것들은 다음과 같습니다:&lt;/p>
&lt;ul>
&lt;li>&lt;code>AzureOpenAIChatCompletion&lt;/code> — Azure OpenAI 서비스용&lt;/li>
&lt;li>&lt;code>OpenAIChatCompletion&lt;/code> — OpenAI API 직접용&lt;/li>
&lt;li>벡터 검색 및 메모리용 커넥터 내장&lt;/li>
&lt;/ul>
&lt;p>시작 시 커널에 이를 등록하면 다른 모든 것이 제대로 작동합니다.&lt;/p>
&lt;h2 id="프로젝트-설정">프로젝트 설정&lt;/h2>
&lt;p>손을 더럽히자. 새 콘솔 애플리케이션을 만드는 것부터 시작하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new console -n SemanticKernelDemo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd SemanticKernelDemo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 Semantic Kernel NuGet 패키지를 추가합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Azure OpenAI 대신 OpenAI를 직접 사용하는 경우:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>메모리 및 임베딩 지원의 경우(나중에 사용함):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Plugins.Memory
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Extensions.VectorData.Abstractions
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.csproj&lt;/code>은 .NET 8 이상을 대상으로 해야 합니다. Semantic Kernel의 최신 버전은 최신 .NET 기능을 최대한 활용합니다.&lt;/p>
&lt;h2 id="첫-번째-커널">첫 번째 커널&lt;/h2>
&lt;p>커널을 생성하고 이를 AI 서비스에 연결하고 질문하는 가장 간단한 예부터 시작해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the kernel with Azure OpenAI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Invoke a simple prompt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokePromptAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Explain dependency injection in C# in three sentences.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>OpenAI를 직접 사용하는 경우 서비스 등록을 교체하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelId: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-openai-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그게 다야. 이를 실행하면 종속성 주입에 대한 간결한 설명을 얻을 수 있습니다. 그러나 이것은 단지 표면적인 것에 불과합니다.&lt;/p>
&lt;h3 id="프롬프트-템플릿-사용">프롬프트 템플릿 사용&lt;/h3>
&lt;p>프롬프트 템플릿을 사용하면 핸들바 스타일 구문을 사용하여 변수로 프롬프트를 매개변수화할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer. Write a brief summary of {{&lt;span style="color:#f85149">$&lt;/span>topic}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> aimed at developers with {{&lt;span style="color:#f85149">$&lt;/span>experienceLevel}} experience.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Keep it under &lt;span style="color:#a5d6ff">200&lt;/span> words.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> function = kernel.CreateFunctionFromPrompt(prompt);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;topic&amp;#34;] = &lt;span style="color:#a5d6ff">&amp;#34;gRPC in .NET&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;experienceLevel&amp;#34;] = &lt;span style="color:#a5d6ff">&amp;#34;intermediate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokeAsync(function, arguments);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것이 Semantic Kernel이 빛을 발하기 시작하는 곳입니다. 재사용 가능한 프롬프트 템플릿을 정의하고 버전을 지정하고 더 큰 워크플로로 구성할 수 있습니다.&lt;/p>
&lt;h2 id="플러그인-및-기본-기능">플러그인 및 기본 기능&lt;/h2>
&lt;p>플러그인은 Semantic Kernel이 AI와 기존 C# 코드 사이의 격차를 해소하는 곳입니다. 네이티브 함수는 커널에 노출되는 일반적인 메서드입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TimePlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_current_time&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Gets the current date and time in UTC&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetCurrentTime()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> DateTime.UtcNow.ToString(&lt;span style="color:#a5d6ff">&amp;#34;yyyy-MM-dd HH:mm:ss UTC&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_time_in_timezone&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Gets the current time in a specific timezone&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetTimeInTimezone(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The IANA timezone identifier, e.g. &amp;#39;America/New_York&amp;#39;&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> timezone)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> time = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> time.ToString(&lt;span style="color:#a5d6ff">&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>[KernelFunction]&lt;/code> 및 &lt;code>[Description]&lt;/code> 속성을 확인하세요. 이는 매우 중요합니다. 설명은 AI가 함수를 호출하는 시기와 방법을 이해하기 위해 읽는 내용입니다. 좋은 설명은 도구를 효과적으로 사용하는 AI와 혼란스러운 AI를 구분합니다.&lt;/p>
&lt;p>커널에 플러그인을 등록합니다:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>서비스를 주입하는 더 복잡한 플러그인을 구축할 수도 있습니다. Semantic Kernel은 &lt;code>Microsoft.Extensions.DependencyInjection&lt;/code>와 통합되므로 플러그인은 다른 서비스와 마찬가지로 생성자 종속성을 받을 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">OrderPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IOrderRepository _repository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OrderPlugin(IOrderRepository repository)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _repository = repository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_order_status&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Retrieves the status of an order by its ID&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetOrderStatus(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The order ID to look up&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> orderId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> order = &lt;span style="color:#ff7b72">await&lt;/span> _repository.GetByIdAsync(orderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> order &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? &lt;span style="color:#a5d6ff">$&amp;#34;No order found with ID {orderId}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#a5d6ff">$&amp;#34;Order {orderId}: {order.Status}, placed on {order.CreatedAt:d}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="함수-호출-및-자동-호출">함수 호출 및 자동 호출&lt;/h2>
&lt;p>이것은 상황이 정말 흥미로워지는 곳입니다. &lt;strong>함수 호출&lt;/strong>(도구 호출이라고도 함)을 사용하면 AI 모델이 대화 컨텍스트에 따라 호출할 등록된 함수를 결정하도록 할 수 있습니다. 모델은 코드를 실행하지 않습니다. &amp;ldquo;이 인수를 사용하여 함수 X를 호출하고 싶습니다&amp;quot;라는 구조화된 요청을 반환하고 커널이 실제 호출을 처리합니다.&lt;/p>
&lt;p>자동 함수 호출을 활성화하는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;WeatherPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Enable automatic function calling&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> OpenAIPromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokePromptAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;What time is it in Tokyo and what&amp;#39;s the weather like there?&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(settings)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>FunctionChoiceBehavior.Auto()&lt;/code>를 사용하면 커널은 다음을 수행합니다.&lt;/p>
&lt;ol>
&lt;li>사용 가능한 모든 기능에 대한 설명과 함께 프롬프트를 AI에 보냅니다.&lt;/li>
&lt;li>AI는 &lt;code>get_time_in_timezone&lt;/code> 및 &lt;code>get_weather&lt;/code>를 호출해야 한다고 결정합니다.&lt;/li>
&lt;li>커널은 해당 기능을 자동으로 실행합니다.&lt;/li>
&lt;li>결과는 AI로 다시 전송됩니다.&lt;/li>
&lt;li>AI는 함수 결과를 사용하여 자연어 응답을 구성합니다.이 루프는 단일 호출에서 여러 번 발생할 수 있습니다. AI는 필요한 모든 정보를 수집하기 위해 여러 함수를 순서대로 호출할 수 있습니다. 또한 &lt;code>FunctionChoiceBehavior.Required()&lt;/code>를 사용하여 AI가 적어도 하나의 기능을 호출하도록 강제하거나 사용이 허용된 특정 기능 목록을 제공할 수도 있습니다.&lt;/li>
&lt;/ol>
&lt;h3 id="기록으로-채팅-완료">기록으로 채팅 완료&lt;/h3>
&lt;p>대화형 애플리케이션의 경우 &lt;code>ChatCompletionService&lt;/code>를 &lt;code>ChatHistory&lt;/code> 객체와 함께 직접 사용하는 것이 좋습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddSystemMessage(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a helpful developer assistant. You have access to tools
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> checking the time and weather. Be concise and friendly.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">while&lt;/span> (&lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;You: &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> input = Console.ReadLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(input)) &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> executionSettings: settings,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kernel: kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddAssistantMessage(response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Assistant: {response.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 대화 기록을 유지하고 필요에 따라 플러그인을 호출할 수 있는 완전한 대화형 챗봇을 제공합니다.&lt;/p>
&lt;h2 id="메모리-및-임베딩">메모리 및 임베딩&lt;/h2>
&lt;p>AI 애플리케이션의 가장 강력한 패턴 중 하나는 **RAG(Retrieval-Augmented Generation)**입니다. 즉, AI가 사용자 데이터를 벡터 공간에 삽입하고 쿼리 시 관련 청크를 검색하여 해당 데이터에 액세스할 수 있도록 합니다.&lt;/p>
&lt;p>시맨틱 커널은 벡터 저장소 및 임베딩 작업을 위한 추상화를 제공합니다. 개발을 위해 메모리 내 벡터 저장소를 설정하는 방법은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.VectorData&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.AzureOpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Embeddings&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0010
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Create an embedding generation service&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAITextEmbeddingGeneration(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;text-embedding-ada-002&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddingService = kernel.GetRequiredService&amp;lt;ITextEmbeddingGenerationService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Generate embeddings for your documents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> documents = &lt;span style="color:#ff7b72">new&lt;/span>[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Semantic Kernel is an open-source SDK for AI orchestration.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Azure OpenAI provides enterprise-grade AI models.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Plugins in SK allow you to expose C# methods to AI models.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddings = &lt;span style="color:#ff7b72">await&lt;/span> embeddingService.GenerateEmbeddingsAsync(documents);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>프로덕션 시나리오의 경우 이러한 임베딩을 Azure AI Search, Qdrant 또는 Pinecone과 같은 전용 벡터 데이터베이스에 저장합니다. 시맨틱 커널에는 &lt;code>Microsoft.Extensions.VectorData&lt;/code> 추상화를 통해 이들 모두에 대한 커넥터가 있습니다.&lt;/p>
&lt;p>일반적인 RAG 흐름은 다음과 같습니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>수집&lt;/strong>: 문서를 청크하고, 임베딩을 생성하고, 벡터 데이터베이스에 저장합니다.&lt;/li>
&lt;li>&lt;strong>검색&lt;/strong>: 사용자가 질문하면 쿼리를 삽입하여 가장 유사한 문서를 찾습니다.&lt;/li>
&lt;li>&lt;strong>생성&lt;/strong>: 검색된 문서를 사용자의 질문과 함께 LLM에 컨텍스트로 전달합니다.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[KernelFunction(&amp;#34;search_knowledge_base&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Description(&amp;#34;Searches the internal knowledge base for relevant information&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SearchKnowledgeBase(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The search query&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> query)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> queryEmbedding = &lt;span style="color:#ff7b72">await&lt;/span> _embeddingService.GenerateEmbeddingAsync(query);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> _vectorStore.SearchAsync(queryEmbedding, limit: &lt;span style="color:#a5d6ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>, results.Select(r =&amp;gt; r.Text));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>RAG 파이프라인을 커널 기능으로 노출함으로써 AI는 지식 기반을 검색해야 할 시기를 자동으로 결정하여 오케스트레이션을 깔끔하게 유지하고 모델이 가장 잘 수행할 수 있도록 합니다.&lt;/p>
&lt;h2 id="기획자-및-에이전트">기획자 및 에이전트&lt;/h2>
&lt;p>AI 애플리케이션이 더욱 복잡해짐에 따라 &lt;strong>다단계 작업을 계획하고 실행&lt;/strong>하려면 AI가 필요합니다. 이것이 Semantic Kernel의 에이전트 프레임워크가 들어오는 곳입니다.&lt;/p>
&lt;h3 id="기본-사항-채팅-완료-에이전트">기본 사항: 채팅 완료 에이전트&lt;/h3>
&lt;p>가장 간단한 에이전트 유형은 지침과 플러그인을 사용하여 채팅 완료 모델을 래핑합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0110
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;DevAssistant&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior .NET developer assistant. Help users with code
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reviews, architecture decisions, and debugging. Always provide
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> code examples when relevant. Use your available tools to look up
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current information when needed.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(settings)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddUserMessage(&lt;span style="color:#a5d6ff">&amp;#34;How should I structure a clean architecture project in .NET 8?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.Add(message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="다중-에이전트-협업">다중 에이전트 협업&lt;/h3>
&lt;p>&lt;strong>여러 상담원이 함께 작업&lt;/strong>할 때 정말 강력해집니다. Semantic Kernel은 다양한 전문 분야를 가진 에이전트가 협업하는 그룹 채팅 패턴을 지원합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0110
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> codeReviewer = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;CodeReviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You review C&lt;span style="color:#f85149">#&lt;/span> code &lt;span style="color:#ff7b72">for&lt;/span> bugs, performance issues, and best practices.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Be specific about what you find and suggest concrete fixes.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> securityAuditor = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;SecurityAuditor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You focus exclusively &lt;span style="color:#ff7b72">on&lt;/span> security vulnerabilities &lt;span style="color:#ff7b72">in&lt;/span> code.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Look &lt;span style="color:#ff7b72">for&lt;/span> injection attacks, authentication issues, data exposure,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and OWASP Top &lt;span style="color:#a5d6ff">10&lt;/span> violations.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> groupChat = &lt;span style="color:#ff7b72">new&lt;/span> AgentGroupChat(codeReviewer, securityAuditor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span> AgentGroupChatSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> MaximumIterationTerminationStrategy(&lt;span style="color:#a5d6ff">4&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>groupChat.AddChatMessage(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(AuthorRole.User, &lt;span style="color:#a5d6ff">&amp;#34;Review this code: ...&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> groupChat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]: {message.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 패턴은 다양한 관점이나 전문 분야를 고려해야 하는 복잡한 작업에 매우 유용합니다. 각 에이전트는 자체 시스템 프롬프트로 작동하며 자체 플러그인 세트를 가질 수 있습니다.&lt;/p>
&lt;h2 id="실제-사례-프로젝트-문서화-도우미-구축">실제 사례: 프로젝트 문서화 도우미 구축&lt;/h2>
&lt;p>파일을 읽고 질문에 답변하여 개발자가 코드베이스를 이해하도록 돕는 AI 도우미인 실제 사례를 통해 모든 것을 하나로 묶어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Define our plugins&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">FileSystemPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _rootPath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> FileSystemPlugin(&lt;span style="color:#ff7b72">string&lt;/span> rootPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _rootPath = rootPath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;read_file&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Reads the contents of a file from the project directory&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ReadFile(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Relative path to the file&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fullPath = Path.Combine(_rootPath, path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(fullPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;File not found: {path}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> content = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(fullPath);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Truncate very large files&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (content.Length &amp;gt; &lt;span style="color:#a5d6ff">8000&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content = content[..&lt;span style="color:#a5d6ff">8000&lt;/span>] + &lt;span style="color:#a5d6ff">&amp;#34;\n... [truncated]&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> content;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;list_files&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Lists files in a directory, optionally filtered by extension&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ListFiles(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Relative directory path&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> directory,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;File extension filter like &amp;#39;.cs&amp;#39; or &amp;#39;.json&amp;#39;&amp;#34;)] &lt;span style="color:#ff7b72">string?&lt;/span> extension = &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fullPath = Path.Combine(_rootPath, directory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Directory.Exists(fullPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;Directory not found: {directory}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> files = Directory.GetFiles(fullPath, &lt;span style="color:#a5d6ff">&amp;#34;*.*&amp;#34;&lt;/span>, SearchOption.AllDirectories)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(f =&amp;gt; Path.GetRelativePath(_rootPath, f))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(f =&amp;gt; extension &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span> || f.EndsWith(extension))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(&lt;span style="color:#a5d6ff">50&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n&amp;#34;&lt;/span>, files);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DocumentationPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;generate_summary&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Generates a structured markdown summary for documentation&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateSummaryTemplate(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Name of the component&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> componentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Brief description&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> description,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Key responsibilities as comma-separated values&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> responsibilities)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> items = responsibilities.Split(&lt;span style="color:#a5d6ff">&amp;#39;,&amp;#39;&lt;/span>, StringSplitOptions.TrimEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bullets = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n&amp;#34;&lt;/span>, items.Select(r =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;- {r}&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ## {componentName}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> {description}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ### Responsibilities
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> {bullets}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ---
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> *Generated documentation — review and expand as needed.*
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Wire it all up&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromObject(&lt;span style="color:#ff7b72">new&lt;/span> FileSystemPlugin(&lt;span style="color:#a5d6ff">&amp;#34;./src&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;DocumentationPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddSystemMessage(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a codebase documentation assistant. You help developers understand
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> projects &lt;span style="color:#ff7b72">by&lt;/span> reading source files and explaining architecture, patterns,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and design decisions.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When asked about code, use your tools to read the actual files rather
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> than guessing. Be specific and reference actual code when possible.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Generate documentation artifacts when asked.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> OpenAIPromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Documentation Assistant ready. Ask me about your codebase!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Type &amp;#39;exit&amp;#39; to quit.\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">while&lt;/span> (&lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;You: &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> input = Console.ReadLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(input) || input.Equals(&lt;span style="color:#a5d6ff">&amp;#34;exit&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> executionSettings: settings,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kernel: kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;\nAssistant: {response.Content}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddAssistantMessage(response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 도우미는 다음을 수행할 수 있습니다.- 프로젝트 디렉터리의 &lt;strong>파일 나열 및 읽기&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>실제 소스 파일을 읽어 코드베이스에 관한 &lt;strong>질문에 답&lt;/strong>하세요.&lt;/li>
&lt;li>마크다운 형식으로 &lt;strong>문서 생성&lt;/strong>&lt;/li>
&lt;li>&lt;strong>대화 맥락을 유지&lt;/strong>하여 후속 질문이 자연스럽게 작동하도록 합니다.&lt;/li>
&lt;/ul>
&lt;p>AI는 사용자가 요청한 내용에 따라 &lt;code>read_file&lt;/code>, &lt;code>list_files&lt;/code> 또는 &lt;code>generate_summary&lt;/code>를 언제 호출할지 자동으로 결정합니다. &amp;ldquo;OrderService는 무엇을 하나요?&amp;ldquo;라고 물어보세요. 파일을 읽고, 분석하고, 설명합니다. &amp;ldquo;인증 모듈에 대한 문서 생성&amp;quot;을 요청하면 파일을 탐색하고 구조를 이해하고 형식화된 요약을 생성합니다.&lt;/p>
&lt;h2 id="제작-팁">제작 팁&lt;/h2>
&lt;p>Semantic Kernel 애플리케이션을 출시하기 전에 제가 힘들게 배운 몇 가지 사항은 다음과 같습니다.&lt;/p>
&lt;p>&lt;strong>종속성 주입을 적절하게 사용하세요.&lt;/strong> ASP.NET Core 앱에서 커널과 서비스를 인라인으로 만드는 대신 DI 컨테이너에 등록하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddKernel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: configuration[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>]!,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: configuration[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>]!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddFromType&amp;lt;OrderPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>오류를 적절하게 처리합니다.&lt;/strong> LLM 호출은 실패하거나 시간 초과되거나 예상치 못한 결과를 반환할 수 있습니다. try-catch 블록으로 호출을 래핑하고 Polly 또는 내장된 복원력 기능을 사용하여 재시도 정책을 구현하세요.&lt;/p>
&lt;p>&lt;strong>토큰 사용량을 모니터링합니다.&lt;/strong> 모든 프롬프트, 모든 기능 설명, 모든 채팅 기록은 토큰을 소비합니다. 필터를 사용하여 사용량을 기록하고 추적하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>kernel.FunctionInvocationFilters.Add(&lt;span style="color:#ff7b72">new&lt;/span> LoggingFilter());
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>함수 설명을 정확하게 유지하세요.&lt;/strong> 모호한 설명으로 인해 AI가 함수를 잘못 호출하게 됩니다. &amp;ldquo;설명만 읽어도 이 기능을 언제, 어떻게 사용하는지 정확히 알 수 있나요?&amp;ldquo;라고 질문하여 설명을 테스트해 보세요.&lt;/p>
&lt;h2 id="결론">결론&lt;/h2>
&lt;p>Semantic Kernel은 애플리케이션 구축에 대한 생각을 근본적으로 바꾸는 라이브러리 중 하나입니다. 이는 단순한 API 래퍼가 아닙니다. 유지 관리 및 테스트가 가능하고 프로덕션에 즉시 사용할 수 있는 방식으로 기존 코드로 AI 기능을 구성할 수 있는 오케스트레이션 프레임워크입니다.&lt;/p>
&lt;p>제가 가장 좋아하는 점은 .NET 생태계를 존중한다는 것입니다. 종속성 주입, 속성, 비동기/대기, 인터페이스 등 이미 알고 있는 패턴을 사용하여 AI 세계로 확장합니다. 완전히 새로운 패러다임을 배울 필요는 없습니다. 툴킷에 AI를 또 다른 기능으로 추가하기만 하면 됩니다.&lt;/p>
&lt;p>.NET 애플리케이션을 구축 중이고 아직 Semantic Kernel을 탐색하지 않았다면 지금이 바로 적기입니다. SDK는 안정적이고 커뮤니티가 활발하며 간단한 프롬프트 오케스트레이션부터 다중 에이전트 협업에 이르기까지 SDK가 지원하는 패턴은 현대 개발자에게 필수적인 기술이 되고 있습니다.&lt;/p>
&lt;p>작게 시작하십시오. 커널을 생성하고, 플러그인을 등록하고, AI가 코드를 호출하는 것을 지켜보세요. 일단 클릭하면 애플리케이션의 모든 곳에 인텔리전스를 추가할 수 있는 기회가 나타나기 시작합니다.&lt;/p>
&lt;p>&lt;a href="https://learn.microsoft.com/semantic-kernel/overview/">공식 문서&lt;/a> 및 &lt;a href="https://github.com/microsoft/semantic-kernel">GitHub 저장소&lt;/a>는 여정을 계속하는 데 훌륭한 리소스입니다. 행복한 건물!&lt;/p></content:encoded><category>AI</category><category>.NET</category><category>Semantic Kernel</category><category>Azure</category></item><item><title>Blazor에서 구성 요소 상속</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-inherit-components/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-inherit-components/</guid><description>ComponentBase 및 공유 기본 클래스를 사용하여 상속을 통해 Blazor 구성 요소를 확장하고 재사용합니다.</description><content:encoded>&lt;p>저는 여러 개의 양식 페이지가 있는 프로젝트를 구축하고 있었는데, 모든 페이지마다 동일한 로드 상태 논리, 동일한 오류 처리 및 동일한 토스트 알림이 있었습니다. 모든 것을 복사하여 붙여넣는 것이 잘못된 것 같아서 Blazor에서 구성 요소 상속을 살펴보았습니다. Blazor 구성 요소는 C# 클래스이므로 매우 간단합니다.&lt;/p>
&lt;h1 id="기본">기본&lt;/h1>
&lt;p>모든 Blazor 구성 요소는 기본적으로 &lt;code>ComponentBase&lt;/code>에서 상속됩니다. &lt;code>ComponentBase&lt;/code>을 확장하는 고유한 기본 클래스를 생성한 다음 구성 요소가 그로부터 상속받도록 할 수 있습니다.&lt;/p>
&lt;p>대부분의 페이지에 로딩 상태와 오류 처리가 필요하다고 가정해 보겠습니다. 기본 클래스를 만들 수 있습니다:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PageBase&lt;/span> : ComponentBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsLoading { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> ErrorMessage { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task LoadDataAsync(Func&amp;lt;Task&amp;gt; action)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsLoading = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ErrorMessage = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> action();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ErrorMessage = ex.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">finally&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsLoading = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="기본-클래스-사용">기본 클래스 사용&lt;/h1>
&lt;p>이제 모든 페이지 구성 요소에서 &lt;code>ComponentBase&lt;/code>에서 상속하는 대신 &lt;code>PageBase&lt;/code>에서 상속합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inherits PageBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@if (IsLoading)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;spinner&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (ErrorMessage &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;alert alert-danger&amp;#34;&lt;/span>&amp;gt;@ErrorMessage&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> user &lt;span style="color:#ff7b72">in&lt;/span> users)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;@user.Name&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;User&amp;gt; users = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> users = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/users&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>@inherits PageBase&lt;/code> 지시문이 핵심입니다. 이는 Blazor에게 기본 &lt;code>ComponentBase&lt;/code> 대신 기본 클래스를 사용하도록 지시합니다. 이제 상속받은 모든 페이지에서 &lt;code>IsLoading&lt;/code>, &lt;code>ErrorMessage&lt;/code> 및 &lt;code>LoadDataAsync()&lt;/code>를 무료로 얻을 수 있습니다.&lt;/p>
&lt;h1 id="기본-클래스에-서비스-주입">기본 클래스에 서비스 주입&lt;/h1>
&lt;p>또한 기본 클래스에 서비스를 삽입하여 모든 하위 구성 요소에서 사용할 수 있도록 할 수도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PageBase&lt;/span> : ComponentBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> NavigationManager Navigation { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">default&lt;/span>!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> IToastService Toast { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">default&lt;/span>!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsLoading { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> ErrorMessage { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> NavigateBack() =&amp;gt; Navigation.NavigateTo(&lt;span style="color:#a5d6ff">&amp;#34;javascript:history.back()&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShowSuccess(&lt;span style="color:#ff7b72">string&lt;/span> message) =&amp;gt; Toast.ShowSuccess(message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>PageBase&lt;/code>에서 상속된 모든 구성 요소는 이제 아무것도 주입하지 않고도 &lt;code>Navigation&lt;/code>, &lt;code>Toast&lt;/code>, &lt;code>NavigateBack()&lt;/code> 및 &lt;code>ShowSuccess()&lt;/code>에 액세스할 수 있습니다.&lt;/p>
&lt;h1 id="일반-기본-클래스에-대해-더-자세히-알아보기">일반 기본 클래스에 대해 더 자세히 알아보기&lt;/h1>
&lt;p>일반적인 CRUD 패턴에 대한 일반 기본 클래스를 만들 수도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CrudPageBase&lt;/span>&amp;lt;T&amp;gt; : PageBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> List&amp;lt;T&amp;gt; Items { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> T? SelectedItem { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task&amp;lt;List&amp;lt;T&amp;gt;&amp;gt; FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task DeleteItem(T item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Items = &lt;span style="color:#ff7b72">await&lt;/span> FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnDelete(T item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> DeleteItem(item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Items = &lt;span style="color:#ff7b72">await&lt;/span> FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ShowSuccess(&lt;span style="color:#a5d6ff">&amp;#34;Item deleted.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그러면 실제 페이지가 매우 깨끗해집니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inherits CrudPageBase&amp;lt;Product&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>* just the markup, all logic lives &lt;span style="color:#ff7b72">in&lt;/span> the &lt;span style="color:#ff7b72">base&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> *&lt;span style="color:#f85149">@&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt; FetchItems()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task DeleteItem(Product item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Http.DeleteAsync(&lt;span style="color:#a5d6ff">$&amp;#34;api/products/{item.Id}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="사용해야-할-때와-사용하지-않을-때">사용해야 할 때와 사용하지 않을 때&lt;/h1>
&lt;p>구성 요소 상속은 로드 상태, 오류 처리, 인증 확인 또는 CRUD 패턴과 같은 공유 동작에 적합합니다. 그러나 깊은 상속 계층 구조를 지나치게 사용하지 마십시오. 두 수준 이상의 깊이로 진행되는 경우 구성을 사용하는 것이 더 나을 것입니다(예: 이전 게시물의 &lt;code>LoadingComponent&lt;/code> 접근 방식).&lt;/p>
&lt;p>나는 보통 페이지의 &amp;ldquo;유형&amp;quot;별로 하나의 기본 클래스를 유지합니다: 일반 페이지의 경우 &lt;code>PageBase&lt;/code>, 양식의 경우 &lt;code>FormPageBase&lt;/code>, 그게 전부입니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! &lt;strong>@emimontesdeoca&lt;/strong>로 소셜 미디어를 통해 언제든지 저에게 연락해주세요.&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/#inheritance">ASP.NET Core Razor 구성 요소 상속&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>.NET Aspire: 올바른 방법으로 클라우드 네이티브 애플리케이션 구축</title><link>https://emimontesdeoca.github.io/ko/posts/dotnet-aspire-cloud-native/</link><pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/dotnet-aspire-cloud-native/</guid><description>.NET에서 관찰 가능하고 프로덕션에 즉시 사용 가능한 분산 애플리케이션을 구축하기 위한 독보적인 스택인 .NET Aspire에 대한 심층 가이드입니다.</description><content:encoded>&lt;p>.NET에서 분산 애플리케이션을 구축해 본 적이 있다면 훈련 방법을 알고 계실 것입니다. 웹 API를 가동하고, 백그라운드 작업자를 추가하고, 캐싱을 위해 Redis를, 지속성을 위해 PostgreSQL을, 메시징을 위해 RabbitMQ를 추가하면 갑자기 비즈니스 로직을 작성하는 것보다 인프라 연결에 더 많은 시간을 소비하게 됩니다. &lt;code>appsettings.json&lt;/code> 파일에 흩어져 있는 연결 문자열, 구성하는 것을 잊은 상태 확인, 항상 &amp;ldquo;다음 스프린트의 문제&amp;quot;인 관찰 가능성.&lt;/p>
&lt;p>나는 거기에 있었다. 내가 인정하고 싶은 것보다 더 많이.&lt;/p>
&lt;p>이것이 바로 &lt;strong>.NET Aspire&lt;/strong>가 해결하도록 설계된 문제입니다. 몇 달 동안 프로덕션 환경에서 실행한 후 제가 배운 좋은 점, 좋은 점, 문제점 등을 공유하고 싶습니다.&lt;/p>
&lt;h2 id="net-aspire란-무엇입니까">.NET Aspire란 무엇입니까?&lt;/h2>
&lt;p>.NET Aspire는 .NET을 사용하여 관찰 가능하고 생산 준비가 완료된 분산 애플리케이션을 구축하기 위한 독보적인 스택입니다. 이는 전통적인 의미의 프레임워크가 아닙니다. ASP.NET Core를 대체하거나 새로운 프로그래밍 모델을 강제로 적용하지 않습니다. 대신, 이미 알고 있는 내용을 바탕으로 클라우드 네이티브 앱을 구축할 때 항상 존재했던 격차를 메웁니다.&lt;/p>
&lt;p>Aspire는 기본적으로 네 가지 기능을 제공합니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>AppHost&lt;/strong> — 전체 분산 애플리케이션 토폴로지를 정의하는 프로젝트입니다. 어떤 서비스가 존재하는지, 무엇에 의존하는지, 어떻게 연결되는지 알아보세요.&lt;/li>
&lt;li>&lt;strong>서비스 기본값&lt;/strong> — 모든 서비스에 대해 상태 확인, 탄력성 정책, OpenTelemetry와 같은 교차 관심사를 한 번에 구성하는 공유 프로젝트입니다.&lt;/li>
&lt;li>&lt;strong>구성 요소&lt;/strong> — Redis, PostgreSQL, RabbitMQ, Azure Storage 등과 같은 지원 서비스와의 표준화된 통합을 제공하는 NuGet 패키지입니다.&lt;/li>
&lt;li>&lt;strong>개발자 대시보드&lt;/strong> — 로컬 개발 중에 전체 배포 앱에 대한 로그, 추적 및 측정항목을 표시하는 실시간 UI입니다.&lt;/li>
&lt;/ol>
&lt;p>철학은 간단합니다. 모든 .NET 클라우드 앱에 이러한 기능이 필요하다면 왜 매번 처음부터 구현해야 할까요?&lt;/p>
&lt;h2 id="첫-번째-aspire-프로젝트-설정">첫 번째 Aspire 프로젝트 설정&lt;/h2>
&lt;p>시작하는 방법은 간단합니다. .NET 8 이상이 필요하고 Aspire 워크로드가 설치되어 있어야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet workload update
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet workload install aspire
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 새로운 Aspire 시작 프로젝트를 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new aspire-starter -n MyCloudApp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그러면 다음 네 가지 프로젝트가 포함된 솔루션이 생성됩니다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>MyCloudApp.AppHost&lt;/code> — 오케스트레이터&lt;/li>
&lt;li>&lt;code>MyCloudApp.ServiceDefaults&lt;/code> — 공유 구성&lt;/li>
&lt;li>&lt;code>MyCloudApp.ApiService&lt;/code> — 샘플 웹 API&lt;/li>
&lt;li>&lt;code>MyCloudApp.Web&lt;/code> — Blazor 프런트엔드&lt;/li>
&lt;/ul>
&lt;p>AppHost를 실행하면 브라우저에 Aspire 대시보드가 열려 모든 서비스, 로그 및 상태를 즉시 볼 수 있습니다. Docker Compose 파일이 없습니다. 수동 포트 관리가 없습니다. 그냥 작동합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet run --project MyCloudApp.AppHost
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그 첫 경험이 나를 매료시켰습니다. 1분 안에 관찰 기능이 내장된 완벽하게 조정된 분산 앱이 완성됩니다.&lt;/p>
&lt;h2 id="apphost-패턴">AppHost 패턴&lt;/h2>
&lt;p>AppHost는 마법이 살아있는 곳입니다. 이는 빌더 패턴을 사용하여 분산 애플리케이션의 토폴로지, 즉 존재하는 리소스와 서비스가 리소스에 연결되는 방식을 정의하는 작은 콘솔 애플리케이션입니다.&lt;/p>
&lt;p>현실적인 AppHost의 모습은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = DistributedApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Infrastructure resources&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithPgAdmin()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> rabbitmq = builder.AddRabbitMQ(&lt;span style="color:#a5d6ff">&amp;#34;messaging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithManagementPlugin();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Application services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> catalogApi = builder.AddProject&amp;lt;Projects.CatalogApi&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(postgres)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(redis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(rabbitmq);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> orderApi = builder.AddProject&amp;lt;Projects.OrderApi&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;order-api&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(postgres)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(rabbitmq);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> frontend = builder.AddProject&amp;lt;Projects.WebFrontend&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;frontend&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithExternalHttpEndpoints()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(catalogApi)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(orderApi);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Build().Run();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```해당&lt;/span> &lt;span style="color:#f85149">코드를&lt;/span> &lt;span style="color:#f85149">소리내어&lt;/span> &lt;span style="color:#f85149">읽어보세요&lt;/span>. &lt;span style="color:#f85149">실제로는&lt;/span> &lt;span style="color:#f85149">그&lt;/span> &lt;span style="color:#f85149">자체를&lt;/span> &lt;span style="color:#f85149">문서화합니다&lt;/span>. &lt;span style="color:#a5d6ff">&amp;#34;카탈로그 API는 PostgreSQL, Redis 및 RabbitMQ를 참조합니다. 프런트엔드는 카탈로그 API 및 주문 API를 참조합니다.&amp;#34;&lt;/span> &lt;span style="color:#f85149">이것이&lt;/span> &lt;span style="color:#f85149">코드로&lt;/span> &lt;span style="color:#f85149">표현된&lt;/span> &lt;span style="color:#f85149">아키텍처입니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">주목할&lt;/span> &lt;span style="color:#f85149">만한&lt;/span> &lt;span style="color:#f85149">몇&lt;/span> &lt;span style="color:#f85149">가지&lt;/span> &lt;span style="color:#f85149">사항&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">`&lt;/span>WithReference&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">무거운&lt;/span> &lt;span style="color:#f85149">작업을&lt;/span> &lt;span style="color:#f85149">수행합니다&lt;/span>. &lt;span style="color:#f85149">환경&lt;/span> &lt;span style="color:#f85149">변수&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">구성을&lt;/span> &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">연결&lt;/span> &lt;span style="color:#f85149">문자열과&lt;/span> &lt;span style="color:#f85149">서비스&lt;/span> URL을 &lt;span style="color:#f85149">소비&lt;/span> &lt;span style="color:#f85149">프로젝트에&lt;/span> &lt;span style="color:#f85149">자동으로&lt;/span> &lt;span style="color:#f85149">삽입합니다&lt;/span>. &lt;span style="color:#f85149">귀하의&lt;/span> &lt;span style="color:#f85149">서비스는&lt;/span> Redis가 *&lt;span style="color:#f85149">어디&lt;/span>* &lt;span style="color:#f85149">실행되고&lt;/span> &lt;span style="color:#f85149">있는지&lt;/span> &lt;span style="color:#f85149">알&lt;/span> &lt;span style="color:#f85149">필요가&lt;/span> &lt;span style="color:#f85149">없습니다&lt;/span>. Aspire가 &lt;span style="color:#f85149">이를&lt;/span> &lt;span style="color:#f85149">처리합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">`&lt;/span>WithPgAdmin()&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">및&lt;/span> **&lt;span style="color:#f85149">`&lt;/span>WithManagementPlugin()&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">실제&lt;/span> &lt;span style="color:#f85149">서비스와&lt;/span> &lt;span style="color:#f85149">함께&lt;/span> PostgreSQL &lt;span style="color:#f85149">및&lt;/span> RabbitMQ용 &lt;span style="color:#f85149">관리&lt;/span> UI를 &lt;span style="color:#f85149">실행합니다&lt;/span>. &lt;span style="color:#f85149">개발하는&lt;/span> &lt;span style="color:#f85149">동안&lt;/span> &lt;span style="color:#f85149">이는&lt;/span> &lt;span style="color:#f85149">매우&lt;/span> &lt;span style="color:#f85149">중요합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">`&lt;/span>WithExternalHttpEndpoints()&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">서비스를&lt;/span> &lt;span style="color:#f85149">외부에서&lt;/span> &lt;span style="color:#f85149">액세스할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있는&lt;/span> &lt;span style="color:#f85149">것으로&lt;/span> &lt;span style="color:#f85149">표시하며&lt;/span> &lt;span style="color:#f85149">이는&lt;/span> &lt;span style="color:#f85149">배포&lt;/span> &lt;span style="color:#f85149">시&lt;/span> &lt;span style="color:#f85149">중요합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">리소스&lt;/span> &lt;span style="color:#f85149">구성&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">세분화된&lt;/span> &lt;span style="color:#f85149">제어를&lt;/span> &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">리소스를&lt;/span> &lt;span style="color:#f85149">구성할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithEnvironment(&lt;span style="color:#a5d6ff">&amp;#34;POSTGRES_MAX_CONNECTIONS&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;200&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDataVolume(&lt;span style="color:#a5d6ff">&amp;#34;postgres-data&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDataVolume(&lt;span style="color:#a5d6ff">&amp;#34;redis-data&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>데이터 볼륨은 로컬 개발 데이터가 컨테이너를 다시 시작해도 유지되도록 보장합니다. 작은 디테일로 삶의 질이 크게 향상됩니다.&lt;/p>
&lt;h2 id="서비스-기본값-알려지지-않은-영웅">서비스 기본값: 알려지지 않은 영웅&lt;/h2>
&lt;p>&lt;code>ServiceDefaults&lt;/code> 프로젝트는 Aspire에서 가장 과소평가된 부분입니다. 이는 솔루션의 모든 서비스가 참조하는 공유 라이브러리이며, 잊어버리거나 일관되지 않게 구현할 수 있는 모든 교차 문제를 구성합니다.&lt;/p>
&lt;p>ServiceDefaults의 일반적인 &lt;code>Extensions.cs&lt;/code>는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Extensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder AddServiceDefaults(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.ConfigureOpenTelemetry();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddDefaultHealthChecks();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddServiceDiscovery();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.ConfigureHttpClientDefaults(http =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> http.AddStandardResilienceHandler();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> http.AddServiceDiscovery();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder ConfigureOpenTelemetry(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Logging.AddOpenTelemetry(logging =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging.IncludeFormattedMessage = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging.IncludeScopes = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddOpenTelemetry()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithMetrics(metrics =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> metrics.AddAspNetCoreInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpClientInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddRuntimeInstrumentation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTracing(tracing =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddAspNetCoreInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpClientInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGrpcClientInstrumentation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddOpenTelemetryExporters();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder AddDefaultHealthChecks(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddHealthChecks()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddCheck(&lt;span style="color:#a5d6ff">&amp;#34;self&amp;#34;&lt;/span>, () =&amp;gt; HealthCheckResult.Healthy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;live&amp;#34;]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 각 서비스의 &lt;code>Program.cs&lt;/code>에서 한 줄로 모든 작업을 수행합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Your service-specific configuration...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDefaultEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>단일 &lt;code>AddServiceDefaults()&lt;/code> 호출은 다음을 제공합니다.&lt;/p>
&lt;ul>
&lt;li>구조화된 로깅, 측정항목 및 분산 추적 기능을 갖춘 &lt;strong>OpenTelemetry&lt;/strong>&lt;/li>
&lt;li>활성 및 준비 엔드포인트를 통한 &lt;strong>상태 확인&lt;/strong>&lt;/li>
&lt;li>&lt;strong>서비스 검색&lt;/strong> - 서비스가 이름으로 서로를 찾을 수 있음&lt;/li>
&lt;li>나가는 모든 HTTP 호출에 대한 &lt;strong>복원력 정책&lt;/strong>(재시도, 회로 차단기, 시간 초과)&lt;/li>
&lt;/ul>
&lt;p>이것은 &amp;ldquo;내 컴퓨터에서 작동하는&amp;rdquo; 데모와 프로덕션 준비 시스템을 구분하는 요소입니다. 그리고 Aspire는 이를 나중에 고려하지 않고 기본값으로 설정합니다.&lt;/p>
&lt;h2 id="aspire-구성요소">Aspire 구성요소&lt;/h2>
&lt;p>Aspire 구성 요소는 서비스가 지원 인프라에 연결되는 방식을 표준화하는 NuGet 패키지입니다. 이는 단순한 클라이언트 라이브러리 그 이상입니다. 상태 확인, 로깅, 추적 및 즉시 구성 가능한 복원력을 포함합니다.&lt;/p>
&lt;h3 id="레디스">레디스&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddRedisDistributedCache(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그게 다야. 연결 문자열은 &lt;code>WithReference&lt;/code>를 통해 AppHost에서 가져옵니다. 구성 요소는 상태 확인 및 OpenTelemetry 계측이 이미 연결되어 있는 Redis에서 지원하는 &lt;code>IDistributedCache&lt;/code>를 등록합니다.&lt;/p>
&lt;p>출력 캐싱에 Redis를 사용할 수도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddRedisOutputCache(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="entity-framework-core가-포함된-postgresql">Entity Framework Core가 포함된 PostgreSQL&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddNpgsqlDbContext&amp;lt;CatalogDbContext&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>, settings =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> settings.DisableRetry = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그러면 AppHost에 정의된 &lt;code>catalogdb&lt;/code> 데이터베이스에 연결하여 &lt;code>DbContext&lt;/code>가 등록됩니다. 여기에는 연결 풀링, 상태 확인 및 재시도 정책이 포함됩니다.&lt;/p>
&lt;h3 id="토끼mq">토끼MQ&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddRabbitMQClient(&lt;span style="color:#a5d6ff">&amp;#34;messaging&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>완전히 구성되고 상태가 확인된 RabbitMQ 클라이언트 라이브러리에서 &lt;code>IConnection&lt;/code>를 등록합니다.&lt;/p>
&lt;h3 id="azure-통합">Azure 통합&lt;/h3>
&lt;p>Aspire에는 Azure 서비스를 위한 최고 수준의 구성 요소도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Blob Storage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureBlobClient(&lt;span style="color:#a5d6ff">&amp;#34;blobs&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Service Bus&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureServiceBusClient(&lt;span style="color:#a5d6ff">&amp;#34;servicebus&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Key Vault&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureKeyVaultClient(&lt;span style="color:#a5d6ff">&amp;#34;secrets&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```패턴은&lt;/span> &lt;span style="color:#f85149">항상&lt;/span> &lt;span style="color:#f85149">동일합니다&lt;/span>. AppHost의 &lt;span style="color:#f85149">한&lt;/span> &lt;span style="color:#f85149">줄은&lt;/span> &lt;span style="color:#f85149">리소스를&lt;/span> &lt;span style="color:#f85149">정의하고&lt;/span> &lt;span style="color:#f85149">소비&lt;/span> &lt;span style="color:#f85149">서비스의&lt;/span> &lt;span style="color:#f85149">한&lt;/span> &lt;span style="color:#f85149">줄은&lt;/span> &lt;span style="color:#f85149">리소스를&lt;/span> &lt;span style="color:#f85149">사용합니다&lt;/span>. &lt;span style="color:#f85149">연결&lt;/span> &lt;span style="color:#f85149">세부정보가&lt;/span> &lt;span style="color:#f85149">자동으로&lt;/span> &lt;span style="color:#f85149">전달됩니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">개발자&lt;/span> &lt;span style="color:#f85149">대시보드&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Aspire &lt;span style="color:#f85149">대시보드는&lt;/span> &lt;span style="color:#f85149">실제로&lt;/span> &lt;span style="color:#f85149">사용하기&lt;/span> &lt;span style="color:#f85149">전까지는&lt;/span> &lt;span style="color:#f85149">가지고&lt;/span> &lt;span style="color:#f85149">있으면&lt;/span> &lt;span style="color:#f85149">좋을&lt;/span> &lt;span style="color:#f85149">것&lt;/span> &lt;span style="color:#f85149">같은&lt;/span> &lt;span style="color:#f85149">기능&lt;/span> &lt;span style="color:#f85149">중&lt;/span> &lt;span style="color:#f85149">하나입니다&lt;/span>. Aspire &lt;span style="color:#f85149">대시보드&lt;/span> &lt;span style="color:#f85149">없이&lt;/span> &lt;span style="color:#f85149">작업하는&lt;/span> &lt;span style="color:#f85149">것은&lt;/span> &lt;span style="color:#f85149">상상할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">없습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AppHost를 &lt;span style="color:#f85149">로컬로&lt;/span> &lt;span style="color:#f85149">실행하면&lt;/span> &lt;span style="color:#f85149">대시보드가&lt;/span> &lt;span style="color:#f85149">시작되고&lt;/span> &lt;span style="color:#f85149">다음을&lt;/span> &lt;span style="color:#f85149">제공합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">리소스&lt;/span> &lt;span style="color:#f85149">개요&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">상태&lt;/span> &lt;span style="color:#f85149">표시기를&lt;/span> &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">모든&lt;/span> &lt;span style="color:#f85149">서비스와&lt;/span> &lt;span style="color:#f85149">인프라를&lt;/span> &lt;span style="color:#f85149">한&lt;/span> &lt;span style="color:#f85149">눈에&lt;/span> &lt;span style="color:#f85149">볼&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">구조화된&lt;/span> &lt;span style="color:#f85149">로그&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">모든&lt;/span> &lt;span style="color:#f85149">서비스의&lt;/span> &lt;span style="color:#f85149">실시간&lt;/span> &lt;span style="color:#f85149">로그&lt;/span> &lt;span style="color:#f85149">스트리밍&lt;/span>, &lt;span style="color:#f85149">필터링&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">검색&lt;/span> &lt;span style="color:#f85149">가능&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">분산&lt;/span> &lt;span style="color:#f85149">추적&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">플레임&lt;/span> &lt;span style="color:#f85149">차트로&lt;/span> &lt;span style="color:#f85149">시각화된&lt;/span> &lt;span style="color:#f85149">여러&lt;/span> &lt;span style="color:#f85149">서비스에&lt;/span> &lt;span style="color:#f85149">걸친&lt;/span> &lt;span style="color:#f85149">엔드투엔드&lt;/span> &lt;span style="color:#f85149">요청&lt;/span> &lt;span style="color:#f85149">추적&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">지표&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">실시간&lt;/span> HTTP &lt;span style="color:#f85149">요청&lt;/span> &lt;span style="color:#f85149">비율&lt;/span>, &lt;span style="color:#f85149">오류율&lt;/span>, &lt;span style="color:#f85149">지연&lt;/span> &lt;span style="color:#f85149">시간&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">사용자&lt;/span> &lt;span style="color:#f85149">지정&lt;/span> &lt;span style="color:#f85149">지표&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">콘솔&lt;/span> &lt;span style="color:#f85149">로그&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">각&lt;/span> &lt;span style="color:#f85149">컨테이너&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">프로젝트의&lt;/span> &lt;span style="color:#f85149">원시&lt;/span> stdout/stderr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">분산&lt;/span> &lt;span style="color:#f85149">추적은&lt;/span> &lt;span style="color:#f85149">특히&lt;/span> &lt;span style="color:#f85149">중요합니다&lt;/span>. &lt;span style="color:#f85149">요청이&lt;/span> &lt;span style="color:#f85149">프런트엔드에&lt;/span> &lt;span style="color:#f85149">도달하고&lt;/span> &lt;span style="color:#f85149">카탈로그&lt;/span> API를 &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">흐르고&lt;/span> Redis &lt;span style="color:#f85149">및&lt;/span> PostgreSQL에 &lt;span style="color:#f85149">도달하면&lt;/span> &lt;span style="color:#f85149">각&lt;/span> &lt;span style="color:#f85149">홉에&lt;/span> &lt;span style="color:#f85149">대한&lt;/span> &lt;span style="color:#f85149">타이밍과&lt;/span> &lt;span style="color:#f85149">함께&lt;/span> &lt;span style="color:#f85149">전체&lt;/span> &lt;span style="color:#f85149">체인을&lt;/span> &lt;span style="color:#f85149">볼&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>. &lt;span style="color:#f85149">더&lt;/span> &lt;span style="color:#f85149">이상&lt;/span> &lt;span style="color:#f85149">병목&lt;/span> &lt;span style="color:#f85149">현상이&lt;/span> &lt;span style="color:#f85149">발생하는&lt;/span> &lt;span style="color:#f85149">위치를&lt;/span> &lt;span style="color:#f85149">추측할&lt;/span> &lt;span style="color:#f85149">필요가&lt;/span> &lt;span style="color:#f85149">없습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">디버깅하는&lt;/span> &lt;span style="color:#f85149">동안&lt;/span> &lt;span style="color:#f85149">대시보드가&lt;/span> &lt;span style="color:#f85149">​​가장&lt;/span> &lt;span style="color:#f85149">유용하다는&lt;/span> &lt;span style="color:#f85149">것을&lt;/span> &lt;span style="color:#f85149">알았습니다&lt;/span>. &lt;span style="color:#f85149">여러&lt;/span> &lt;span style="color:#f85149">터미널&lt;/span> &lt;span style="color:#f85149">창을&lt;/span> &lt;span style="color:#f85149">추적하거나&lt;/span> &lt;span style="color:#f85149">로그&lt;/span> &lt;span style="color:#f85149">파일&lt;/span> &lt;span style="color:#f85149">사이를&lt;/span> &lt;span style="color:#f85149">이동하는&lt;/span> &lt;span style="color:#f85149">대신&lt;/span> &lt;span style="color:#f85149">서비스&lt;/span> &lt;span style="color:#f85149">전반에&lt;/span> &lt;span style="color:#f85149">걸쳐&lt;/span> &lt;span style="color:#f85149">관련&lt;/span> &lt;span style="color:#f85149">이벤트를&lt;/span> &lt;span style="color:#f85149">연결하는&lt;/span> &lt;span style="color:#f85149">상관&lt;/span> &lt;span style="color:#f85149">관계&lt;/span> ID를 &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">모든&lt;/span> &lt;span style="color:#f85149">것이&lt;/span> &lt;span style="color:#f85149">한&lt;/span> &lt;span style="color:#f85149">곳에&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">대시보드는&lt;/span> &lt;span style="color:#f85149">독립형&lt;/span> &lt;span style="color:#f85149">컨테이너로도&lt;/span> &lt;span style="color:#f85149">제공됩니다&lt;/span>. &lt;span style="color:#f85149">즉&lt;/span>, &lt;span style="color:#f85149">스테이징&lt;/span> &lt;span style="color:#f85149">환경이나&lt;/span> CI &lt;span style="color:#f85149">파이프라인에서&lt;/span> &lt;span style="color:#f85149">사용할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker run --rm -p &lt;span style="color:#a5d6ff">18888&lt;/span>:&lt;span style="color:#a5d6ff">18888&lt;/span> \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mcr.microsoft.com/dotnet/aspire-dashboard:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="배포">배포&lt;/h2>
&lt;p>Aspire는 개발 중에 도움이 되지만 생산은 어떻습니까? 이것이 실제적인 일이 되는 곳입니다.&lt;/p>
&lt;h3 id="azure-컨테이너-앱">Azure 컨테이너 앱&lt;/h3>
&lt;p>가장 간단한 배포 경로는 최고 수준의 Aspire를 지원하는 ACA(Azure Container Apps)입니다. Azure 개발자 CLI를 사용하여 직접 배포할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>azd init
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>azd up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>azd init&lt;/code> 명령은 Aspire AppHost를 감지하고 필요한 코드형 인프라를 생성합니다. &lt;code>azd up&lt;/code>는 AppHost 토폴로지를 기반으로 컨테이너 레지스트리, 컨테이너 앱, 데이터베이스, Redis 인스턴스 등 모든 것을 프로비저닝합니다.&lt;/p>
&lt;p>AppHost는 기본적으로 배포 매니페스트가 됩니다. &amp;ldquo;catalog-api는 PostgreSQL 및 Redis에 따라 달라집니다&amp;quot;를 정의하는 동일한 코드가 인프라 프로비저닝을 구동합니다.&lt;/p>
&lt;h3 id="쿠버네티스">쿠버네티스&lt;/h3>
&lt;p>Kubernetes 배포의 경우 Aspire는 매니페스트를 직접 생성하지 않지만 AppHost 토폴로지는 Kubernetes 리소스에 깔끔하게 매핑됩니다. 커뮤니티 도구 &lt;code>aspirate&lt;/code>(Aspir8)는 AppHost에서 Helm 차트 또는 Kubernetes 매니페스트를 생성할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet tool install -g aspirate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aspirate generate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aspirate apply
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="배포-고려-사항">배포 고려 사항&lt;/h3>
&lt;p>명심해야 할 몇 가지 사항:- &lt;strong>연결 문자열은 환경에 따라 변경됩니다.&lt;/strong> 로컬에서 Aspire는 컨테이너를 회전하고 연결 문자열을 자동으로 주입합니다. 프로덕션에서는 관리형 서비스를 가리킵니다. Aspire는 표준 .NET 구성을 사용하므로 환경 변수와 Azure Key Vault가 예상대로 작동합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AppHost는 프로덕션 환경에서 실행되지 않습니다.&lt;/strong> 개발 및 배포 조정 도구입니다. 프로덕션에서 서비스는 환경 변수 및 오케스트레이터를 통해 구성되어 독립적으로 실행됩니다.&lt;/li>
&lt;li>&lt;strong>인프라 리소스는 관리형 서비스가 됩니다.&lt;/strong> 로컬 PostgreSQL 컨테이너는 Azure Database for PostgreSQL이 됩니다. 로컬 Redis 컨테이너는 Azure Cache for Redis가 됩니다. 소비 코드는 변경되지 않습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="실제-팁">실제 팁&lt;/h2>
&lt;p>한동안 프로덕션 환경에서 Aspire를 실행한 후 시간을 절약할 수 있었던 교훈은 다음과 같습니다.&lt;/p>
&lt;h3 id="1-맞춤형-리소스-수명-주기-확인을-사용하세요">1. 맞춤형 리소스 수명 주기 확인을 사용하세요&lt;/h3>
&lt;p>리소스가 준비되었는지 확인하기 위해 컨테이너 시작에만 의존하지 마세요. PostgreSQL은 실제로 쿼리를 제공할 준비가 되기 전에 TCP 연결을 수락할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithHealthCheck();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Aspire는 리소스에 대해 상태 확인을 실행하고 실제로 준비될 때까지 종속 서비스를 보유할 수 있습니다.&lt;/p>
&lt;h3 id="2-공통-패턴을-확장으로-추출">2. 공통 패턴을 확장으로 추출&lt;/h3>
&lt;p>여러 서비스가 비슷한 구성을 공유하는 경우 확장 메서드를 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AppHostExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IResourceBuilder&amp;lt;ProjectResource&amp;gt; AddWorkerService(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IDistributedApplicationBuilder builder,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IResourceBuilder&amp;lt;IResourceWithConnectionString&amp;gt; db,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IResourceBuilder&amp;lt;IResourceWithConnectionString&amp;gt; messaging)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder.AddProject&amp;lt;Projects.WorkerService&amp;gt;(name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(messaging)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReplicas(&lt;span style="color:#a5d6ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-부하-테스트에-withreplicas-활용">3. 부하 테스트에 WithReplicas 활용&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> catalogApi = builder.AddProject&amp;lt;Projects.CatalogApi&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(postgres)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(redis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReplicas(&lt;span style="color:#a5d6ff">5&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>WithReplicas&lt;/code>는 서비스의 여러 인스턴스를 로컬에서 실행합니다. 이는 클러스터에 배포하지 않고도 로드 밸런싱, 동시성 버그 및 분산 캐싱 동작을 테스트하는 데 유용합니다.&lt;/p>
&lt;h3 id="4-민감한-값에-대한-매개변수-사용">4. 민감한 값에 대한 매개변수 사용&lt;/h3>
&lt;p>로컬 개발의 경우에도 자격 증명을 하드코딩하지 마세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> dbPassword = builder.AddParameter(&lt;span style="color:#a5d6ff">&amp;#34;db-password&amp;#34;&lt;/span>, secret: &lt;span style="color:#79c0ff">true&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>, password: dbPassword)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>로컬로 실행할 때 Aspire는 값을 묻는 메시지를 표시하거나 사용자 비밀에서 값을 읽습니다. CI/CD에서는 환경 변수에서 가져옵니다.&lt;/p>
&lt;h3 id="5-통합-테스트가-쉬워졌습니다">5. 통합 테스트가 쉬워졌습니다.&lt;/h3>
&lt;p>Aspire에는 분산 앱 통합 테스트를 매우 쉽게 만드는 테스트 패키지가 포함되어 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Fact]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task CatalogApiReturnsProducts()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> appHost = &lt;span style="color:#ff7b72">await&lt;/span> DistributedApplicationTestingBuilder
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .CreateAsync&amp;lt;Projects.MyCloudApp_AppHost&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> app = &lt;span style="color:#ff7b72">await&lt;/span> appHost.BuildAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> app.StartAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = app.CreateHttpClient(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetAsync(&lt;span style="color:#a5d6ff">&amp;#34;/api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.EnsureSuccessStatusCode();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> products = &lt;span style="color:#ff7b72">await&lt;/span> response.Content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ReadFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.NotEmpty(products);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 데이터베이스 및 메시지 브로커를 포함한 &lt;em>전체&lt;/em> 분산 애플리케이션을 가동하여 이에 대해 테스트를 실행하고 해체합니다. CI 파이프라인에서 실제 인프라에 대한 실제 통합 테스트를 수행합니다. 조롱하지 마세요.&lt;/p>
&lt;h3 id="6-로컬에서-리소스-사용량-모니터링">6. 로컬에서 리소스 사용량 모니터링&lt;/h3>
&lt;p>여러 컨테이너를 로컬에서 실행하는 경우 리소스 소비를 주의 깊게 살펴보세요. 관리 UI가 포함된 PostgreSQL, Redis 및 RabbitMQ 인스턴스는 2~3GB의 RAM을 쉽게 사용할 수 있습니다. 제한된 시스템을 사용하는 경우 더 가벼운 리소스 구성을 사용하는 것이 좋습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithContainerRuntimeArgs(&lt;span style="color:#a5d6ff">&amp;#34;--memory=256m&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="결론">결론&lt;/h2>
&lt;p>.NET Aspire는 분산 애플리케이션을 구축하는 방식을 근본적으로 변경했습니다. 혁신적인 개념을 도입했기 때문이 아닙니다. 상태 확인, OpenTelemetry 및 컨테이너 오케스트레이션은 새로운 것이 아닙니다. 하지만 &lt;em>기본값&lt;/em>으로 만들기 때문입니다. 일반적으로 내려야 하는 수백 가지의 작은 결정을 내리고 이를 합리적인 기본값으로 구현하며 필요할 때 재정의할 수 있습니다.제 생각에는 AppHost 패턴이 가장 큰 승리라고 생각합니다. Docker Compose 파일, Kubernetes 매니페스트 및 README 문서에 분산되지 않고 코드로 표현된 분산 애플리케이션 토폴로지를 가짐으로써 시스템을 이해하기 쉽게 만듭니다. 새로운 팀 구성원은 AppHost에서 &lt;code>Program.cs&lt;/code>를 열고 몇 분 안에 전체 아키텍처를 이해할 수 있습니다.&lt;/p>
&lt;p>.NET으로 분산 애플리케이션을 구축하고 있다면 Aspire를 진지하게 살펴보세요. &lt;code>aspire-starter&lt;/code> 템플릿으로 시작하여 대시보드를 탐색하고 필요에 따라 구성 요소를 점진적으로 채택하세요. 첫날부터 올인할 필요는 없습니다. Aspire는 설계상 추가 기능을 제공합니다.&lt;/p>
&lt;p>인프라 상용구를 배선하는 데 첫 번째 스프린트를 소비하는 시대는 끝났습니다. Aspire가 배관 작업을 처리하도록 하여 실제로 중요한 것, 즉 애플리케이션에 집중할 수 있도록 하세요.&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>Cloud</category><category>Docker</category></item><item><title>Blazor의 인증 및 권한 부여: 실용 가이드</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-authentication-authorization/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-authentication-authorization/</guid><description>ASP.NET ID부터 OAuth, 역할 기반 액세스 및 구성 요소 보안에 이르기까지 Blazor 앱에서 인증 및 권한 부여를 구현하기 위한 실용적인 가이드입니다.</description><content:encoded>&lt;p>ASP.NET MVC 또는 Razor Pages를 사용하여 작업한 경우 인증 작동 방식에 대한 정신적 모델이 있을 것입니다. 미들웨어가 요청을 가로채고, 쿠키 또는 토큰을 확인하고, &lt;code>HttpContext.User&lt;/code>를 채우면 경쟁이 시작됩니다. Blazor는 미묘하지만 중요한 방식으로 정신 모델을 변경합니다. 이러한 차이점을 조기에 이해하지 못하면 불가능하다고 느껴지는 인증 문제를 디버깅하게 됩니다.&lt;/p>
&lt;p>이 게시물에서는 서버 및 WebAssembly 호스팅 모델을 모두 다루면서 Blazor에서 인증 및 권한 부여가 실제로 어떻게 작동하는지 살펴보고 싶습니다. 우리는 사용자 지정 공급자, 외부 OAuth 및 숙련된 .NET 개발자라도 겪었던 함정을 통해 기본부터 끝까지 진행할 것입니다.&lt;/p>
&lt;h2 id="blazor의-인증이-다른-이유">Blazor의 인증이 다른 이유&lt;/h2>
&lt;p>기존 ASP.NET에서는 모든 사용자 상호 작용이 HTTP 요청입니다. 서버는 자격 증명을 확인하고 쿠키를 설정하며 모든 후속 요청에는 해당 쿠키가 전달됩니다. 인증 파이프라인은 선형적이고 예측 가능합니다.&lt;/p>
&lt;p>Blazor Server는 지속적인 SignalR 연결을 통해 작동합니다. 초기 HTTP 요청이 페이지를 로드한 후 모든 후속 상호 작용은 WebSocket을 통해 발생합니다. 각 버튼 클릭에 대한 새로운 HTTP 요청이 없으므로 미들웨어는 모든 상호 작용에서 다시 실행되지 않습니다. &lt;code>HttpContext&lt;/code>은 초기 연결 중에 사용할 수 있지만 회로 수명 내내 이에 의존하면 버그가 발생할 수 있습니다.&lt;/p>
&lt;p>Blazor WebAssembly는 전적으로 브라우저에서 실행됩니다. 서버 측 &lt;code>HttpContext&lt;/code>가 전혀 없습니다. 인증 상태는 API에서 가져와서 클라이언트측에 저장하고 토큰(일반적으로 JWT)을 통해 관리해야 합니다. 서버는 토큰 유효성 검사가 진행되는 동안에만 클라이언트를 신뢰합니다.&lt;/p>
&lt;p>이는 Blazor가 호스팅 모델에 관계없이 작동하는 인증 상태에 대한 자체 추상화가 필요함을 의미합니다. 그 추상화는 &lt;code>AuthenticationStateProvider&lt;/code>입니다.&lt;/p>
&lt;h2 id="인증-상태-재단">인증 상태: 재단&lt;/h2>
&lt;p>Blazor 인증 시스템의 핵심은 &lt;code>AuthenticationStateProvider&lt;/code>입니다. 이는 단일 중요한 메서드를 노출하는 추상 클래스입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>AuthenticationState&lt;/code> 개체는 .NET 전체에서 사용되는 것과 동일한 ID 모델인 &lt;code>ClaimsPrincipal&lt;/code>를 래핑합니다. 구성 요소는 쿠키나 토큰과 직접 대화하지 않습니다. 그들은 현재 상태에 대해 &lt;code>AuthenticationStateProvider&lt;/code>에게 묻습니다.&lt;/p>
&lt;p>전체 구성 요소 트리에서 이 상태를 사용할 수 있도록 Blazor는 &lt;code>CascadingAuthenticationState&lt;/code>를 제공합니다. 일반적으로 라우터를 &lt;code>App.razor&lt;/code> 또는 레이아웃으로 래핑합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;CascadingAuthenticationState&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Router AppAssembly=&amp;#34;@typeof(App).Assembly&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Found Context=&amp;#34;routeData&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;AuthorizeRouteView RouteData=&amp;#34;@routeData&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DefaultLayout=&amp;#34;@typeof(MainLayout)&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;You&amp;#39;re not authorized to view this page.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/AuthorizeRouteView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Found&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotFound&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;LayoutView Layout=&amp;#34;@typeof(MainLayout)&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Page not found.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/LayoutView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotFound&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Router&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/CascadingAuthenticationState&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>AuthorizeRouteView&lt;/code>는 여기서 이중 임무를 수행합니다. 즉, 일치하는 페이지 구성 요소를 렌더링하기 전에 사용자가 인증 및 승인되었는지 확인하고 그렇지 않은 경우 대체 UI를 제공합니다.&lt;/p>
&lt;p>통합 Blazor 모델이 포함된 .NET 8 이상에서는 &lt;code>App.razor&lt;/code>에서 이를 구성하고 서비스 등록에서 &lt;code>AddCascadingAuthenticationState()&lt;/code>를 사용할 때 프레임워크가 계단식 매개 변수를 자동으로 처리합니다.&lt;/p>
&lt;h2 id="aspnet-id-통합">ASP.NET ID 통합&lt;/h2>
&lt;p>대부분의 프로젝트에서는 처음부터 인증을 구축할 필요가 없습니다. ASP.NET ID는 사용자 관리, 암호 해싱, 2단계 인증 및 계정 확인 기능을 즉시 제공합니다.Blazor를 사용한 설정은 &lt;code>Program.cs&lt;/code>에서 시작됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddDbContext&amp;lt;ApplicationDbContext&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.UseSqlServer(builder.Configuration.GetConnectionString(&lt;span style="color:#a5d6ff">&amp;#34;DefaultConnection&amp;#34;&lt;/span>)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddDefaultIdentity&amp;lt;IdentityUser&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.SignIn.RequireConfirmedAccount = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Password.RequireDigit = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Password.RequiredLength = &lt;span style="color:#a5d6ff">8&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddRoles&amp;lt;IdentityRole&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEntityFrameworkStores&amp;lt;ApplicationDbContext&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthentication(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.DefaultScheme = IdentityConstants.ApplicationScheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>.NET 8+의 Blazor 웹앱 템플릿을 사용하면 스캐폴드된 ID UI가 Razor 구성 요소를 직접 사용합니다. 나머지 Blazor 애플리케이션과 자연스럽게 통합되는 로그인, 등록 및 계정 관리 페이지가 제공됩니다. 더 이상 Razor Pages와 Blazor 구성 요소를 어색하게 혼합할 필요가 없습니다.&lt;/p>
&lt;p>&lt;code>ApplicationDbContext&lt;/code>는 &lt;code>IdentityDbContext&lt;/code>에서 상속되며 ID 테이블을 생성하려면 마이그레이션을 실행해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ApplicationDbContext&lt;/span> : IdentityDbContext&amp;lt;IdentityUser&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ApplicationDbContext(DbContextOptions&amp;lt;ApplicationDbContext&amp;gt; options)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#ff7b72">base&lt;/span>(options) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet ef migrations add InitialIdentity
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet ef database update
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="authorizeview-구성-요소">AuthorizeView 구성 요소&lt;/h2>
&lt;p>인증 상태가 구성 요소 트리를 통해 흐르면 &lt;code>AuthorizeView&lt;/code>를 사용하면 조건부로 UI를 렌더링할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Welcome, @context.User.Identity?.Name!&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/manage&amp;#34;&amp;gt;Manage Account&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form method=&amp;#34;post&amp;#34; action=&amp;#34;/account/logout&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Log Out&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/login&amp;#34;&amp;gt;Log In&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/register&amp;#34;&amp;gt;Register&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>&amp;lt;Authorized&amp;gt;&lt;/code> 내부의 &lt;code>context&lt;/code> 매개변수는 &lt;code>AuthenticationState&lt;/code>에 대한 액세스를 제공하므로 마크업에서 직접 클레임, 역할 및 사용자 ID를 검사할 수 있습니다.&lt;/p>
&lt;p>역할 및 정책과 함께 &lt;code>AuthorizeView&lt;/code>를 사용할 수도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView Roles=&amp;#34;Admin,Moderator&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button @onclick=&amp;#34;DeletePost&amp;#34;&amp;gt;Delete Post&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView Policy=&amp;#34;CanEditArticles&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button @onclick=&amp;#34;EditArticle&amp;#34;&amp;gt;Edit&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>명심해야 할 한 가지: &lt;code>AuthorizeView&lt;/code>는 UI 문제입니다. 요소를 숨기거나 표시하지만 기본 논리를 보호하지는 않습니다. 누군가가 API 엔드포인트를 호출하거나 메소드를 직접 호출할 수 있으면 &lt;code>AuthorizeView&lt;/code>를 완전히 우회합니다. 항상 서버 측에서도 인증을 시행하십시오.&lt;/p>
&lt;h2 id="authorize-속성">[Authorize] 속성&lt;/h2>
&lt;p>전체 페이지를 보호하려면 &lt;code>[Authorize]&lt;/code> 속성을 적용하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/admin/dashboard&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@attribute [Authorize(Roles = &amp;#34;Admin&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Admin Dashboard&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Only administrators can see this page.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>인증되지 않은 사용자가 이 페이지를 탐색하면 &lt;code>AuthorizeRouteView&lt;/code>가 시작되어 이전에 정의한 &lt;code>&amp;lt;NotAuthorized&amp;gt;&lt;/code> 템플릿을 렌더링합니다. 대신 탐색을 통해 &lt;code>NotAuthorized&lt;/code> 사례를 처리하여 로그인 페이지로 리디렉션할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-zed" data-lang="zed">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>NotAuthorized&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">@&lt;/span>if&lt;span style="color:#6e7681"> &lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>context.User.Identity&lt;span style="color:#ff7b72;font-weight:bold">?&lt;/span>.IsAuthenticated&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">??&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>true)&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>RedirectToLogin&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>else&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>You&lt;span style="color:#6e7681"> &lt;/span>don&lt;span style="color:#f85149">&amp;#39;&lt;/span>t&lt;span style="color:#6e7681"> &lt;/span>have&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">permission&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>to&lt;span style="color:#6e7681"> &lt;/span>access&lt;span style="color:#6e7681"> &lt;/span>this&lt;span style="color:#6e7681"> &lt;/span>page.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>NotAuthorized&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>간단한 &lt;code>RedirectToLogin&lt;/code> 구성 요소는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject NavigationManager &lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override void OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> returnUrl &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> Uri&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>EscapeDataString(&lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Uri);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NavigateTo(&lt;span style="color:#ff7b72;font-weight:bold">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/account/login?returnUrl={returnUrl}&amp;#34;&lt;/span>, forceLoad: true);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>여기서는 &lt;code>forceLoad: true&lt;/code>가 중요합니다. 서버 측 인증 미들웨어가 로그인 흐름을 적절하게 처리할 수 있도록 실제 HTTP 탐색이 필요합니다.&lt;/p>
&lt;h2 id="역할-기반-및-정책-기반-인증">역할 기반 및 정책 기반 인증&lt;/h2>
&lt;p>역할은 가장 간단한 모델입니다. 사용자를 &amp;ldquo;관리자&amp;rdquo; 또는 &amp;ldquo;편집자&amp;quot;와 같은 그룹에 할당한 다음 멤버십을 확인하세요. 그러나 정책은 훨씬 더 많은 유연성을 제공합니다.&lt;/p>
&lt;p>&lt;code>Program.cs&lt;/code>에 정책을 등록합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthorizationCore(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;CanPublish&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.RequireClaim(&lt;span style="color:#a5d6ff">&amp;#34;Permission&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Publish&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;MinimumAge&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.Requirements.Add(&lt;span style="color:#ff7b72">new&lt;/span> MinimumAgeRequirement(&lt;span style="color:#a5d6ff">18&lt;/span>)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;PremiumUser&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.RequireRole(&lt;span style="color:#a5d6ff">&amp;#34;Premium&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequireClaim(&lt;span style="color:#a5d6ff">&amp;#34;Subscription&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Active&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>사용자 정의 요구 사항에는 처리기가 필요합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MinimumAgeRequirement&lt;/span> : IAuthorizationRequirement
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> MinimumAge { &lt;span style="color:#ff7b72">get&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> MinimumAgeRequirement(&lt;span style="color:#ff7b72">int&lt;/span> minimumAge) =&amp;gt; MinimumAge = minimumAge;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MinimumAgeHandler&lt;/span> : AuthorizationHandler&amp;lt;MinimumAgeRequirement&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task HandleRequirementAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthorizationHandlerContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MinimumAgeRequirement requirement)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> dateOfBirthClaim = context.User.FindFirst(&lt;span style="color:#a5d6ff">&amp;#34;DateOfBirth&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (dateOfBirthClaim &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (age &amp;gt;= requirement.MinimumAge)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Succeed(requirement);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>핸들러를 등록합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;IAuthorizationHandler, MinimumAgeHandler&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>구성 요소에서 동적 논리가 필요할 때 프로그래밍 방식으로 인증을 확인할 수도 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject IAuthorizationService AuthorizationService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject AuthenticationStateProvider AuthStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> canPublish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> authState &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await AuthStateProvider&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetAuthenticationStateAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await AuthorizationService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AuthorizeAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>User, &lt;span style="color:#a5d6ff">&amp;#34;CanPublish&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> canPublish &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> result&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Succeeded;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="외부-oauth-제공자">외부 OAuth 제공자&lt;/h2>
&lt;p>ASP.NET 인증 미들웨어를 사용하면 &amp;ldquo;Google로 로그인&amp;rdquo; 또는 &amp;ldquo;GitHub으로 로그인&amp;rdquo; 지원이 간단해집니다. OAuth 흐름에는 HTTP 리디렉션이 필요하므로 서버 측에서 구성됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthentication()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGoogle(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Google:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Google:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Scope.Add(&lt;span style="color:#a5d6ff">&amp;#34;profile&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddMicrosoftAccount(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Microsoft:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Microsoft:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGitHub(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:GitHub:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:GitHub:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>GitHub의 경우 기본 ASP.NET 라이브러리에 포함되어 있지 않으므로 &lt;code>AspNet.Security.OAuth.GitHub&lt;/code> NuGet 패키지가 필요합니다.&lt;/p>
&lt;p>그런 다음 로그인 UI는 외부 인증 확인을 트리거하는 링크를 제공합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/account/external-login&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h2&amp;gt;Sign in with an external provider&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;form method=&amp;#34;post&amp;#34; action=&amp;#34;/api/auth/external-login&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;Google&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with Google
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;Microsoft&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with Microsoft
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;GitHub&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with GitHub
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>API 엔드포인트는 챌린지를 트리거하고 콜백을 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/api/auth/external-login&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string&lt;/span> provider, HttpContext context) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> properties = &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationProperties
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RedirectUri = &lt;span style="color:#a5d6ff">&amp;#34;/api/auth/external-callback&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Challenge(properties, [provider]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Blazor의 외부 인증에는 항상 전체 페이지 탐색이 필요합니다. 서버를 통하지 않고는 SignalR 회로 또는 WebAssembly 앱 내에서 OAuth 리디렉션을 완료할 수 없습니다.&lt;/p>
&lt;h2 id="blazor-webassembly의-토큰-기반-인증blazor-webassembly는-클라이언트에서-실행되므로-쿠키-기반-인증은-동일한-방식으로-적용되지-않습니다-대신-일반적으로-메모리에-저장되어-나가는-http-요청에-연결된-jwt를-사용합니다">Blazor WebAssembly의 토큰 기반 인증Blazor WebAssembly는 클라이언트에서 실행되므로 쿠키 기반 인증은 동일한 방식으로 적용되지 않습니다. 대신 일반적으로 메모리에 저장되어 나가는 HTTP 요청에 연결된 JWT를 사용합니다.&lt;/h2>
&lt;p>프레임워크는 토큰을 자동으로 연결하기 위해 &lt;code>AuthorizationMessageHandler&lt;/code>를 제공합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddHttpClient(&lt;span style="color:#a5d6ff">&amp;#34;API&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client =&amp;gt; client.BaseAddress = &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;https://api.example.com&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpMessageHandler&amp;lt;AuthorizationMessageHandler&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped(sp =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sp.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;().CreateClient(&lt;span style="color:#a5d6ff">&amp;#34;API&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>자체 API에 대해 인증하는 독립 실행형 Blazor WASM 앱의 경우 JWT를 구문 분석하는 사용자 지정 &lt;code>AuthenticationStateProvider&lt;/code>을 구현합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">JwtAuthenticationStateProvider&lt;/span> : AuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILocalStorageService _localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> JwtAuthenticationStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILocalStorageService localStorage,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _localStorage = localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> token = &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.GetItemAsync&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(token))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient.DefaultRequestHeaders.Authorization =
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationHeaderValue(&lt;span style="color:#a5d6ff">&amp;#34;Bearer&amp;#34;&lt;/span>, token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> claims = ParseClaimsFromJwt(token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identity = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity(claims, &lt;span style="color:#a5d6ff">&amp;#34;jwt&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(identity));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> NotifyAuthStateChanged()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IEnumerable&amp;lt;Claim&amp;gt; ParseClaimsFromJwt(&lt;span style="color:#ff7b72">string&lt;/span> jwt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> payload = jwt.Split(&lt;span style="color:#a5d6ff">&amp;#39;.&amp;#39;&lt;/span>)[&lt;span style="color:#a5d6ff">1&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> padded = payload.Length % &lt;span style="color:#a5d6ff">4&lt;/span> &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2&lt;/span> =&amp;gt; payload + &lt;span style="color:#a5d6ff">&amp;#34;==&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3&lt;/span> =&amp;gt; payload + &lt;span style="color:#a5d6ff">&amp;#34;=&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; payload
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bytes = Convert.FromBase64String(padded);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> json = JsonSerializer.Deserialize&amp;lt;Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, JsonElement&amp;gt;&amp;gt;(bytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> json?.Select(kvp =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(kvp.Key, kvp.Value.ToString()))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ?? Enumerable.Empty&amp;lt;Claim&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>로그인에 성공하면 토큰을 저장하고 인증 상태를 알립니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AuthService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILocalStorageService _localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> JwtAuthenticationStateProvider _authStateProvider;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> AuthService(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpClient httpClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILocalStorageService localStorage,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthenticationStateProvider authStateProvider)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _localStorage = localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; LoginAsync(&lt;span style="color:#ff7b72">string&lt;/span> email, &lt;span style="color:#ff7b72">string&lt;/span> password)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.PostAsJsonAsync(&lt;span style="color:#a5d6ff">&amp;#34;/api/auth/login&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> { Email = email, Password = password });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> response.Content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ReadFromJsonAsync&amp;lt;LoginResponse&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.SetItemAsync(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>, result!.Token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider.NotifyAuthStateChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task LogoutAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.RemoveItemAsync(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider.NotifyAuthStateChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>주의 사항: JWT를 &lt;code>localStorage&lt;/code>에 저장하면 XSS 공격에 노출됩니다. 보안 수준이 높은 애플리케이션의 경우 토큰을 메모리에만 유지하고 새로 고침 토큰을 사용하거나 서버가 토큰을 관리하고 클라이언트가 HTTP 전용 쿠키를 사용하는 BFF(Backend-for-Frontend) 패턴을 채택하는 것이 좋습니다.&lt;/p>
&lt;h2 id="blazor-서버와-webassembly-보안-고려-사항">Blazor 서버와 WebAssembly: 보안 고려 사항&lt;/h2>
&lt;p>호스팅 모델은 보안 상태를 근본적으로 변화시킵니다.&lt;/p>
&lt;p>&lt;strong>Blazor Server&lt;/strong>는 모든 구성 요소 논리를 서버에 유지합니다. 클라이언트는 SignalR을 통해 렌더링된 HTML diff만 볼 수 있습니다. 이는 다음을 의미합니다.&lt;/p>
&lt;ul>
&lt;li>민감한 로직은 절대 서버를 떠나지 않습니다.&lt;/li>
&lt;li>구성 요소에서 직접 데이터베이스 및 내부 서비스에 액세스할 수 있습니다.&lt;/li>
&lt;li>인증 상태는 초기 연결 시 서버의 &lt;code>HttpContext&lt;/code>에서 가져옵니다.&lt;/li>
&lt;li>회로는 인증 쿠키보다 오래 지속될 수 있습니다. 사용자의 쿠키가 만료되면 회로는 연결이 끊어질 때까지 활성 상태로 유지됩니다.&lt;/li>
&lt;li>회로 연결 해제를 적절하게 처리하고 다시 연결 시 인증 상태를 다시 검증해야 합니다.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Blazor WebAssembly&lt;/strong>는 전적으로 브라우저에서 실행됩니다. 이는 다음을 의미합니다.&lt;/p>
&lt;ul>
&lt;li>모든 구성 요소 코드를 다운로드하고 검사할 수 있습니다.&lt;/li>
&lt;li>WASM 구성 요소에 비밀, 연결 문자열 또는 민감한 비즈니스 논리를 넣지 마십시오.&lt;/li>
&lt;li>인증은 UX용 클라이언트에만 적용됩니다. 실제 시행은 API 계층에서 이루어져야 합니다.&lt;/li>
&lt;li>토큰 관리는 귀하의 책임입니다&lt;/li>
&lt;li>서버 프로젝트가 인증을 처리하고 WASM 앱을 제공하는 호스팅 모델 사용을 고려하세요.&lt;/li>
&lt;/ul>
&lt;p>WebAssembly 앱에 대해 제가 권장하는 패턴은 모든 구성 요소를 &amp;ldquo;신뢰할 수 없는 UI&amp;quot;인 것처럼 처리하고 모든 API 엔드포인트를 알 수 없는 클라이언트에 의해 호출되는 것처럼 처리하는 것입니다. 클라이언트가 확인하는 내용에 관계없이 서버 측의 모든 것을 검증합니다.&lt;/p>
&lt;h2 id="사용자-정의-authenticationstateprovider-구축">사용자 정의 AuthenticationStateProvider 구축&lt;/h2>
&lt;p>때로는 기본 제공 공급자가 아키텍처에 맞지 않는 경우가 있습니다. 레거시 인증 시스템과 통합하거나 인증 상태 변경을 폴링해야 할 수도 있습니다. Blazor Server에 대한 보다 완전한 사용자 지정 공급자는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomAuthStateProvider&lt;/span> : AuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IHttpContextAccessor _httpContextAccessor;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IUserService _userService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomAuthStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IHttpContextAccessor httpContextAccessor,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IUserService userService)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpContextAccessor = httpContextAccessor;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _userService = userService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpContext = _httpContextAccessor.HttpContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (httpContext?.User?.Identity?.IsAuthenticated != &lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (userId &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> user = &lt;span style="color:#ff7b72">await&lt;/span> _userService.GetUserWithClaimsAsync(userId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (user &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> claims = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Claim&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.NameIdentifier, user.Id),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.Name, user.DisplayName),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.Email, user.Email)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> claims.AddRange(user.Roles.Select(r =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(ClaimTypes.Role, r)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> claims.AddRange(user.Permissions.Select(p =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(&lt;span style="color:#a5d6ff">&amp;#34;Permission&amp;#34;&lt;/span>, p)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identity = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity(claims, &lt;span style="color:#a5d6ff">&amp;#34;Custom&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(identity));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> MarkUserAsAuthenticated(&lt;span style="color:#ff7b72">string&lt;/span> userId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> MarkUserAsLoggedOut()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> anonymous = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> authState = Task.FromResult(&lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(anonymous));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(authState);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Program.cs&lt;/code>에 등록하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;AuthenticationStateProvider, CustomAuthStateProvider&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;CustomAuthStateProvider&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>여기서 핵심 통찰력은 &lt;code>NotifyAuthenticationStateChanged&lt;/code>입니다. 이를 호출하면 계단식 매개변수가 업데이트되어 구성 요소 트리의 모든 &lt;code>AuthorizeView&lt;/code> 및 &lt;code>AuthorizeRouteView&lt;/code>가 다시 평가됩니다. 이는 전체 페이지 새로 고침 없이 UI가 로그인/로그아웃 이벤트에 반응하도록 만드는 방법입니다.&lt;/p>
&lt;h2 id="일반적인-함정-및-솔루션">일반적인 함정 및 솔루션&lt;/h2>
&lt;p>여러 프로젝트에서 Blazor 인증을 사용한 후 가장 자주 보게 되는 문제는 다음과 같습니다.&lt;/p>
&lt;h3 id="1-blazor-서버-구성-요소에서-httpcontext-사용httpcontext는-초기-http-요청-중에-사용할-수-있지만-signalr-상호-작용-중에는-null이거나-오래되었습니다-초기-렌더링-후에-실행되는-구성-요소에-ihttpcontextaccessor를-삽입하지-마세요">1. Blazor 서버 구성 요소에서 HttpContext 사용&lt;code>HttpContext&lt;/code>는 초기 HTTP 요청 중에 사용할 수 있지만 SignalR 상호 작용 중에는 &lt;code>null&lt;/code>이거나 오래되었습니다. 초기 렌더링 후에 실행되는 구성 요소에 &lt;code>IHttpContextAccessor&lt;/code>를 삽입하지 마세요.&lt;/h3>
&lt;p>&lt;strong>해결책:&lt;/strong> 초기화 중에 &lt;code>HttpContext&lt;/code>에서 필요한 것을 캡처하고 범위가 지정된 서비스에 저장합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">UserContext&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> UserId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> AccessToken { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// In a component that renders during the initial HTTP request:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject IHttpContextAccessor HttpContextAccessor
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject UserContext UserContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> context = HttpContextAccessor.HttpContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UserContext.AccessToken = context?.Request.Headers.Authorization
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToString().Replace(&lt;span style="color:#a5d6ff">&amp;#34;Bearer &amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-로그인-후-인증-상태가-업데이트되지-않습니다">2. 로그인 후 인증 상태가 업데이트되지 않습니다.&lt;/h3>
&lt;p>로그인 API를 호출하면 성공하지만 UI에는 여전히 &amp;ldquo;로그인&amp;quot;이 표시됩니다.&lt;/p>
&lt;p>&lt;strong>해결책:&lt;/strong> 인증 상태가 변경된 후 &lt;code>AuthenticationStateProvider&lt;/code>에서 &lt;code>NotifyAuthenticationStateChanged&lt;/code>를 호출해야 합니다. 프레임워크는 토큰이 저장되었거나 쿠키가 설정되었음을 마법처럼 감지하지 않습니다.&lt;/p>
&lt;h3 id="3-구성-요소에서-작동하지-않는-속성-승인">3. 구성 요소에서 작동하지 않는 속성 승인&lt;/h3>
&lt;p>구성 요소에 &lt;code>[Authorize]&lt;/code>를 추가했지만 인증되지 않은 사용자를 차단하지는 않습니다.&lt;/p>
&lt;p>&lt;strong>해결책:&lt;/strong> &lt;code>App.razor&lt;/code>에서 일반 &lt;code>RouteView&lt;/code> 대신 &lt;code>AuthorizeRouteView&lt;/code>를 사용하고 있는지 확인하세요. 표준 &lt;code>RouteView&lt;/code>는 인증 속성을 완전히 무시합니다.&lt;/p>
&lt;h3 id="4-사전-렌더링으로-인해-인증-상태가-중단됩니다">4. 사전 렌더링으로 인해 인증 상태가 중단됩니다.&lt;/h3>
&lt;p>Blazor WebAssembly에서 서버 측 사전 렌더링 중에는 사용 가능한 인증 토큰이 없습니다. 구성 요소는 인증되지 않은 것으로 렌더링된 다음 WASM이 로드된 후 인증된 상태로 깜박입니다.&lt;/p>
&lt;p>&lt;strong>해결책:&lt;/strong> &lt;code>@rendermode InteractiveWebAssembly&lt;/code>를 사용하여 인증에 민감한 페이지에 대한 사전 렌더링을 비활성화하거나(사전 렌더링 없이) 로드 상태를 적절하게 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Welcome back, @context.User.Identity?.Name&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorizing&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorizing&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;Sign in&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="5-장기-실행-회로에서의-토큰-만료">5. 장기 실행 회로에서의 토큰 만료&lt;/h3>
&lt;p>Blazor Server 회로는 몇 시간 동안 활성 상태를 유지할 수 있습니다. 토큰 또는 세션이 만료되면 사용자는 UI에서 &amp;ldquo;인증된&amp;rdquo; 상태를 유지하지만 API 호출은 실패하기 시작합니다.&lt;/p>
&lt;p>&lt;strong>해결책:&lt;/strong> 정기적인 점검을 구현하거나 &lt;code>RevalidatingServerAuthenticationStateProvider&lt;/code>를 사용하십시오.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">RevalidatingAuthStateProvider&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : RevalidatingServerAuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IServiceScopeFactory _scopeFactory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> RevalidatingAuthStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILoggerFactory loggerFactory,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IServiceScopeFactory scopeFactory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#ff7b72">base&lt;/span>(loggerFactory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scopeFactory = scopeFactory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> TimeSpan RevalidationInterval =&amp;gt; TimeSpan.FromMinutes(&lt;span style="color:#a5d6ff">30&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; ValidateAuthenticationStateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthenticationState authenticationState, CancellationToken cancellationToken)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> scope = _scopeFactory.CreateAsyncScope();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userManager = scope.ServiceProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetRequiredService&amp;lt;UserManager&amp;lt;IdentityUser&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> user = &lt;span style="color:#ff7b72">await&lt;/span> userManager.GetUserAsync(authenticationState.User);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> user &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 30분마다 사용자가 여전히 존재하는지(그리고 보안 스탬프가 변경되지 않았는지) 확인합니다.&lt;/p>
&lt;h2 id="결론">결론&lt;/h2>
&lt;p>Blazor의 인증 및 권한 부여에는 기존 요청-응답 ASP.NET에서 생각의 전환이 필요합니다. &lt;code>AuthenticationStateProvider&lt;/code> 추상화는 모든 것이 어떻게 조화를 이루는지 이해하는 열쇠입니다. 일단 이를 내면화하면 나머지는 자연스럽게 따라옵니다.&lt;/p>
&lt;p>대부분의 애플리케이션에서는 ASP.NET Identity 및 기본 제공 템플릿으로 시작합니다. 이들은 사용자 관리, 비밀번호 해싱 및 토큰 생성의 무거운 작업을 처리합니다. 요구 사항이 증가함에 따라 정책 및 클레임 기반 인증을 계층화합니다. 사용자가 원할 때 외부 OAuth 공급자를 추가하세요.&lt;/p>
&lt;p>호스팅 모델이 중요합니다. Blazor Server는 코드가 서버에 유지되는 보다 전통적인 보안 상태를 제공하는 반면, WebAssembly는 설계상 클라이언트를 신뢰할 수 없는 API 우선 사고를 지향하도록 유도합니다. 둘 다 본질적으로 더 안전하지는 않습니다. 단지 위협 모델이 다를 뿐입니다.&lt;/p>
&lt;p>어떤 접근 방식을 선택하든 황금률을 기억하세요. &lt;strong>UI에서의 인증은 사용자 경험을 위한 것이고, 서버의 인증은 보안을 위한 것입니다.&lt;/strong> 항상 두 가지 모두를 적용하세요.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Security</category><category>Web Development</category></item><item><title>함께 배치된 JS 파일을 사용하여 Blazor에서 격리된 JavaScript</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-isolated-js/</link><pubDate>Wed, 18 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-isolated-js/</guid><description>Blazor에서 함께 배치된 JavaScript 파일을 사용하여 JS 논리 범위를 개별 구성 요소로 유지합니다.</description><content:encoded>&lt;p>이전에 Blazor 및 JS Interop을 사용해 본 적이 있다면 아마도 다양한 구성 요소에 대한 임의의 기능으로 가득 찬 대규모 &lt;code>app.js&lt;/code> 파일을 갖게 되었을 것입니다. 작동하지만 빨리 지저분해집니다. 운 좋게도 훨씬 더 깔끔한 접근 방식이 있습니다. 바로 JavaScript 파일을 함께 배치하는 것입니다.&lt;/p>
&lt;h1 id="아이디어">아이디어&lt;/h1>
&lt;p>CSS 격리와 마찬가지로 Blazor를 사용하면 구성 요소 옆에 &lt;code>.razor.js&lt;/code> 파일을 배치할 수 있습니다. JavaScript 모듈은 구성 요소에 실제로 필요한 경우에만 요청 시 로드됩니다. 전역 스크립트도 없고 오염도 없습니다.&lt;/p>
&lt;h1 id="설정하기">설정하기&lt;/h1>
&lt;p>텍스트를 클립보드에 복사하는 &lt;code>Clipboard.razor&lt;/code> 구성 요소가 있다고 가정해 보겠습니다. 바로 옆에 &lt;code>Clipboard.razor.js&lt;/code>라는 파일을 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">export&lt;/span> &lt;span style="color:#ff7b72">function&lt;/span> copyToClipboard(text) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> navigator.clipboard.writeText(text).then(() =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> console.log(&lt;span style="color:#a5d6ff">&amp;#34;Copied to clipboard!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>export&lt;/code> 키워드에 주목하세요. 이는 중요합니다. Blazor는 이를 표준 ES 모듈로 로드합니다.&lt;/p>
&lt;h1 id="blazor에-모듈-로드">Blazor에 모듈 로드&lt;/h1>
&lt;p>구성 요소에서 &lt;code>IJSRuntime&lt;/code>를 사용하여 모듈을 가져옵니다. 경로는 라이브러리의 경우 &lt;code>./_content/{ASSEMBLY_NAME}/{COMPONENT_PATH}.razor.js&lt;/code> 또는 현재 프로젝트의 상대 경로라는 규칙을 따릅니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@inject IJSRuntime JS
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@implements IAsyncDisposable
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button @onclick=&lt;span style="color:#a5d6ff">&amp;#34;Copy&amp;#34;&lt;/span>&amp;gt;Copy to clipboard&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Text { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IJSObjectReference? module;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module = &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeAsync&amp;lt;IJSObjectReference&amp;gt;(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;import&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;./Components/Clipboard.razor.js&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task Copy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (module &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> module.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;copyToClipboard&amp;#34;&lt;/span>, Text);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask DisposeAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (module &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> module.DisposeAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>여기서 주목해야 할 몇 가지 사항은 다음과 같습니다.&lt;/p>
&lt;ul>
&lt;li>서버 측 사전 렌더링 중에는 JS Interop을 사용할 수 없기 때문에 &lt;code>OnAfterRenderAsync&lt;/code>에 모듈을 로드합니다.&lt;/li>
&lt;li>&lt;code>IJSObjectReference&lt;/code>을 사용하여 모듈에 대한 참조를 유지합니다.&lt;/li>
&lt;li>구성요소가 파괴될 때 모듈을 정리하기 위해 &lt;code>IAsyncDisposable&lt;/code>를 구현합니다.&lt;/li>
&lt;/ul>
&lt;h1 id="이게-더-나은-이유">이게 더 나은 이유&lt;/h1>
&lt;p>이전에는 모든 것을 단일 &lt;code>wwwroot/js/app.js&lt;/code>에 덤프했습니다. 작동했지만 기능을 찾는 것이 어려웠고 모든 페이지에 필요하지 않은 JavaScript가 로드되었습니다. 함께 배치된 JS 파일 사용:&lt;/p>
&lt;ul>
&lt;li>각 구성 요소는 자체 JavaScript를 소유합니다.&lt;/li>
&lt;li>모듈은 필요할 때만 느리게 로드됩니다.&lt;/li>
&lt;li>전역 함수 이름이 충돌하지 않습니다.&lt;/li>
&lt;li>유지 관리 및 삭제가 더 쉽습니다. 구성 요소를 삭제하면 해당 구성 요소와 함께 JS도 삭제됩니다.&lt;/li>
&lt;/ul>
&lt;h1 id="경로-문제">경로 문제&lt;/h1>
&lt;p>&lt;code>import&lt;/code>에 전달하는 경로는 독립 실행형 Blazor 앱에 있는지 아니면 Razor 클래스 라이브러리에 있는지에 따라 달라집니다. 일반 Blazor 앱의 경우 경로는 &lt;code>wwwroot&lt;/code>에 상대적입니다. &lt;code>.razor.js&lt;/code> 파일은 빌드 시 복사되므로 프로젝트의 구성 요소 위치에서 참조할 수 있습니다.&lt;/p>
&lt;p>모듈을 로드할 때 404가 표시되면 경로를 다시 확인하고 파일이 &lt;code>.js&lt;/code>가 아닌 &lt;code>.razor.js&lt;/code>로 끝나는지 확인하세요.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! &lt;strong>@emimontesdeoca&lt;/strong>로 소셜 미디어를 통해 언제든지 저에게 연락해주세요.&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/collocated-js">병치된 JS가 포함된 ASP.NET Core Blazor JavaScript&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>JavaScript</category></item><item><title>.NET 9 및 .NET 10의 Blazor 상호 작용: 전체 가이드</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-interactivity-dotnet-9-10/</link><pubDate>Sun, 15 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-interactivity-dotnet-9-10/</guid><description>Blazor의 렌더링 모드, 스트리밍 SSR, 향상된 탐색, .NET 9 및 .NET 10의 새로운 상호 작용 기능에 대해 자세히 알아보세요.</description><content:encoded>&lt;p>지난 몇 년 동안 Blazor를 사용하여 웹 애플리케이션을 구축해 왔다면 프레임워크가 &lt;em>먼&lt;/em> 발전했다는 것을 알 것입니다. Blazor Server와 Blazor WebAssembly 사이의 선택으로 시작된 것은 애플리케이션의 각 구성 요소에 대해 올바른 상호 작용 전략을 선택할 수 있는 유연한 통합 렌더링 모델로 발전했습니다.&lt;/p>
&lt;p>.NET 8에서는 기본적으로 렌더링 모드와 정적 서버 측 렌더링(SSR)이 도입되었습니다. 이제 .NET 9 및 .NET 10은 개발자 환경을 보다 원활하게 만들고 최종 사용자 환경을 더욱 빠르게 만드는 개선 사항을 통해 이러한 기반 위에 구축되었습니다.&lt;/p>
&lt;p>이 게시물에서는 렌더링 모드 작동 방식, 스트리밍 SSR이 테이블에 제공하는 것, 향상된 탐색 및 양식 처리가 게임을 어떻게 변화시키는지, 최신 릴리스의 새로운 기능 등 현재의 Blazor 상호 작용에 대한 전체 그림을 안내하고 싶습니다. 새로운 Blazor 프로젝트를 계획하고 있거나 업그레이드를 고려하고 있다면 이 가이드가 제가 이 모든 것을 파헤치기 시작할 때 갖고 싶었던 가이드입니다.&lt;/p>
&lt;h2 id="간략한-되돌아보기-우리가-여기까지-온-과정">간략한 되돌아보기: 우리가 여기까지 온 과정&lt;/h2>
&lt;p>.NET 8 이전에는 프로젝트 수준에서 호스팅 모델을 커밋해야 했습니다. Blazor Server는 SignalR 연결을 통해 서버에서 모든 것이 실행됨을 의미합니다. Blazor WebAssembly는 모든 것이 브라우저에서 실행된다는 것을 의미했습니다. 각각에는 장단점이 있었고 이를 혼합하는 것은 고통스러웠습니다.&lt;/p>
&lt;p>.NET 8은 두 모델을 통합하는 단일 프로젝트 템플릿인 Blazor Web App을 도입하여 판도를 바꾸었습니다. 핵심 개념은 &lt;strong>렌더링 모드&lt;/strong>입니다. &lt;em>구성 요소별로&lt;/em> 렌더링 방법과 상호 작용이 발생하는 위치를 결정합니다. 기본값은 정적 SSR이 되었습니다. 즉, 구성 요소가 서버에서 렌더링되고 일반 HTML을 브라우저에 보냅니다. SignalR도 없고 WebAssembly도 없고 빠른 HTML만 있습니다.&lt;/p>
&lt;p>.NET 9는 이러한 개념을 다듬고 개발자 환경을 개선하며 성능을 최적화했습니다. .NET 10은 더 나은 재연결 처리, 렌더링 모드 전반의 지속적인 구성 요소 상태, Blazor 스크립트 자체 전달 방식 개선을 통해 이를 더욱 발전시켰습니다.&lt;/p>
&lt;p>모든 것을 분석해 봅시다.&lt;/p>
&lt;h2 id="net-9의-렌더링-모드">.NET 9의 렌더링 모드&lt;/h2>
&lt;p>최신 Blazor의 핵심은 렌더링 모드의 개념입니다. 알아야 할 네 가지 모드는 다음과 같습니다.&lt;/p>
&lt;h3 id="1-정적-ssr기본값">1. 정적 SSR(기본값)&lt;/h3>
&lt;p>새 Blazor 웹앱을 만들면 구성 요소는 기본적으로 서버에서 정적으로 렌더링됩니다. 서버는 Razor 구성 요소를 처리하고 HTML을 생성하여 브라우저로 보냅니다. 지속적인 연결도 없고 WebAssembly 런타임도 없습니다. 단지 전통적인 요청/응답만 있을 뿐입니다.&lt;/p>
&lt;p>이는 콘텐츠가 많은 페이지, 주로 데이터를 표시하는 대시보드 또는 실시간 상호 작용이 필요하지 않은 페이지에 적합합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Our Products&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> products)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;product-card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Description&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;price&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> products &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ProductService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetAllAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 구성 요소는 서버에서 렌더링하고 HTML을 생성하며 그게 전부입니다. 진행 중인 연결이 없습니다. 빠른 초기 로드, SEO에 적합, 서버 리소스 사용량 최소화.&lt;/p>
&lt;h3 id="2-대화형-서버버튼-클릭-처리-사용자-입력-처리-동적인-ui-업데이트-등-실시간-상호-작용이-필요한-경우-대화형-서버-모드를-선택할-수-있습니다-그러면-브라우저와-서버-간에-signalr-연결이-설정되고-해당-연결을-통해-ui-업데이트가-발생합니다">2. 대화형 서버버튼 클릭 처리, 사용자 입력 처리, 동적인 UI 업데이트 등 실시간 상호 작용이 필요한 경우 대화형 서버 모드를 선택할 수 있습니다. 그러면 브라우저와 서버 간에 SignalR 연결이 설정되고 해당 연결을 통해 UI 업데이트가 발생합니다.&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/counter&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Counter&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Current count: @currentCount&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button class=&amp;#34;btn btn-primary&amp;#34; @onclick=&amp;#34;IncrementCount&amp;#34;&amp;gt;Click me&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int currentCount = 0;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void IncrementCount()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>@rendermode InteractiveServer&lt;/code> 지시어만 있으면 됩니다. 구성 요소는 처음에 서버에서 렌더링된 다음 후속 상호 작용을 처리하기 위해 SignalR 연결이 설정됩니다. C# 이벤트 핸들러는 서버에서 실행되고 UI 차이점은 브라우저로 전송됩니다.&lt;/p>
&lt;p>&lt;strong>사용 시기:&lt;/strong> 상호 작용이 필요할 때 구성 요소는 서버 측 리소스(데이터베이스, API, 파일 시스템)에 액세스해야 하며 WebAssembly가 다운로드될 때까지 기다리지 않고 빠른 초기 로드를 원합니다.&lt;/p>
&lt;h3 id="3-대화형-웹어셈블리">3. 대화형 웹어셈블리&lt;/h3>
&lt;p>서버 연결을 유지하지 않고 상호 작용을 원하는 경우 Interactive WebAssembly는 .NET WebAssembly 런타임을 사용하여 브라우저에서 직접 구성 요소 논리를 실행합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/search&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveWebAssembly
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product Search&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;searchTerm&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind:event&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;oninput&amp;#34;&lt;/span> placeholder&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Search products...&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (filteredProducts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ul&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> filteredProducts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>ul&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string searchTerm &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> allProducts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private IEnumerable&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> filteredProducts &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> allProducts
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Where(p &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> p&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Contains(searchTerm, StringComparison&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>OrdinalIgnoreCase));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> allProducts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await Http&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetFromJsonAsync&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>) &lt;span style="color:#f85149">??&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>단점: .NET 런타임 및 어셈블리에 대한 초기 다운로드 비용이 있습니다. 그러나 일단 로드되면 구성 요소는 UI 상호 작용을 위한 서버 왕복 없이 브라우저에서 완전히 실행됩니다.&lt;/p>
&lt;p>&lt;strong>사용 시기:&lt;/strong> 대기 시간이 중요한 대화형 구성 요소(예: 서식 있는 텍스트 편집기, 그리기 도구, 실시간 필터링)의 경우, 서버 로드를 줄이려는 경우 또는 프로그레시브 웹 앱(PWA)을 구축할 때.&lt;/p>
&lt;h3 id="4-대화형-자동">4. 대화형 자동&lt;/h3>
&lt;p>이것이 실용적인 중간 지점이자 제가 가장 좋아하는 기능 중 하나입니다. Interactive Auto는 첫 번째 로드를 위해 SignalR을 통한 서버 측 렌더링으로 시작한 다음 백그라운드에서 WebAssembly 런타임을 자동으로 다운로드합니다. 후속 방문에서는 구성 요소가 WebAssembly에서 실행됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/dashboard&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveAuto
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Dashboard&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>DashboardWidget Title&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Sales&amp;#34;&lt;/span> Value&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@salesTotal&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>DashboardWidget Title&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Users&amp;#34;&lt;/span> Value&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@activeUsers&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;RefreshData&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Refresh&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private decimal salesTotal;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">int&lt;/span> activeUsers;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await RefreshData();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task RefreshData()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await DashboardService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetSummaryAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> salesTotal &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> data&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>SalesTotal;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> activeUsers &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> data&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ActiveUsers;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 빠른 첫 번째 렌더링(WASM 다운로드를 기다리지 않음)과 결국 구성 요소가 클라이언트 측에서 실행된다는 두 가지 장점을 모두 제공합니다. 사용자는 전환을 알아차리지 못합니다.&lt;/p>
&lt;p>&lt;strong>사용 시기:&lt;/strong> 빠른 초기 로드와 최종 클라이언트 측 실행을 모두 원하는 경우. 이는 많은 대화형 구성 요소에 대한 훌륭한 기본 선택입니다.&lt;/p>
&lt;h2 id="스트리밍-ssr-양쪽-세계의-최고">스트리밍 SSR: 양쪽 세계의 최고&lt;/h2>
&lt;p>스트리밍 SSR은 단순해 보이지만 인지된 성능에 엄청난 차이를 만드는 기능 중 하나입니다. 아이디어는 다음과 같습니다. HTML을 보내기 전에 모든 데이터가 로드될 때까지 기다리는 대신 서버는 페이지 셸을 즉시 보낸 다음 데이터가 사용 가능해지면 콘텐츠 업데이트를 &lt;em>스트림&lt;/em>합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/reports&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>attribute [StreamRendering]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Monthly Reports&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (reports is null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;loading-spinner&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Loading reports&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Month&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Revenue&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Growth&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> report &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> reports)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Month&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Revenue&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(report.Growth &amp;gt;= 0 ? &amp;#34;&lt;/span>text&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>success&lt;span style="color:#a5d6ff">&amp;#34; : &amp;#34;&lt;/span>text&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>danger&lt;span style="color:#a5d6ff">&amp;#34;)&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Growth&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;P1&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>MonthlyReport&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">?&lt;/span> reports;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> This might take a couple of seconds
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reports &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ReportService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetMonthlyReportsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>[StreamRendering]&lt;/code> 속성을 사용하면 다음과 같은 일이 발생합니다.&lt;/p>
&lt;ol>
&lt;li>서버는 로딩 스피너와 함께 HTML을 즉시 보냅니다.&lt;/li>
&lt;li>&lt;code>OnInitializedAsync&lt;/code>가 실행되어 데이터를 가져옵니다.&lt;/li>
&lt;li>데이터가 도착하면 서버는 업데이트된 HTML(테이블)을 브라우저로 스트리밍합니다.&lt;/li>
&lt;li>브라우저는 전체 페이지를 다시 로드하지 않고도 DOM을 패치합니다.&lt;/li>
&lt;/ol>
&lt;p>사용자는 로딩 표시기와 함께 페이지를 &lt;em>즉시&lt;/em> 본 다음 콘텐츠가 채워집니다. JavaScript 프레임워크가 필요하지 않습니다. WebSocket 연결이 없습니다. HTTP 스트리밍을 영리하게 사용하는 것뿐입니다.&lt;strong>사용 시기:&lt;/strong> 렌더링 중에 데이터를 가져오는 정적 SSR 페이지. 제품 목록 페이지, 대시보드, 보고서 등 어디에서나 초기 데이터 로드에 수백 밀리초 이상이 걸릴 수 있습니다.&lt;/p>
&lt;p>&lt;strong>사용하지 말아야 할 경우:&lt;/strong> 데이터가 매우 빠르게(100ms 미만) 로드되는 경우 스트리밍 오버헤드는 그만한 가치가 없습니다. 또한 스트리밍 SSR은 지속적인 상호작용을 제공하지 않습니다. 이를 위해서는 여전히 대화형 렌더링 모드가 필요합니다.&lt;/p>
&lt;h2 id="향상된-탐색-및-양식-처리">향상된 탐색 및 양식 처리&lt;/h2>
&lt;p>최신 Blazor의 미묘하지만 강력한 개선 사항 중 하나는 향상된 탐색 기능입니다. 기본적으로 Blazor는 내부 링크 클릭 및 양식 제출을 가로채서 &lt;code>fetch&lt;/code>를 통해 새 페이지 콘텐츠를 가져오고 전체 브라우저 탐색을 수행하는 대신 DOM을 패치합니다.&lt;/p>
&lt;p>이는 정적 SSR 페이지 사이를 탐색하는 것이 SPA처럼 느껴진다는 것을 의미합니다. 전체 페이지 플래시가 없고 스크롤 위치가 유지될 수 있으며 경험이 매우 매끄러워집니다.&lt;/p>
&lt;h3 id="작동-방식">작동 방식&lt;/h3>
&lt;p>Blazor 스크립트(&lt;code>blazor.web.js&lt;/code>)가 로드되면 내부 링크에 대한 클릭을 자동으로 차단합니다. 기존 브라우저 탐색 대신 다음을 수행합니다.&lt;/p>
&lt;ol>
&lt;li>대상 URL에 &lt;code>fetch&lt;/code> 요청을 보냅니다.&lt;/li>
&lt;li>HTML 응답을 받습니다.&lt;/li>
&lt;li>새 콘텐츠를 기존 DOM에 병합합니다.&lt;/li>
&lt;li>브라우저의 URL과 기록을 업데이트합니다.&lt;/li>
&lt;/ol>
&lt;p>이 기능을 활성화하기 위해 아무것도 할 필요가 없습니다. 기본적으로 켜져 있습니다. 하지만 다음과 같이 제어할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&amp;lt;!-- Disable enhanced navigation for a specific link --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">a&lt;/span> href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/legacy-page&amp;#34;&lt;/span> data-enhance-nav&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;false&amp;#34;&lt;/span>&amp;gt;Legacy Page&amp;lt;/&lt;span style="color:#7ee787">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&amp;lt;!-- Force a full page reload for external-like behavior --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">a&lt;/span> href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/downloads/report.pdf&amp;#34;&lt;/span> data-enhance-nav&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;false&amp;#34;&lt;/span>&amp;gt;Download Report&amp;lt;/&lt;span style="color:#7ee787">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="향상된-양식-처리">향상된 양식 처리&lt;/h3>
&lt;p>양식도 동일한 처리를 받습니다. Blazor의 양식 처리와 함께 &lt;code>EditForm&lt;/code> 또는 표준 &lt;code>&amp;lt;form&amp;gt;&lt;/code> 요소를 사용하면 제출이 가로채서 &lt;code>fetch&lt;/code>를 통해 처리됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/contact&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=&amp;#34;contactModel&amp;#34; OnValidSubmit=&amp;#34;HandleSubmit&amp;#34; FormName=&amp;#34;contact&amp;#34; Enhance&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;DataAnnotationsValidator /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;name&amp;#34;&amp;gt;Name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText id=&amp;#34;name&amp;#34; @bind-Value=&amp;#34;contactModel.Name&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Name&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;email&amp;#34;&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText id=&amp;#34;email&amp;#34; @bind-Value=&amp;#34;contactModel.Email&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Email&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;message&amp;#34;&amp;gt;Message&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputTextArea id=&amp;#34;message&amp;#34; @bind-Value=&amp;#34;contactModel.Message&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Message&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; class=&amp;#34;btn btn-primary&amp;#34;&amp;gt;Send&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [SupplyParameterFromForm]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private ContactModel contactModel { get; set; } = new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task HandleSubmit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await ContactService.SubmitAsync(contactModel);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> contactModel = new ContactModel();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>EditForm&lt;/code>의 &lt;code>Enhance&lt;/code> 속성은 Blazor에게 양식 제출을 가로채도록 지시합니다. 양식이 서버에 게시되고, 서버가 이를 처리하고 페이지를 다시 렌더링하며, 업데이트된 HTML이 다시 스트리밍됩니다. 이 모든 작업은 전체 페이지 탐색 없이 수행됩니다. 대화식으로 느껴지지만 완전히 서버에서 렌더링됩니다.&lt;/p>
&lt;p>&lt;code>[SupplyParameterFromForm]&lt;/code> 속성에 주목하세요. 이는 Blazor가 정적 SSR 시나리오에서 게시된 양식 데이터를 모델에 바인딩하는 방법입니다. 이는 전통적인 양식 게시물과 Blazor의 구성 요소 모델 사이를 연결하는 다리입니다.&lt;/p>
&lt;h2 id="페이지별-및-구성-요소별-상호-작용">페이지별 및 구성 요소별 상호 작용&lt;/h2>
&lt;p>새 모델의 가장 강력한 측면 중 하나는 단일 애플리케이션 내에서 렌더링 모드를 혼합할 수 있다는 것입니다. 제품 목록 페이지는 정적 SSR일 수 있고, 장바구니는 Interactive Server일 수 있으며, 제품 구성기는 Interactive WebAssembly일 수 있습니다. 이 모든 것이 동일한 앱에 있습니다.&lt;/p>
&lt;h3 id="구성-요소-수준에서-렌더링-모드-설정">구성 요소 수준에서 렌더링 모드 설정&lt;/h3>
&lt;p>&lt;code>@rendermode&lt;/code> 지시문을 사용하여 구성 요소에서 직접 렌더링 모드를 설정할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@* This component is interactive via Server *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h3&amp;gt;Live Chat&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- chat UI here --&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>또는 상위 구성 요소를 사용할 때 설정할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/product/{Id:int}&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@product?.Name&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;@product?.Description&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- This child component gets its own interactive render mode --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ProductConfigurator Product=&amp;#34;product&amp;#34; @rendermode=&amp;#34;InteractiveWebAssembly&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- This stays static --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ProductReviews ProductId=&amp;#34;Id&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public int Id { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private Product? product;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> product = await ProductService.GetByIdAsync(Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 예에서 페이지 자체는 정적 SSR입니다. &lt;code>ProductConfigurator&lt;/code> 구성 요소는 풍부한 클라이언트 측 상호 작용을 위해 WebAssembly에서 실행됩니다. &lt;code>ProductReviews&lt;/code> 구성 요소는 단지 데이터만 표시하기 때문에 정적으로 유지됩니다.&lt;/p>
&lt;h3 id="렌더링-모드를-전역적으로-설정하기">렌더링 모드를 전역적으로 설정하기&lt;/h3>
&lt;p>기본적으로 모든 페이지를 대화형으로 만들려면 &lt;code>App.razor&lt;/code>의 루트 수준에서 렌더링 모드를 설정할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-mysql" data-lang="mysql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Routes&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">@&lt;/span>rendermode&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;InteractiveServer&amp;#34;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>&lt;span style="color:#f85149">이는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">서버&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">렌더링을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">통해&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모든&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">페이지를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">대화형으로&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">만듭니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">필요한&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경우&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">개별&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">이를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">재정의할&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">수&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있습니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### 기억해야 할 중요한 규칙
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">렌더링&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모드를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">혼합할&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">때&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">염두에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">두어야&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">할&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">몇&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">가지&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">제약&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">조건이&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있습니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">하위&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">상위&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소보다&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;더 대화형&amp;#34;&lt;/span>&lt;span style="color:#f85149">인&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">렌더링&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모드를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">가질&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">수&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">없습니다&lt;/span>.&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">상위&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">정적이면&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">하위&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">정적이거나&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">대화형일&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">수&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있습니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">그러나&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">부모가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>Server인&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경우&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">자식은&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>WebAssembly가&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">될&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">수&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">없습니다&lt;/span>(Server&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">또는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Auto여야&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">함&lt;/span>).&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">대화형&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">정적&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">부모의&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">범위&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">서비스를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">직접&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">사용할&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">수&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">없습니다&lt;/span>.&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>WebAssembly에서&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">실행되는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경우&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">서버&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">측&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>DbContext&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">인스턴스에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">직접&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">액세스할&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">수&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">없습니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>API&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">계층이&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">필요합니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">상태는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">렌더링&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모드&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">간에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">자동으로&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">전송되지&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">않습니다&lt;/span>.&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">구성&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">요소가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">서버에서&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">사전&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">렌더링된&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">다음&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>WebAssembly로&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">전환하는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경우&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">상태&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">지속성을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">명시적으로&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">처리해야&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">합니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">이에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">대한&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">자세한&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">내용은&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">섹션에서&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">확인하세요&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">## .NET 10의 새로운 기능
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#f85149">은&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">안정성과&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">개발자&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경험에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">초점을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">맞춰&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">점진적인&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">개선&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">접근&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">방식을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">이어갑니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>Blazor&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">상호&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">작용의&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">주요&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">내용은&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">다음과&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">같습니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### 향상된 재연결 경험
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">프로덕션에서&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">대화형&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">서버&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">모드를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">사용한&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경우&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">아마도&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">재연결&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">오버레이를&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">접했을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">것입니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">즉&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>SignalR&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">연결이&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">끊어지고&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">사용자에게&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;재연결 중...&amp;#34;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">메시지가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">표시되는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">순간입니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#f85149">에서는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">이&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">환경이&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">훨씬&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">더&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">좋아졌습니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">이제&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">재연결&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">논리가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">재시도에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">대해&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">더&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">똑똑해졌습니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">고정된&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">재시도&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">간격&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">대신&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">상황에&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">맞는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">백오프&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">전략을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">사용합니다&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">다시&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">연결하는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">동안의&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>UI도&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">더욱&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">세련되었으며&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">경험을&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">사용자&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">정의할&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">수&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있는&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">더&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">나은&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">후크가&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">있습니다&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>razor&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!&lt;/span>&lt;span style="color:#8b949e;font-style:italic">-- In your App.razor or layout --&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;components-reconnect-modal&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-visible&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Connection&lt;span style="color:#6e7681"> &lt;/span>lost.&lt;span style="color:#6e7681"> &lt;/span>Attempting&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>reconnect...&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;spinner&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-failed&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Could&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">not&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>reconnect&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>the&lt;span style="color:#6e7681"> &lt;/span>server.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button&lt;span style="color:#6e7681"> &lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;location.reload()&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Reload&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-rejected&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Your&lt;span style="color:#6e7681"> &lt;/span>session&lt;span style="color:#6e7681"> &lt;/span>has&lt;span style="color:#6e7681"> &lt;/span>expired.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>a&lt;span style="color:#6e7681"> &lt;/span>href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#ff7b72">Return&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Home&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>a&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 프레임워크는 일시적인 오류가 발생할 경우 더욱 적극적으로 재연결을 시도하며 회로 상태를 보다 안정적으로 복원할 수 있습니다. 이는 사용자의 &amp;ldquo;페이지를 다시 로드하세요&amp;quot;라는 순간이 줄어든다는 것을 의미합니다.&lt;/p>
&lt;h3 id="렌더링-모드-전반에-걸쳐-지속적인-구성-요소-상태">렌더링 모드 전반에 걸쳐 지속적인 구성 요소 상태&lt;/h3>
&lt;p>이것은 큰 것입니다. .NET 9에서는 구성 요소가 서버에서 사전 렌더링된 다음 WebAssembly로 전환되거나 렌더링 모드 간에 전환되면 구성 요소 상태가 손실됩니다. 구성요소가 효과적으로 다시 초기화되므로 데이터 가져오기가 중복되고 UI가 깜박일 수 있습니다.&lt;/p>
&lt;p>.NET 10은 렌더링 모드 전환 전반에 걸쳐 보다 원활하게 작동하도록 &lt;code>PersistentComponentState&lt;/code> 서비스를 개선합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/weather&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveWebAssembly
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject PersistentComponentState ApplicationState
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Weather Forecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (forecasts is null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Loading&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> forecast &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> forecasts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;forecast-card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h4&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToShortDateString()&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h4&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Summary &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>TemperatureC&lt;span style="color:#f85149">°&lt;/span>C&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">?&lt;/span> forecasts;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private PersistingComponentStateSubscription persistingSubscription;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> persistingSubscription &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>RegisterOnPersisting(PersistData);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>TryTakeFromJson&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;weather-forecasts&amp;#34;&lt;/span>, out &lt;span style="color:#ff7b72">var&lt;/span> restored))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> Data wasn&lt;span style="color:#a5d6ff">&amp;#39;t persisted from prerendering — fetch it&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> forecasts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await Http&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetFromJsonAsync&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#34;api/weather&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> forecasts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> restored;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private Task PersistData()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>PersistAsJson(&lt;span style="color:#a5d6ff">&amp;#34;weather-forecasts&amp;#34;&lt;/span>, forecasts);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public void Dispose()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> persistingSubscription&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Dispose();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>.NET 10의 개선 사항을 통해 이 패턴은 더욱 안정적으로 작동합니다. 사전 렌더링 중에 직렬화된 상태는 구성 요소가 대화형 모드에서 초기화될 때 적절하게 사용할 수 있으므로 이중 데이터 가져오기 및 사용자에게 표시될 수 있는 짧은 깜박임이 방지됩니다.&lt;/p>
&lt;h3 id="정적-웹-자산으로서의-blazor-스크립트">정적 웹 자산으로서의 Blazor 스크립트&lt;/h3>
&lt;p>이전 버전에서는 Blazor JavaScript 파일(&lt;code>blazor.web.js&lt;/code>)이 프레임워크의 내부 엔드포인트에서 제공되었습니다. .NET 10에서는 정적 웹 자산으로 제공됩니다. 이는 작은 변화처럼 들릴 수도 있지만 실질적인 이점이 있습니다.- &lt;strong>더 나은 캐싱:&lt;/strong> 정적 웹 자산은 적절한 캐시 헤더와 지문 URL을 가져오므로 브라우저는 이를 더욱 효과적으로 캐시합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CDN 친화적:&lt;/strong> 일반 정적 파일이므로 CDN은 이를 엣지 위치에서 캐시하고 제공할 수 있습니다.&lt;/li>
&lt;li>&lt;strong>압축:&lt;/strong> 정적 웹 자산 압축이 자동으로 적용되어 전송 중인 스크립트 크기가 줄어듭니다.&lt;/li>
&lt;/ul>
&lt;p>참조 방법을 변경할 필요가 없습니다. 프레임워크가 업데이트된 경로를 자동으로 처리합니다.&lt;/p>
&lt;h2 id="모범-사례-올바른-렌더링-모드-선택">모범 사례: 올바른 렌더링 모드 선택&lt;/h2>
&lt;p>여러 프로젝트에 걸쳐 이러한 모든 모드를 사용한 후의 결정 프레임워크는 다음과 같습니다.&lt;/p>
&lt;h3 id="정적-ssr로-시작">정적 SSR로 시작&lt;/h3>
&lt;p>정적 SSR을 기본값으로 설정하세요. 대부분의 애플리케이션에서 대부분의 페이지는 주로 데이터 표시에 관한 것입니다. 제품 페이지, 블로그 게시물, 사용자 프로필, 설정 페이지 등에는 실시간 상호 작용이 필요하지 않습니다. 정적 SSR은 최고의 성능, 가장 낮은 리소스 사용량 및 가장 단순한 정신 모델을 제공합니다.&lt;/p>
&lt;h3 id="필요한-경우에만-상호작용-기능-추가">필요한 경우에만 상호작용 기능 추가&lt;/h3>
&lt;p>사용자 상호 작용에 실시간으로 응답해야 하는 특정 구성 요소를 식별합니다. &amp;lsquo;좋아요&amp;rsquo; 버튼, 채팅 위젯, 드래그 앤 드롭 인터페이스 등에는 상호작용이 필요합니다. 그러나 그들을 둘러싼 페이지는 아마도 그렇지 않을 것입니다.&lt;/p>
&lt;h3 id="대화형-자동-모드를-대화형-모드로-사용하세요">대화형 자동 모드를 대화형 모드로 사용하세요&lt;/h3>
&lt;p>대화형 기능이 필요한 경우 Interactive Auto가 기본적으로 최선의 선택인 경우가 많습니다. 최종 클라이언트 측 실행(WebAssembly)을 통해 빠른 초기 로드(서버 렌더링)를 제공합니다. 사용자는 두 가지 장점을 최대한 활용하고 코드를 한 번만 작성하면 됩니다.&lt;/p>
&lt;h3 id="특정-사례를-위한-대화형-서버-예약">특정 사례를 위한 대화형 서버 예약&lt;/h3>
&lt;p>다음과 같은 경우 대화형 서버를 사용하세요.&lt;/p>
&lt;ul>
&lt;li>구성 요소는 서버 리소스(데이터베이스, 파일 시스템, 내부 API)에 직접 액세스해야 합니다.&lt;/li>
&lt;li>WebAssembly 다운로드 크기가 문제로 Auto를 사용할 수 없습니다.&lt;/li>
&lt;li>보안상의 이유로(예: 민감한 데이터 처리) 구성 요소가 항상 서버에서 실행되어야 합니다.&lt;/li>
&lt;/ul>
&lt;h3 id="스트리밍-ssr을-아낌없이-사용하세요">스트리밍 SSR을 아낌없이 사용하세요&lt;/h3>
&lt;p>정적 SSR 페이지가 데이터를 가져오는 경우 &lt;code>[StreamRendering]&lt;/code>를 추가하세요. 비용은 최소화되며 인지된 성능 향상은 상당합니다. 사용자는 빈 페이지를 쳐다보는 것이 아니라 점진적으로 나타나는 콘텐츠를 봅니다.&lt;/p>
&lt;h3 id="상태-전환을-신중하게-처리">상태 전환을 신중하게 처리&lt;/h3>
&lt;p>Interactive Auto를 사용하거나 WebAssembly와 사전 렌더링을 혼합하는 경우 중복된 데이터 가져오기를 방지하려면 항상 &lt;code>PersistentComponentState&lt;/code>를 사용하세요. 사용자는 깜박이는 콘텐츠가 없다는 점에 감사할 것입니다.&lt;/p>
&lt;h3 id="구성요소-트리를-염두에-두세요">구성요소 트리를 염두에 두세요&lt;/h3>
&lt;p>렌더링 모드 계층 구조 규칙을 기억하세요. 대화형 경계가 이해되도록 구성 요소 트리를 계획합니다. 일반적인 패턴은 필요한 곳에 대화형 &amp;ldquo;섬&amp;quot;이 포함된 정적 레이아웃을 갖는 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@* Layout: Static SSR *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NavMenu /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;UserMenu @rendermode=&amp;#34;InteractiveServer&amp;#34; /&amp;gt; @* Needs real-time auth state *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;main&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Body
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/main&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;footer&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ChatWidget @rendermode=&amp;#34;InteractiveAuto&amp;#34; /&amp;gt; @* Rich interactivity *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/footer&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="결론">결론&lt;/h2>
&lt;p>.NET 9 및 .NET 10의 Blazor 상호 작용 모델은 웹 애플리케이션 구축에 대한 성숙하고 신중한 접근 방식을 나타냅니다. 구성 요소별로 렌더링 모드를 선택할 수 있는 기능, 원활하게 향상된 탐색, 스트리밍 SSR, 재연결 및 상태 관리의 지속적인 개선으로 인해 광범위한 애플리케이션에 대한 강력한 선택이 됩니다.핵심 통찰력은 &lt;strong>상호작용은 이분법적인 선택이 아니라 스펙트럼입니다&lt;/strong>라는 것입니다. 대부분의 애플리케이션은 정적일 수 있습니다. 일부 부품에는 서버 기반 상호 작용이 필요합니다. 일부는 브라우저에서 실행하면 이점을 얻을 수 있습니다. 이제 Blazor를 사용하면 프레임워크와 싸우지 않고도 가장 세밀한 수준(개별 구성 요소)에서 선택할 수 있습니다.&lt;/p>
&lt;p>새 프로젝트를 시작하는 경우 제 조언은 간단합니다. Blazor 웹 앱을 만들고, 정적 SSR로 시작하고, 데이터가 많은 페이지에 &lt;code>[StreamRendering]&lt;/code>를 추가하고, 필요할 때만 개별 구성 요소를 대화형 모드로 승격하세요. 빠르고 효율적이며 확장성이 뛰어난 애플리케이션을 얻게 될 것입니다.&lt;/p>
&lt;p>즐거운 코딩 되시기 바랍니다. 언제나 그렇듯이 궁금한 점이 있으면 언제든지 문의해 주세요!&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category></item><item><title>.NET의 최소 API: 경량 HTTP API 구축</title><link>https://emimontesdeoca.github.io/ko/posts/minimal-apis-dotnet/</link><pubDate>Thu, 10 Apr 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/minimal-apis-dotnet/</guid><description>경로 처리기 및 매개변수 바인딩부터 필터 및 OpenAPI 통합에 이르기까지 .NET Minimal API를 사용하여 깔끔하고 빠른 HTTP API를 구축하기 위한 포괄적인 가이드입니다.</description><content:encoded>&lt;p>한동안 ASP.NET Core를 사용하여 API를 구축했다면 아마도 컨트롤러 기반 접근 방식에 매우 익숙할 것입니다. 즉, 컨트롤러 클래스를 만들고 특성으로 장식하고 생성자를 통해 서비스를 삽입하고 경로를 연결합니다. 그것은 작동하고 잘 작동합니다. 그러나 때때로 &amp;ldquo;이 요청을 받고, 무언가를 하고, 응답을 반환&amp;quot;하는 것에 대한 많은 의식을 작성하고 있는 것처럼 느껴질 때가 있습니다.&lt;/p>
&lt;p>이것이 바로 Minimal API가 해결하려고 하는 문제입니다. .NET 6에 도입된 Minimal API를 사용하면 매우 적은 상용구를 사용하여 HTTP 엔드포인트를 정의할 수 있습니다. 컨트롤러도 없고, 속성도 없고, 시작 클래스 저글링도 없습니다. 깔끔하고 기능적인 스타일로 경로에서 핸들러까지 직접 매핑만 하면 됩니다.&lt;/p>
&lt;p>이 게시물에서는 첫 번째 엔드포인트부터 대규모 애플리케이션 구성, 인증 처리, 유효성 검사, OpenAPI 문서 및 성능 고려 사항에 이르기까지 최소 API에 대해 알아야 할 모든 것을 안내하고 싶습니다. 시작해 봅시다.&lt;/p>
&lt;h2 id="왜-최소-api인가">왜 최소 API인가?&lt;/h2>
&lt;p>시작하기 전에 기존 컨트롤러 기반 접근 방식 대신 최소 API를 선택하는 &lt;em>이유&lt;/em>에 대해 이야기해 보겠습니다.&lt;/p>
&lt;p>&lt;strong>컨트롤러&lt;/strong>는 구조화되고 독선적인 프레임워크가 필요할 때 유용합니다. 이는 모델 바인딩, 필터, 콘텐츠 협상 및 기본적으로 우려 사항을 명확하게 분리하는 기능을 제공합니다. 수십 명의 개발자가 있는 대규모 엔터프라이즈 애플리케이션의 경우 컨트롤러가 적용하는 일관성은 실질적인 이점이 될 수 있습니다.&lt;/p>
&lt;p>반면에 &lt;strong>최소 API&lt;/strong>는 원할 때 빛을 발합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>상용구 감소&lt;/strong> — 컨트롤러 클래스 없음, &lt;code>[ApiController]&lt;/code> 속성 없음, 별도의 시작 구성 없음.&lt;/li>
&lt;li>&lt;strong>빠른 시작&lt;/strong> — 부팅 시 반사 기반 작업이 줄어듭니다.&lt;/li>
&lt;li>&lt;strong>더 단순한 정신 모델&lt;/strong> — 경로가 핸들러에 직접 매핑됩니다. 그게 다야.&lt;/li>
&lt;li>&lt;strong>마이크로서비스 친화적&lt;/strong> — API에 5~10개의 엔드포인트가 있는 경우 전체 컨트롤러 설정이 과잉처럼 느껴질 수 있습니다.&lt;/li>
&lt;/ul>
&lt;p>좋은 소식은 이것이 둘 중 하나의 결정이 아니라는 것입니다. 동일한 프로젝트에서 컨트롤러와 최소 API를 혼합할 수 있습니다. 그러나 일단 최소한의 접근 방식에 익숙해지면 예상보다 더 자주 접근하게 될 수도 있습니다.&lt;/p>
&lt;h2 id="시작하기">시작하기&lt;/h2>
&lt;p>Minimal API를 처음부터 만들어 보겠습니다. .NET SDK가 설치되어 있는 경우 다음과 같이 간단합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new web -n MyMinimalApi
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd MyMinimalApi
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그러면 다음과 같은 &lt;code>Program.cs&lt;/code>가 제공됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Hello World!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그게 다야. 그것은 작동하는 API입니다. &lt;code>Startup.cs&lt;/code> 없음, 컨트롤러 클래스 없음, 라우팅 구성 없음. &lt;code>dotnet run&lt;/code>를 실행하고 &lt;code>http://localhost:5000&lt;/code>를 누르면 &amp;ldquo;Hello World!&amp;ldquo;가 표시됩니다. 뒤쪽에. 단순함이 핵심입니다.&lt;/p>
&lt;p>클래식 todo API를 사용하여 좀 더 유용하게 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> todos = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Todo&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, () =&amp;gt; todos);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todos.FirstOrDefault(t =&amp;gt; t.Id == id) &lt;span style="color:#ff7b72">is&lt;/span> { } todo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? Results.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : Results.NotFound());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDelete(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = todos.FirstOrDefault(t =&amp;gt; t.Id == id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todos.Remove(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Todo&lt;/span>(&lt;span style="color:#ff7b72">int&lt;/span> Id, &lt;span style="color:#ff7b72">string&lt;/span> Title, &lt;span style="color:#ff7b72">bool&lt;/span> IsComplete);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>우리는 30줄 미만의 전체 CRUD를 가지고 있습니다. 이것이 바로 최소한의 접근 방식의 힘입니다.&lt;/p>
&lt;h2 id="경로-처리기">경로 처리기&lt;/h2>
&lt;p>경로 핸들러는 요청이 경로와 일치할 때 실행되는 기능입니다. 이를 정의하는 데는 여러 가지 옵션이 있습니다.&lt;/p>
&lt;h3 id="람다-표현식">람다 표현식&lt;/h3>
&lt;p>가장 일반적인 접근 방식과 대부분의 예에서 볼 수 있는 내용은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/hello&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Hello!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/hello/{name}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string&lt;/span> name) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Hello, {name}!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="메소드-그룹">메소드 그룹&lt;/h3>
&lt;p>더 복잡한 논리의 경우 명명된 메서드를 지정할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, GetProducts);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, CreateProduct);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> IResult GetProducts(AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Results.Ok(db.Products.ToList());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> IResult CreateProduct(Product product, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Products.Add(product);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.SaveChanges();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/products/{product.Id}&amp;#34;&lt;/span>, product);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```이것은&lt;/span> &lt;span style="color:#f85149">한&lt;/span> &lt;span style="color:#f85149">줄&lt;/span> &lt;span style="color:#f85149">이상의&lt;/span> &lt;span style="color:#f85149">내용에&lt;/span> &lt;span style="color:#f85149">대해&lt;/span> &lt;span style="color:#f85149">내가&lt;/span> &lt;span style="color:#f85149">선호하는&lt;/span> &lt;span style="color:#f85149">접근&lt;/span> &lt;span style="color:#f85149">방식입니다&lt;/span>. &lt;span style="color:#f85149">경로&lt;/span> &lt;span style="color:#f85149">매핑&lt;/span> &lt;span style="color:#f85149">섹션을&lt;/span> &lt;span style="color:#f85149">깔끔하고&lt;/span> &lt;span style="color:#f85149">읽기&lt;/span> &lt;span style="color:#f85149">쉽게&lt;/span> &lt;span style="color:#f85149">유지합니다&lt;/span>. &lt;span style="color:#f85149">구현&lt;/span> &lt;span style="color:#f85149">세부&lt;/span> &lt;span style="color:#f85149">사항을&lt;/span> &lt;span style="color:#f85149">살펴보지&lt;/span> &lt;span style="color:#f85149">않고도&lt;/span> &lt;span style="color:#f85149">어떤&lt;/span> &lt;span style="color:#f85149">엔드포인트가&lt;/span> &lt;span style="color:#f85149">있는지&lt;/span> &lt;span style="color:#f85149">한눈에&lt;/span> &lt;span style="color:#f85149">확인할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">로컬&lt;/span> &lt;span style="color:#f85149">함수&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">로컬&lt;/span> &lt;span style="color:#f85149">함수를&lt;/span> &lt;span style="color:#f85149">사용할&lt;/span> &lt;span style="color:#f85149">수도&lt;/span> &lt;span style="color:#f85149">있는데&lt;/span>, &lt;span style="color:#f85149">이는&lt;/span> &lt;span style="color:#f85149">핸들러를&lt;/span> &lt;span style="color:#f85149">경로&lt;/span> &lt;span style="color:#f85149">정의에&lt;/span> &lt;span style="color:#f85149">가깝게&lt;/span> &lt;span style="color:#f85149">유지하려는&lt;/span> &lt;span style="color:#f85149">경우에&lt;/span> &lt;span style="color:#f85149">유용합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/health&amp;#34;&lt;/span>, CheckHealth);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>IResult CheckHealth()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Some health check logic&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> { Status = &lt;span style="color:#a5d6ff">&amp;#34;Healthy&amp;#34;&lt;/span>, Timestamp = DateTime.UtcNow });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="매개변수-바인딩">매개변수 바인딩&lt;/h2>
&lt;p>Minimal API에 대해 제가 정말 높이 평가하는 점 중 하나는 매개변수 바인딩이 얼마나 직관적인지입니다. 프레임워크는 컨텍스트와 유형을 기반으로 어디에서 값을 가져올지 파악합니다.&lt;/p>
&lt;h3 id="경로-매개변수">경로 매개변수&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/users/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;User {id}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/files/{*path}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string&lt;/span> path) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;File: {path}&amp;#34;&lt;/span>); &lt;span style="color:#8b949e;font-style:italic">// catch-all&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="쿼리-문자열-매개변수">쿼리 문자열 매개변수&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/search&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string?&lt;/span> query, &lt;span style="color:#ff7b72">int&lt;/span> page = &lt;span style="color:#a5d6ff">1&lt;/span>, &lt;span style="color:#ff7b72">int&lt;/span> pageSize = &lt;span style="color:#a5d6ff">20&lt;/span>) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> { query, page, pageSize }));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Null 허용 유형은 선택적 매개변수가 됩니다. 기본값은 예상한 대로 정확하게 작동합니다.&lt;/p>
&lt;h3 id="요청-본문">요청 본문&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/orders&amp;#34;&lt;/span>, (Order order) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// &amp;#39;order&amp;#39; is automatically deserialized from the JSON body&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/orders/{order.Id}&amp;#34;&lt;/span>, order);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="헤더-및-서비스-바인딩">헤더 및 서비스 바인딩&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/protected&amp;#34;&lt;/span>, (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [FromHeader(Name = &amp;#34;X-Api-Key&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> apiKey,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [FromServices] ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Request with API key: {Key}&amp;#34;&lt;/span>, apiKey[..&lt;span style="color:#a5d6ff">4&lt;/span>] + &lt;span style="color:#a5d6ff">&amp;#34;****&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#a5d6ff">&amp;#34;Authorized&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="httpcontext-및-httprequest">HttpContext 및 HttpRequest&lt;/h3>
&lt;p>낮은 수준의 액세스가 필요한 경우:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/info&amp;#34;&lt;/span>, (HttpContext context) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userAgent = context.Request.Headers.UserAgent.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> ip = context.Connection.RemoteIpAddress?.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> { userAgent, ip });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="bindasync를-사용한-사용자-정의-바인딩">BindAsync를 사용한 사용자 정의 바인딩&lt;/h3>
&lt;p>복합 유형의 경우 정적 &lt;code>BindAsync&lt;/code> 메소드를 구현할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PaginationParams&lt;/span>(&lt;span style="color:#ff7b72">int&lt;/span> Page, &lt;span style="color:#ff7b72">int&lt;/span> PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> ValueTask&amp;lt;PaginationParams?&amp;gt; BindAsync(HttpContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span>.TryParse(context.Request.Query[&lt;span style="color:#a5d6ff">&amp;#34;page&amp;#34;&lt;/span>], &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> page);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span>.TryParse(context.Request.Query[&lt;span style="color:#a5d6ff">&amp;#34;pageSize&amp;#34;&lt;/span>], &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> pageSize);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">new&lt;/span> PaginationParams(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Page: page &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span> ? page : &lt;span style="color:#a5d6ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PageSize: pageSize &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span> ? Math.Min(pageSize, &lt;span style="color:#a5d6ff">100&lt;/span>) : &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ValueTask.FromResult&amp;lt;PaginationParams?&amp;gt;(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/items&amp;#34;&lt;/span>, (PaginationParams pagination, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> items = db.Items
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Skip((pagination.Page - &lt;span style="color:#a5d6ff">1&lt;/span>) * pagination.PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(pagination.PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToList();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(items);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것은 엄청나게 강력합니다. 바인딩 논리를 한 번 정의하면 모든 엔드포인트에서 재사용됩니다.&lt;/p>
&lt;h2 id="검증">검증&lt;/h2>
&lt;p>Minimal API가 컨트롤러에 비해 기본적으로 많은 것을 제공하지 못하는 영역 중 하나는 모델 검증입니다. 자동 &lt;code>[Required]&lt;/code> 또는 &lt;code>[StringLength]&lt;/code> 시행은 없습니다. 하지만 이를 처리하기 위한 깔끔한 패턴이 있습니다.&lt;/p>
&lt;h3 id="수동-검증">수동 검증&lt;/h3>
&lt;p>가장 간단한 접근 방식은 처리기에서 유효성을 검사하는 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>, (CreateUserRequest request) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(request.Email))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Email is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (request.Name?.Length &amp;gt; &lt;span style="color:#a5d6ff">100&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Name must be 100 characters or less&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create user...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/users/{request.Email}&amp;#34;&lt;/span>, request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="유효성-검사-라이브러리-사용">유효성 검사 라이브러리 사용&lt;/h3>
&lt;p>중요하지 않은 경우에는 FluentValidation 또는 유사한 라이브러리에 접근하는 것이 좋습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CreateUserValidator&lt;/span> : AbstractValidator&amp;lt;CreateUserRequest&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CreateUserValidator()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Email).NotEmpty().EmailAddress();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Name).NotEmpty().MaximumLength(&lt;span style="color:#a5d6ff">100&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Age).InclusiveBetween(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">150&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>엔드포인트 필터를 통해 이를 엔드포인트에 연결할 수 있으며 다음 섹션으로 넘어갑니다.&lt;/p>
&lt;h2 id="엔드포인트-필터">엔드포인트 필터&lt;/h2>
&lt;p>엔드포인트 필터는 Minimal API의 가장 뛰어난 기능 중 하나입니다. 이를 미들웨어로 생각하지만 전체 파이프라인 대신 특정 엔드포인트로 범위가 지정됩니다. 이는 .NET 7에서 도입되었으며 교차 편집 문제에 환상적입니다.&lt;/p>
&lt;h3 id="기본-필터">기본 필터&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Handle the request&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.AddEndpointFilter(&lt;span style="color:#ff7b72">async&lt;/span> (context, next) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = context.GetArgument&amp;lt;Todo&amp;gt;(&lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Title is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="fluentvalidation을-사용한-유효성-검사-필터">FluentValidation을 사용한 유효성 검사 필터&lt;/h3>
&lt;p>재사용 가능한 검증 필터가 정말 강력해지는 부분은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ValidationFilter&lt;/span>&amp;lt;T&amp;gt; : IEndpointFilter &lt;span style="color:#ff7b72">where&lt;/span> T : &lt;span style="color:#ff7b72">class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IValidator&amp;lt;T&amp;gt; _validator;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ValidationFilter(IValidator&amp;lt;T&amp;gt; validator)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _validator = validator;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask&amp;lt;&lt;span style="color:#ff7b72">object?&lt;/span>&amp;gt; InvokeAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EndpointFilterInvocationContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EndpointFilterDelegate next)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> model = context.Arguments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OfType&amp;lt;T&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (model &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Request body is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> _validator.ValidateAsync(model);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!result.IsValid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> errors = result.Errors
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(e =&amp;gt; e.PropertyName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToDictionary(g =&amp;gt; g.Key, g =&amp;gt; g.Select(e =&amp;gt; e.ErrorMessage).ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.ValidationProblem(errors);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Usage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>, (CreateUserRequest request) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Only reached if validation passes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/users/{request.Email}&amp;#34;&lt;/span>, request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.AddEndpointFilter&amp;lt;ValidationFilter&amp;lt;CreateUserRequest&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="로깅-필터">로깅 필터&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, (AppDbContext db) =&amp;gt; db.Products.ToList())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEndpointFilter(&lt;span style="color:#ff7b72">async&lt;/span> (context, next) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> logger = context.HttpContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequestServices.GetRequiredService&amp;lt;ILogger&amp;lt;Program&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sw = Stopwatch.StartNew();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sw.Stop();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Endpoint executed in {Elapsed}ms&amp;#34;&lt;/span>, sw.ElapsedMilliseconds);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> result;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>여러 필터를 연결할 수 있으며 미들웨어처럼 추가된 순서대로 실행됩니다.&lt;/p>
&lt;h2 id="openapi--swagger-통합">OpenAPI / Swagger 통합&lt;/h2>
&lt;p>좋은 API 문서화는 더 이상 선택 사항이 아닙니다. 다행히도 Minimal API는 &lt;code>Microsoft.AspNetCore.OpenApi&lt;/code> 패키지를 통해 OpenAPI를 최고 수준으로 지원합니다.&lt;/p>
&lt;h3 id="기본-설정">기본 설정&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Todo&amp;gt;())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithName(&lt;span style="color:#a5d6ff">&amp;#34;GetTodos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDescription(&lt;span style="color:#a5d6ff">&amp;#34;Retrieves all todo items&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="풍부한-엔드포인트-메타데이터">풍부한 엔드포인트 메타데이터&lt;/h3>
&lt;p>엔드포인트에 대한 자세한 정보를 제공할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithName(&lt;span style="color:#a5d6ff">&amp;#34;CreateTodo&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithDescription(&lt;span style="color:#a5d6ff">&amp;#34;Creates a new todo item&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Accepts&amp;lt;Todo&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Produces&amp;lt;Todo&amp;gt;(StatusCodes.Status201Created)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Produces(StatusCodes.Status400BadRequest)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.ProducesValidationProblem();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="사용자-정의를-위해-withopenapi-사용">사용자 정의를 위해 WithOpenApi 사용&lt;/h3>
&lt;p>생성된 OpenAPI 문서를 세부적으로 제어하려면 다음을 수행하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt; Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> Todo(id, &lt;span style="color:#a5d6ff">&amp;#34;Sample&amp;#34;&lt;/span>, &lt;span style="color:#79c0ff">false&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithOpenApi(operation =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Summary = &lt;span style="color:#a5d6ff">&amp;#34;Get a specific todo&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Description = &lt;span style="color:#a5d6ff">&amp;#34;Retrieves a single todo item by its unique identifier.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Parameters[&lt;span style="color:#a5d6ff">0&lt;/span>].Description = &lt;span style="color:#a5d6ff">&amp;#34;The unique identifier of the todo item&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> operation;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이를 통해 컨트롤러에서 Swashbuckle XML 주석을 사용하여 얻을 수 있는 것과 동일한 수준의 문서 제어 기능을 제공하지만 보다 명시적인 코드 우선 방식을 사용합니다.&lt;/p>
&lt;h2 id="인증-및-승인">인증 및 승인&lt;/h2>
&lt;p>최소 API 엔드포인트 보안은 나머지 ASP.NET Core와 동일한 패턴을 따르므로 다르게 적용하면 됩니다.&lt;/p>
&lt;h3 id="기본-설정csharp">기본 설정```csharp&lt;/h3>
&lt;p>var builder = WebApplication.CreateBuilder(args);&lt;/p>
&lt;p>builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =&amp;gt;
{
options.Authority = &amp;ldquo;&lt;a href="https://your-identity-provider.com">https://your-identity-provider.com&lt;/a>&amp;rdquo;;
options.Audience = &amp;ldquo;your-api&amp;rdquo;;
});&lt;/p>
&lt;p>builder.Services.AddAuthorizationBuilder()
.AddPolicy(&amp;ldquo;AdminOnly&amp;rdquo;, policy =&amp;gt; policy.RequireRole(&amp;ldquo;Admin&amp;rdquo;))
.AddPolicy(&amp;ldquo;PremiumUser&amp;rdquo;, policy =&amp;gt; policy.RequireClaim(&amp;ldquo;subscription&amp;rdquo;, &amp;ldquo;premium&amp;rdquo;));&lt;/p>
&lt;p>var app = builder.Build();&lt;/p>
&lt;p>app.UseAuthentication();
app.UseAuthorization();&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>### 엔드포인트에 권한 부여 적용
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Require any authenticated user
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&amp;#34;/profile&amp;#34;, (ClaimsPrincipal user) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return Results.Ok(new
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = user.FindFirstValue(ClaimTypes.Name),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Email = user.FindFirstValue(ClaimTypes.Email)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}).RequireAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Require a specific policy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDelete(&amp;#34;/admin/users/{id}&amp;#34;, (int id) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> // Delete user logic
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}).RequireAuthorization(&amp;#34;AdminOnly&amp;#34;);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Allow anonymous access
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&amp;#34;/public/health&amp;#34;, () =&amp;gt; Results.Ok(new { Status = &amp;#34;Healthy&amp;#34; }))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AllowAnonymous();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>핸들러 매개변수에 &lt;code>ClaimsPrincipal&lt;/code>를 직접 주입하는 방법에 주목하세요. 나머지는 프레임워크가 처리합니다. 이는 Minimal API를 정말 우아하게 느끼게 만드는 작은 요소 중 하나입니다.&lt;/p>
&lt;h2 id="대규모-api-정리">대규모 API 정리&lt;/h2>
&lt;p>최소 API를 사용하는 방의 코끼리는 조직입니다. &lt;code>Program.cs&lt;/code>에 엔드포인트가 50개 있으면 엉망이 됩니다. 다음은 작업을 관리하기 쉽게 유지하기 위해 사용하는 패턴입니다.&lt;/p>
&lt;h3 id="경로-그룹">경로 그룹&lt;/h3>
&lt;p>경로 그룹(.NET 7에 도입됨)을 사용하면 관련 엔드포인트 간에 구성을 공유할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> todos = app.MapGroup(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequireAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, (AppDbContext db) =&amp;gt; db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db) =&amp;gt; db.Todos.FindAsync(id));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그룹의 모든 엔드포인트는 &lt;code>/todos&lt;/code> 접두사, &lt;code>Todos&lt;/code> 태그 및 권한 부여 요구 사항을 공유합니다. 깨끗한.&lt;/p>
&lt;h3 id="확장-방법">확장 방법&lt;/h3>
&lt;p>이것은 실제로 확장되는 패턴입니다. 각 엔드포인트 그룹을 자체 정적 클래스로 이동합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Endpoints/TodoEndpoints.cs&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TodoEndpoints&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> RouteGroupBuilder MapTodoEndpoints(&lt;span style="color:#ff7b72">this&lt;/span> WebApplication app)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> &lt;span style="color:#ff7b72">group&lt;/span> = app.MapGroup(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>).WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, GetAll);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, GetById);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, Create);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapPut(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, Update);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapDelete(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, Delete);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">group&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; GetAll(AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Results.Ok(&lt;span style="color:#ff7b72">await&lt;/span> db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; GetById(&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id) &lt;span style="color:#ff7b72">is&lt;/span> { } todo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? Results.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Create(Todo todo, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Update(&lt;span style="color:#ff7b72">int&lt;/span> id, Todo updated, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo.Title = updated.Title;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo.IsComplete = updated.IsComplete;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Delete(&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Remove(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Program.cs — stays clean&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapTodoEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapUserEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapOrderEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Program.cs&lt;/code>는 API의 목차가 됩니다. 각 끝점 그룹은 자체 파일에 있습니다. 이것이 제가 프로덕션 애플리케이션에 권장하는 접근 방식입니다.&lt;/p>
&lt;h3 id="카터-도서관">카터 도서관&lt;/h3>
&lt;p>더 많은 구조를 원할 경우 Carter 라이브러리는 모듈 기반 접근 방식을 제공합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TodoModule&lt;/span> : ICarterModule
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> AddRoutes(IEndpointRouteBuilder app)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Carter는 모든 모듈을 자동으로 검색하고 등록합니다. 원시 최소 API 접근 방식과 전체 컨트롤러 사이의 좋은 중간 지점입니다.&lt;/p>
&lt;h2 id="typedresults-및-응답-유형">TypedResults 및 응답 유형&lt;/h2>
&lt;p>.NET 7부터 형식이 안전한 응답을 위해 &lt;code>Results&lt;/code> 대신 &lt;code>TypedResults&lt;/code>를 사용할 수 있습니다. 이는 작은 변화처럼 보일 수 있지만 OpenAPI 문서화 및 테스트 가능성에 실질적인 이점이 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;Results&amp;lt;Ok&amp;lt;Todo&amp;gt;, NotFound&amp;gt;&amp;gt; (&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> todo &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? TypedResults.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : TypedResults.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>반환 유형 &lt;code>Results&amp;lt;Ok&amp;lt;Todo&amp;gt;, NotFound&amp;gt;&lt;/code>는 이 끝점이 생성할 수 있는 응답 유형을 정확히 프레임워크(및 OpenAPI 문서)에 명시적으로 알려줍니다. 더 이상 추측이나 기본 사례에 대한 수동 &lt;code>Produces&amp;lt;&amp;gt;()&lt;/code> 호출이 없습니다.&lt;/p>
&lt;p>여러 가능한 결과의 경우:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;Results&amp;lt;Created&amp;lt;Todo&amp;gt;, ValidationProblem, Conflict&amp;gt;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.ValidationProblem(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>[]&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> { &lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">new&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;Title is required&amp;#34;&lt;/span> } }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">await&lt;/span> db.Todos.AnyAsync(t =&amp;gt; t.Title == todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.Conflict();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>나는 모든 새 프로젝트에서 &lt;code>TypedResults&lt;/code>를 사용하기 시작했습니다. 컴파일러는 선언된 반환 유형과 실제로 반환하는 것 사이의 불일치를 포착하여 전체 런타임 놀라움을 제거합니다.&lt;/p>
&lt;h2 id="성능-고려-사항">성능 고려 사항&lt;/h2>
&lt;p>Minimal API의 판매 포인트 중 하나는 성능이며, &lt;em>왜&lt;/em> 더 빠른지 이해할 가치가 있습니다.&lt;/p>
&lt;p>&lt;strong>시작 오버헤드 감소.&lt;/strong> 컨트롤러는 엔드포인트를 검색하고, 모델을 바인딩하고, 필터를 적용하기 위해 리플렉션에 크게 의존합니다. 최소 API는 소스 생성기(.NET 7부터 시작)를 사용하여 컴파일 타임에 바인딩 코드를 생성합니다. 이는 시작 시 작업량이 적고 요청당 메모리 할당량이 적다는 것을 의미합니다.&lt;/p>
&lt;p>&lt;strong>MVC 파이프라인 없음.&lt;/strong> 컨트롤러 기반 API는 전체 MVC 파이프라인(작업 선택, 모델 바인딩, 작업 필터, 결과 실행)을 거칩니다. 최소 API는 이 모든 것을 건너뛰고 라우팅에서 핸들러로 바로 이동합니다.&lt;/p>
&lt;p>&lt;strong>RequestDelegate 컴파일.&lt;/strong> 프레임워크는 람다 표현식을 최적화된 &lt;code>RequestDelegate&lt;/code> 인스턴스로 컴파일합니다. 결과 코드는 &lt;code>HttpContext&lt;/code>로 직접 작업하는 경우 직접 작성하는 코드와 매우 유사합니다.&lt;/p>
&lt;p>성능을 극대화하기 위한 몇 가지 실용적인 팁은 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Use AsNoTracking for read-only queries&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Products.AsNoTracking().ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Return results directly — avoid unnecessary allocations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/count&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Products.CountAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Use cancellation tokens for long-running operations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/report&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db, CancellationToken ct) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsNoTracking()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(o =&amp;gt; o.Status)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(g =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> { Status = g.Key, Count = g.Count() })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync(ct));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```컨트롤러와&lt;/span> &lt;span style="color:#f85149">최소&lt;/span> API &lt;span style="color:#f85149">간의&lt;/span> &lt;span style="color:#f85149">성능&lt;/span> &lt;span style="color:#f85149">격차가&lt;/span> .NET &lt;span style="color:#f85149">릴리스마다&lt;/span> &lt;span style="color:#f85149">계속해서&lt;/span> &lt;span style="color:#f85149">줄어들고&lt;/span> &lt;span style="color:#f85149">있다는&lt;/span> &lt;span style="color:#f85149">점도&lt;/span> &lt;span style="color:#f85149">언급할&lt;/span> &lt;span style="color:#f85149">가치가&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>. &lt;span style="color:#f85149">대부분의&lt;/span> &lt;span style="color:#f85149">애플리케이션의&lt;/span> &lt;span style="color:#f85149">경우&lt;/span> &lt;span style="color:#f85149">병목&lt;/span> &lt;span style="color:#f85149">현상이&lt;/span> &lt;span style="color:#f85149">아니라&lt;/span> &lt;span style="color:#f85149">데이터베이스&lt;/span> &lt;span style="color:#f85149">쿼리와&lt;/span> &lt;span style="color:#f85149">외부&lt;/span> &lt;span style="color:#f85149">서비스&lt;/span> &lt;span style="color:#f85149">호출의&lt;/span> &lt;span style="color:#f85149">차이가&lt;/span> &lt;span style="color:#f85149">있을&lt;/span> &lt;span style="color:#f85149">것입니다&lt;/span>. &lt;span style="color:#f85149">벤치마크가&lt;/span> &lt;span style="color:#f85149">아닌&lt;/span> &lt;span style="color:#f85149">개발자&lt;/span> &lt;span style="color:#f85149">경험과&lt;/span> &lt;span style="color:#f85149">프로젝트&lt;/span> &lt;span style="color:#f85149">요구&lt;/span> &lt;span style="color:#f85149">사항을&lt;/span> &lt;span style="color:#f85149">기준으로&lt;/span> &lt;span style="color:#f85149">선택하세요&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">결론&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">최소&lt;/span> API는 .NET &lt;span style="color:#a5d6ff">6&lt;/span>&lt;span style="color:#f85149">에&lt;/span> &lt;span style="color:#f85149">도입된&lt;/span> &lt;span style="color:#f85149">이후&lt;/span> &lt;span style="color:#f85149">많은&lt;/span> &lt;span style="color:#f85149">발전을&lt;/span> &lt;span style="color:#f85149">이루었습니다&lt;/span>. &lt;span style="color:#a5d6ff">&amp;#34;hello world&amp;#34;&lt;/span> &lt;span style="color:#f85149">데모&lt;/span> &lt;span style="color:#f85149">기능으로&lt;/span> &lt;span style="color:#f85149">시작된&lt;/span> &lt;span style="color:#f85149">것이&lt;/span> &lt;span style="color:#f85149">프로덕션&lt;/span> API를 &lt;span style="color:#f85149">위한&lt;/span> &lt;span style="color:#f85149">합법적인&lt;/span> &lt;span style="color:#f85149">선택으로&lt;/span> &lt;span style="color:#f85149">발전했습니다&lt;/span>. &lt;span style="color:#f85149">엔드포인트&lt;/span> &lt;span style="color:#f85149">필터&lt;/span>, &lt;span style="color:#f85149">경로&lt;/span> &lt;span style="color:#f85149">그룹&lt;/span>, &lt;span style="color:#f85149">입력된&lt;/span> &lt;span style="color:#f85149">결과&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">견고한&lt;/span> OpenAPI &lt;span style="color:#f85149">지원을&lt;/span> &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">잘&lt;/span> &lt;span style="color:#f85149">구조화되고&lt;/span> &lt;span style="color:#f85149">유지&lt;/span> &lt;span style="color:#f85149">관리&lt;/span> &lt;span style="color:#f85149">가능한&lt;/span> &lt;span style="color:#f85149">서비스를&lt;/span> &lt;span style="color:#f85149">구축하는&lt;/span> &lt;span style="color:#f85149">데&lt;/span> &lt;span style="color:#f85149">필요한&lt;/span> &lt;span style="color:#f85149">모든&lt;/span> &lt;span style="color:#f85149">것을&lt;/span> &lt;span style="color:#f85149">갖추고&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">내&lt;/span> &lt;span style="color:#f85149">추천&lt;/span>? &lt;span style="color:#f85149">새로운&lt;/span> API &lt;span style="color:#f85149">프로젝트&lt;/span>, &lt;span style="color:#f85149">특히&lt;/span> &lt;span style="color:#f85149">마이크로서비스나&lt;/span> &lt;span style="color:#f85149">집중적인&lt;/span> &lt;span style="color:#f85149">내부&lt;/span> API를 &lt;span style="color:#f85149">시작하는&lt;/span> &lt;span style="color:#f85149">경우&lt;/span> Minimal API를 &lt;span style="color:#f85149">진지하게&lt;/span> &lt;span style="color:#f85149">시도해&lt;/span> &lt;span style="color:#f85149">보세요&lt;/span>. &lt;span style="color:#f85149">조직에&lt;/span> &lt;span style="color:#f85149">확장&lt;/span> &lt;span style="color:#f85149">메서드&lt;/span> &lt;span style="color:#f85149">패턴을&lt;/span> &lt;span style="color:#f85149">사용하고&lt;/span>, &lt;span style="color:#f85149">교차&lt;/span> &lt;span style="color:#f85149">문제에&lt;/span> &lt;span style="color:#f85149">대해&lt;/span> &lt;span style="color:#f85149">끝점&lt;/span> &lt;span style="color:#f85149">필터를&lt;/span> &lt;span style="color:#f85149">사용하고&lt;/span>, &lt;span style="color:#f85149">유형&lt;/span> &lt;span style="color:#f85149">안전성을&lt;/span> &lt;span style="color:#f85149">위해&lt;/span> &lt;span style="color:#f85149">`&lt;/span>TypedResults&lt;span style="color:#f85149">`를&lt;/span> &lt;span style="color:#f85149">활용합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">기존&lt;/span> &lt;span style="color:#f85149">컨트롤러&lt;/span> &lt;span style="color:#f85149">기반&lt;/span> &lt;span style="color:#f85149">프로젝트의&lt;/span> &lt;span style="color:#f85149">경우&lt;/span> &lt;span style="color:#f85149">마이그레이션을&lt;/span> &lt;span style="color:#f85149">서두르지&lt;/span> &lt;span style="color:#f85149">않습니다&lt;/span>. &lt;span style="color:#f85149">두&lt;/span> &lt;span style="color:#f85149">접근&lt;/span> &lt;span style="color:#f85149">방식&lt;/span> &lt;span style="color:#f85149">모두&lt;/span> &lt;span style="color:#f85149">잘&lt;/span> &lt;span style="color:#f85149">작동하며&lt;/span> &lt;span style="color:#f85149">나란히&lt;/span> &lt;span style="color:#f85149">사용할&lt;/span> &lt;span style="color:#f85149">수도&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>. &lt;span style="color:#f85149">하지만&lt;/span> &lt;span style="color:#f85149">다음에&lt;/span> &lt;span style="color:#f85149">소규모&lt;/span> &lt;span style="color:#f85149">서비스나&lt;/span> &lt;span style="color:#f85149">빠른&lt;/span> &lt;span style="color:#f85149">내부&lt;/span> API를 &lt;span style="color:#f85149">추가해야&lt;/span> &lt;span style="color:#f85149">할&lt;/span> &lt;span style="color:#f85149">경우&lt;/span> &lt;span style="color:#f85149">컨트롤러를&lt;/span> &lt;span style="color:#f85149">건너뛰고&lt;/span> &lt;span style="color:#f85149">최소화하세요&lt;/span>. &lt;span style="color:#f85149">돌아가지&lt;/span> &lt;span style="color:#f85149">않을&lt;/span> &lt;span style="color:#f85149">수도&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">즐거운&lt;/span> &lt;span style="color:#f85149">코딩하세요&lt;/span>!
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded><category>.NET</category><category>API</category><category>Web Development</category></item><item><title>Blazor 구성 요소에서 격리된 CSS 사용</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-isolated-css/</link><pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-isolated-css/</guid><description>전역 스타일 충돌을 방지하려면 CSS 격리를 사용하여 CSS 스타일을 개별 Blazor 구성 요소로 지정하세요.</description><content:encoded>&lt;p>Blazor로 작업할 때 항상 나를 괴롭혔던 한 가지는 실수로 구성 요소 전체에서 스타일을 깨뜨리는 것이 얼마나 쉬운지였습니다. 한 구성요소에 클래스를 추가했는데 갑자기 완전히 다른 페이지에 있는 다른 항목이 이상해 보였습니다. 알고 보니 Blazor에는 이를 위한 내장 솔루션인 CSS 격리가 있습니다.&lt;/p>
&lt;h1 id="css-격리란-무엇인가요">CSS 격리란 무엇인가요?&lt;/h1>
&lt;p>CSS 격리를 사용하면 스타일 범위를 특정 구성 요소로 지정할 수 있습니다. Blazor는 빌드 시 각 구성 요소에 대해 고유한 특성을 생성하고 이를 CSS 선택기에 추가하여 이를 수행합니다. 따라서 스타일은 해당 스타일이 속한 구성 요소에만 적용됩니다.&lt;/p>
&lt;p>BEM 이름 지정도 필요 없고, 말도 안 되는 특정성 해킹도 필요하지 않습니다. 깨끗하고 범위가 지정된 CSS입니다.&lt;/p>
&lt;h1 id="사용방법">사용방법&lt;/h1>
&lt;p>&lt;code>Card.razor&lt;/code>라는 구성요소가 있다고 가정해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span> class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#7ee787">h3&lt;/span> class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-title&amp;#34;&lt;/span>&amp;gt;@Title&amp;lt;/&lt;span style="color:#7ee787">h3&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#7ee787">p&lt;/span> class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&amp;gt;@ChildContent&amp;lt;/&lt;span style="color:#7ee787">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public string Title { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public RenderFragment ChildContent { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>분리된 스타일을 추가하려면 이름은 같지만 확장자는 &lt;code>.razor.css&lt;/code>인 파일을 생성하면 됩니다. 이 경우: &lt;code>Card.razor.css&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">border&lt;/span>: &lt;span style="color:#a5d6ff">1&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span> &lt;span style="color:#79c0ff">solid&lt;/span> &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>border);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">border-radius&lt;/span>: &lt;span style="color:#a5d6ff">8&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">padding&lt;/span>: &lt;span style="color:#a5d6ff">1&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">background&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>bg&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>secondary);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card-title&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">font-size&lt;/span>: &lt;span style="color:#a5d6ff">1.1&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">font-weight&lt;/span>: &lt;span style="color:#a5d6ff">600&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">margin-bottom&lt;/span>: &lt;span style="color:#a5d6ff">0.5&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card-body&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">text&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>secondary);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">font-size&lt;/span>: &lt;span style="color:#a5d6ff">0.9&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 그게 다야! Blazor는 자동으로 이를 선택하고 해당 스타일의 범위를 &lt;code>Card&lt;/code> 구성 요소로만 지정합니다. 다른 곳에 또 다른 &lt;code>.card&lt;/code> 클래스가 있으면 영향을 받지 않습니다.&lt;/p>
&lt;h1 id="내부적으로는-어떻게-작동하나요">내부적으로는 어떻게 작동하나요?&lt;/h1>
&lt;p>프로젝트를 빌드하면 Blazor는 HTML과 CSS를 다시 작성합니다. 구성 요소는 &lt;code>b-3x8qz7k2f1&lt;/code>와 같은 고유한 속성을 가지며 CSS 선택기는 해당 속성을 추가합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#7ee787">b-3x8qz7k2f1&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">border&lt;/span>: &lt;span style="color:#a5d6ff">1&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span> &lt;span style="color:#79c0ff">solid&lt;/span> &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>border);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">/* ... */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>생성된 CSS 번들은 &lt;code>{ProjectName}.styles.css&lt;/code>로 제공됩니다. &lt;code>index.html&lt;/code> 또는 &lt;code>_Host.cshtml&lt;/code>에 이것이 있는지 확인하십시오.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">link&lt;/span> href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;YourApp.styles.css&amp;#34;&lt;/span> rel&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;stylesheet&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>해당 링크가 없으면 격리된 스타일이 표시되지 않으며 무슨 일이 일어나고 있는지 궁금해하는 데 30분을 소비하게 됩니다. 저를 믿으세요. 저는 거기에 가봤습니다.&lt;/p>
&lt;h1 id="하위-요소-타겟팅">하위 요소 타겟팅&lt;/h1>
&lt;p>한 가지 명심해야 할 점은 기본적으로 격리된 CSS는 현재 구성 요소의 요소에만 적용된다는 것입니다. 하위 구성 요소 내부의 요소 스타일을 지정하려면 &lt;code>::deep&lt;/code> 연결자가 필요합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>::&lt;span style="color:#d2a8ff;font-weight:bold">deep&lt;/span> .&lt;span style="color:#f0883e;font-weight:bold">child-element&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#79c0ff">red&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 Blazor가 하위 구성 요소 내의 일치하는 요소에도 스타일을 적용하도록 지시합니다. 하위 항목의 스타일을 지정해야 하는 래퍼 구성 요소가 있는 경우 매우 편리합니다.&lt;/p>
&lt;h1 id="언제-사용하는가">언제 사용하는가&lt;/h1>
&lt;p>저는 현재 거의 모든 구성 요소에 CSS 격리를 사용합니다. 모든 것을 깨끗하고 예측 가능하게 유지합니다. 내가 사용하지 않는 유일한 때는 재설정, 타이포그래피 또는 테마 변수와 같은 진정한 전역 스타일(공유 CSS 파일에 포함됨)에 사용하는 것입니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! &lt;strong>@emimontesdeoca&lt;/strong>로 소셜 미디어를 통해 언제든지 저에게 연락해주세요.&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation">ASP.NET Core Blazor CSS 격리&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>CSS</category></item><item><title>Semantic Kernel을 사용하여 크리스마스 지출 제어</title><link>https://emimontesdeoca.github.io/ko/posts/keeping-christmas-spending-with-semantic-kernel/</link><pubDate>Sat, 28 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/keeping-christmas-spending-with-semantic-kernel/</guid><description>Semantic Kernel, Azure OpenAI 및 Blazor를 사용하여 영수증을 분석하고 크리스마스 지출을 추적하세요.</description><content:encoded>&lt;p>﻿## 소개&lt;/p>
&lt;p>연휴 시즌이 다가옴에 따라, 특히 쇼핑과 선물 구매가 붐비는 상황에서 비용 관리가 어려워질 수 있습니다. 이 블로그 게시물에서는 인공 지능을 활용하여 .NET 기술을 사용하여 크리스마스 지출을 추적하는 방법을 살펴보겠습니다. Semantic Kernel과 AI의 힘으로 영수증을 분석하여 매장명, 날짜, 품목 목록, 총액 등 주요 세부정보를 효율적으로 추출할 수 있습니다. 이 솔루션을 사용하면 크리스마스 지출을 쉽게 모니터링하고 관리할 수 있으므로 영수증을 수동으로 검토하는 번거로움 없이 예산을 최대한 활용할 수 있습니다.&lt;/p>
&lt;h2 id="calendario-de-adviento-de-inteligencia-artificial-2024-en-español">Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/h2>
&lt;p 정렬="중앙">
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdkovc8lzahgm8pvsblk.png"/>
&lt;/p>
&lt;p>이 프로젝트는 AI에 관한 온라인 이벤트인 &lt;strong>Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/strong>에 참여하면서 영감을 얻었습니다. &lt;a href="https://dev.to/roberto_navarro_mate/calendario-de-adviento-de-inteligencia-artificial-2024-en-espanol-bdb">이 Dev.to 링크&lt;/a>에서 이벤트에 대한 자세한 내용을 확인할 수 있습니다.&lt;/p>
&lt;h2 id="프로젝트">프로젝트&lt;/h2>
&lt;p>이 프로젝트에서는 GPT-4와 같은 강력한 AI 모델을 활용하여 이미지를 처리하고 분석할 수 있는 서비스인 &lt;strong>Azure OpenAI&lt;/strong>를 사용할 예정입니다. 이 프로세스에는 백엔드 API 서비스 설정부터 이미지 업로드를 위한 Blazor 프런트 엔드 통합까지 여러 단계가 포함됩니다. 또한 모든 것을 원활하게 연결하는 데 도움이 되는 구성 요소인 &lt;strong>.NET Aspire&lt;/strong>를 사용할 것입니다.&lt;/p>
&lt;h2 id="전제-조건">전제 조건&lt;/h2>
&lt;p>코드를 살펴보기 전에 다음 전제 조건이 충족되었는지 확인하세요.&lt;/p>
&lt;ul>
&lt;li>.NET 9&lt;/li>
&lt;li>Azure OpenAI 액세스(API 키)&lt;/li>
&lt;li>비주얼 스튜디오 또는 비주얼 스튜디오 코드&lt;/li>
&lt;li>Blazor, HTTP 클라이언트 및 API 개발에 대한 기본 지식&lt;/li>
&lt;/ul>
&lt;h2 id="visual-studio-솔루션">Visual Studio 솔루션&lt;/h2>
&lt;p>우리는 결국 다음과 같은 결과를 얻게 될 것입니다. 저는 항목을 분리하고 멋진 이름을 사용하는 것을 좋아하므로 다음과 같습니다.&lt;/p>
&lt;p 정렬="중앙">
&lt;img src="https://imgur.com/gAnGLhM.png">
&lt;/p>
&lt;p>하지만 단계별로 물건을 만들어 봅시다!&lt;/p>
&lt;h2 id="0단계-모델">0단계: 모델&lt;/h2>
&lt;p>영수증 스캐너 애플리케이션의 핵심은 프런트 엔드, API 및 AI 서비스 간의 상호 작용을 촉진하는 여러 주요 모델에 의존합니다. 이 프로젝트에 사용된 주요 모델은 다음과 같습니다.&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>AnalyzeReceiptRequest&lt;/strong>&lt;br>
이 모델은 영수증 분석을 위한 요청 구조를 나타냅니다. 여기에는 처리될 영수증 이미지의 바이트 배열을 보유하는 &lt;code>ImageBytes&lt;/code> 속성이 포함되어 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AnalyzeReceiptRequest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">byte&lt;/span>[] ImageBytes { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>영수증분석결과&lt;/strong>&lt;br>
이 모델은 영수증 처리 후 결과를 캡처합니다. 영수증에서 추출한 매장명, 날짜, 품목, 총액 등 구조화된 데이터를 담고 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ReceiptAnalyzeResult&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ReceiptData Result { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>영수증 데이터&lt;/strong>&lt;br>
구조화된 영수증 데이터를 보유하는 모델입니다. 여기에는 매장 이름, 날짜, 품목 목록(각 품목의 이름과 가격 포함), 영수증의 총액에 대한 속성이 포함됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ReceiptData&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Store { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime? Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;ReceiptItem&amp;gt; Items { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal?&lt;/span> Total { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>영수증 항목&lt;/strong>&lt;br>
영수증의 각 항목은 이 모델로 표시됩니다. 품목 이름과 가격이 들어 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ReceiptItem&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal?&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```이러한&lt;/span> &lt;span style="color:#f85149">모델은&lt;/span> &lt;span style="color:#f85149">클라이언트와&lt;/span> &lt;span style="color:#f85149">서버&lt;/span> &lt;span style="color:#f85149">간에&lt;/span> &lt;span style="color:#f85149">데이터를&lt;/span> &lt;span style="color:#f85149">전달하기&lt;/span> &lt;span style="color:#f85149">위한&lt;/span> &lt;span style="color:#f85149">기반&lt;/span> &lt;span style="color:#f85149">역할을&lt;/span> &lt;span style="color:#f85149">하여&lt;/span> &lt;span style="color:#f85149">원활한&lt;/span> &lt;span style="color:#f85149">정보&lt;/span> &lt;span style="color:#f85149">흐름을&lt;/span> &lt;span style="color:#f85149">보장합니다&lt;/span>. API는 &lt;span style="color:#f85149">영수증&lt;/span> &lt;span style="color:#f85149">이미지를&lt;/span> &lt;span style="color:#f85149">수신하고&lt;/span> &lt;span style="color:#f85149">그&lt;/span> &lt;span style="color:#f85149">대가로&lt;/span> &lt;span style="color:#f85149">프런트엔드에서&lt;/span> &lt;span style="color:#f85149">쉽게&lt;/span> &lt;span style="color:#f85149">사용할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있는&lt;/span> &lt;span style="color:#f85149">구조화된&lt;/span> JSON &lt;span style="color:#f85149">개체를&lt;/span> &lt;span style="color:#f85149">처리하고&lt;/span> &lt;span style="color:#f85149">반환합니다&lt;/span>.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h2 id="1단계-백엔드-api-서비스-설정">1단계: 백엔드 API 서비스 설정&lt;/h2>
&lt;p>이 애플리케이션을 구축하는 첫 번째 단계는 영수증 이미지를 분석하기 위한 API 서비스를 설정하는 것입니다. &lt;strong>Azure OpenAI&lt;/strong> API를 사용하여 영수증 이미지에서 정보를 추출하겠습니다. 모든 것이 어떻게 조화를 이루는지에 대한 분석은 다음과 같습니다.&lt;/p>
&lt;h3 id="ai-서비스---심층-분석">AI 서비스 - 심층 분석&lt;/h3>
&lt;p>AI 서비스는 당사 영수증 분석 시스템의 핵심입니다. Azure OpenAI의 API와 통신하여 이미지 데이터를 처리하고 의미 있는 통찰력을 반환하는 역할을 담당합니다. &lt;strong>AiApiClient&lt;/strong> 클래스는 Azure OpenAI API와의 모든 상호 작용을 처리하는 클라이언트입니다.&lt;/p>
&lt;h4 id="ai-클라이언트-구현">AI 클라이언트 구현&lt;/h4>
&lt;p>&lt;code>AiApiClient&lt;/code>는 영수증 이미지(바이트 배열 형식)를 Azure OpenAI API로 보내는 주요 구성 요소입니다. 통신, 오류 로깅 및 데이터 구문 분석을 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AiApiClient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILogger&amp;lt;AiApiClient&amp;gt; _logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> AiApiClient(HttpClient httpClient, ILogger&amp;lt;AiApiClient&amp;gt; logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger = logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ReceiptAnalyzeResult?&amp;gt; AnalyzeAsync(&lt;span style="color:#ff7b72">byte&lt;/span>[] imageBytes, CancellationToken cancellationToken = &lt;span style="color:#ff7b72">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (imageBytes == &lt;span style="color:#79c0ff">null&lt;/span> || imageBytes.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is null or empty.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Sending analyze request with image bytes of length: {Length}&amp;#34;&lt;/span>, imageBytes.Length);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> request = &lt;span style="color:#ff7b72">new&lt;/span> AnalyzeReceiptRequest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ImageBytes = imageBytes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.PostAsJsonAsync(&lt;span style="color:#a5d6ff">&amp;#34;/analyze-receipt&amp;#34;&lt;/span>, request, cancellationToken);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;Failed to analyze receipt. StatusCode: {StatusCode}&amp;#34;&lt;/span>, response.StatusCode);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> analyzeResult = &lt;span style="color:#ff7b72">await&lt;/span> response.Content.ReadFromJsonAsync&amp;lt;ReceiptAnalyzeResult&amp;gt;(cancellationToken: cancellationToken);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (analyzeResult == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;No content received from AI API service.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Analysis result received: {AnalyzeResult}&amp;#34;&lt;/span>, analyzeResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> analyzeResult;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;An error occurred while analyzing the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>코드의 이 부분에서는 다음을 담당하는 &lt;code>AnalyzeAsync&lt;/code> 메서드를 정의합니다.&lt;/p>
&lt;ol>
&lt;li>이미지 바이트 배열을 Azure OpenAI API로 보냅니다.&lt;/li>
&lt;li>API의 오류나 실패한 응답을 처리합니다.&lt;/li>
&lt;li>반환된 JSON 데이터를 구조화된 결과(&lt;code>ReceiptAnalyzeResult&lt;/code>)로 구문 분석합니다.&lt;/li>
&lt;/ol>
&lt;p>이 기능을 전용 서비스(AiApiClient)로 분리하면 다음과 같은 이점이 있습니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>오류 처리:&lt;/strong> 네트워크 문제 또는 잘못된 응답과 같은 오류를 중앙 집중식으로 처리합니다.&lt;/li>
&lt;li>&lt;strong>로깅:&lt;/strong> 시스템 동작을 모니터링하기 위해 요청 및 응답을 적절하게 로깅합니다.&lt;/li>
&lt;/ul>
&lt;p 정렬="중앙">
&lt;img src="https://imgur.com/q3EpCSy.png"/>
&lt;/p>
&lt;h3 id="api-서비스---요청-및-응답-처리">API 서비스 - 요청 및 응답 처리&lt;/h3>
&lt;p>&lt;strong>API 서비스&lt;/strong>는 프런트엔드 Blazor 애플리케이션과 AI 서비스 간의 중개자 역할을 합니다. 이 서비스는 이미지 데이터를 받아 AI 서비스에 전달하고 분석 결과를 클라이언트에 반환하는 역할을 담당합니다.&lt;/p>
&lt;h4 id="api-엔드포인트">API 엔드포인트&lt;/h4>
&lt;p>이 단계에서는 영수증 이미지를 수락하고, 처리를 위해 AI 서비스에 전달하고, 결과를 클라이언트에 반환하는 간단한 API 엔드포인트를 정의합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ReceiptScanner.Shared.Clients&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ReceiptScanner.Shared.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add service defaults &amp;amp; Aspire client integrations.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddProblemDetails();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Register AiApiClient with HttpClient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddHttpClient&amp;lt;AiApiClient&amp;gt;(client =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.BaseAddress = &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;https+http://aiservice&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseExceptionHandler();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// POST endpoint to analyze receipt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/api/analyze-receipt&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AnalyzeReceiptRequest request, AiApiClient aiApiClient, ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (request.ImageBytes == &lt;span style="color:#79c0ff">null&lt;/span> || request.ImageBytes.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is null or empty.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is required.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Received analyze receipt request with image bytes of length: {Length}&amp;#34;&lt;/span>, request.ImageBytes.Length);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> aiApiClient.AnalyzeAsync(request.ImageBytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (result == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;Failed to analyze the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Problem(&lt;span style="color:#a5d6ff">&amp;#34;Unable to process the receipt at this time. Please try again later.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Analysis completed successfully. Result: {Result}&amp;#34;&lt;/span>, result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;An error occurred while processing the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Problem(&lt;span style="color:#a5d6ff">&amp;#34;An error occurred while processing the receipt. Please try again later.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDefaultEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 끝점은 다음과 같습니다.&lt;/p>
&lt;ol>
&lt;li>요청 본문의 일부로 영수증 이미지를 수락합니다.&lt;/li>
&lt;li>&lt;code>AiService&lt;/code> endopint 메서드를 호출하여 처리를 위해 이미지를 Azure OpenAI로 보냅니다.&lt;/li>
&lt;li>분석 결과를 클라이언트에게 다시 반환합니다.&lt;/li>
&lt;/ol>
&lt;p 정렬="중앙">
&lt;img src="https://imgur.com/u9mQrpq.png"/>
&lt;/p>
&lt;h2 id="2단계-blazor-프런트엔드-설정">2단계: Blazor 프런트엔드 설정&lt;/h2>
&lt;p>이제 백엔드가 설정되었으므로 &lt;strong>Blazor 프런트엔드&lt;/strong>에 주목해 보겠습니다. 여기에서 사용자는 분석을 위해 영수증 이미지를 업로드하고 결과를 확인할 수 있습니다.&lt;/p>
&lt;h3 id="blazor-페이지-구현">Blazor 페이지 구현&lt;/h3>
&lt;p>Blazor 페이지는 사용자가 여러 영수증 이미지를 업로드한 다음 테이블에 표시되는 분석 결과를 볼 수 있는 간단한 인터페이스를 제공합니다. 페이지 코드는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/analyzer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using ReceiptScanner&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Shared&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clients
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using ReceiptScanner&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Shared&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using System&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Globalization
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ApiServiceClient ApiClient
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ILogger&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Program&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Logger
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>attribute [StreamRendering]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>PageTitle&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Receipt Analyzer&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>PageTitle&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;text-center my-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Receipt Analyzer&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;lead text-center mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Upload receipt images below to extract their data&lt;span style="color:#ff7b72;font-weight:bold">.&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">File&lt;/span> Upload Section &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>InputFile OnChange&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;HandleFileSelected&amp;#34;&lt;/span> multiple &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;form-control mb-3&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary w-100&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;ProcessReceipts&amp;#34;&lt;/span> disabled&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(!hasFiles)&amp;#34;&lt;/span> type&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;button&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(!processing ? &amp;#34;&lt;/span>d&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>none&lt;span style="color:#a5d6ff">&amp;#34; : &amp;#34;&amp;#34;) spinner-border spinner-border-sm&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;status&amp;#34;&lt;/span> aria&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>hidden&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (processing)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Processing&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Process Receipts&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Uploaded Images Preview &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-header&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h5 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;mb-0&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Uploaded Receipt Images&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h5&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> fileBytes &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> fileBytesList)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;col-12 col-md-4 mb-3&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@($&amp;#34;&lt;/span>data:image&lt;span style="color:#ff7b72;font-weight:bold">/&lt;/span>jpeg;base64,{Convert&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToBase64String(fileBytes)}&lt;span style="color:#a5d6ff">&amp;#34;)&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;img-fluid rounded&amp;#34;&lt;/span> alt&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Uploaded receipt&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Processing Indicator &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (processing)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert alert-info text-center&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Processing receipts&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Please wait &lt;span style="color:#ff7b72">while&lt;/span> we analyze the uploaded files&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Analysis Results Section &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">!=&lt;/span> null &lt;span style="color:#ff7b72;font-weight:bold">&amp;amp;&amp;amp;&lt;/span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-header&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h5 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;mb-0&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Analysis Results&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h5&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table table-striped table-bordered&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Store&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Total&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Items&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> receipt &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> analyzedReceipts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Store &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString() &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Total&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>, ci) &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ul &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;list-unstyled&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Items is &lt;span style="color:#ff7b72;font-weight:bold">not&lt;/span> null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Items&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>item&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span> &lt;span style="color:#f85149">@&lt;/span>item&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>, ci)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>ul&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (processed &lt;span style="color:#ff7b72;font-weight:bold">&amp;amp;&amp;amp;&lt;/span> (analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">==&lt;/span> null &lt;span style="color:#ff7b72;font-weight:bold">||&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any()))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert alert-warning text-center&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>No results found&lt;span style="color:#ff7b72;font-weight:bold">.&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Please try again with different images &lt;span style="color:#ff7b72;font-weight:bold">or&lt;/span> ensure they are clear &lt;span style="color:#ff7b72;font-weight:bold">and&lt;/span> legible&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> hasFiles;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> processing;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> processed;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>byte[]&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> fileBytesList &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ReceiptAnalyzeResult&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CultureInfo ci &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;es-es&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task HandleFileSelected(InputFileChangeEventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> try
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> file &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> e&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetMultipleFiles())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> memoryStream &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new MemoryStream();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await file&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>OpenReadStream()&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>CopyToAsync(memoryStream);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(memoryStream&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> hasFiles &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> catch (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;Error while handling file upload.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task ProcessReceipts()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>hasFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processing &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> true;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> try
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> fileBytes &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> fileBytesList)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ApiClient&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AnalyzeReceiptAsync(fileBytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (result &lt;span style="color:#ff7b72;font-weight:bold">!=&lt;/span> null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> catch (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;Error while processing receipts.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> finally
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processing &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> false;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processed &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> true;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```이&lt;/span> &lt;span style="color:#f85149">페이지에서는&lt;/span> &lt;span style="color:#f85149">영수증을&lt;/span> &lt;span style="color:#f85149">업로드할&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있으며&lt;/span> &lt;span style="color:#f85149">분석&lt;/span> &lt;span style="color:#f85149">결과를&lt;/span> &lt;span style="color:#f85149">매장명&lt;/span>, &lt;span style="color:#f85149">날짜&lt;/span>, &lt;span style="color:#f85149">총액&lt;/span>, &lt;span style="color:#f85149">품목&lt;/span> &lt;span style="color:#f85149">목록이&lt;/span> &lt;span style="color:#f85149">포함된&lt;/span> &lt;span style="color:#f85149">테이블로&lt;/span> &lt;span style="color:#f85149">표시합니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p &lt;span style="color:#f85149">정렬&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;중앙&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;https://imgur.com/BLswKhm.png&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">## 3단계: .NET Aspire&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p &lt;span style="color:#f85149">정렬&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;중앙&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;https://imgur.com/ja56RWN.png&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">### .NET Aspire란 무엇입니까?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire는 &lt;span style="color:#f85149">관찰&lt;/span> &lt;span style="color:#f85149">가능한&lt;/span> &lt;span style="color:#f85149">프로덕션&lt;/span> &lt;span style="color:#f85149">지원&lt;/span> &lt;span style="color:#f85149">앱을&lt;/span> &lt;span style="color:#f85149">구축하기&lt;/span> &lt;span style="color:#f85149">위한&lt;/span> &lt;span style="color:#f85149">강력한&lt;/span> &lt;span style="color:#f85149">도구&lt;/span>, &lt;span style="color:#f85149">템플릿&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">패키지&lt;/span> &lt;span style="color:#f85149">세트입니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire는 &lt;span style="color:#f85149">특정&lt;/span> &lt;span style="color:#f85149">클라우드&lt;/span> &lt;span style="color:#f85149">기반&lt;/span> &lt;span style="color:#f85149">문제를&lt;/span> &lt;span style="color:#f85149">처리하는&lt;/span> NuGet &lt;span style="color:#f85149">패키지&lt;/span> &lt;span style="color:#f85149">컬렉션을&lt;/span> &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">제공됩니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">클라우드&lt;/span> &lt;span style="color:#f85149">네이티브&lt;/span> &lt;span style="color:#f85149">앱은&lt;/span> &lt;span style="color:#f85149">단일한&lt;/span> &lt;span style="color:#f85149">모놀리식&lt;/span> &lt;span style="color:#f85149">코드&lt;/span> &lt;span style="color:#f85149">기반이&lt;/span> &lt;span style="color:#f85149">아닌&lt;/span> &lt;span style="color:#f85149">상호&lt;/span> &lt;span style="color:#f85149">연결된&lt;/span> &lt;span style="color:#f85149">작은&lt;/span> &lt;span style="color:#f85149">조각이나&lt;/span> &lt;span style="color:#f85149">마이크로서비스로&lt;/span> &lt;span style="color:#f85149">구성되는&lt;/span> &lt;span style="color:#f85149">경우가&lt;/span> &lt;span style="color:#f85149">많습니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">클라우드&lt;/span> &lt;span style="color:#f85149">네이티브&lt;/span> &lt;span style="color:#f85149">앱은&lt;/span> &lt;span style="color:#f85149">일반적으로&lt;/span> &lt;span style="color:#f85149">데이터베이스&lt;/span>, &lt;span style="color:#f85149">메시징&lt;/span>, &lt;span style="color:#f85149">캐싱과&lt;/span> &lt;span style="color:#f85149">같은&lt;/span> &lt;span style="color:#f85149">많은&lt;/span> &lt;span style="color:#f85149">서비스를&lt;/span> &lt;span style="color:#f85149">사용합니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">지원에&lt;/span> &lt;span style="color:#f85149">대한&lt;/span> &lt;span style="color:#f85149">자세한&lt;/span> &lt;span style="color:#f85149">내용은&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">지원&lt;/span> &lt;span style="color:#f85149">정책을&lt;/span> &lt;span style="color:#f85149">참조하세요&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">분산&lt;/span> &lt;span style="color:#f85149">애플리케이션은&lt;/span> &lt;span style="color:#f85149">서로&lt;/span> &lt;span style="color:#f85149">다른&lt;/span> &lt;span style="color:#f85149">호스트에서&lt;/span> &lt;span style="color:#f85149">실행되는&lt;/span> &lt;span style="color:#f85149">컨테이너와&lt;/span> &lt;span style="color:#f85149">같이&lt;/span> &lt;span style="color:#f85149">여러&lt;/span> &lt;span style="color:#f85149">노드에서&lt;/span> &lt;span style="color:#f85149">컴퓨팅&lt;/span> &lt;span style="color:#f85149">리소스를&lt;/span> &lt;span style="color:#f85149">사용하는&lt;/span> &lt;span style="color:#f85149">애플리케이션입니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">이러한&lt;/span> &lt;span style="color:#f85149">노드는&lt;/span> &lt;span style="color:#f85149">사용자에게&lt;/span> &lt;span style="color:#f85149">응답을&lt;/span> &lt;span style="color:#f85149">전달하기&lt;/span> &lt;span style="color:#f85149">위해&lt;/span> &lt;span style="color:#f85149">네트워크&lt;/span> &lt;span style="color:#f85149">경계를&lt;/span> &lt;span style="color:#f85149">통해&lt;/span> &lt;span style="color:#f85149">통신해야&lt;/span> &lt;span style="color:#f85149">합니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">클라우드&lt;/span> &lt;span style="color:#f85149">네이티브&lt;/span> &lt;span style="color:#f85149">앱은&lt;/span> &lt;span style="color:#f85149">클라우드&lt;/span> &lt;span style="color:#f85149">인프라의&lt;/span> &lt;span style="color:#f85149">확장성&lt;/span>, &lt;span style="color:#f85149">탄력성&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">관리&lt;/span> &lt;span style="color:#f85149">용이성을&lt;/span> &lt;span style="color:#f85149">최대한&lt;/span> &lt;span style="color:#f85149">활용하는&lt;/span> &lt;span style="color:#f85149">특정&lt;/span> &lt;span style="color:#f85149">유형의&lt;/span> &lt;span style="color:#f85149">분산&lt;/span> &lt;span style="color:#f85149">앱입니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">이&lt;/span> &lt;span style="color:#f85149">프로젝트에&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">**.&lt;/span>NET Aspire&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">를&lt;/span> &lt;span style="color:#f85149">사용하면&lt;/span> &lt;span style="color:#f85149">다음과&lt;/span> &lt;span style="color:#f85149">같이&lt;/span> &lt;span style="color:#f85149">전체&lt;/span> &lt;span style="color:#f85149">시스템&lt;/span> &lt;span style="color:#f85149">품질을&lt;/span> &lt;span style="color:#f85149">향상시키는&lt;/span> &lt;span style="color:#f85149">여러&lt;/span> &lt;span style="color:#f85149">가지&lt;/span> &lt;span style="color:#f85149">이점을&lt;/span> &lt;span style="color:#f85149">얻을&lt;/span> &lt;span style="color:#f85149">수&lt;/span> &lt;span style="color:#f85149">있습니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">### 1. **중앙 집중식 로깅**&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire는 &lt;span style="color:#f85149">전체&lt;/span> &lt;span style="color:#f85149">애플리케이션에&lt;/span> &lt;span style="color:#f85149">걸쳐&lt;/span> &lt;span style="color:#f85149">로깅을&lt;/span> &lt;span style="color:#f85149">자동으로&lt;/span> &lt;span style="color:#f85149">통합합니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">즉&lt;/span>, &lt;span style="color:#f85149">각&lt;/span> &lt;span style="color:#f85149">서비스에&lt;/span> &lt;span style="color:#f85149">대한&lt;/span> &lt;span style="color:#f85149">로깅을&lt;/span> &lt;span style="color:#f85149">수동으로&lt;/span> &lt;span style="color:#f85149">구성할&lt;/span> &lt;span style="color:#f85149">필요가&lt;/span> &lt;span style="color:#f85149">없습니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">이렇게&lt;/span> &lt;span style="color:#f85149">하면&lt;/span> &lt;span style="color:#f85149">로그가&lt;/span> &lt;span style="color:#f85149">일관되고&lt;/span> &lt;span style="color:#f85149">중앙&lt;/span> &lt;span style="color:#f85149">위치에&lt;/span> &lt;span style="color:#f85149">저장되므로&lt;/span> &lt;span style="color:#f85149">디버깅&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">모니터링이&lt;/span> &lt;span style="color:#f85149">훨씬&lt;/span> &lt;span style="color:#f85149">쉬워집니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">예를&lt;/span> &lt;span style="color:#f85149">들어&lt;/span> &lt;span style="color:#f85149">`&lt;/span>AiApiClient&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">클래스는&lt;/span> &lt;span style="color:#f85149">로깅을&lt;/span> &lt;span style="color:#f85149">사용하여&lt;/span> AI &lt;span style="color:#f85149">서비스로&lt;/span> &lt;span style="color:#f85149">전송된&lt;/span> &lt;span style="color:#f85149">이미지&lt;/span> &lt;span style="color:#f85149">바이트&lt;/span>, API &lt;span style="color:#f85149">응답&lt;/span> &lt;span style="color:#f85149">및&lt;/span> &lt;span style="color:#f85149">분석&lt;/span> &lt;span style="color:#f85149">프로세스&lt;/span> &lt;span style="color:#f85149">중에&lt;/span> &lt;span style="color:#f85149">발생하는&lt;/span> &lt;span style="color:#f85149">모든&lt;/span> &lt;span style="color:#f85149">오류를&lt;/span> &lt;span style="color:#f85149">기록합니다&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>_logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Sending analyze request with image bytes of length: {Length}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> imageBytes&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Length);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p 정렬="중앙">
&lt;img src="https://imgur.com/5NS416X.png">
&lt;/p>
&lt;h3 id="2-자동-측정항목-수집">2. &lt;strong>자동 측정항목 수집&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire는 또한 응답 시간, 요청 수, 오류율과 같은 중요한 애플리케이션 지표를 자동으로 추적하고 보고합니다. 이를 통해 애플리케이션의 성능을 이해하고 병목 현상이나 문제를 신속하게 감지할 수 있습니다.&lt;/p>
&lt;p 정렬="중앙">
&lt;img src="https://imgur.com/SGawOY3.png">
&lt;/p>
&lt;h3 id="3-향상된-성능">3. &lt;strong>향상된 성능&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire는 HTTP 호출을 최적화하여 응답 시간을 낮게 유지하고 불필요한 리소스 소비를 줄이는 데 도움이 됩니다. 연결 풀링, 요청 재시도 및 지능형 라우팅과 같은 기능을 제공합니다.&lt;/p>
&lt;h3 id="4-완벽한-통합">4. &lt;strong>완벽한 통합&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire는 다양한 서비스(예: 이 프로젝트의 AI 및 API 서비스)의 통합을 단순화하고 배포 프로세스를 간소화합니다. Aspire가 인프라 관련 작업을 자동으로 처리하므로 낮은 수준 구성에 대해 걱정할 필요가 없습니다.&lt;/p>
&lt;p 정렬="중앙">
&lt;img src="https://imgur.com/OSHhWVb.png">
&lt;/p>
&lt;h3 id="결론ai는-더-이상-공상과학-영화에서나-볼-수-있는-유행어가-아닙니다-영수증에서-구조화된-데이터를-추출하여-이-프로젝트에서-다룬-문제와-같이-오늘날-실제-문제를-적극적으로-해결하고-있습니다-azure-openai-net-aspire-및-blazor의-도움으로-시간이-많이-걸리고-오류가-발생하기-쉬운-수동-작업을-자동화할-수-있습니다-ai는-단순히-chatgpt와-같은-메시지에-채팅하거나-응답하는-것이-아닙니다-이미지를-해석하고-귀중한-정보를-추출하고-몇-초-만에-실행-가능한-통찰력을-제공합니다">결론AI는 더 이상 공상과학 영화에서나 볼 수 있는 유행어가 아닙니다. 영수증에서 구조화된 데이터를 추출하여 이 프로젝트에서 다룬 문제와 같이 오늘날 실제 문제를 적극적으로 해결하고 있습니다. &lt;strong>Azure OpenAI&lt;/strong>, &lt;strong>.NET Aspire&lt;/strong> 및 &lt;strong>Blazor&lt;/strong>의 도움으로 시간이 많이 걸리고 오류가 발생하기 쉬운 수동 작업을 자동화할 수 있습니다. AI는 단순히 ChatGPT와 같은 메시지에 채팅하거나 응답하는 것이 아닙니다. 이미지를 해석하고, 귀중한 정보를 추출하고, 몇 초 만에 실행 가능한 통찰력을 제공합니다.&lt;/h3>
&lt;p>영수증 분석에 &lt;strong>Azure OpenAI&lt;/strong>를 사용하고 로깅 및 메트릭과의 원활한 통합을 위해 &lt;strong>.NET Aspire&lt;/strong>를 사용하여 강력하고 확장 가능한 솔루션을 만들었습니다. 비즈니스 프로세스를 간소화하고, 지루한 작업을 자동화하고, 정확성을 향상시키는 AI의 잠재력은 엄청나며, 이는 AI가 어떻게 적용될 수 있는지 보여주는 한 가지 예일 뿐입니다.&lt;/p>
&lt;p>이 게시물은 실제 AI 애플리케이션을 선보이고 스페인어를 사용하는 기술 커뮤니티에 최신 트렌드를 교육하는 이벤트인 &lt;strong>Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/strong>의 일부입니다. AI와 그 가능성에 대해 더 깊이 알아보고 싶다면 이 이벤트를 시작하는 것이 좋습니다.&lt;/p>
&lt;p>AI는 우리가 일하는 방식을 변화시키고 있으며, 이 프로젝트는 무엇이 가능한지 보여줍니다. AI의 진정한 힘은 영수증 처리, 이미지 분석, 추세 예측 등 실제 문제를 해결하는 능력에 있습니다. 우리는 단지 표면만 긁고 있을 뿐입니다.&lt;/p>
&lt;h3 id="소스-코드">소스 코드&lt;/h3>
&lt;p>이 프로젝트의 전체 소스 코드는 &lt;a href="https://github.com/emimontesdeoca/ReceiptScannerPoc">GitHub&lt;/a>에서 확인할 수 있습니다. 자유롭게 다운로드하여 AI와 API 서비스가 어떻게 함께 작동하는지 살펴보고 자신의 사용 사례에 맞게 조정하세요. 문제가 발생하거나 개선을 위한 아이디어가 있는 경우 주저하지 말고 문제를 생성하거나 끌어오기 요청을 제출하세요. 기여는 언제나 환영하며 귀하의 피드백은 이 프로젝트를 더욱 개선하는 데 도움이 됩니다!&lt;/p>
&lt;p>즐거운 코딩하세요!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>Azure</category><category>NuGet</category><category>Docker</category><category>AI</category></item><item><title>컨테이너를 사용하여... 크리스마스 선물 가격을 추적하시나요?</title><link>https://emimontesdeoca.github.io/ko/posts/monitoring-prices-containers/</link><pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/monitoring-prices-containers/</guid><description>Docker 컨테이너, Blazor 및 .NET API 백엔드를 사용하여 크리스마스 선물 가격 모니터링을 자동화합니다.</description><content:encoded>&lt;p>크리스마스가 코앞으로 다가왔습니다. 사랑하는 사람을 위한 완벽한 선물을 찾는 즐거운 일이 다가오고 있습니다. 저와 같은 사람이라면 좋은 상품을 구매하는 것을 좋아할 것입니다. 하지만 연휴 동안 치솟는 인기 품목 가격을 탐색하는 것은 실제 썰매를 타는 것처럼 느껴질 수 있습니다. 흥미롭지만 약간 부담스럽습니다! 올해는 매일 가격을 확인하느라 정신없게 지내는 대신 기술 능력을 잘 활용하고 프로세스를 자동화하기로 결정했습니다.&lt;/p>
&lt;p 정렬="중앙">
&lt;img src="https://imgur.com/7K7QC08.png" />
&lt;/p>
&lt;p>소프트웨어 개발자이자 기술을 활용하여 일상적인 문제를 해결하는 데 열정을 갖고 있는 사람으로서 저는 가격 확인 프로세스를 자동화하는 방법을 찾았습니다. 매일 수동으로 여러 온라인 매장을 방문하여 가격을 비교하는 대신, 이를 대신해 줄 수 있는 시스템을 만들기로 결정했습니다. 이를 통해 시간을 절약할 수 있을 뿐만 아니라 매일 확인하는 스트레스 없이 최고의 거래를 얻을 수 있습니다.&lt;/p>
&lt;h2 id="가격-모니터링을-자동화하는-이유는-무엇입니까">가격 모니터링을 자동화하는 이유는 무엇입니까?&lt;/h2>
&lt;p>가격 모니터링을 자동화하는 것은 손가락 하나 까딱하지 않고도 최고의 거래를 얻을 수 있도록 24시간 내내 일하는 산타의 요정 팀을 갖는 것과 같습니다. 당신이 그것을 좋아할 이유는 다음과 같습니다:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>비싼 품목&lt;/strong>: 우리 모두는 크리스마스 선물이 상당히 비쌀 수 있다는 것을 알고 있습니다. 특히 장치와 장난감이 목록에 있을 때 더욱 그렇습니다. 이것에 대한 비용을 절약하는 것은 재고에서 마지막 나무 꼭대기를 찾는 것만큼 만족스러울 수 있습니다.&lt;/li>
&lt;li>&lt;strong>일일 확인&lt;/strong>: 매일 가격을 확인할 시간(또는 인내심)이 있는 사람은 누구입니까? 나 말고!&lt;/li>
&lt;li>&lt;strong>자동화&lt;/strong>: 나눔이라는 명절 정신을 받아들이고 자신에게 효율성을 선물하세요.&lt;/li>
&lt;li>&lt;strong>정확성&lt;/strong>: 자동화는 루돌프의 코처럼 정확한 가격 확인을 보장합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="핵심-기술">핵심 기술&lt;/h2>
&lt;p>나만의 산타 도우미를 만들기 위해 저는 몇 가지 핵심 기술을 사용하기로 결정했습니다.&lt;/p>
&lt;h3 id="컨테이너">컨테이너&lt;/h3>
&lt;p>컨테이너는 소프트웨어의 크리스마스 포장과 같습니다. 애플리케이션을 모든 기능(종속성)과 함께 묶어 신선한 눈 속에서 썰매를 타는 것처럼 모든 것이 원활하게 실행되도록 합니다. Docker는 이러한 컨테이너를 만드는 데 사용되는 도구입니다.&lt;/p>
&lt;h3 id="블레이저">블레이저&lt;/h3>
&lt;p>Blazor는 .NET을 사용하여 대화형 웹 애플리케이션을 구축하기 위한 멋진 프레임워크입니다. 일반적인 크리스마스 캐롤을 맞춤화되고 효율적이며 재미있는 나만의 휴일 재생목록으로 바꾸는 것과 같습니다.&lt;/p>
&lt;h3 id="docker-작성">Docker-작성&lt;/h3>
&lt;p>Docker-Compose는 North Pole 운영의 관리자입니다. 이는 API 및 Blazor 프런트 엔드와 같은 모든 서비스가 완벽한 조화를 이루며 함께 실행되는 데 도움이 됩니다. 그를 우리 명절 교향곡의 지휘자로 생각해보세요.&lt;/p>
&lt;h2 id="단계별-가이드">단계별 가이드&lt;/h2>
&lt;p>이제 산타의 작업장에 들어가 이 프로젝트에 생기를 불어넣어 보세요!&lt;/p>
&lt;h3 id="1단계-api-생성">1단계: API 생성&lt;/h3>
&lt;h4 id="11-프로젝트-설정">1.1 프로젝트 설정&lt;/h4>
&lt;p>코딩 산타 모자를 쓰고 새로운 ASP.NET Core Web API 프로젝트를 설정하세요. 터미널을 열고(강림절 달력을 여는 것과 약간 비슷함) 다음을 실행합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new webapi -o PriceMonitorApi
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd PriceMonitorApi
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 명령은 &lt;code>PriceMonitorApi&lt;/code>라는 새 디렉터리를 생성하고 기본 웹 API 프로젝트를 설정합니다. 진저브레드 하우스를 위한 견고한 기반을 만드는 것과 같다고 상상해 보세요.&lt;/p>
&lt;h4 id="12-httpclient-추가-및-라이브러리-스크랩다음으로-httpclient-및-라이브러리를-추가하여-html을-구문-분석합니다-이는-가격-데이터를-가져오고-읽는-데-있어-신뢰할-수-있는-썰매가-될-것입니다">1.2 HttpClient 추가 및 라이브러리 스크랩다음으로 &lt;code>HttpClient&lt;/code> 및 라이브러리를 추가하여 HTML을 구문 분석합니다. 이는 가격 데이터를 가져오고 읽는 데 있어 신뢰할 수 있는 썰매가 될 것입니다.&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package HtmlAgilityPack
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>HtmlAgilityPack은 HTML 문서를 구문 분석하는 데 도움이 되는 엘프입니다.&lt;/p>
&lt;h4 id="13-모델-생성">1.3 모델 생성&lt;/h4>
&lt;p>모델은 장난감의 청사진과 같습니다. 제품 정보를 나타내는 모델을 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ProductInfo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>우리는 데이터에 딱 맞는 템플릿을 만들었습니다.&lt;/p>
&lt;h4 id="14-스크레이퍼-서비스-구현">1.4 스크레이퍼 서비스 구현&lt;/h4>
&lt;p>우리의 스크레이퍼 서비스는 산타의 작은 도우미와 같습니다. 우리를 위해 정보를 가져오고 처리합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HtmlAgilityPack&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Globalization&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; ScrapeProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetStringAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> htmlDoc = &lt;span style="color:#ff7b72">new&lt;/span> HtmlDocument();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> htmlDoc.LoadHtml(response);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> title = htmlDoc.DocumentNode.SelectSingleNode(&lt;span style="color:#a5d6ff">&amp;#34;//h1[@class=&amp;#39;product-title&amp;#39;]&amp;#34;&lt;/span>).InnerText.Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceString = htmlDoc.DocumentNode.SelectSingleNode(&lt;span style="color:#a5d6ff">&amp;#34;//span[@class=&amp;#39;product-price&amp;#39;]&amp;#34;&lt;/span>).InnerText.Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">decimal&lt;/span>.TryParse(priceString, NumberStyles.Currency, CultureInfo.InvariantCulture, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> price))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ProductInfo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title = title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Price = price,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = DateTime.UtcNow
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Unable to parse price&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 스니펫은 &lt;code>HtmlAgilityPack&lt;/code>를 휴일 마술 지팡이로 바꿔줍니다.&lt;/p>
&lt;h4 id="15-api-컨트롤러-생성">1.5 API 컨트롤러 생성&lt;/h4>
&lt;p>데이터의 게이트키퍼 역할을 하는 컨트롤러를 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Services&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Controllers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ApiController]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;api/[controller]&lt;span style="color:#a5d6ff">&amp;#34;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperController&lt;/span> : ControllerBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ScraperService _scraperService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperController(ScraperService scraperService)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scraperService = scraperService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet(&amp;#34;productinfo&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ActionResult&amp;lt;ProductInfo&amp;gt;&amp;gt; GetProductInfo(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo = &lt;span style="color:#ff7b72">await&lt;/span> _scraperService.ScrapeProductInfoAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Ok(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Message = ex.Message });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>우리 컨트롤러는 굴뚝 아래로 산타보다 데이터가 더 빨리 전달되도록 보장합니다.&lt;/p>
&lt;h4 id="16-di-컨테이너에-서비스-등록">1.6 DI 컨테이너에 서비스 등록&lt;/h4>
&lt;p>마지막으로 &lt;code>ScraperService&lt;/code>를 등록하여 필요할 때 사용할 수 있는지 확인하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>services.AddHttpClient&amp;lt;ScraperService&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 크리스마스 이브의 산타 썰매처럼 API가 준비되었습니다!&lt;/p>
&lt;hr>
&lt;h3 id="2단계-blazor-애플리케이션-만들기">2단계: Blazor 애플리케이션 만들기&lt;/h3>
&lt;p>Blazor를 사용하면 프로젝트를 크리스마스 트리처럼 장식하여 시각적으로 매력적이고 대화형으로 만들 수 있습니다.&lt;/p>
&lt;h4 id="21-blazor-프로젝트-설정">2.1 Blazor 프로젝트 설정&lt;/h4>
&lt;p>다음으로 가격 모니터링 썰매 타기를 위한 인터페이스 역할을 하는 Blazor 프로젝트를 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new blazor -o PriceMonitorBlazor
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd PriceMonitorBlazor
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 명령은 기본 Blazor WebAssembly 프로젝트를 설정하기 위해 몇 가지 휴일 마법을 뿌립니다.&lt;/p>
&lt;h4 id="22-모델-추가">2.2 모델 추가&lt;/h4>
&lt;p>장식품 설정과 마찬가지로 Blazor 프로젝트에 &lt;code>ProductInfo&lt;/code> 모델을 추가합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ProductInfo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="23-api-호출을-위한-서비스-생성">2.3 API 호출을 위한 서비스 생성&lt;/h4>
&lt;p>API에서 데이터를 가져오는 서비스를 만듭니다. 온라인 쇼핑 친구라고 생각하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Threading.Tasks&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; GetProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetFromJsonAsync&amp;lt;ProductInfo&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;https://localhost:5001/api/scraper/productinfo?url={url}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="24-di-컨테이너에-서비스-등록">2.4 DI 컨테이너에 서비스 등록&lt;/h4>
&lt;p>구성요소에 주입할 수 있도록 &lt;code>ScraperService&lt;/code>가 등록되어 있는지 확인하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;ScraperService&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="25-blazor-구성-요소-만들기">2.5 Blazor 구성 요소 만들기&lt;/h3>
&lt;p>재미있고 대화형 인터페이스를 포함하도록 &lt;code>Pages/Index.razor&lt;/code>를 업데이트하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ScraperService ScraperService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price Monitor&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>label &lt;span style="color:#ff7b72">for&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product URL:&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>label&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;productUrl&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;FetchProductInfo&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Fetch Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> productInfos)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string productUrl;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> productInfos &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task FetchProductInfo()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>IsNullOrEmpty(productUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetProductInfoAsync(productUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 축제의 휴일 디스플레이를 설정하여 앱을 대화형이고 즐겁게 만드는 것과 같습니다.&lt;/p>
&lt;hr>
&lt;h3 id="3단계-docker-compose를-사용하여-모든-항목-연결">3단계: Docker-Compose를 사용하여 모든 항목 연결&lt;/h3>
&lt;p>이제 Docker-Compose를 사용하여 모든 것을 연결하여 프로젝트를 기름진 썰매 타기로 바꿔 보겠습니다.&lt;/p>
&lt;h4 id="31-dockerfile-만들기">3.1 Dockerfile 만들기&lt;/h4>
&lt;p>API 및 Blazor 프로젝트 모두에 대한 Dockerfile을 만듭니다.&lt;/p>
&lt;p>&lt;strong>PriceMonitorApi/Dockerfile&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/aspnet:6.0 AS base&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 80&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 443&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/sdk:6.0 AS build&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /src&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/PriceMonitorApi.csproj&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/PriceMonitorApi.csproj&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> . .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> &amp;#34;/src/PriceMonitorApi&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet build &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.csproj&amp;#34;&lt;/span> -c Release -o /app/build&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> build AS publish&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet publish &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.csproj&amp;#34;&lt;/span> -c Release -o /app/publish&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> base AS final&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> --from&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>publish /app/publish .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">ENTRYPOINT&lt;/span> [&lt;span style="color:#a5d6ff">&amp;#34;dotnet&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.dll&amp;#34;&lt;/span>]&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>PriceMonitorBlazor/Dockerfile&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/aspnet:6.0 AS base&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 80&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 443&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/sdk:6.0 AS build&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /src&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/PriceMonitorBlazor.csproj&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/PriceMonitorBlazor.csproj&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> . .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> &amp;#34;/src/PriceMonitorBlazor&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet build &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.csproj&amp;#34;&lt;/span> -c Release -o /app/build&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> build AS publish&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet publish &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.csproj&amp;#34;&lt;/span> -c Release -o /app/publish&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> base AS final&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> --from&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>publish /app/publish .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">ENTRYPOINT&lt;/span> [&lt;span style="color:#a5d6ff">&amp;#34;dotnet&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.dll&amp;#34;&lt;/span>]&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="32-docker-composeyml-만들기">3.2 docker-compose.yml 만들기&lt;/h4>
&lt;p>단일 &lt;code>docker-compose.yml&lt;/code> 파일로 모든 점(크리스마스 조명)을 연결합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">version&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;3.4&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">services&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">api&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">image&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">pricemonitorapi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">context&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">./PriceMonitorApi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dockerfile&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Dockerfile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">ports&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">&amp;#34;5000:80&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">price-monitor-network&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">blazor&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">image&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">pricemonitorblazor&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">context&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">./PriceMonitorBlazor&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dockerfile&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Dockerfile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">ports&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">&amp;#34;5001:80&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">depends_on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">api&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">price-monitor-network&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">price-monitor-network&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">driver&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bridge&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="다이어그램">다이어그램&lt;/h3>
&lt;p>다음은 다양한 구성 요소와 데이터 흐름을 시각화하는 데 도움이 되는 다이어그램입니다. 이는 산타 세계에서 실제로 무슨 일이 일어나고 있는지 이해하는 데 도움이 됩니다.&lt;/p>
&lt;h4 id="시스템-아키텍처-다이어그램">시스템 아키텍처 다이어그램&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/nE0hSJ4.png" alt="Imgur">&lt;/p>
&lt;h4 id="데이터-흐름-다이어그램">데이터 흐름 다이어그램&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/hJXv4Jw.png" alt="이미지">&lt;/p>
&lt;h4 id="시퀀스-다이어그램">시퀀스 다이어그램&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/FH6VzuH.png" alt="이미지">&lt;/p>
&lt;h4 id="docker-설정을-위한-구성-요소-다이어그램">Docker 설정을 위한 구성 요소 다이어그램&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/efQ9WuT.png" alt="이미지">&lt;/p>
&lt;h3 id="4단계-url-관리-새로-고침-및-자동-주기적-새로-고침이-데모에서는-이를-메모리에-저장하지만-실제-애플리케이션에서는-데이터베이스를-사용합니다-이-재미있는-프로젝트에서는-메모리-내-저장소를-고수하겠습니다">4단계: URL 관리, 새로 고침 및 자동 주기적 새로 고침이 데모에서는 이를 메모리에 저장하지만 실제 애플리케이션에서는 데이터베이스를 사용합니다. 이 재미있는 프로젝트에서는 메모리 내 저장소를 고수하겠습니다.&lt;/h3>
&lt;h4 id="41-url-관리를-위한-서비스-수정">4.1 URL 관리를 위한 서비스 수정&lt;/h4>
&lt;p>먼저, 우리 서비스가 URL 추가 및 검색은 물론 제품 정보 가져오기도 처리할 수 있는지 확인하겠습니다.&lt;/p>
&lt;p>&lt;code>Services/ScraperService.cs&lt;/code> 업데이트:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Threading.Tasks&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; urls = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; GetProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetFromJsonAsync&amp;lt;ProductInfo&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;http://localhost:5000/api/scraper/productinfo?url={url}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> AddUrl(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!urls.Contains(url))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urls.Add(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetUrls()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> urls;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ClearUrls()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urls.Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="42-url-관리-및-새로-고침을-위한-blazor-구성-요소-업데이트">4.2 URL 관리 및 새로 고침을 위한 Blazor 구성 요소 업데이트&lt;/h4>
&lt;p>다음으로 &lt;code>Pages/Index.razor&lt;/code>를 업데이트하여 URL 관리, 새로 고침 및 자동 주기적 새로 고침을 추가합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ScraperService ScraperService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price Monitor&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>label &lt;span style="color:#ff7b72">for&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product URL:&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>label&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;productUrl&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;AddUrl&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Add URL&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;RefreshPrices&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Refresh All Prices&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> productInfos)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string productUrl;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> productInfos &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">Timer&lt;/span> _timer;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override void OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StartTimer();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void StartTimer()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> Set up the timer to call RefreshPrices every minute (&lt;span style="color:#a5d6ff">60000&lt;/span> ms)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _timer &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new &lt;span style="color:#f0883e;font-weight:bold">Timer&lt;/span>(async _ &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> await InvokeAsync(RefreshPrices), null, TimeSpan&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Zero, TimeSpan&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>FromMinutes(&lt;span style="color:#a5d6ff">1&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void AddUrl()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>IsNullOrEmpty(productUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AddUrl(productUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productUrl &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task RefreshPrices()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> urls &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetUrls();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> url &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> urls)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetProductInfoAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 업데이트된 구성 요소에서는 다음을 수행합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>Timer&lt;/code>를 사용하여 1분마다 &lt;code>RefreshPrices&lt;/code> 메서드를 주기적으로 호출합니다.&lt;/li>
&lt;li>&lt;code>StartTimer&lt;/code> 메소드는 타이머를 초기화하여 즉시 시작하고 60초마다 트리거합니다.&lt;/li>
&lt;li>&lt;code>OnInitialized&lt;/code> 수명 주기 메서드는 구성 요소가 초기화되어 주기적 새로 고침을 시작할 때 &lt;code>StartTimer&lt;/code>를 호출합니다.&lt;/li>
&lt;/ul>
&lt;h4 id="43-솔루션-실행">4.3 솔루션 실행&lt;/h4>
&lt;p>새로운 기능으로 업데이트된 Blazor 애플리케이션을 실행하려면 Docker 컨테이너를 다시 빌드하고 다시 시작하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker-compose build
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>브라우저에 &lt;code>http://localhost:5001&lt;/code>를 로드한 후 Blazor 앱은 수동 새로 고침 및 URL 관리를 허용하는 것 외에도 매분 자동으로 제품 가격을 새로 고쳐야 합니다.&lt;/p>
&lt;h2 id="결론">결론&lt;/h2>
&lt;p>이 가격 모니터링 시스템을 구축하는 것은 정말 즐거운 일이었습니다! 매일 가격을 확인하는 스트레스에서 벗어날 수 있었을 뿐만 아니라 현대 웹 기술의 마법도 보여주었습니다.&lt;/p>
&lt;h1 id="축제-기술-달력-2024">축제 기술 달력 2024&lt;/h1>
&lt;p 정렬="중앙">
&lt;img src="https://festivetechcalendar.com/assets/images/Heading.png" />
&lt;/p>
&lt;p>저는 기술 애호가, 혁신가, 디지털 몽상가들이 함께 모여 지식을 공유하고 축제 정신과 기술의 경이로움의 융합을 축하하는 &lt;strong>Festive Tech Calendar 2024&lt;/strong> 이벤트의 일환으로 이 게시물을 작성했습니다. 이 이니셔티브는 학습과 연결뿐만 아니라 환원에 관한 것입니다.&lt;/p>
&lt;p>&lt;strong>Festive Tech Calendar 2024&lt;/strong>는 올해 Beatson Cancer Charity를 지원합니다. Beatson Cancer Charity는 암으로 고통받는 사람들과 그 가족, 그들을 돌보는 의료 전문가를 지원하는 데 최선을 다하고 있습니다. 그들의 놀라운 작업에 대한 자세한 내용은 &lt;a href="https://www.beatsoncancercharity.org/">https://www.beatsoncancercharity.org/&lt;/a>에서 확인할 수 있습니다.&lt;/p>
&lt;p>자주 묻는 질문과 이벤트에 대한 자세한 내용은 Festive Tech Calendar 웹사이트 &lt;a href="https://festivetechcalendar.com">https://festivetechcalendar.com&lt;/a>에서 확인하세요.&lt;/p>
&lt;h1 id="호호호">&lt;strong>호호호!&lt;/strong>&lt;/h1>
&lt;p>이 프로젝트를 만드는 것은 축제 기술 커뮤니티에 기여하고 동시에 훌륭한 목적을 지원할 수 있는 훌륭한 방법이었습니다.&lt;/p>
&lt;p>이 가이드가 도움이 되기를 바라며 기술을 사용하여 일상적인 작업을 단순화하는 더 많은 방법을 탐색하는 데 영감을 주기를 바랍니다.&lt;/p>
&lt;p>궁금한 점이 있거나 추가 지원이 필요한 경우 언제든지 문의해 주세요.&lt;/p>
&lt;p>즐거운 코딩하세요!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>Docker</category><category>API</category></item><item><title>사용자 지정 ValidationAttribute 및 Blazor 유효성 검사</title><link>https://emimontesdeoca.github.io/ko/posts/custom-validationattribute-blazor/</link><pubDate>Fri, 29 Mar 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/custom-validationattribute-blazor/</guid><description>데이터 주석을 사용하여 Blazor 양식 유효성 검사를 위해 재사용 가능한 사용자 지정 ValidationAttribute 클래스를 만듭니다.</description><content:encoded>&lt;p>﻿# 모든 것을 맞춤설정하세요&lt;/p>
&lt;p>아마도 내 모든 게시물에서 보셨겠지만 저는 이미 사용자 정의 속성, 사용자 정의 예외 처리, 서비스 컬렉션 주입 등에 관한 게시물을 작성했기 때문에 모든 것을 가능한 한 깔끔하게 유지하려고 노력합니다.&lt;/p>
&lt;p>나는 이런 종류의 코딩 방식이 나와 내 팀에게 초과 근무 시간을 개선하고, 문제를 더 쉽게 찾고, 코드를 최대한 분리할 수 있는 방법을 제공한다는 것을 시간이 지나면서 깨달았습니다.&lt;/p>
&lt;p>네, 이 멋진 이야기를 마치고 지금 여러분을 모시고 있습니다. 저는 최근 평소처럼 Blazor를 작업해 왔으며 수년 간의 개발 끝에 사용자 지정 유효성 검사 속성을 만들 수 있다는 것을 발견했습니다.&lt;/p>
&lt;p>응, 웃기네, 몇년이 지나서야&amp;hellip;&lt;/p>
&lt;h1 id="사용자-정의-유효성-검사-속성">사용자 정의 유효성 검사 속성&lt;/h1>
&lt;p>아이디어는 실제로 작업에서 나왔습니다. 우리는 항상 모든 곳에서 유효성 검사를 수행하지만 동일한 유효성 검사 프로세스가 필요한 일부 필드가 있었기 때문에 거기에 뭔가가 있을 수 있다고 생각했습니다&amp;hellip;. 사용자 정의 유효성 검사 속성과 같은 것입니다!&lt;/p>
&lt;p>그래서 이에 대한 Microsoft 문서를 실행한 후 다음과 같이 사용자 지정 유효성 검사 특성을 만들고 속성에 할당할 수 있다는 것을 알게 되었습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">StringLengthRangeAttribute&lt;/span> : ValidationAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Minimum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Maximum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> StringLengthRangeAttribute()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.Minimum = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.Maximum = &lt;span style="color:#ff7b72">int&lt;/span>.MaxValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsValid(&lt;span style="color:#ff7b72">object&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> strValue = &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">as&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(strValue))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> len = strValue.Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> len &amp;gt;= &lt;span style="color:#ff7b72">this&lt;/span>.Minimum &amp;amp;&amp;amp; len &amp;lt;= &lt;span style="color:#ff7b72">this&lt;/span>.Maximum;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>다음과 같은 간단한 클래스에서 사용하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Required]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[StringLengthRange(Minimum = 10, ErrorMessage = &amp;#34;Must be &amp;gt;10 characters.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[StringLengthRange(Maximum = 20)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Required]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[StringLengthRange(Minimum = 10, Maximum = 20)]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="사용자-정의-유효성-검사기">사용자 정의 유효성 검사기&lt;/h1>
&lt;p>그래서 저는 9개의 숫자와 하이픈으로 끝나는 20개의 첫 번째 문자를 계산하고 일반적으로 국가 코드가 되는 2개의 문자로 끝나는 특정 비즈니스 사례에 필요한 유효성 검사기를 가지고 있습니다. &lt;strong>123456789-123456789-ES&lt;/strong>&lt;/p>
&lt;p>나는 결국 다음과 같은 결과를 얻었습니다. 정말 간단하지만 작동합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel.DataAnnotations&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text.RegularExpressions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">SpecialStringValidatorAttribute&lt;/span> : ValidationAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">const&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> TotalLength = &lt;span style="color:#a5d6ff">22&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">const&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Pattern = &lt;span style="color:#a5d6ff">@&amp;#34;^(\d{10})-(\d{10})-([A-Za-z]{2})$&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> ValidationResult IsValid(&lt;span style="color:#ff7b72">object&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>, ValidationContext validationContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> strValue = &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">as&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(strValue))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (strValue.Length != TotalLength)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationResult(&lt;span style="color:#a5d6ff">$&amp;#34;The string must be {TotalLength} characters long.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Regex.IsMatch(strValue, Pattern))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationResult(&lt;span style="color:#a5d6ff">&amp;#34;The string must follow the pattern: 1234567890-1234567890-AB&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ValidationResult.Success;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationResult(&lt;span style="color:#a5d6ff">&amp;#34;The string cannot be null or empty.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="테스트">테스트&lt;/h2>
&lt;p>만일의 경우를 대비해 나는 그들을 위해 몇 가지 테스트도 작성했습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">SpecialStringValidatorTests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Theory]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-AB&amp;#34;, true)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-XY&amp;#34;, true)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-A1&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-A&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-123456789-AB&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-ABCDE&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> SpecialStringValidatorTest(&lt;span style="color:#ff7b72">string&lt;/span> input, &lt;span style="color:#ff7b72">bool&lt;/span> expectedResult)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Arrange&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> validator = &lt;span style="color:#ff7b72">new&lt;/span> SpecialStringValidatorAttribute();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Act&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = validator.IsValid(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.Equal(expectedResult, result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 실행해보니 다음과 같은 결과가 나왔습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>Microsoft &lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>R&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span> Test Execution Command Line Tool Version 16.9.1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Copyright &lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>c&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span> Microsoft Corporation. All rights reserved.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Starting test execution, please wait...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A total of &lt;span style="color:#a5d6ff">1&lt;/span> test files matched the specified pattern.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Test run in progress...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-AB&amp;#34;&lt;/span>, expectedResult: True&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-XY&amp;#34;&lt;/span>, expectedResult: True&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-A1&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-A&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-123456789-AB&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-ABCDE&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Test Run Successful.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Total tests: &lt;span style="color:#a5d6ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Passed: &lt;span style="color:#a5d6ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Total time: 1.7296 Seconds
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="사용자-정의-유효성-검사-및-blazor">사용자 정의 유효성 검사 및 Blazor&lt;/h1>
&lt;p>이제 그의 것을 사용할 수 있다는 것을 알았으니 Blazor에 구현하는 것이 확실히 좋은 생각이겠죠?&lt;/p>
&lt;p>이전에 보여드린 &lt;code>Person&lt;/code> 모델을 사용하는 다음 양식이 있다고 가정해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@using Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Home&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Hello, world!&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=@Person FormName=&lt;span style="color:#a5d6ff">&amp;#34;PersonForm&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;DataAnnotationsValidator/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationSummary/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-group&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Name&amp;#34;&lt;/span>&amp;gt;Name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText @bind-Value=Person.Name class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;Name&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&lt;span style="color:#a5d6ff">&amp;#34;() =&amp;gt; Person.Name&amp;#34;&lt;/span>/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-group&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;MySpecialString&amp;#34;&lt;/span>&amp;gt;My special &lt;span style="color:#ff7b72">string&lt;/span>&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText @bind-Value=Person.MySpecialString class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;Name&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&lt;span style="color:#a5d6ff">&amp;#34;() =&amp;gt; Person.MySpecialString&amp;#34;&lt;/span>/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-group&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Age&amp;#34;&lt;/span>&amp;gt;Age&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputNumber @bind-Value=Person.Age class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;Age&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&lt;span style="color:#f85149">@&lt;/span>(() =&amp;gt; Person.Age) /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Save&amp;#34;&lt;/span>/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Person Person = &lt;span style="color:#ff7b72">new&lt;/span> Person();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것을 실행하자마자 다음과 같은 오류가 발생합니다.&lt;/p>
&lt;img src="https://i.imgur.com/psfpzdr.png">
&lt;p>그리고 우리가 원하는 것을 넣으면 다음과 같은 결과가 나옵니다.&lt;/p>
&lt;img src="https://i.imgur.com/RPoUq02.png">
&lt;p>솔직히 말해서 최소한 해당 양식의 유효성을 검사하는 논리를 사용자 정의 유효성 검사 속성으로 옮겨야 한다는 점은 매우 분명합니다. 그러면 해당 로그인에 대한 코드를 한 곳에 자유롭게 저장할 수 있으며 나중에 API나 다른 애플리케이션에 사용할 수 있습니다.&lt;/p>
&lt;p>재미있게 보셨기를 바라며, 궁금하신 점이나 연락하고 싶으신 점이 있으시면 주저하지 마시고 연락주세요!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>.NET API의 사용자 정의 예외 처리</title><link>https://emimontesdeoca.github.io/ko/posts/custom-exception-handler-api/</link><pubDate>Sun, 01 Oct 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/custom-exception-handler-api/</guid><description>.NET API에서 깨끗한 오류 응답을 반환하는 사용자 지정 예외 처리 미들웨어를 구축합니다.</description><content:encoded>&lt;p>﻿예외는 좋지 않습니다. 우리도 알고 있죠? 하지만 우리가 그것들을 처리해야 한다면 어떨까요?&lt;/p>
&lt;p>예를 들어 API에서 예외가 발생하면 사용자가 받는 응답에서 제거하고 싶은 많은 정보가 포함된 스택 메시지가 표시됩니다.&lt;/p>
&lt;p>데모를 위해 dotnet API를 만들고 예외를 발생시키는 메서드를 추가했습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>예외를 던지면 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>System.Exception: This &lt;span style="color:#ff7b72">is&lt;/span> a custom exception!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at lambda_method16(Closure , Object , Object[] )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeActionMethodAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">12_1&lt;/span>(ControllerActionInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeNextActionFilterAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">10_0&lt;/span>(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State&amp;amp; next, Scope&amp;amp; scope, Object&amp;amp; state, Boolean&amp;amp; isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>--- End of stack trace &lt;span style="color:#ff7b72">from&lt;/span> previous location ---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeFilterPipelineAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">20_0&lt;/span>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Routing.EndpointMiddleware.&amp;lt;Invoke&amp;gt;g__AwaitRequestTask|&lt;span style="color:#a5d6ff">6_0&lt;/span>(Endpoint endpoint, Task requestTask, ILogger logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것은 정상으로 보입니다. 예외에서 얻은 정보이지만 저에게는 많은 정보가 표시됩니다. 라이브러리 등이 있는 경우 클라이언트, 프로젝트 또는 기타 항목에 관한 합리적인 정보를 보려고 하는 사람에게 표시될 수 있습니다.&lt;/p>
&lt;p>Swagger에서는 다음과 같이 표시됩니다.&lt;/p>
&lt;img src="https://imgur.com/fUujR3s.png"/>
&lt;h3 id="사용자-정의-예외-만들기">사용자 정의 예외 만들기&lt;/h3>
&lt;p>어떤 예외(이 경우에는 [[TOK_3]]])가 발생할지 알고 있으므로 이 단계를 피할 수 있지만, 나에게는 사용자 정의 예외를 갖는 것이 더 좋습니다. 왜냐하면 여러분이 던지는 것을 더 잘 제어할 수 있기 때문입니다.&lt;/p>
&lt;p>이 경우에는 &lt;code>Exception&lt;/code>에서 상속받은 &lt;code>CustomException&lt;/code> 객체를 생성했습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomException&lt;/span> : Exception
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;CustomException&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomException() { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;CustomException&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;string&amp;#34; name=&amp;#34;message&amp;#34;&amp;gt;Parameter for message&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomException(&lt;span style="color:#ff7b72">string&lt;/span> message) : &lt;span style="color:#ff7b72">base&lt;/span>(message) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;CustomException&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;string&amp;#34; name=&amp;#34;message&amp;#34;&amp;gt;Parameter for message&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;Exception&amp;#34; name=&amp;#34;inner&amp;#34;&amp;gt;Parameter for inner&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomException(&lt;span style="color:#ff7b72">string&lt;/span> message, Exception inner) : &lt;span style="color:#ff7b72">base&lt;/span>(message, inner) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>사용자 정의 예외를 생성한 후 &lt;code>Exception&lt;/code> 대신 &lt;code>CustomException&lt;/code>을 던지도록 메서드를 업데이트해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것은 지금은 아무것도 변경하지 않지만 스택 추적은 던져진 객체가 &lt;code>Exception&lt;/code> 대신 &lt;code>CustomException&lt;/code>임을 보여줍니다. 스택 추적의 시작 부분을 살펴보십시오.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>CustomExceptionHandleDemo.Exceptions.CustomException: This &lt;span style="color:#ff7b72">is&lt;/span> a custom exception!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at lambda_method24(Closure , Object , Object[] )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeActionMethodAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">12_1&lt;/span>(ControllerActionInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeNextActionFilterAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">10_0&lt;/span>(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State&amp;amp; next, Scope&amp;amp; scope, Object&amp;amp; state, Boolean&amp;amp; isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>--- End of stack trace &lt;span style="color:#ff7b72">from&lt;/span> previous location ---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeFilterPipelineAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">20_0&lt;/span>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Routing.EndpointMiddleware.&amp;lt;Invoke&amp;gt;g__AwaitRequestTask|&lt;span style="color:#a5d6ff">6_0&lt;/span>(Endpoint endpoint, Task requestTask, ILogger logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="exceptionfilterattribute-만들기">ExceptionFilterAttribute 만들기&lt;/h3>
&lt;p>Microsoft는 예외가 발생한 후 예외를 처리하는 방법을 제공했습니다. 자세한 내용은 &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute?view=aspnetcore-7.0">여기&lt;/a>에서 확인할 수 있습니다.&lt;/p>
&lt;p>그러나 지금까지 그들이 우리에게 제공한 문서는 다음과 같습니다:&lt;/p>
&lt;blockquote>
&lt;p>작업에서 예외가 발생한 후 비동기적으로 실행되는 추상 필터입니다. 서브클래스는 OnException(ExceptionContext) 또는 OnExceptionAsync(ExceptionContext)를 재정의해야 하지만 둘 다 재정의할 수는 없습니다.&lt;/p>&lt;/blockquote>
&lt;p>그럼 하나를 만들어 보겠습니다. &lt;code>OnException&lt;/code>를 재정의할 &lt;code>CustomExceptionFilterAttribute&lt;/code>를 생성하겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc.Filters&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.ExceptionFilterAttributes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomExceptionFilterAttribute&lt;/span> : ExceptionFilterAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// OnException&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;ExceptionContext&amp;#34; name=&amp;#34;context&amp;#34;&amp;gt;Parameter for context&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnException(ExceptionContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (context.Exception &lt;span style="color:#ff7b72">is&lt;/span> CustomException)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.HttpContext.Response.StatusCode = &lt;span style="color:#a5d6ff">500&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Result = &lt;span style="color:#ff7b72">new&lt;/span> ObjectResult(context.Exception.Message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>보시다시피, 우리는 &lt;code>ExceptionContext&lt;/code>를 살펴보고 있는데, 예외가 &lt;code>CustomException&lt;/code> 유형일 때 뭔가를 합니다.&lt;/p>
&lt;p>이것은 우리가 반환할 응답과 상태 코드를 업데이트하는 것입니다.&lt;/p>
&lt;p>상태 코드를 업데이트하려면 &lt;code>context.HttpContext.Response.StatusCode&lt;/code>를 업데이트해야 하며, 결과를 반환하려면 &lt;code>ActionResult&lt;/code>에서 상속된 객체를 제공하여 &lt;code>context.Result&lt;/code>을 업데이트해야 합니다.&lt;/p>
&lt;p>이것은 필터이므로 &lt;code>[CustomExceptionFilter]&lt;/code>를 추가하여 무언가를 추가해야 함을 의미합니다.&lt;/p>
&lt;h3 id="필터-사용하기">필터 사용하기&lt;/h3>
&lt;p>이제 우리가 가지고 있는 메소드를 복제하고 이 필터를 추가하여 조치를 취하도록 하겠습니다. API 엔드포인트는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.ExceptionFilterAttributes&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Controllers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ApiController]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;[controller]&lt;span style="color:#a5d6ff">&amp;#34;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WeatherForecastController&lt;/span> : ControllerBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> WeatherForecastController()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWithExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [CustomExceptionFilter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Task GetWithExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>보시다시피 &lt;code>GetWithExceptionHandler&lt;/code>라는 새로운 메서드가 있습니다. 이는 &lt;code>GetWithoutExceptionHandler&lt;/code>와 동일한 논리를 가지고 있지만 이 경우에는 메서드에 &lt;code>[CustomExceptionFilter]&lt;/code> 필터를 추가했습니다.&lt;/p>
&lt;p>메서드를 실행한 후 결과는 다음과 같습니다. 더 이상 스택 추적이 표시되지 않으므로 이미지를 표시하겠습니다.&lt;/p>
&lt;p>&lt;img src="https://imgur.com/a8TSAy2.png"/>따라서 이를 통해 우리는 사용자 정의 예외, 즉 예외를 던지고 메서드에서 사용할 때 발생하는 일을 재정의하는 필터를 만들었습니다.&lt;/p>
&lt;p>이는 로깅 및 오류가 언제, 어디서 발생하는지 파악하는 등 많은 작업에 사용될 수 있습니다.&lt;/p></content:encoded><category>.NET</category><category>API</category></item><item><title>물건을 주입하는 더 좋은 방법</title><link>https://emimontesdeoca.github.io/ko/posts/custom-iservicecollection-services/</link><pubDate>Mon, 04 Sep 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/custom-iservicecollection-services/</guid><description>깔끔한 IServiceCollection 확장 메서드를 사용하여 .NET 종속성 주입 등록을 구성합니다.</description><content:encoded>&lt;p>서비스, ​​리포지토리, 속성 또는 애플리케이션에 삽입할 모든 항목을 구축할 때마다 실제로 애플리케이션에 서비스를 추가하는 이 단계를 수행해야 합니다.&lt;/p>
&lt;p>이것은 항상 동일합니다. &lt;code>Program.cs&lt;/code>로 이동한 다음 서비스를 주입하기 위해 파일 추가 &lt;code>builder.Services.AddScoped&amp;lt;MyService&amp;gt;();&lt;/code>의 일부로 진행합니다.&lt;/p>
&lt;p>다음과 같은 것 :&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Repositories&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;ARepository&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;BRepository&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;AService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;BService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;CService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;DService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>제 말은, 효과는 있지만 제가 까다롭고 별로 좋아하지 않는다는 거죠.&lt;/p>
&lt;p>우리가 수행하려는 종속성 주입이 여러 개 있고 실제로는 동일한 것이 아니라고 가정해 보겠습니다. 제 경우에는 모든 저장소를 포함하는 라이브러리, 모든 서비스를 포함하는 또 다른 라이브러리, 마지막으로 속성을 포함하는 또 다른 라이브러리를 갖는 것과 같을 수 있습니다.&lt;/p>
&lt;p>그렇다면 &lt;code>Program.cs&lt;/code>에 추가해야 할 줄의 양을 상상할 수 있습니까?&lt;/p>
&lt;p>일부 서비스가 포함된 라이브러리가 있다고 가정해 보겠습니다. 모든 서비스를 포함하려면 &lt;code>IServiceCollection&lt;/code>를 사용하여 작업해야 합니다.&lt;/p>
&lt;p>따라서 &lt;code>IServiceCollection&lt;/code>를 반환하는 &lt;code>AddServices&lt;/code>라는 &lt;code>static&lt;/code> 메서드를 갖는 &lt;code>static class&lt;/code>를 생성하겠습니다.&lt;/p>
&lt;p>이 경우 이름은 &lt;code>IServiceCollectionServicesExtensions&lt;/code>로 지정됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// IServiceCollectionServicesExtensions class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">IServiceCollectionServicesExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// AddCoreServices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;IServiceCollection&amp;#34; name=&amp;#34;services&amp;#34;&amp;gt;Parameter for &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;An object of type &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IServiceCollection AddServices(&lt;span style="color:#ff7b72">this&lt;/span> IServiceCollection services)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;AService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;BService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;CService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;DService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> services;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>뿐만 아니라 이 서비스에서 사용되는 일부 저장소를 포함하는 또 다른 라이브러리도 있으므로 동일한 작업을 수행해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// IServiceCollectionServicesExtensions class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">IServiceCollectionServicesExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// AddCoreServices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;IServiceCollection&amp;#34; name=&amp;#34;services&amp;#34;&amp;gt;Parameter for &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;An object of type &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IServiceCollection AddRepositories(&lt;span style="color:#ff7b72">this&lt;/span> IServiceCollection services)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;ARepository&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;BRepository&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> services;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 주입할 리포지토리와 서비스 메서드가 생겼습니다. 그런데 이를 어떻게 사용합니까?&lt;/p>
&lt;p>&lt;code>Program.cs&lt;/code>로 돌아가서 다음을 추가해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Repositories&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRepositories(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddServices(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이게 훨씬 더 깔끔해 보이지 않나요? 글쎄요, 우리는 일부 서비스와 리포지토리를 성공적으로 주입했지만 이제 더 보기 좋아 보이고 실제로 외부 라이브러리에 주입한 내용을 갖게 되었습니다.&lt;/p></content:encoded><category>.NET</category><category>Docker</category></item><item><title>.NET API의 특성을 사용한 종속성 주입</title><link>https://emimontesdeoca.github.io/ko/posts/api-di-attributes/</link><pubDate>Tue, 22 Aug 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/api-di-attributes/</guid><description>ActionAttribute 대신 TypeFilterAttribute를 사용하여 .NET API 작업 필터에서 종속성 주입을 활성화합니다.</description><content:encoded>&lt;p>종속성 주입은 아마도 현재 .NET에서 제공하는 최고의 기능 중 하나일 것입니다. 당신이 그것을 사용하지 않을 가능성은 전혀 없습니다. 따라서 당신이 나와 같다면 당신이 만드는 모든 구현에 그것을 추가하고 싶을 것입니다.&lt;/p>
&lt;p>공식 Microsoft의 &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1">문서&lt;/a>에 따른 필터:&lt;/p>
&lt;blockquote>
&lt;p>ASP.NET Core의 필터를 사용하면 요청 처리 파이프라인의 특정 단계 전후에 코드를 실행할 수 있습니다.&lt;/p>
&lt;p>내장 필터는 다음과 같은 작업을 처리합니다.&lt;/p>
&lt;ul>
&lt;li>승인, 사용자가 승인되지 않은 리소스에 대한 액세스를 방지합니다.&lt;/li>
&lt;li>응답 캐싱, 캐시된 응답을 반환하기 위해 요청 파이프라인을 단락시킵니다.&lt;/li>
&lt;/ul>
&lt;p>교차 편집 문제를 처리하기 위해 사용자 정의 필터를 만들 수 있습니다. 교차 편집 문제의 예로는 오류 처리, 캐싱, 구성, 권한 부여 및 로깅이 있습니다. 필터는 코드 중복을 방지합니다.&lt;/p>&lt;/blockquote>
&lt;p>저는 API로 많은 작업을 하고 있으며 모든 단일 요청 또는 거의 모든 요청을 실행해야 하는 몇 가지 작업이 있습니다. 따라서 이상적으로 우리가 원하는 것은 API를 사용하여 작업하는 것입니다&amp;hellip;. 종속성 주입!&lt;/p>
&lt;p>하지만 때로는 약간의 트릭일 수도 있습니다. &lt;code>ActionAttribute&lt;/code>에서 상속하려는 경우 원하는 대로 작동하지 않으므로 &lt;code>OnActionExecutionAsync&lt;/code>를 재정의할 때 작업을 수행할 수 있는 &lt;code>TypeFilterAttribute&lt;/code>로 작업해야 합니다.&lt;/p>
&lt;p>저는 보통 로깅을 수행하기 위해 이 필터를 생성하므로 이를 예로 사용하겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// LoggedQueryAttribute class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">LoggedQueryTypeFilterAttribute&lt;/span> : TypeFilterAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;LoggedQueryTypeFilterAttribute&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> LoggedQueryTypeFilterAttribute() : &lt;span style="color:#ff7b72">base&lt;/span>(&lt;span style="color:#ff7b72">typeof&lt;/span>(LoggedQueryFilter))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// LoggedQueryFilter class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">LoggedQueryFilter&lt;/span> : IAsyncActionFilter
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;see cref=&amp;#34;_loggingService&amp;#34;/&amp;gt; object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> LoggingService _loggingService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;LoggedQueryFilter&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;LoggingService&amp;#34; name=&amp;#34;loggingService&amp;#34;&amp;gt;Parameter for loggingService&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> LoggedQueryFilter(LoggingService loggingService)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _loggingService = loggingService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// OnActionExecutionAsync&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;ActionExecutingContext&amp;#34; name=&amp;#34;context&amp;#34;&amp;gt;Parameter for context&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;ActionExecutionDelegate&amp;#34; name=&amp;#34;next&amp;#34;&amp;gt;Parameter for next&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get properties&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> properties = (Request)context.ActionArguments.First().Value!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get call from context&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> call = context.HttpContext.Request.Path.Value!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Logging&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _loggingService.LogCustomEvent(call);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Continue call&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> next();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>논리는 매우 간단합니다. &lt;code>context.ActionArguments.First().Value&lt;/code>를 사용하여 &lt;code>context&lt;/code> 객체에 액세스하여 본문을 얻고, 또한 &lt;code>context.HttpContext.Request.Path.Value&lt;/code>를 사용하여 메서드 호출을 수행합니다.&lt;/p>
&lt;p>그런 다음 서비스에서 메서드를 호출합니다. 이 경우에는 &lt;code> _loggingService.LogCustomEvent(call)&lt;/code>입니다.&lt;/p>
&lt;p>그런 다음 파이프라인이 계속되어야 하므로 &lt;code>await next();&lt;/code>를 호출해야 합니다.&lt;/p>
&lt;p>이는 속성에 대한 것이므로 실제로 이 속성을 메소드에 포함해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[LoggedQueryTypeFilterAttribute]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> ActionResult&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; TestFilter()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Ok(&lt;span style="color:#a5d6ff">&amp;#34;Hello world!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>재미있게 보셨기를 바라며, 궁금하신 점이나 연락하고 싶으신 점이 있으시면 주저하지 마시고 연락주세요!&lt;/p></content:encoded><category>.NET</category></item><item><title>외부 라이브러리가 포함된 Swagger 문서</title><link>https://emimontesdeoca.github.io/ko/posts/swagger-libraries-documentation/</link><pubDate>Fri, 17 Feb 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/swagger-libraries-documentation/</guid><description>외부 .NET 클래스 라이브러리에 정의된 모델의 XML 문서를 표시하려면 Swagger를 활성화하세요.</description><content:encoded>&lt;p>여러 라이브러리를 사용하여 향후 사례를 위해 코드를 분할하는 것은 좋은 일입니다. 개인적으로 나는 로직을 분할하여 다른 프로젝트나 프로젝트의 일부에서 재사용할 수 있다면 이 작업을 좋아합니다.&lt;/p>
&lt;p>적어도 나에게는 &lt;code>Entity Framework&lt;/code>을 포함하는 데이터 모델을 분할하여 &lt;code>Console application&lt;/code>, &lt;code>Blazor application&lt;/code> 또는 &lt;code>API&lt;/code>에서 참조하고 사용할 수 있도록 해야 합니다.&lt;/p>
&lt;p>제가 작업을 수행하는 방법에 대한 예를 살펴보겠습니다. 이것은 몇 가지 애플리케이션이 있는 프로젝트의 스크린샷과 모든 모델이 있는 공유 라이브러리입니다.&lt;/p>
&lt;img src="https://imgur.com/gdg2nn3.png">
&lt;p>이것은 각 속성과 클래스에 대한 &lt;code>summary&lt;/code> 문서가 포함된 &lt;code>API&lt;/code> 프로젝트에서 이동한 클래스의 예입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomLibrariesDocumentation.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// WeatherForecast class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WeatherForecast&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the Date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateOnly Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the TemperatureC&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> TemperatureC { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the TemperatureF&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> TemperatureF =&amp;gt; &lt;span style="color:#a5d6ff">32&lt;/span> + (&lt;span style="color:#ff7b72">int&lt;/span>)(TemperatureC / &lt;span style="color:#a5d6ff">0.5556&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the Summary&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> Summary { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Console application&lt;/code>를 참조하고 모델을 사용하기 시작하면 문서를 볼 수 있습니다. 이는 Visual Studio의 표준 기능이므로 여기에서 볼 수 있듯이 잘 작동합니다.&lt;/p>
&lt;img src="https://imgur.com/Djf1MeO.png">
&lt;p>그러나 &lt;code>API&lt;/code>를 참조하고 &lt;code>Swagger&lt;/code>로 이동하면 요약 문서가 없습니다.&lt;/p>
&lt;img src="https://imgur.com/3Dg2Xpm.png">
&lt;p>이 문제를 어떻게 해결하나요?&lt;/p>
&lt;p>먼저, 라이브러리에서 XML 주석을 활성화해야 합니다. 이를 위해서는 프로젝트 설정을 업데이트하고 ``:&lt;/p>
&lt;img src="https://imgur.com/R1j8SfX.png">
&lt;p>그러면 여기에서 볼 수 있듯이 빌드 폴더에 파일 확장자 &lt;code>xml&lt;/code>로 끝나는 일부 파일이 생성됩니다.&lt;/p>
&lt;img src="https://imgur.com/O8ywjr2.png">
&lt;p>이제 이 작업이 완료되었으므로 &lt;code>Program.cs&lt;/code>에 몇 가지 항목을 추가해야 해당 파일을 읽을 수 있습니다. 이는 기본적으로 해당 프로젝트의 XML 정의만 로드하기 때문입니다.&lt;/p>
&lt;p>이는 &lt;code>AddSwaggerGen&lt;/code> 내부에 있는 &lt;code>IncludeXmlComments&lt;/code>라는 메서드를 사용합니다.&lt;/p>
&lt;p>아이디어는 &lt;code>xml&lt;/code> 파일이 모두 있으면 해당 파일을 Swagger에 강제로 로드한다는 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddSwaggerGen(s =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Comments&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> allXmlFiles = Directory.GetFiles(AppContext.BaseDirectory, &lt;span style="color:#a5d6ff">&amp;#34;*.xml&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span> xmlFiles &lt;span style="color:#ff7b72">in&lt;/span> allXmlFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s.IncludeXmlComments(xmlFiles);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>간단히 말해, 빌드 디렉터리에서 &lt;code>xml&lt;/code> 파일을 가져와 &lt;code>IncludeXmlComments&lt;/code> 메서드를 사용하여 추가합니다.&lt;/p>
&lt;p>이제 &lt;code>API&lt;/code>를 다시 로드하고 문서를 볼 수 있는지 확인합니다.&lt;/p>
&lt;img src="https://imgur.com/uNVNswn.png">
&lt;p>그리고 우리가 문서를 볼 수 있다는 것을 알 수 있습니다!&lt;/p>
&lt;p>도움이 되셨길 바라며, 궁금한 점이 있으시면 언제든지 연락주세요!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>API</category></item><item><title>Git의 로컬 지점 정리</title><link>https://emimontesdeoca.github.io/ko/posts/cleanup-local-branches/</link><pubDate>Mon, 30 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/cleanup-local-branches/</guid><description>단일 PowerShell 명령을 사용하여 기본 분기를 제외한 모든 로컬 Git 분기를 삭제합니다.</description><content:encoded>&lt;p>﻿현지 지점이 너무 많아진 적이 있나요? 우리는 각 기능, 버그 또는 잡일에 대해 브랜치를 사용하기 때문에 이런 일이 많이 발생합니다.&lt;/p>
&lt;p>결국 이런 일이 생기고 시간이 지나면 너무 더러워지죠&lt;/p>
&lt;img src="https://imgur.com/W3OGJE7.png">
&lt;p>정말 짜증나서 이 정리 작업을 수행하기 위해 콘솔에서 실행할 수 있는 빠른 명령을 공유하겠습니다!&lt;/p>
&lt;p>Powerhsell에서 실행되는 버전인 &lt;a href="https://stackoverflow.com/users/529612/robert-corvus">Robert Corvus&lt;/a>의 다음 &lt;a href="https://stackoverflow.com/a/56671336/7823470">답변&lt;/a>에서 스택 오버플로에서 이 명령을 찾았습니다.&lt;/p>
&lt;p>&lt;strong>변경 사항이 손실될 수 있으므로 이 명령을 실행할 때 주의하세요&lt;/strong>&lt;/p>
&lt;p>실행하기 전에 &lt;code>MY_MASTER_BRANCH_NAME&lt;/code>를 기본 브랜치로 업데이트해야 합니다. 이는 제가 사용하는 것처럼 &lt;code>master&lt;/code>이거나 기본적으로 제공되는 &lt;code>main&lt;/code>라는 새로운 브랜치일 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-powershell" data-lang="powershell">&lt;span style="display:flex;">&lt;span>git branch | %{ &lt;span style="color:#79c0ff">$_&lt;/span>.&lt;span style="color:#79c0ff">Trim&lt;/span>() } | ?{ &lt;span style="color:#79c0ff">$_&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">-ne&lt;/span> &lt;span style="color:#a5d6ff">&amp;#39;MY_MASTER_BRANCH_NAME&amp;#39;&lt;/span> } | %{ git branch -D &lt;span style="color:#79c0ff">$_&lt;/span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 명령을 실행하면 다음과 같은 출력이 표시됩니다.&lt;/p>
&lt;img src="https://imgur.com/VJn89OZ.png">
&lt;p>그것이 당신에게 도움이 되기를 바랍니다!&lt;/p></content:encoded><category>Git</category></item><item><title>ASP.NET Core 7에서 ID의 경로 업데이트</title><link>https://emimontesdeoca.github.io/ko/posts/identity-url-change/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/identity-url-change/</guid><description>기본 ASP.NET Core ID 로그인을 사용자 지정하고 ID 페이지를 스캐폴딩하여 URL을 등록합니다.</description><content:encoded>&lt;p>﻿Microsoft라는 이름이 왜 그렇게 희귀한지 궁금하십니까? 나는 항상 그들이 그렇게 훌륭하게 해내지는 않는다고 생각했지만, 글쎄요, 그게 바로 그거예요!&lt;/p>
&lt;p>이것의 좋은 점은 개발하는 동안 거의 모든 것을 변경할 수 있다는 것입니다!&lt;/p>
&lt;p>페이지에 들어가서 등록이나 로그인 프로세스를 수행하는 것만으로 이것이 ASP.NET 웹 프로젝트라는 것을 즉시 깨달은 적이 있습니까?&lt;/p>
&lt;img src="https://imgur.com/8NMNHGp.png">
&lt;p>여러분이 생성한 프로젝트에는 기본적으로 로그인 및 등록 프로세스를 수행하는 데 필요한 URL이 있기 때문에 이런 일이 많이 발생했습니다(또한 다른 페이지도 많이 있습니다).&lt;/p>
&lt;p>따라서 이 튜토리얼에서는 해당 URL을 업데이트하여 프로젝트가 더 멋지게 보이도록 하는 방법을 보여줍니다!!&lt;/p>
&lt;h2 id="기본-동작">기본 동작&lt;/h2>
&lt;p>Blazor 프로젝트를 생성하고 이를 ID와 함께 사용하기로 결정하면 다음과 같이 표시됩니다.&lt;/p>
&lt;img align="center" src="https://i.imgur.com/2W8Oou9.png">
&lt;p>그리고 로그인이나 등록 프로세스를 수행하려고 하면 &lt;code>/Identity/Account/Login&lt;/code> 또는 &lt;code>/Identity/Account/Register&lt;/code>로 이동합니다.&lt;/p>
&lt;p>하지만 해당 URL을 다르게 업데이트할 수 있다고 말하면 어떻게 될까요?&lt;/p>
&lt;h2 id="login--및-register-페이지-스캐폴딩">&lt;code>Login &lt;/code> 및 &lt;code>Register&lt;/code> 페이지 스캐폴딩&lt;/h2>
&lt;p>Microsoft는 이러한 페이지를 업데이트하기 위해 해당 페이지를 숨기지만 사용자는 신속하게 페이지를 스캐폴드하고 원하는 변경을 수행할 수 있습니다!&lt;/p>
&lt;p>그렇게 하려면 다음과 같이 프로젝트의 컨텍스트 메뉴에서 &lt;code>Add Scaffolded Item&lt;/code>로 이동해야 합니다.&lt;/p>
&lt;img src="https://imgur.com/F3C4C9b.png">
&lt;p>그런 다음 모달이 팝업되고 &lt;code>Identity&lt;/code>를 두 번 선택하고 &lt;code>Add&lt;/code>를 클릭해야 합니다.&lt;/p>
&lt;img src="https://imgur.com/tZUqUlY.png">
&lt;p>이 후에는 또 다른 모달이 팝업되어 전체 ID에서 업데이트할 페이지를 선택할 수 있습니다. 업데이트할 수 있는 페이지가 많지만 여기서는 &lt;code>Account/Login&lt;/code> 및 &lt;code>Account/Register&lt;/code>에 중점을 둘 것입니다.&lt;/p>
&lt;img src="https://imgur.com/BS6ZLas.png">
&lt;p>이제 잠시 작동시킨 후 솔루션 탐색기를 확인하면 몇 가지 새로운 파일을 찾을 수 있습니다.&lt;/p>
&lt;img src="https://imgur.com/5lWHLyI.png">
&lt;p>이러한 새 파일은 ID를 추가하기 위해 ASP.NET을 선택하면 프로젝트에 추가되는 로그인 및 등록 페이지입니다!&lt;/p>
&lt;h2 id="url-업데이트-중">URL 업데이트 중&lt;/h2>
&lt;p>아마도 눈치채셨겠지만 이 파일은 확장자가 &lt;code>cshtml&lt;/code>이므로 면도기 파일이므로 지시문을 사용하여 페이지의 URL을 업데이트하겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/login&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@model LoginModel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>] = &lt;span style="color:#a5d6ff">&amp;#34;Log in&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>]&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-4&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;account&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;Use a local account to log &lt;span style="color:#ff7b72">in&lt;/span>.&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div asp-validation-summary=&lt;span style="color:#a5d6ff">&amp;#34;ModelOnly&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;username&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;name@example.com&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;current-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;checkbox mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.RememberMe&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input class=&lt;span style="color:#a5d6ff">&amp;#34;form-check-input&amp;#34;&lt;/span> asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.RememberMe&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Html.DisplayNameFor(m =&amp;gt; m.Input.RememberMe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button id=&lt;span style="color:#a5d6ff">&amp;#34;login-submit&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;w-100 btn btn-lg btn-primary&amp;#34;&lt;/span>&amp;gt;Log &lt;span style="color:#ff7b72">in&lt;/span>&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a id=&lt;span style="color:#a5d6ff">&amp;#34;forgot-password&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ForgotPassword&amp;#34;&lt;/span>&amp;gt;Forgot your password?&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./Register&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span>&amp;gt;Register &lt;span style="color:#ff7b72">as&lt;/span> a &lt;span style="color:#ff7b72">new&lt;/span> user&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a id=&lt;span style="color:#a5d6ff">&amp;#34;resend-confirmation&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ResendEmailConfirmation&amp;#34;&lt;/span>&amp;gt;Resend email confirmation&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-6 col-md-offset-2&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h3&amp;gt;Use another service to log &lt;span style="color:#ff7b72">in&lt;/span>.&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> ((Model.ExternalLogins?.Count ?? &lt;span style="color:#a5d6ff">0&lt;/span>) == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> There are no external authentication services configured. See &lt;span style="color:#ff7b72">this&lt;/span> &amp;lt;a href=&lt;span style="color:#a5d6ff">&amp;#34;https://go.microsoft.com/fwlink/?LinkID=532715&amp;#34;&lt;/span>&amp;gt;article
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> about setting up &lt;span style="color:#ff7b72">this&lt;/span> ASP.NET application to support logging &lt;span style="color:#ff7b72">in&lt;/span> via external services&amp;lt;/a&amp;gt;.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;external-account&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ExternalLogin&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-horizontal&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> provider &lt;span style="color:#ff7b72">in&lt;/span> Model.ExternalLogins!)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;provider&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;@provider.Name&amp;#34;&lt;/span> title=&lt;span style="color:#a5d6ff">&amp;#34;Log in using your @provider.DisplayName account&amp;#34;&lt;/span>&amp;gt;@provider.DisplayName&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@section Scripts {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff7b72">partial&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;_ValidationScriptsPartial&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>보시다시피 대부분의 내용은 동일하지만 수업의 첫 번째 줄을 살펴보면 이전에 있었던 내용을 다음과 같이 업데이트했습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>에&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/login&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>글쎄, 그건 쉬웠지, 그렇지? 이제 간단한 테스트를 통해 제대로 작동하는지 확인해 보겠습니다.&lt;/p>
&lt;p>우선, 처음에 있는 기본 페이지로 이동하여 아직 작동하는지 확인하겠습니다.&lt;/p>
&lt;img src="https://imgur.com/ngbRNaG.png">
&lt;p>그렇지 않습니다! 이제 &lt;code>/login&lt;/code>인 새 URL을 테스트해 보겠습니다.&lt;/p>
&lt;img src="https://imgur.com/R067PnF.png">
&lt;p>그리고 그것은 작동합니다 !!&lt;/p>
&lt;p>이제 레지스터에 대해서도 동일한 작업을 수행해 보겠습니다. 페이지를 업데이트하고 &lt;code>@page&lt;/code> 지시문에 원하는 경로를 추가한 후 테스트해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/register&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@model RegisterModel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>] = &lt;span style="color:#a5d6ff">&amp;#34;Register&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>]&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-4&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;registerForm&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;Create a &lt;span style="color:#ff7b72">new&lt;/span> account.&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div asp-validation-summary=&lt;span style="color:#a5d6ff">&amp;#34;ModelOnly&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;username&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;name@example.com&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span>&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;new-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span>&amp;gt;Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;new-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span>&amp;gt;Confirm Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button id=&lt;span style="color:#a5d6ff">&amp;#34;registerSubmit&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;w-100 btn btn-lg btn-primary&amp;#34;&lt;/span>&amp;gt;Register&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-6 col-md-offset-2&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h3&amp;gt;Use another service to register.&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> ((Model.ExternalLogins?.Count ?? &lt;span style="color:#a5d6ff">0&lt;/span>) == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> There are no external authentication services configured. See &lt;span style="color:#ff7b72">this&lt;/span> &amp;lt;a href=&lt;span style="color:#a5d6ff">&amp;#34;https://go.microsoft.com/fwlink/?LinkID=532715&amp;#34;&lt;/span>&amp;gt;article
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> about setting up &lt;span style="color:#ff7b72">this&lt;/span> ASP.NET application to support logging &lt;span style="color:#ff7b72">in&lt;/span> via external services&amp;lt;/a&amp;gt;.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;external-account&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ExternalLogin&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-horizontal&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> provider &lt;span style="color:#ff7b72">in&lt;/span> Model.ExternalLogins!)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;provider&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;@provider.Name&amp;#34;&lt;/span> title=&lt;span style="color:#a5d6ff">&amp;#34;Log in using your @provider.DisplayName account&amp;#34;&lt;/span>&amp;gt;@provider.DisplayName&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@section Scripts {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff7b72">partial&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;_ValidationScriptsPartial&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>@page &amp;quot;/login&amp;quot;&lt;/code>로 상단 부분을 업데이트했으며 이제 작동하는지 테스트합니다.&lt;/p>
&lt;img src="https://imgur.com/M7KakaF.png">
&lt;p>또한 작동합니다 !!&lt;/p>
&lt;h2 id="그게-다야이-url을-업데이트하는-방법을-배웠기를-바랍니다-주로-일부-프로젝트에서-url을-특정-방식으로-수행하면-id가-달라-보이기-때문입니다-하하">그게 다야이 URL을 업데이트하는 방법을 배웠기를 바랍니다. 주로 일부 프로젝트에서 URL을 특정 방식으로 수행하면 ID가 달라 보이기 때문입니다. 하하!&lt;/h2>
&lt;p>필요한 것이 있으면 저에게 트윗을 보내거나 이메일을 보내주세요. 제가 도와드리겠습니다!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>.NET 6 Core API의 사용자 정의 특성</title><link>https://emimontesdeoca.github.io/ko/posts/custom-attributes-net-6-core-api/</link><pubDate>Fri, 09 Dec 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/custom-attributes-net-6-core-api/</guid><description>.NET 6 Core API에서 요청 헤더의 유효성을 검사하는 사용자 지정 ActionFilterAttribute 클래스를 만듭니다.</description><content:encoded>&lt;p>사용자 정의 특성은 정말 사용하기 좋은 것입니다. 최근에 사용하기 시작했습니다. 그 중 하나를 만들고 컨트롤러, 클래스 또는 메서드 자체에서 재사용할 수 있기 때문입니다.&lt;/p>
&lt;p>헤더 확인과 같은 일부 보안 작업을 수행하거나 반드시 필요한 매개변수 값을 확인하려는 경우 정말 도움이 됩니다.&lt;/p>
&lt;p>제 경우에는 .NET Core API 프로젝트에서 이를 사용하여 모든 요청에 ​​특정 헤더가 포함되어 있는지 확인하겠습니다.&lt;/p>
&lt;h1 id="headercheck속성">HeaderCheck속성&lt;/h1>
&lt;p>멋진 .NET Core API를 만든 후에는 폴더 사용을 좋아하므로 항목을 저장할 폴더를 만들어 보겠습니다.&lt;/p>
&lt;img src="https://i.imgur.com/i2VKbZN.png"/>
&lt;p>그런 다음 &lt;code>HeaderCheckAttribute&lt;/code> 클래스에 논리를 추가하겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc.Filters&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">DotNet6CustomAttribute.Attributes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">HeaderCheckAttribute&lt;/span> : ActionFilterAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnActionExecuting(ActionExecutingContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get all headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> headers = context.HttpContext.Request.Headers;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Check if headers has x-dotnet-6-custom-attribute&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!headers.ContainsKey(&lt;span style="color:#a5d6ff">&amp;#34;x-dotnet-6-custom-attribute&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Result = &lt;span style="color:#ff7b72">new&lt;/span> BadRequestObjectResult(&lt;span style="color:#a5d6ff">&amp;#34;The header x-dotnet-6-custom-attribute is missing&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(headers[&lt;span style="color:#a5d6ff">&amp;#34;x-dotnet-6-custom-attribute&amp;#34;&lt;/span>]))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Result = &lt;span style="color:#ff7b72">new&lt;/span> BadRequestObjectResult(&lt;span style="color:#a5d6ff">&amp;#34;The header x-dotnet-6-custom-attribute can&amp;#39;t be null or empty&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">base&lt;/span>.OnActionExecuting(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>기본적으로 논리는 먼저 &lt;code>x-dotnet-6-custom-attribute&lt;/code> 키가 있는 헤더를 확인하고 헤더가 있으면 값이 있는지 확인하는 것입니다.&lt;/p>
&lt;p>두 표현식이 모두 true이면 특정 메시지와 함께 &lt;code>BadRequestObjectResult&lt;/code>를 다시 반환합니다.&lt;/p>
&lt;h1 id="컨트롤러에-추가하기">컨트롤러에 추가하기&lt;/h1>
&lt;p>이 로직을 여러 위치에 추가할 수 있고, 전체 컨트롤러에 직접 추가할 수도 있고, 일부 메서드에 추가할 수도 있습니다. 먼저 메서드에 추가한 다음 전체 컨트롤러에 추가하겠습니다.&lt;/p>
&lt;p>그럼 &lt;code>WeatherForecastController&lt;/code> 클래스를 그것들로 장식해 봅시다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">DotNet6CustomAttribute.Attributes&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">DotNet6CustomAttribute.Controllers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ApiController]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;[controller]&lt;span style="color:#a5d6ff">&amp;#34;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WeatherForecastController&lt;/span> : ControllerBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] Summaries = &lt;span style="color:#ff7b72">new&lt;/span>[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Freezing&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Bracing&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Chilly&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Cool&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Mild&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Warm&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Balmy&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Hot&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Sweltering&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Scorching&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILogger&amp;lt;WeatherForecastController&amp;gt; _logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> WeatherForecastController(ILogger&amp;lt;WeatherForecastController&amp;gt; logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger = logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWeatherForecastWithCheck&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HeaderCheckAttribute]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> IEnumerable&amp;lt;WeatherForecast&amp;gt; GetWithCheck()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Enumerable.Range(&lt;span style="color:#a5d6ff">1&lt;/span>, &lt;span style="color:#a5d6ff">5&lt;/span>).Select(index =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> WeatherForecast
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = DateTime.Now.AddDays(index),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TemperatureC = Random.Shared.Next(-&lt;span style="color:#a5d6ff">20&lt;/span>, &lt;span style="color:#a5d6ff">55&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Summary = Summaries[Random.Shared.Next(Summaries.Length)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToArray();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWeatherForecastWithoutCheck&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> IEnumerable&amp;lt;WeatherForecast&amp;gt; GetWithoutCheck()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Enumerable.Range(&lt;span style="color:#a5d6ff">1&lt;/span>, &lt;span style="color:#a5d6ff">5&lt;/span>).Select(index =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> WeatherForecast
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = DateTime.Now.AddDays(index),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TemperatureC = Random.Shared.Next(-&lt;span style="color:#a5d6ff">20&lt;/span>, &lt;span style="color:#a5d6ff">55&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Summary = Summaries[Random.Shared.Next(Summaries.Length)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToArray();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>프로젝트를 실행해보자!&lt;/p>
&lt;img src="https://i.imgur.com/ZvO4LnE.png"/>
&lt;p>여기에는 &lt;code>GetWeatherForecastWithCheck&lt;/code> 및 &lt;code>GetWeatherForecastWithoutCheck&lt;/code>라는 두 가지 기능이 있습니다. 그 중 하나는 실패하고 다른 하나는 실패하지만 Swagger에서 확인해 보겠습니다!&lt;/p>
&lt;img src="https://i.imgur.com/x1yRb9j.png"/>
&lt;img src="https://i.imgur.com/XiO4GC9.png">
&lt;p>그 중 하나는 메시지와 함께 400 오류를 반환하고 다른 하나는 값을 반환하는 것을 볼 수 있듯이 이제 이를 완전히 테스트하기 위해 Postman을 실행하고 헤더를 추가하여 &lt;code>GetWeatherForecastWithCheck&lt;/code>를 사용하여 데이터도 볼 수 있도록 하겠습니다.&lt;/p>
&lt;h1 id="우편배달부">우편배달부&lt;/h1>
&lt;p>이제 Postman에서 실행하면서 헤더를 추가하고 오류 메시지가 변경된 것을 확인합니다. 이제 헤더를 제공하지만 값이 없기 때문입니다.&lt;/p>
&lt;img src="https://i.imgur.com/JHP8ZXZ.png"/>
&lt;p>여기에 값을 추가하면 마침내 값을 얻게 됩니다!&lt;/p>
&lt;img src="https://i.imgur.com/jxt6xFe.png"/>
&lt;h1 id="그게-다야">그게 다야&lt;/h1>
&lt;p>그게 다야! 아주 간단하죠? 이제 속성을 생성하고 이를 메서드와 컨트롤러에 할당하는 방법을 알았습니다!&lt;/p>
&lt;p>그들과 함께 즐거운 시간을 보내세요!&lt;/p>
&lt;h1 id="코드">코드&lt;/h1>
&lt;p>이 전체 프로젝트는 Github에 있으며 &lt;a href="https://github.com/emimontesdeoca/dotnet-6-attribute-post">여기&lt;/a>에서 찾을 수 있습니다!&lt;/p>
&lt;p>문제나 질문이 있는 경우 소셜 미디어 @emimontesdeoca로 언제든지 저에게 연락해 주세요(트위터에서는 실제로 &lt;code>@emimontesdeocaa&lt;/code>이고 끝에 두 개의 &lt;code>aa&lt;/code>가 있습니다). 블로그 헤더에서 내 소셜 미디어의 대부분을 찾을 수도 있습니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! 시아!&lt;/p></content:encoded><category>.NET</category><category>API</category></item><item><title>Blazor에서 구성 요소 로드 처리</title><link>https://emimontesdeoca.github.io/ko/posts/loading-component-blazor/</link><pubDate>Tue, 19 Jul 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/loading-component-blazor/</guid><description>RenderFragment 및 ChildContent를 사용하여 Blazor에서 재사용 가능한 로딩 스피너 래퍼 구성 요소를 빌드합니다.</description><content:encoded>&lt;p>Blazor는 정말 훌륭합니다. 특히 상자 로드와 같은 비동기 작업을 수행할 때 보기에도 좋습니다.&lt;/p>
&lt;p>저는 페이지, 상태, 구성 요소 등의 로딩을 처리하는 방법에 대해 여러 가지 방법을 시도해 왔습니다. 그리고 마침내 제가 원하는 방식으로 수행할 수 있는 완벽한 방법을 찾은 것 같습니다.&lt;/p>
&lt;h1 id="아이디어">아이디어&lt;/h1>
&lt;p>각 페이지나 구성 요소에 로드하기 위한 논리를 다시 작성하는 대신 &lt;code>ChildComponent&lt;/code>를 사용하여 상위 구성 요소를 빌드하면 여러 번 재사용할 수 있는 변경 사항이 제공됩니다.&lt;/p>
&lt;h1 id="loadingcomponent-코드">LoadingComponent 코드&lt;/h1>
&lt;p>코드는 매우 간단합니다. 할 일이 그리 많지 않습니다. 로딩 속성이 있는 기본 &lt;code>if&lt;/code>, 내부에 토글 기능이 있으면 완료됩니다!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@if (_loaded)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;text-center&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;spinner-border&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;status&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#a5d6ff">&amp;#34;sr-only&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @ChildContent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> _loaded = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> RenderFragment? ChildContent { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ToggleLoad(&lt;span style="color:#ff7b72">bool&lt;/span> state)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _loaded = state;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="사용법">사용법&lt;/h1>
&lt;p>사용법은 매우 간단합니다. 테스트를 위해 새 페이지를 사용하고 방금 생성한 &lt;code>LoadingComponent&lt;/code> 구성 요소에 콘텐츠를 넣을 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/loading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using LoadingBoxes.Components
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;LoadingBoxes.Components.LoadingComponent @ref=loadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> This &lt;span style="color:#ff7b72">is&lt;/span> some content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/LoadingBoxes.Components.LoadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> LoadingComponent loadingComponent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// We are going to simulate some load&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">5000&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> loadingComponent.ToggleLoad(&lt;span style="color:#79c0ff">false&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이렇게 생겼어요&lt;/p>
&lt;img src="https://i.gyazo.com/cb892d796f396d43d5c54e30e1e87568.gif"/>
&lt;h1 id="더-재미있는-예">더 재미있는 예&lt;/h1>
&lt;p>각각의 로딩 시간이 있는 여러 구성요소가 있다고 가정해 보겠습니다. 그에 따라 좋아 보이는 것을 만들 수 있습니다!&lt;/p>
&lt;p>재사용할 수 있는 &lt;code>FakeLoadingComponent&lt;/code>라는 가짜 로딩 구성 요소를 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@using LoadingBoxes.Components
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;LoadingBoxes.Components.LoadingComponent @ref=loadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> It took @ellapsedTime seconds!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/LoadingBoxes.Components.LoadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> LoadingComponent loadingComponent = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> ellapsedTime = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> Random rnd = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> random = rnd.Next(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">5000&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// We are going to simulate some load&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(random);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ellapsedTime = random/ &lt;span style="color:#a5d6ff">1000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> loadingComponent.ToggleLoad(&lt;span style="color:#79c0ff">false&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 &lt;code>Loading&lt;/code> 페이지를 여러 &lt;code>FakeLoadingComponent&lt;/code> 구성 요소로 업데이트하고 결과를 확인합니다!!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/loading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using LoadingBoxes.Components
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 이게 훨씬 좋아 보이는데&lt;/p>
&lt;img src="https://i.gyazo.com/e427ca31af410314762446979d1c3739.gif">
&lt;p>그리고 그게 다야!&lt;/p>
&lt;p>문제나 질문이 있는 경우 소셜 미디어 @emimontesdeoca로 언제든지 저에게 연락해 주세요(Twitter에서는 실제로 &lt;code>@emimontesdeocaa&lt;/code>이고 끝에 두 개의 &lt;code>aa&lt;/code>가 있습니다). 블로그 헤더에서 내 소셜 미디어의 대부분을 찾을 수도 있습니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>인증서에서 만료 날짜 가져오기</title><link>https://emimontesdeoca.github.io/ko/posts/expiration-date-certificate/</link><pubDate>Fri, 27 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/expiration-date-certificate/</guid><description>C# HttpClient 및 X509Certificate2를 사용하여 프로그래밍 방식으로 SSL 인증서 만료 날짜를 검색합니다.</description><content:encoded>&lt;p>﻿저희가 사용하는 도메인의 인증서 현재 상태를 확인하는 데 도움이 되는 몇 가지 웹 앱이 있으며 그 중 몇 가지가 있습니다.&lt;/p>
&lt;p>하루에 한 번 실행되고 살펴봐야 할 도메인 몇 개를 확인하는 Azure 함수를 만들었습니다. 인증서 만료 날짜가 30일 미만이면 이메일을 보내는 매우 간단한 콘솔 애플리케이션입니다.&lt;/p>
&lt;p>함수가 어떻게 작동하는지에 대한 논리 자체는 표시되지 않으며, 인증서의 기본 확인을 수행하는 함수와 우리가 얻는 데이터를 보여주고 싶습니다.&lt;/p>
&lt;h2 id="함수">함수&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;X509Certificate2&amp;gt; CheckCertificateAsync(&lt;span style="color:#ff7b72">string&lt;/span> urlPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClientHandler = &lt;span style="color:#ff7b72">new&lt;/span> HttpClientHandler
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ServerCertificateCustomValidationCallback = (request, cert, chain, policyErrors) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2(cert);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HttpClient&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient(httpClientHandler);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.SendAsync(&lt;span style="color:#ff7b72">new&lt;/span> HttpRequestMessage(HttpMethod.Head, urlPath));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> certificate;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 &lt;code>CheckCertificateAsync&lt;/code> 메소드는 만료 날짜 확인을 포함하여 다양한 작업을 수행할 수 있는 &lt;code>X509Certificate2&lt;/code> 인증서를 반환합니다.&lt;/p>
&lt;h2 id="직렬화된-결과">직렬화된 결과&lt;/h2>
&lt;p>이것은 &lt;code>certificate&lt;/code> 객체의 직렬화된 값입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Archived&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Extensions&amp;#34;&lt;/span>:[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyUsages&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">160&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.15&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Key Usage&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;AwIFoA==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;EnhancedKeyUsages&amp;#34;&lt;/span>:[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.5.5.7.3.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Server Authentication&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.5.5.7.3.2&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Client Authentication&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.37&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Enhanced Key Usage&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MBQGCCsGAQUFBwMBBggrBgEFBQcDAg==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;CertificateAuthority&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;HasPathLengthConstraint&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;PathLengthConstraint&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.19&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Basic Constraints&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MAA=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SubjectKeyIdentifier&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;634E1585565AA49402C21642A4A5979A38025797&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.14&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Subject Key Identifier&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;BBRjThWFVlqklALCFkKkpZeaOAJXlw==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.35&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Authority Key Identifier&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MBaAFBQusxe3WFbLrlAJQOYfr52LFMLG&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.5.5.7.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Authority Information Access&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iub3JnLw==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.17&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Subject Alternative Name&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MB6CHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.32&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Certificate Policies&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3Jn&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.4.1.11129.2.4.2&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;SCT List&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;BIH0APIAdwBByMqx3yJGShDGoToJQodeTjGLGwPr60vHaPCQYpYG9gAAAYCeJIwYAAAEAwBIMEYCIQCG8sf4iBitUjNCc1dsxVd5mdRQCKapRqqnTHKxSKHjHgIhAJFGNXEZkCHKygT1T7bE4orpd6p2l1+GmifMEIuRsgHbAHcARqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUcAAAGAniSMNgAABAMASDBGAiEAoxv1LBn/vfyR7s67kRLB/n1tq3eicuA/8/V0S2YzQCYCIQDXaS3FZbdIVNxQvKxPFxM1awBO/sGxBXafz0lspOoWSA==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;HasPrivateKey&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;PrivateKey&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IssuerName&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Name&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=R3, O=Let&amp;#39;s Encrypt, C=US&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJSMw==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;NotAfter&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2022-08-05T10:50:35+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;NotBefore&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2022-05-07T10:50:36+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;PublicKey&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;EncodedKeyValue&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MIIBCgKCAQEAq8cbDO3GAfjqqbPPCBdPost8NMRmEubv85gXecll7mZMH5qSfTPuB/ouFWL3tPMf1U8usWeoSUK/48yatzBGwmj1KKlkaW9MS2QkydztRp+kH8LvbzbQvGknuOLWGHBALLT17o/3DYxuA5LnXdY+vLvJWygQoFr2N/XhnhUjcm6OaQEJpIykydfbBQGQSEuQIIw4egpgdHkYJjCOYAsXuSSggN8/FADTCec0RzVjfFTSoJ3hV9HLE9M8MCSXjuo0AJ/MbAxq91S8XmDcRjHCCd7Zw+NjHo8cxZCQ6NqGvn3xwx8ahmmbC+CyDEcIyJJZK2Yv+qE4oS8QZfaX/RaHMwIDAQAB&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;EncodedParameters&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;BQA=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Key&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Key&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Algorithm&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Algorithm&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;AlgorithmGroup&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;AlgorithmGroup&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ExportPolicy&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Handle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsInvalid&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsClosed&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsEphemeral&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsMachineKey&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeySize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">2048&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyUsage&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">16777215&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ParentWindowHandle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Provider&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Provider&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Microsoft Software Key Storage Provider&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ProviderHandle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsInvalid&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsClosed&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;UIPolicy&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ProtectionLevel&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Description&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;UseContext&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;CreationTitle&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;UniqueName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;LegalKeySizes&amp;#34;&lt;/span>:[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;MinSize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">512&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;MaxSize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">16384&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SkipSize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">64&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyExchangeAlgorithm&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SignatureAlgorithm&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeySize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">2048&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MIIFQDCCBCigAwIBAgISBNwTmwP/RTcrEeIgAdMrpaFtMA0GCSqGSIb3DQEBCwUAMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJSMzAeFw0yMjA1MDcwOTUwMzZaFw0yMjA4MDUwOTUwMzVaMCcxJTAjBgNVBAMTHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrxxsM7cYB+Oqps88IF0+iy3w0xGYS5u/zmBd5yWXuZkwfmpJ9M+4H+i4VYve08x/VTy6xZ6hJQr/jzJq3MEbCaPUoqWRpb0xLZCTJ3O1Gn6Qfwu9vNtC8aSe44tYYcEAstPXuj/cNjG4Dkudd1j68u8lbKBCgWvY39eGeFSNybo5pAQmkjKTJ19sFAZBIS5AgjDh6CmB0eRgmMI5gCxe5JKCA3z8UANMJ5zRHNWN8VNKgneFX0csT0zwwJJeO6jQAn8xsDGr3VLxeYNxGMcIJ3tnD42MejxzFkJDo2oa+ffHDHxqGaZsL4LIMRwjIklkrZi/6oTihLxBl9pf9FoczAgMBAAGjggJZMIICVTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFGNOFYVWWqSUAsIWQqSll5o4AleXMB8GA1UdIwQYMBaAFBQusxe3WFbLrlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5vcmcvMCcGA1UdEQQgMB6CHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20wTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEGBgorBgEEAdZ5AgQCBIH3BIH0APIAdwBByMqx3yJGShDGoToJQodeTjGLGwPr60vHaPCQYpYG9gAAAYCeJIwYAAAEAwBIMEYCIQCG8sf4iBitUjNCc1dsxVd5mdRQCKapRqqnTHKxSKHjHgIhAJFGNXEZkCHKygT1T7bE4orpd6p2l1+GmifMEIuRsgHbAHcARqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUcAAAGAniSMNgAABAMASDBGAiEAoxv1LBn/vfyR7s67kRLB/n1tq3eicuA/8/V0S2YzQCYCIQDXaS3FZbdIVNxQvKxPFxM1awBO/sGxBXafz0lspOoWSDANBgkqhkiG9w0BAQsFAAOCAQEAjSEID5MWonbSiyHbmPYWO8ImCCOjkLGxgY8WJODbrWxFy+xU44UwrWOCkqYZUlv2LRmPqSyZDrIeeHK9VMbGh71oXX+XovikgAr6PpI0Mp897nPWj0XvOBaSYG0s+f+CXMtyt0tWCsQOcl+iT82+Ja71f8gbVL6l7xESewEE78pTKEH8EqD22r8VSD7FNICD8EYQr13v3AuVWObSU/R8Td6SrSVEknw1HgJS4e9nvmrMxBGKOJ+aWrAGiUydehg8M9o2gbGckMhz6D7cwB5l618cYaXKkW1dEOYZHl++qUj1/VPK+FNkiDZOPVNN//PbZuOLwAUIlZvhqGWX5/9PBg==&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SerialNumber&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;04DC139B03FF45372B11E22001D32BA5A16D&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SignatureAlgorithm&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.11&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;sha256RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SubjectName&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Name&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=blog.emilianomontesdeoca.com&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MCcxJTAjBgNVBAMTHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Thumbprint&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;28CF960F772ABFF22AA193C291492C27F8E13D4D&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Version&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">3&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Handle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">2658150705632&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Issuer&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=R3, O=Let&amp;#39;s Encrypt, C=US&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Subject&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=blog.emilianomontesdeoca.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="유효기간">유효기간&lt;/h2>
&lt;p>만료 시간을 알기 위해서는 이 객체 내부에 있는 &lt;code>NotAfter&lt;/code> 및 &lt;code>NotBefore&lt;/code>를 살펴봐야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;NotAfter&amp;#34;&lt;/span>&lt;span style="color:#f85149">:&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;2022-08-05T10:50:35+01:00&amp;#34;&lt;/span>&lt;span style="color:#f85149">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;NotBefore&amp;#34;&lt;/span>&lt;span style="color:#f85149">:&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;2022-05-07T10:50:36+01:00&amp;#34;&lt;/span>&lt;span style="color:#f85149">,&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="콘솔-애플리케이션">콘솔 애플리케이션&lt;/h2>
&lt;p>다음 코드 조각은 .NET 6을 기반으로 구축된 간단한 콘솔 애플리케이션으로, 원하는 인증을 확인할 수 있는 다음 결과를 생성합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Newtonsoft.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Security.Cryptography.X509Certificates&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> url = &lt;span style="color:#a5d6ff">&amp;#34;https://blog.emilianomontesdeoca.com/&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;X509Certificate2&amp;gt; CheckCertificateAsync(&lt;span style="color:#ff7b72">string&lt;/span> urlPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClientHandler = &lt;span style="color:#ff7b72">new&lt;/span> HttpClientHandler
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ServerCertificateCustomValidationCallback = (request, cert, chain, policyErrors) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2(cert);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HttpClient&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient(httpClientHandler);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.SendAsync(&lt;span style="color:#ff7b72">new&lt;/span> HttpRequestMessage(HttpMethod.Head, urlPath));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> certificate;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> cert = &lt;span style="color:#ff7b72">await&lt;/span> CheckCertificateAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> serializedValue = JsonConvert.SerializeObject(cert);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(serializedValue);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.ReadLine();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="데모-프로젝트">데모 프로젝트&lt;/h2>
&lt;p>내 Github의 &lt;a href="https://github.com/emimontesdeoca/expiration-date-certificate">expiration-date-certificate&lt;/a>라는 저장소에서 콘솔 애플리케이션을 찾을 수 있습니다.&lt;/p></content:encoded><category>.NET</category><category>Azure</category></item><item><title>Blazor에서 요소에 초점 맞추기</title><link>https://emimontesdeoca.github.io/ko/posts/focus-element-blazor/</link><pubDate>Thu, 05 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/focus-element-blazor/</guid><description>JavaScript Interop 및 요소 참조를 사용하여 Blazor 구성 요소의 HTML 요소에 초점을 설정합니다.</description><content:encoded>&lt;p>﻿최근에 했던 Wordlzor 게임을 작업한 후, 매우 간단한 기능을 추가해야 했습니다. 즉, 게임에 들어갈 때 전체 게임에 집중해야 했습니다.&lt;/p>
&lt;p>사용자가 화면 키보드를 사용할 뿐만 아니라 실제로 게임에 입력할 수 있기 때문에 이 작업이 필요했습니다.&lt;/p>
&lt;h2 id="자바스크립트-파일">자바스크립트 파일&lt;/h2>
&lt;p>이를 위해서는 함수를 담을 &lt;code>app.js&lt;/code>라는 Javascript 파일을 생성해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>window.FocusElement &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> (element) =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> element.focus();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 &lt;code>index.html&lt;/code> 파일에 스크립트를 포함해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">script&lt;/span> src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;js/app.js&amp;#34;&lt;/span>&amp;gt;&amp;lt;/&lt;span style="color:#7ee787">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="blazor-구성-요소">Blazor 구성 요소&lt;/h2>
&lt;p>스크립트를 초기화한 후에는 집중할 요소를 찾아야 하므로 모든 구성 요소에서 해당 요소를 개체에 참조해야 합니다.&lt;/p>
&lt;p>따라서 Blazor 구성 요소에 &lt;code> @ref&lt;/code> 속성:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span> &lt;span style="color:#f85149">@&lt;/span>ref&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">@elementToFocus&lt;/span> tabindex&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> &amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> This is my focus component
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 구성 요소 논리에서 속성을 &lt;code>ElementReference&lt;/code>로 가질 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Reference to element&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> ElementReference elementToFocus;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="jsinterop-주입">JSInterop 주입&lt;/h2>
&lt;p>이제 Javascript 파일에 있는 함수를 호출하려면 &lt;code>JSInterop&lt;/code>를 사용해야 합니다.&lt;/p>
&lt;p>우선 다음 구문을 사용하여 구성 요소에 주입해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> IJSRuntime JSRuntime { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>서비스가 주입되면 이제 &lt;code>InvokeVoidAsync&lt;/code>와 같은 모든 메서드를 호출할 수 있습니다. 이 메서드는 함수를 호출합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task Focus()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Focus when initializing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JSRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;window.FocusElement&amp;#34;&lt;/span>, elementToFocus);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 함수 &lt;code>Focus()&lt;/code>를 호출하면 우리가 만든 요소에 초점을 맞추게 됩니다. 분명히 요소 자체를 매개변수로 리팩토링하고 전달할 수 있습니다.&lt;/p>
&lt;p>제가 어떻게 구현했는지 보고 싶으시면 &lt;a href="https://github.com/emimontesdeoca/Wordlzor">Wordlzor&lt;/a> 소스 코드를 보시면 됩니다. 명령어 모달을 닫을 때 경고와 포커스에 사용했습니다.&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>GitHub Actions의 VS 데이터베이스 프로젝트에서 *.dacpac 파일 생성</title><link>https://emimontesdeoca.github.io/ko/posts/generate-dacpacs-github-actions/</link><pubDate>Mon, 18 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/generate-dacpacs-github-actions/</guid><description>GitHub Actions 파이프라인을 사용하여 Visual Studio 데이터베이스 프로젝트에서 dacpac 파일 생성을 자동화합니다.</description><content:encoded>&lt;p>데이터베이스 작업이 필요하고 민첩한 개발을 수행하려면 테이블, SP, 함수를 추가하거나 수정할 때마다 이를 컴파일하고 데이터베이스에 대한 배포 파일을 생성하는 흐름이 있어야 합니다.&lt;/p>
&lt;p>저는 오랫동안 이 접근 방식을 사용해 왔습니다. 로컬에서 변경을 수행하고 데이터베이스 프로젝트와 비교한 다음 변경 사항을 커밋하고 푸시하면 데이터베이스로 이동될 bacpac 파일이 생성됩니다.&lt;/p>
&lt;p>이는 인간의 실수를 야기할 수 있고 데이터베이스에서는 정말 나쁜 소식이 될 수 있는 손으로 작업을 수행하는 모든 문제를 제거합니다.&lt;/p>
&lt;h1 id="데이터베이스-프로젝트">데이터베이스 프로젝트&lt;/h1>
&lt;p>Visual Studio에서 데이터베이스 프로젝트를 만들고 데이터베이스를 가져오면 다음과 같이 됩니다.&lt;/p>
&lt;img src="https://i.gyazo.com/c0e11b14c707db66b8dbb591031cc527.png" />
&lt;p>이 프로젝트에서 컴파일을 실행하면 모든 구조를 포함하는 &lt;code>dacpac&lt;/code> 파일이 생성됩니다.&lt;/p>
&lt;img src="https://i.gyazo.com/c883f3e329c7deb033564f7b5e9be7d4.png" />
&lt;h1 id="github-파이프라인">Github 파이프라인&lt;/h1>
&lt;p>이제 저장소에 코드가 게시되었으므로 이 프로젝트를 컴파일하고 이 &lt;code>dacpac&lt;/code> 파일을 생성하고 다운로드하거나 다른 단계에서 사용할 수 있는 위치에 저장하는 작업을 생성해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">DacpacGithubActions project build&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">push&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">branches&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">main&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">runs-on&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">windows-latest&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">steps&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/checkout@v2&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Setup MSBuild&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">microsoft/setup-msbuild@v1&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Navigate to Workspace&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">cd $GITHUB_WORKSPACE&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Create Build Directory&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">mkdir artifacts&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Build Solution&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>|&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> msbuild.exe /t:DacpacGithubActions /p:DebugSymbols=false /p:DebugType=None /p:DeployOnBuild=true /p:WebPublishMethod=FileSystem /p:OutDir=&amp;#34;../artifacts&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Upload artifact&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/upload-artifact@v1.0.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">with&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">DacpacGithubActionsArtifacts&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">path&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;../artifacts&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 작업은 여러 작업을 수행하고 솔루션을 빌드하고 결과를 미리 생성된 &lt;code>artifacts&lt;/code> 폴더에 저장한 다음 해당 폴더의 파일을 아티팩트에 업로드합니다.&lt;/p>
&lt;h1 id="파이프라인-실행">파이프라인 실행&lt;/h1>
&lt;p>이제 빌드를 트리거해 보세요. 결과는 다음과 같습니다.&lt;/p>
&lt;img src="https://i.gyazo.com/9fe0b7a44be0f07bcc41fe0862183a54.png" />
&lt;p>실제로 아티팩트를 다운로드하고 콘텐츠를 살펴보면 향후 지속적인 배포 단계를 구현할 때 필요한 것이 바로 &lt;code>dacpac&lt;/code> 파일입니다!&lt;/p>
&lt;img src="https://i.gyazo.com/287a392df0c5e966958262644e335149.png" />
&lt;h1 id="코드">코드&lt;/h1>
&lt;p>이 전체 프로젝트는 Github에 있으며 &lt;a href="https://github.com/emimontesdeoca/dacpac-github-actions">여기&lt;/a>에서 찾을 수 있습니다!&lt;/p>
&lt;p>문제나 질문이 있는 경우 @emimontesdeoca로 소셜 미디어를 통해 저에게 연락해 주세요(Twitter에서는 실제로 &lt;code>@emimontesdeocaa&lt;/code>이고 끝에 두 개의 &lt;code>aa&lt;/code>가 있습니다). 블로그 헤더에서 내 소셜 미디어의 대부분을 찾을 수도 있습니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! 시아!&lt;/p></content:encoded><category>CI/CD</category></item><item><title>Blazor에서 Javascript Interop을 사용하여 테마 전환</title><link>https://emimontesdeoca.github.io/ko/posts/blazor-toggle-darkmode/</link><pubDate>Fri, 01 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/blazor-toggle-darkmode/</guid><description>JavaScript Interop 및 CSS 데이터 속성을 사용하여 Blazor에서 밝은 테마와 어두운 테마 토글을 구현합니다.</description><content:encoded>&lt;p>웹 페이지를 열 때마다 플래시백으로 인해 어려움을 겪는 저와 같은 사람들을 위해 Javascript를 사용하여 밝은 모드와 어두운 모드 사이를 전환하고 Javascript Interop을 사용하여 Blazor에서 호출하는 방법에 대한 매우 간단한 솔루션을 제시했습니다.&lt;/p>
&lt;img src="https://media-exp1.licdn.com/dms/image/C4D22AQEYLTFA1e7i9A/feedshare-shrink_800/0/1641493691714?e=1651708800&amp;v=beta&amp;t=j6RxTrY--qUwxOcbt8Xh4QE9nYYF1zlJBMZP9dDLuC8" />
&lt;h1 id="부모-속성-설정">부모 속성 설정&lt;/h1>
&lt;p>우선, 식별자로 상위 요소를 식별해야 합니다. 이는 해당 식별자의 값에 따라 색상을 변경하기 때문입니다.&lt;/p>
&lt;p>Javascript에서 이를 수행하려면 다음 명령을 실행해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>document.documentElement.setAttribute(&lt;span style="color:#a5d6ff">&amp;#39;data-theme&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;YOUR_IDFENTIFIER&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>우리의 경우 각 스타일 유형에 대해 &lt;code>light&lt;/code> 및 &lt;code>dark&lt;/code>라는 두 개의 식별자를 갖고 싶습니다.&lt;/p>
&lt;h2 id="로직을-처리하기-위한-자바스크립트-파일-생성">로직을 처리하기 위한 자바스크립트 파일 생성&lt;/h2>
&lt;p>이제 식별자를 업데이트하는 기능이 생겼습니다. 이제 해당 로직을 실행하는 기능을 사용하여 Javscript 파일을 만들어 보겠습니다. 파일 이름은 &lt;code>app.js&lt;/code>입니다.&lt;/p>
&lt;img src="https://i.gyazo.com/c481aa0ed8329e8592832e9da2921cea.png" />
&lt;p>해당 파일에는 앞서 언급한 코드를 호출하는 함수가 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> toggleTheme &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#ff7b72">function&lt;/span> (identifier) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> document.documentElement.setAttribute(&lt;span style="color:#a5d6ff">&amp;#39;data-theme&amp;#39;&lt;/span>, identifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="blazor에-javascript-파일-추가">Blazor에 Javascript 파일 추가&lt;/h2>
&lt;p>스크립트가 있는 위치에 추가하여 Blazor 애플리케이션에 스크립트를 추가합니다.&lt;/p>
&lt;p>&lt;code>&amp;lt;script src=&amp;quot;~/js/app.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code>&lt;/p>
&lt;h1 id="서비스-생성">서비스 생성&lt;/h1>
&lt;p>이제 Javascript 코드가 있으므로 &lt;code>ThemeToggleService&lt;/code>라는 서비스를 생성해야 합니다.&lt;/p>
&lt;img src="https://i.gyazo.com/e6480acaf46eef68f65b16a0738683ce.png" />
&lt;p>이 서비스는 &lt;code>light&lt;/code> 테마와 &lt;code>dark&lt;/code> 테마 사이를 전환하는 로직을 처리합니다.&lt;/p>
&lt;h2 id="jsinterop-주입">JSInterop 주입&lt;/h2>
&lt;p>Javascipt를 호출하려면 JSInterop을 호출해야 하므로 주입할 속성을 생성해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.JSInterop&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IJSRuntime jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ThemeToggleService(IJSRuntime jSRuntime)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.jSRuntime = jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="식별자를-설정하는-함수">식별자를 설정하는 함수&lt;/h2>
&lt;p>이제 서비스가 생성되었으므로 식별자를 설정하는 함수를 생성해 보겠습니다. 이 함수는 페이지의 &lt;code>data-theme&lt;/code>만 업데이트합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.JSInterop&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ThemeToggleService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IJSRuntime jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ThemeToggleService(IJSRuntime jSRuntime)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.jSRuntime = jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsLightTheme { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetIdentifier =&amp;gt; IsLightTheme ? &lt;span style="color:#a5d6ff">&amp;#34;light&amp;#34;&lt;/span> : &lt;span style="color:#a5d6ff">&amp;#34;dark&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ToggleTheme() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsLightTheme = !IsLightTheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> jSRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;toggleTheme&amp;#34;&lt;/span>, GetIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="시작-시-서비스-추가">시작 시 서비스 추가&lt;/h2>
&lt;p>모든 구성 요소와 페이지에서 서비스를 사용할 수 있도록 하려면 해당 서비스를 &lt;code>Program.cs&lt;/code> 파일에 추가해야 합니다.&lt;/p>
&lt;p>&lt;code>builder.Services.AddSingleton&amp;lt;ThemeToggleService&amp;gt;();&lt;/code>&lt;/p>
&lt;p>그러면 파일은 다음과 같이 끝납니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components.Web&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddServerSideBlazor();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;WeatherForecastService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;ThemeToggleService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapBlazorHub();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapFallbackToPage(&lt;span style="color:#a5d6ff">&amp;#34;/_Host&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Blazor &lt;code>Server&lt;/code> 또는 Blazor &lt;code>WebAssembly&lt;/code>를 사용하는지에 따라 변경될 수 있다는 점에 유의하세요.&lt;/p>
&lt;h1 id="css-굽기">CSS 굽기&lt;/h1>
&lt;p>이제 코드의 일부가 준비되었으므로 CSS 작업을 시작해야 합니다. 우리가 해야 할 일은 CSS 색상, 배경 등을 처리하는 모든 CSS 속성을 변수로 설정하는 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>:&lt;span style="color:#d2a8ff;font-weight:bold">root&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--background-color&lt;/span>:&lt;span style="color:#a5d6ff">#ffffff&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--text-color&lt;/span>:&lt;span style="color:#a5d6ff">#000000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 작업을 완료하면 식별자를 사용하여 식별자 간에 쉽게 변경할 수 있으며 다른 값 중 하나를 사용하게 됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#7ee787">data-theme&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;dark&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--background-color&lt;/span>: &lt;span style="color:#a5d6ff">#000000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--text-color&lt;/span>: &lt;span style="color:#a5d6ff">#ffffff&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>멋진 애니메이션 페이딩을 위해 &lt;code>transition&lt;/code> 속성을 모두 추가하면 보기에도 좋습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">*&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">transition&lt;/span>: &lt;span style="color:#79c0ff">all&lt;/span> &lt;span style="color:#a5d6ff">250&lt;/span>&lt;span style="color:#ff7b72">ms&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 이 변수를 일부 CSS 클래스에 할당하여 Blazor 페이지에서 볼 수 있도록 하겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">app-background&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">background-color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>background&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">width&lt;/span>: &lt;span style="color:#a5d6ff">200&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">height&lt;/span>: &lt;span style="color:#a5d6ff">200&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">app-text&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">text&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>head&lt;/code>에 파일을 추가하여 Blazor 애플리케이션에 파일을 추가하는 것을 잊지 마세요.&lt;/p>
&lt;p>&lt;code>&amp;lt;link href=&amp;quot;css/app.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot; /&amp;gt;&lt;/code>&lt;/p>
&lt;h1 id="blazor에서-호출">Blazor에서 호출&lt;/h1>
&lt;p>그럼 이것을 어떻게 테스트해볼까요? 간단하게 유지하기 위해 &lt;code>app-background&lt;/code> 클래스가 있는 div를 추가하고 그 안에 &lt;code>app-text&lt;/code> 클래스가 있는 &lt;code>p&lt;/code>가 있습니다.이제 &lt;code>Pages&lt;/code> 폴더 아래에 &lt;code>Theme.razor&lt;/code>라는 페이지를 만들고 이에 대한 코드를 추가해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/theme&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using BlazorDarkmodeToggle.Data
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject ThemeToggleService ThemeToggleService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> @onclick=Toggle&amp;gt;Toggle theme&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;br /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;app-background&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p class=&lt;span style="color:#a5d6ff">&amp;#34;app-text&amp;#34;&lt;/span>&amp;gt;Hello world!&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task Toggle()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">this&lt;/span>.ThemeToggleService.ToggleTheme();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>/theme&lt;/code>로 이동하여 이를 테스트할 수 있지만 탐색 모음에도 추가해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;nav-item px-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NavLink class=&lt;span style="color:#a5d6ff">&amp;#34;nav-link&amp;#34;&lt;/span> href=&lt;span style="color:#a5d6ff">&amp;#34;theme&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#a5d6ff">&amp;#34;oi oi-list-rich&amp;#34;&lt;/span> aria-hidden=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt; Theme togle
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NavLink&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="테스트">테스트&lt;/h1>
&lt;img src="https://i.gyazo.com/a4894edb345cfd8c9f0cf2de869dba32.gif" />
&lt;p>보시다시피, 테마, 배경 및 글꼴 색상 변경을 전환하자마자 이것이 얼마나 강력한지 알 수 있습니다. 실제로 CSS에서 이 변수를 사용하여 전체 페이지를 개발하면 다양한 테마를 갖고 전환할 수 있습니다!&lt;/p>
&lt;p>정말 대단해요!!&lt;/p>
&lt;h1 id="코드">코드&lt;/h1>
&lt;p>이 전체 프로젝트는 Github에 있으며 &lt;a href="https://github.com/emimontesdeoca/BlazorDarkmodeToggle">여기&lt;/a>에서 찾을 수 있습니다!&lt;/p>
&lt;p>문제나 질문이 있는 경우 소셜 미디어 @emimontesdeoca로 언제든지 저에게 연락해 주세요(트위터에서는 실제로 &lt;code>@emimontesdeocaa&lt;/code>이고 끝에 두 개의 &lt;code>aa&lt;/code>가 있습니다). 블로그 헤더에서 내 소셜 미디어의 대부분을 찾을 수도 있습니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다!&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0">ASP.NET Core Blazor의 .NET 메서드에서 JavaScript 함수 호출&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>Docker</category></item><item><title>Blazor의 Azure Blob Storage에 파일 업로드</title><link>https://emimontesdeoca.github.io/ko/posts/uploading-files-az-blob-blazor/</link><pubDate>Wed, 23 Mar 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/uploading-files-az-blob-blazor/</guid><description>기본 HTML5 파일 입력을 사용하여 Blazor 앱에서 Azure Blob Storage에 파일을 업로드합니다.</description><content:encoded>&lt;p>Azure Blob 저장소 서비스는 Azure 생태계에서 가장 많이 사용되는 서비스 중 하나이며, 매우 저렴하면서 많은 파일을 클라우드에 업로드할 수 있습니다. 또한 직접 링크를 통해 액세스하거나 다운로드할 수 있는 직관적인 웹이 있습니다.&lt;/p>
&lt;p>전반적으로 꽤 좋은 서비스입니다. 저는 이 서비스를 업무와 개인 프로젝트 모두에 사용해 왔으며 솔직히 가장 좋은 점은 작동 방법이 단순하다는 것입니다.&lt;/p>
&lt;p>제가 Azure Blob Storage를 사용한 대부분의 경우는 백엔드에서 작업 등의 결과를 직접 업로드하는 것입니다. 그래서 저는 실제로 웹사이트 등에서 어떤 종류의 파일 업로드도 해본 적이 없습니다. 불길한!&lt;/p>
&lt;p>저는 Blazor의 팬이기 때문에 HTML5의 기본 파일 입력을 사용하여 Azure Blob Storage에 파일을 업로드하는 방법을 소개하겠습니다.&lt;/p>
&lt;h1 id="전제-조건">전제 조건&lt;/h1>
&lt;ul>
&lt;li>Azure 계정(없으면 &lt;a href="aka.ms/free">여기&lt;/a>에서 $$$를 사용하여 계정을 만드세요.&lt;/li>
&lt;li>IDE(VS, Code 등 모두 작동)&lt;/li>
&lt;li>.NET Core 3.0 이상&lt;/li>
&lt;/ul>
&lt;h1 id="azure-blob-저장소-생성">Azure Blob 저장소 생성&lt;/h1>
&lt;p>Azure Portal로 이동하여 스토리지 계정을 만들어 보겠습니다.&lt;/p>
&lt;p>먼저 &lt;code>Storage accounts&lt;/code>를 검색하고 결과를 클릭하세요.&lt;/p>
&lt;img src="https://i.gyazo.com/dfc7db88129cd5e2a5015a7bfd846685.png" />
&lt;p>로드된 후 &lt;code>Create&lt;/code>를 클릭합니다.&lt;/p>
&lt;p>여러 단계가 포함된 양식이 표시되므로 계속해서 작성하세요.&lt;/p>
&lt;img src="https://i.gyazo.com/2293db6fea38aa8b9c61980d088c56c4.png" />
&lt;p>원하는 설정으로 모든 항목을 채운 후 유효성 검사를 통과하고 리소스를 생성합니다.&lt;/p>
&lt;img src="https://i.gyazo.com/f416db52c958744f8e9ca1dbe904a790.png" />
&lt;p>생성된 후 &lt;code>Go to resource&lt;/code>를 클릭합니다. 우리는 API를 가지고 놀 수 있는 연결 문자열을 얻을 것입니다.&lt;/p>
&lt;img src="https://i.gyazo.com/713702e57e8c18db16ec214ea999507f.png" />
&lt;h1 id="리소스-키를-가져옵니다">리소스 키를 가져옵니다&lt;/h1>
&lt;p>새로 생성된 리소스에 도달한 후 &lt;code>Security + networking&lt;/code> 아래의 &lt;code>Acccess keys&lt;/code>로 이동합니다. 로드한 후에는 &lt;code>Connection string&lt;/code> 및 &lt;code>Key&lt;/code>가 표시됩니다. 나중에 필요하므로 복사하세요!&lt;/p>
&lt;img src="https://i.gyazo.com/87af79ff72e5973f3fbcdf22547d2c54.png" />
&lt;h1 id="멋진-blazor-서버-프로젝트-만들기">멋진 Blazor 서버 프로젝트 만들기&lt;/h1>
&lt;p>이 튜토리얼에서는 Visual Studio 2022를 사용할 예정이지만 앞서 말했듯이 다른 IDE를 사용하고 &lt;code>dotnet&lt;/code> CLI를 사용하여 프로젝트를 생성하면 됩니다.&lt;/p>
&lt;p>그런 다음 Visual Studio를 사용하여 여러 단계를 거쳐 Blazor 프로젝트를 매우 빠르게 만들어 보겠습니다.&lt;/p>
&lt;img src="https://i.gyazo.com/fab7bbabc9601132c8554c0b83ff4f58.png" />
&lt;img src="https://i.gyazo.com/dd015b5c8f1a46c2df0fa5af7cfc08e4.png" />
&lt;img src="https://i.gyazo.com/e7225a77fb3a91c2fb5d39877165e9b8.png" />
&lt;p>방금 만든 것을 실행하면 다음과 같이 보일 것입니다. 평범한 일반 Blazor 애플리케이션입니다.&lt;/p>
&lt;img src="https://i.gyazo.com/95785a6c6cc68050d9989c489df0f599.png" />
&lt;p>이제 프로젝트가 생성되었으므로 몇 가지 UI 작업을 수행하여 리소스의 키, 파일에 대한 &lt;code>InputFile&lt;/code>, 작업을 수행하기 위한 버튼 및 작업으로 인해 발생하는 메시지를 채울 수 있는 여러 입력이 포함된 새 페이지를 만들어 보겠습니다.&lt;/p>
&lt;h2 id="모델-생성">모델 생성&lt;/h2>
&lt;p>이를 제대로 수행하려면 몇 가지 모델이 필요합니다. 먼저 연결 문자열, 컨테이너 이름 및 파일을 처리할 &lt;code>BlobRequest&lt;/code> 클래스가 있습니다.&lt;/p>
&lt;p>또한 모든 것을 깔끔하게 유지하기 위해 &lt;code>Name&lt;/code> 및 &lt;code>Data&lt;/code>를 저장할 &lt;code>BlobFile&lt;/code>라는 클래스를 생성합니다.&lt;/p>
&lt;p>&lt;code>Models&lt;/code>라는 폴더를 만들어서 모든 것을 분리했습니다.&lt;/p>
&lt;p>수업은 매우 간단합니다.```csharp
using System.ComponentModel.DataAnnotations;&lt;/p>
&lt;p>namespace UploadingFilesAzBlobBlazor.Models {
public class BlobRequest
{
[Required(ErrorMessage = &amp;ldquo;Connection string is required&amp;rdquo;)]
public string? ConnectionString { get; set; }
[Required(ErrorMessage = &amp;ldquo;Container name is required&amp;rdquo;)]
public string? Container { get; set; }
public List&lt;BlobFile>? Files { get; set; }
}
}&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>namespace UploadingFilesAzBlobBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public &lt;span style="color:#ff7b72">class&lt;/span> BlobFile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public string&lt;span style="color:#f85149">?&lt;/span> Name { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public byte[]&lt;span style="color:#f85149">?&lt;/span> Data { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ui-수정">UI 수정&lt;/h2>
&lt;p>이제 모델이 생성되었으므로 이를 사용하여 UI에 마법을 걸어보겠습니다!&lt;/p>
&lt;p>먼저 &lt;code>Pages&lt;/code> 폴더 아래에 &lt;code>Upload.razor&lt;/code>라는 페이지를 만듭니다.&lt;/p>
&lt;p>이 페이지에서는 &lt;code>BlobRequest&lt;/code> 클래스를 채울 작은 양식을 추가할 것이므로 &lt;code>ConnectionString&lt;/code>, &lt;code>Container&lt;/code> 및 &lt;code>Files&lt;/code>에 대한 입력을 추가해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/upload&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using UploadingFilesAzBlobBlazor.Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject IJSRuntime JsRuntime
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt; Upload&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt; Azure blob storage uploader!&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=&lt;span style="color:#a5d6ff">&amp;#34;@modal&amp;#34;&lt;/span> OnValidSubmit=&lt;span style="color:#a5d6ff">&amp;#34;@HandleValidSubmit&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;DataAnnotationsValidator /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ValidationSummary /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;connectionString&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Connection &lt;span style="color:#ff7b72">string&lt;/span>&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputText class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;connectionString&amp;#34;&lt;/span> @bind-Value=&lt;span style="color:#a5d6ff">&amp;#34;modal.ConnectionString&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Container name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputText class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span> @bind-Value=&lt;span style="color:#a5d6ff">&amp;#34;modal.Container&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;formFileMultiple&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Files&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputFile OnChange=&lt;span style="color:#a5d6ff">&amp;#34;OnInputFileChange&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;formFileMultiple&amp;#34;&lt;/span> multiple /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span>&amp;gt; Submit&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> BlobRequest modal = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> EditContext editContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IReadOnlyList &amp;lt;IBrowserFile&amp;gt; selectedFiles;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInputFileChange(InputFileChangeEventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> selectedFiles = e.GetMultipleFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> .StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task HandleValidSubmit() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This is where we are going to upload stuff ! &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Files uploaded!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>약간의 코드이지만 기본적으로 양식을 렌더링하고 유효성 검사를 처리합니다.&lt;/p>
&lt;img src="https://i.gyazo.com/00d0ad6d23fa93bdb0fb354218018c5f.png"/>
&lt;p>업로드 버튼을 누르고 모든 것이 정상이면 경고가 표시됩니다.&lt;/p>
&lt;img src="https://i.gyazo.com/27a3ff831fa9936bac21d8aa8ff60936.gif"/>
&lt;h2 id="azure-blob-storage에-업로드">Azure Blob Storage에 업로드&lt;/h2>
&lt;p>이제 대부분의 UI가 완료되었으므로 Azure Blob Storage API를 처리하고 파일을 업로드할 수 있는 패키지를 설치해 보겠습니다. 꽤 간단합니다.&lt;/p>
&lt;h3 id="nuget-패키지-추가">NuGet 패키지 추가&lt;/h3>
&lt;p>프로젝트에서 NuGet 패키지를 관리하고 &lt;code>Azure.Storage.Blob&lt;/code>를 추가해 보겠습니다.&lt;/p>
&lt;img src="https://i.gyazo.com/c6ecd7b13c0a63a50f38342407acee49.png"/>
&lt;h3 id="azure-blob-storee에-업로드">Azure Blob Storee에 업로드&lt;/h3>
&lt;p>이제 실제로 파일을 업로드하기 위한 논리를 수행해 보겠습니다. 일반적으로 연결 문자열과 app.config의 키를 사용하지만 이 자습서에서는 입력을 사용하고 거기에 데이터를 제공합니다.&lt;/p>
&lt;p>먼저 Azure Blob Storage 클래스에 &lt;code>using&lt;/code>를 추가해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@using Azure.Storage.Blobs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using Azure.Storage.Blobs.Models
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 Blob Storage에 연결하고 컨테이너가 없으면 컨테이너를 만든 다음 파일을 업로드하는 &lt;code>HandleValidSubmit&lt;/code> 메서드에 대해 작업하겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task HandleValidSubmit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Instantiate container&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> container = &lt;span style="color:#ff7b72">new&lt;/span> BlobContainerClient(modal.ConnectionString, modal.Container);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create container if not exists&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createResponse = &lt;span style="color:#ff7b72">await&lt;/span> container.CreateIfNotExistsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Set access policy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (createResponse != &lt;span style="color:#79c0ff">null&lt;/span> &amp;amp;&amp;amp; createResponse.GetRawResponse().Status == &lt;span style="color:#a5d6ff">201&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> container.SetAccessPolicyAsync(Azure.Storage.Blobs.Models.PublicAccessType.Blob);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// For each file that we have uploaded&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> file &lt;span style="color:#ff7b72">in&lt;/span> selectedFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// New blob&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> blob = container.GetBlobClient(file.Name);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Delete any blob with the same name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> blob.DeleteIfExistsAsync(Azure.Storage.Blobs.Models.DeleteSnapshotsOption.IncludeSnapshots);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create a file stream and use the UploadSync method to upload the Blob.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> fileStream = file.OpenReadStream())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> blob.UploadAsync(fileStream, &lt;span style="color:#ff7b72">new&lt;/span> BlobHttpHeaders { ContentType = file.ContentType });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Display success message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Files uploaded!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Display error message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;An error ocurred!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="코드-테스트하기">코드 테스트하기&lt;/h3>
&lt;p>이제 코드를 테스트할 시간입니다. 테스트하려면 양식을 작성하고 제출을 클릭하세요.&lt;/p>
&lt;img src="https://i.gyazo.com/4ce26ec6487038f3bc70c8e977b16215.png"/>
&lt;p>경고가 표시되고 파일이 Azure Blob Storage에 업로드되어야 합니다. Azure Portal로 이동하여 살펴보세요.&lt;/p>
&lt;img src="https://i.gyazo.com/8c070489d4aa6a9f917d685a2ba670f3.png"/>
&lt;p>보시다시피 컨테이너도 생성되었고, 이제 컨테이너 안으로 들어가면 우리가 업로드한 파일이 보입니다.&lt;/p>
&lt;img src="https://i.gyazo.com/166076b94e065578a3aaa4012d3b5c37.png"/>
&lt;p>잘했어요!&lt;/p>
&lt;h3 id="리팩터링">리팩터링&lt;/h3>
&lt;p>이제 모든 것이 작동했으므로 코드를 &lt;code>.razor&lt;/code> 페이지에서 &lt;code>.razor.cs&lt;/code> 클래스로 이동하여 더 보기 좋게 만들어 보겠습니다.&lt;/p>
&lt;p>Visual Studio 2022를 사용하는 경우 &lt;code>@code&lt;/code>에 마우스를 가져가면 전구가 있고 &lt;code>Extract block to code behind&lt;/code> 옵션이 표시되며 표시된 대로 작동합니다!&lt;/p>
&lt;img src="https://i.gyazo.com/1a4b52c9adf1728860b1965bc9b11bdd.png"/>
&lt;p>이제 모든 것이 분리되어 완료됩니다!&lt;/p>
&lt;h1 id="코드">코드&lt;/h1>
&lt;p>이 전체 프로젝트는 Github에 있으며 &lt;a href="https://github.com/emimontesdeoca/UploadingFilesAzBlobBlazor">여기&lt;/a>에서 찾을 수 있습니다!&lt;/p>
&lt;p>문제나 질문이 있는 경우 @emimontesdeoca로 소셜 미디어를 통해 저에게 연락해 주세요. (트위터에서는 실제로 &lt;code>@emimontesdeocaa&lt;/code> 끝에 두 개의 &lt;code>aa&lt;/code>가 있습니다.) 블로그 헤더에서 내 소셜 미디어의 대부분을 찾을 수도 있습니다.&lt;/p>
&lt;p>게시물이 마음에 드셨기를 바랍니다! 시아!&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://www.nuget.org/packages/Azure.Storage.Blobs">https://www.nuget.org/packages/Azure.Storage.Blobs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet">https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>Azure</category><category>NuGet</category><category>Docker</category></item><item><title>Pimcore의 핵심 컨트롤러 재정의</title><link>https://emimontesdeoca.github.io/ko/posts/override-method-pimcore-core-bundles/</link><pubDate>Thu, 10 Dec 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/override-method-pimcore-core-bundles/</guid><description>서비스 구성으로 사용자 정의 Symfony 번들을 생성하여 핵심 Pimcore 컨트롤러를 재정의합니다.</description><content:encoded>&lt;p>﻿저는 한동안 Pimcore 프로젝트를 진행해 왔지만 평생 PHP와 Symfony를 접해본 적이 없었기 때문에 정말 큰 도전이었습니다.&lt;/p>
&lt;p>문서는 훌륭합니다. 정말 훌륭하지만 언어/프레임워크 초보자를 위해 만들어진 것이 아닌 것 같아서 제가 수행한 모든 작업을 기록해야 했습니다.&lt;/p>
&lt;p>번들을 사용하여 Pimcore를 확장할 수 있다는 사실을 배운 후 컨트롤러, 자바스크립트 메서드 등을 재정의하는 데 많은 시간을 보냈습니다.&lt;/p>
&lt;p>그래서 이번 포스팅에서는 컨트롤러를 어떻게 재정의했는지 설명하겠습니다. 자바스크립트 파일을 재정의하는 방법은 나중에 나올 예정입니다 8.&lt;/p>
&lt;h1 id="번들-생성">번들 생성&lt;/h1>
&lt;p>우선 번들을 생성해야 합니다. &lt;a href="https://pimcore.com/docs/pimcore/current/Development_Documentation/Extending_Pimcore/Bundle_Developers_Guide/index.html">Pimcore 문서&lt;/a>에 설명된 대로 매우 간단합니다. 프로젝트 폴더에서 &lt;code>bin/console pimcore:generate:bundle --namespace=EmiDemo/EmiDemoBundle&lt;/code>을 실행하기만 하면 됩니다.&lt;/p>
&lt;p>몇 가지 질문이 표시되지만 걱정할 것은 없습니다.&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/9aa04169e668506d18638388d0061910.png" />&lt;/div>
&lt;p>이전에 선언한 네임스페이스를 사용하여 &lt;code>src&lt;/code> 폴더 아래에 새 폴더가 생성되며, 그 안에는 번들에 필요한 모든 파일이 포함됩니다.&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/4d3329c303bb43012faaae43290c613b.png" />&lt;/div>
&lt;p>또한 플러그인은 Pimcore 관리 사이트에서 감지되지만 비활성화되므로 사용을 시작하려면 활성화해야 합니다.&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/e0170db9111df69a00bdb3f10473c9a7.png" />&lt;/div>
&lt;h1 id="컨트롤러에서-메서드-재정의">컨트롤러에서 메서드 재정의&lt;/h1>
&lt;p>컨트롤러에서 메서드를 재정의하려면 먼저 업데이트하려는 작업을 찾아야 합니다. 이것은 쉬운 것 같지만 제가 일반적으로 수행하는 방법을 알려 드리겠습니다(팀 동료인 &lt;a href="https://twitter.com/cesabreu">Cesar&lt;/a>에게서 배웠습니다).&lt;/p>
&lt;p>먼저 컨트롤러가 작업을 수행한다고 생각되는 페이지로 이동합니다. 제 경우에는 자산을 열 때 로드되는 페이지를 확인하고 싶습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/df3833858806b14a39f50a0707a19dcd">&lt;img src="https://i.gyazo.com/df3833858806b14a39f50a0707a19dcd.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그런 다음 콘솔을 열고 네트워크 탭으로 이동하여 동일한 단계를 다시 수행하고 작업을 찾으십시오.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5fbd9496d585d145bea3f9a3b950de73">&lt;img src="https://i.gyazo.com/5fbd9496d585d145bea3f9a3b950de73.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>이 정보를 통해 자산을 로드하는 작업이 &lt;code>http://localhost/admin/asset/get-data-by-id?_dc=1607601778450&amp;amp;id=2&amp;amp;type=image&lt;/code>임을 알 수 있습니다. 이를 통해 컨트롤러 작업이 &lt;code>get-data-by-id&lt;/code>임을 알 수 있습니다.&lt;/p>
&lt;h2 id="코어-컨트롤러에서-액션-찾기">코어 컨트롤러에서 액션 찾기&lt;/h2>
&lt;p>나에게 가장 간단한 방법은 Visual Studio Code의 모든 파일을 찾는 것입니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/96cbeea01a1174d45e5a263a997c882a">&lt;img src="https://i.gyazo.com/96cbeea01a1174d45e5a263a997c882a.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>아마도 둘 이상을 찾을 것이므로 살펴보고 어느 것이 하나인지 결정해야 합니다. 우리의 경우 자산으로 작업 중이므로 &lt;code>AssetController.php&lt;/code>를 사용하려는 것이 분명합니다.&lt;/p>
&lt;h2 id="코어-컨트롤러-수정">코어 컨트롤러 수정&lt;/h2>
&lt;p>개발자에 따라 다르며 컨트롤러를 재정의하고 거기에서 개발하는 것이 더 빠르기 때문에 일반적으로 수행하지 않습니다. 하지만 먼저 코어 번들의 컨트롤러를 업데이트하여 변경 사항이 작동하는지 확인하는 것이 좋습니다.&lt;/p>
&lt;p>우리의 경우에는 메서드 시작 부분에 메시지를 반환하여 제대로 작동하는지 확인하겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">$this&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&amp;gt;&lt;/span>adminJson([&lt;span style="color:#a5d6ff">&amp;#39;success&amp;#39;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#ff7b72">false&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;message&amp;#39;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Overriding the getDataByIdAction in the core!!&amp;#34;&lt;/span>]);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://gyazo.com/32139d67b9e5369d5ab38daed1b229ea">&lt;img src="https://i.gyazo.com/32139d67b9e5369d5ab38daed1b229ea.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그런 다음 자산을 다시 로드하고 네트워크 탭을 확인해 보겠습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d1653f36e7bece9da6232ede0a431c05">&lt;img src="https://i.gyazo.com/d1653f36e7bece9da6232ede0a431c05.png" alt="Gyazo의 이미지">&lt;/a>거기에 있는 것을 유지하여 나중에 핵심 메시지 대신 번들 메시지를 사용하고 있음을 알 수 있도록 합시다.&lt;/p>
&lt;p>이제 우리가 한 일을 추적하기 위해 임시 파일에 복사해 보겠습니다.&lt;/p>
&lt;h2 id="해당-변경-사항을-번들로-이동">해당 변경 사항을 번들로 이동&lt;/h2>
&lt;p>이제 이러한 변경 사항을 번들로 이동하려면 먼저 핵심 컨트롤러에서 몇 가지 항목이 필요합니다.&lt;/p>
&lt;ul>
&lt;li>네임스페이스&lt;/li>
&lt;li>수입품&lt;/li>
&lt;li>컨트롤러 이름&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://gyazo.com/416b3a527be397aaf6d9f89073c02428">&lt;img src="https://i.gyazo.com/416b3a527be397aaf6d9f89073c02428.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그리고 분명히 우리가 재정의하려는 메서드인 &lt;code>getDataByIdAction&lt;/code>가 있습니다.&lt;/p>
&lt;h2 id="컨트롤러-노출">컨트롤러 노출&lt;/h2>
&lt;p>컨트롤러를 노출해야 합니다. 이를 위해서는 &lt;code>/src/EmiDemo/EmiDemoBundle/Resources/config/pimcore/routing.yml&lt;/code> 파일로 이동하여 다음을 추가해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">options&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">expose&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#79c0ff">true&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>또한 &lt;code>prefix&lt;/code>를 재정의할 항목(이 경우에는 &lt;code>admin&lt;/code> 컨트롤러)으로 변경해야 합니다.&lt;/p>
&lt;p>그래서 결국에는 이런 모습이 될 것입니다&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">emi_demo_emi_demo&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">resource&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@EmiDemoEmiDemoBundle/Controller/&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">type&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">annotation&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">prefix&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">/admin&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">options&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">expose&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#79c0ff">true&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="defaultcontrollerphp">DefaultController.php&lt;/h2>
&lt;p>파일에서 가져오기를 추가하고 재정의할 컨트롤러로 컨트롤러를 확장합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b8d798436ed111d4676312f8aeba443d">&lt;img src="https://i.gyazo.com/b8d798436ed111d4676312f8aeba443d.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그런 다음 코어 컨트롤러에서 메서드를 복사하고, 이 경우 반환되는 메시지를 업데이트합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dcdd9f8166e4ba8820cb8e285c43dec8">&lt;img src="https://i.gyazo.com/dcdd9f8166e4ba8820cb8e285c43dec8.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>기억하신다면 코어 컨트롤러에 이미 &lt;code>Overriding the getDataByIdAction in the core!!&lt;/code> 메시지가 있으며 이제 &lt;code>Overriding the getDataByIdAction in the bundle!!&lt;/code>가 표시되어야 합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8e16b1de2a40292c843c51576acf43c4">&lt;img src="https://i.gyazo.com/8e16b1de2a40292c843c51576acf43c4.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>아무것도 반환하지 않고 이제 이전과 같이 페이지를 볼 수 있는지 살펴보겠습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dda8df5f6d721f5ba25d6a056ac7f9bf">&lt;img src="https://i.gyazo.com/dda8df5f6d721f5ba25d6a056ac7f9bf.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>return 문을 주석 처리하고 다시 로드하세요.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2fdbff7f45c38fb2bc56a3fc73661077">&lt;img src="https://i.gyazo.com/2fdbff7f45c38fb2bc56a3fc73661077.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="그게-다야">그게 다야&lt;/h2>
&lt;p>이 작은 데모를 통해 기존 Pimcore 코어 컨트롤러를 얼마나 쉽게 재정의할 수 있는지 확인할 수 있습니다. 따라야 할 몇 가지 단계가 있지만 어렵지 않습니다. 변경하는 동안 때때로 컨트롤러를 노출하고 캐시를 지우는지 확인하십시오. 때로는 캐시되어 작동하지 않고 단지 캐시만 되어 있다고 생각하게 됩니다.&lt;/p>
&lt;p>자바스크립트 파일을 재정의하는 방법이 궁금하다면 다른 튜토리얼을 통해 확인하세요 😁.&lt;/p></content:encoded></item><item><title>Windows 10에서 Apple Magic Keyboard 2 구성</title><link>https://emimontesdeoca.github.io/ko/posts/configure-apple-keyboard-windows/</link><pubDate>Wed, 11 Nov 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/configure-apple-keyboard-windows/</guid><description>Windows 10에서 제대로 작동하도록 드라이버를 설치하고 Apple Magic Keyboard 2를 구성하십시오.</description><content:encoded>&lt;p>&lt;a href="https://gyazo.com/9c8641cdd22bd528b2141bad1322c74a">&lt;img src="https://i.gyazo.com/9c8641cdd22bd528b2141bad1322c74a.jpg" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>바로 어제 저는 기계식 키보드가 5개 정도 있더라도 Apple Magic Keyboard 2를 구입했습니다. 사용해 보고 싶었고 무선이었기 때문입니다.&lt;/p>
&lt;p>내 주요 OS는 Windows 10입니다. 나는 그것을 좋아하고 그것을 바꾸고 싶지 않습니다. 그래서 이를 염두에 두고 키보드가 완벽하게 작동하려면 몇 가지 작업을 수행해야 한다는 것을 알았습니다. 저는 Apple이 어떻게 작동하는지, 그리고 Apple이 장치를 생태계에 유지하는 방법을 알고 있습니다.&lt;/p>
&lt;h2 id="문제">문제&lt;/h2>
&lt;p>키보드를 페어링하면 다음과 같은 몇 가지 사항을 인식할 수 있습니다.&lt;/p>
&lt;ul>
&lt;li>기능키가 작동하지 않습니다&lt;/li>
&lt;li>일부 키가 잘못 매핑되었습니다(스페인어 버전에서 이런 일이 발생했습니다).&lt;/li>
&lt;/ul>
&lt;h2 id="문서">문서&lt;/h2>
&lt;p>이 기능을 작동시키려면 웹에서 많은 내용을 읽어야 했지만 작동하는 데 도움이 된 링크는 다음 두 가지입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.bluetoothgoodies.com/info/apple-devices/">Windows에서 Apple Magic Keyboard/Mouse/Trackpad를 최대한 활용하세요&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://superuser.com/questions/82826/how-do-i-use-my-f-1-f12-keys-without-pressing-fn-on-windows-7-using-bootcamp-o">Macbook Pro에서 bootcamp를 사용하여 Windows 7에서 FN을 누르지 않고 f-1 - f12 키를 어떻게 사용합니까?&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="apple-키보드-드라이버-설치">Apple 키보드 드라이버 설치&lt;/h2>
&lt;p>이 단계 중 일부는 이전에 언급한 문서에서 가져온 것입니다.&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://www.7-zip.org/">7zip&lt;/a>이 없으면 컴퓨터에 설치합니다.&lt;/li>
&lt;li>&lt;a href="https://www.python.org/downloads/">Python(버전 2.x)&lt;/a>이 없으면 컴퓨터에 설치합니다.
&lt;ul>
&lt;li>중요: 최신 버전의 Python은 3.x입니다. 하지만 Brigadier 스크립트는 버전 3.x와 호환되지 않으므로 버전 2.x가 필요합니다.&lt;/li>
&lt;li>(옵션) 기본적으로 설치 프로그램은 python.exe를 PATH에 추가하지 않습니다. 원한다면 이 옵션을 활성화해야 합니다. (오른쪽 스크린샷 참조)&lt;/li>
&lt;li>이미 다른 버전의 Python이 있는 경우 이 옵션을 활성화하고 싶지 않을 것입니다.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Brigadier(최신 Boot Camp 버전을 다운로드하는 데 도움이 되는 Python 스크립트)를 다운로드합니다.&lt;/li>
&lt;li>다음 링크를 마우스 오른쪽 버튼으로 클릭한 후 &amp;ldquo;다른 이름으로 링크 저장&amp;hellip;&amp;ldquo;을 사용하여 파일을 저장하세요. &lt;a href="https://raw.githubusercontent.com/timsutton/brigadier/master/brigadier">https://raw.githubusercontent.com/timsutton/brigadier/master/brigadier&lt;/a>&lt;/li>
&lt;li>명령 프롬프트 창(DOS 상자라고도 함)을 열고 디렉터리를 Brigadier 스크립트를 다운로드한 위치로 변경합니다.&lt;/li>
&lt;li>Brigadier 스크립트가 &amp;ldquo;brigadier.txt&amp;quot;로 저장되었다고 가정하고 다음 명령을 실행하십시오.
&lt;ul>
&lt;li>경로에 Python 버전 2.x가 있는 경우: python brigadier.txt &amp;ndash;model=MacBook13,2&lt;/li>
&lt;li>그렇지 않은 경우: [Python 버전 2.x 경로]\python.exe brigadier.txt &amp;ndash;model=MacBook13,2&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>부트캠프의 모든 드라이버가 포함된 큰 번들을 다운로드합니다.&lt;/li>
&lt;li>&lt;code>BootCamp&lt;/code>라는 폴더를 생성하고 &lt;code>BootCamp-xxx-yyyyyy\BootCamp\Drivers\Apple\BootCamp.msi&lt;/code> 및 &lt;code>BootCamp-xxx-yyyyyy\BootCamp\Drivers\Apple\AppleKeyboardMagic2&lt;/code>를 여기에 복사합니다.&lt;/li>
&lt;li>관리자 Powershell을 실행하고 &lt;code>BootCamp.msi&lt;/code>를 실행하면 일부 항목이 설치되지만 &lt;code>AppleKeyboardMagic2&lt;/code> 폴더의 내용을 사용하여 드라이버를 업데이트해야 합니다.&lt;/li>
&lt;li>장치 관리자 시작(&lt;code>devmgmt.msc&lt;/code>)&lt;/li>
&lt;li>&lt;code>Human Interface Devices&lt;/code> 노드를 확장합니다.&lt;/li>
&lt;li>&lt;code>Bluetooth HID Device&lt;/code>를 찾습니다.&lt;/li>
&lt;li>&lt;code>AppleKeyboardMagic2&lt;/code> 폴더의 내용을 사용하여 드라이버를 업데이트합니다.&lt;/li>
&lt;li>컴퓨터 재부팅&lt;/li>
&lt;/ol>
&lt;p>이제 Bluetooth 키보드가 Apple 키보드로 감지된 것을 볼 수 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/278f6bd3e419d6688ccfadf6918ff309">&lt;img src="https://i.gyazo.com/278f6bd3e419d6688ccfadf6918ff309.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h3 id="fn-키-동작-업데이트모든-것을-올바르게-설치했다면-fn-키가-기본적으로-활성화되어-있음을-알-수-있습니다-이는-실제로-f5-버튼을-누르려면-fn--f5를-눌러야-함을-의미합니다">FN 키 동작 업데이트모든 것을 올바르게 설치했다면 FN 키가 기본적으로 활성화되어 있음을 알 수 있습니다. 이는 실제로 &lt;code>F5&lt;/code> 버튼을 누르려면 &lt;code>fn&lt;/code> + &lt;code>F5&lt;/code>를 눌러야 함을 의미합니다.&lt;/h3>
&lt;p>이 문제를 해결하기 위해 설명서 섹션에 언급된 대로 regedit의 일부 항목을 변경하여 작동하는 솔루션을 찾았습니다.&lt;/p>
&lt;ol>
&lt;li>regedit 열기&lt;/li>
&lt;li>&lt;code>HKEY_CURRENT_USER\SOFTWARE\Apple Inc.\Apple Keyboard Support&lt;/code>로 이동&lt;/li>
&lt;li>&lt;code>OSXFnBehavior&lt;/code>를 생성하거나 업데이트하고 &lt;code>0&lt;/code>로 설정합니다.&lt;/li>
&lt;li>컴퓨터 재부팅&lt;/li>
&lt;/ol>
&lt;h3 id="키-매핑-업데이트">키 매핑 업데이트&lt;/h3>
&lt;p>매핑에 문제가 있는 경우 &lt;a href="https://www.randyrants.com/category/sharpkeys/">SharpKeys&lt;/a>를 사용하여 업데이트할 수 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ee0301205ffeddaae4241db40002864d">&lt;img src="https://i.gyazo.com/ee0301205ffeddaae4241db40002864d.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>사용하기는 정말 간단하지만 레지스트리를 업데이트하므로 업데이트를 활성화하려면 로그아웃하거나 컴퓨터를 다시 시작해야 합니다.&lt;/p>
&lt;p>내 경우에는 &lt;code>Windows&lt;/code>, &lt;code>alt&lt;/code>, &lt;code>º&lt;/code> 및 &lt;code>&amp;lt;&amp;gt;&lt;/code> 키를 업데이트해야 했습니다.&lt;/p></content:encoded></item><item><title>NuGet 패키지에 컴파일된 라이브러리 추가</title><link>https://emimontesdeoca.github.io/ko/posts/adding-compiled-dll-to-nuget/</link><pubDate>Thu, 01 Oct 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/adding-compiled-dll-to-nuget/</guid><description>nuspec 파일에 컴파일된 라이브러리 참조를 포함하여 NuGet 패키지에서 누락된 DLL을 수정합니다.</description><content:encoded>&lt;p>얼마 전 우리가 업로드한 너겟 패키지에서 나온 것이므로 누락된 라이브러리에서 &lt;code>NullReferenceException&lt;/code>에 대한 버그를 발견했습니다.&lt;/p>
&lt;p>기본적으로 몇 가지 프로젝트를 참조하는 라이브러리 클래스를 컴파일했습니다. 빌드 단계에서는 &lt;code>dll&lt;/code> 파일을 &lt;code>bin&lt;/code> 폴더에 추가하지만 &lt;code>packaging&lt;/code>를 너겟 패키지로 설치하고 다른 솔루션에 설치하면 내가 참조하고 bin 폴더에 복사해야 하는 &lt;code>dll&lt;/code> 파일이 없습니다.&lt;/p>
&lt;h3 id="해결-방법">해결 방법&lt;/h3>
&lt;p>이 문제를 해결하는 것은 매우 간단합니다. &lt;code>nuspec&lt;/code> 파일을 업데이트하고 복사하려는 각 라이브러리에 대해 &lt;code>files&lt;/code> 부분에 &lt;code>file&lt;/code>를 추가해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;package&lt;/span> &lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;metadata&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;id&amp;gt;&lt;/span>$id$&lt;span style="color:#7ee787">&amp;lt;/id&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;version&amp;gt;&lt;/span>$version$&lt;span style="color:#7ee787">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;title&amp;gt;&lt;/span>$title$&lt;span style="color:#7ee787">&amp;lt;/title&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;authors&amp;gt;&lt;/span>$author$&lt;span style="color:#7ee787">&amp;lt;/authors&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;owners&amp;gt;&lt;/span>$author$&lt;span style="color:#7ee787">&amp;lt;/owners&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;requireLicenseAcceptance&amp;gt;&lt;/span>false&lt;span style="color:#7ee787">&amp;lt;/requireLicenseAcceptance&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;license&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;expression&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>MIT&lt;span style="color:#7ee787">&amp;lt;/license&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;projectUrl&amp;gt;&lt;/span>$projectUrl$&lt;span style="color:#7ee787">&amp;lt;/projectUrl&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;iconUrl&amp;gt;&lt;/span>$projectIconUrl$&lt;span style="color:#7ee787">&amp;lt;/iconUrl&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;description&amp;gt;&lt;/span>$description$&lt;span style="color:#7ee787">&amp;lt;/description&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;releaseNotes&amp;gt;&amp;lt;/releaseNotes&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;copyright&amp;gt;&lt;/span>$copyright$&lt;span style="color:#7ee787">&amp;lt;/copyright&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;tags&amp;gt;&amp;lt;/tags&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/metadata&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;files&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile1.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile2.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile3.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile4.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/files&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/package&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded><category>NuGet</category></item><item><title>WSL 홈 디렉터리에서 터미널 로드 bash 만들기</title><link>https://emimontesdeoca.github.io/ko/posts/bash-straight-to-wsl-machine-with-terminal/</link><pubDate>Tue, 22 Sep 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/bash-straight-to-wsl-machine-with-terminal/</guid><description>.bashrc를 통해 Linux 홈 디렉터리에서 직접 WSL 세션을 열도록 Windows 터미널을 구성합니다.</description><content:encoded>&lt;p>출시된 날부터 개발 및 테스트를 위해 WSL을 사랑해 왔지만 터미널 앱이 중단되었고 더할 나위 없이 기뻤고, 보기에도 좋고, 완벽하게 작동하고, 훌륭한 성능을 발휘했습니다.&lt;/p>
&lt;p>제공되는 콘솔을 사용하는 대신 WSL 배포판과 함께 사용하지 않는 것은 어리석은 일입니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/245fb4ff4fbd5297b2a8d9917dbee236">&lt;img src="https://i.gyazo.com/245fb4ff4fbd5297b2a8d9917dbee236.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>이 콘솔은 확실히 훌륭하지만 터미널을 사용한 후에는 돌아갈 수 없습니다. &lt;em>컬러&lt;/em>도 훨씬 더 좋습니다.&lt;/p>
&lt;p>이제 터미널을 실행하고 새 탭을 열어 WSL 배포판을 로드하면 탑재된 사용자 폴더가 로드됩니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dac1264f7b8dbae1f64e769b64c551da">&lt;img src="https://i.gyazo.com/dac1264f7b8dbae1f64e769b64c551da.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>&lt;code>cd ~&lt;/code>만 수행하면 distro 사용자 폴더만 로드되기 때문에 이것은 실제로 문제가 되지 않습니다.&lt;/p>
&lt;p>문제는 이 작업을 계속해서 수행하고 싶지 않으므로 파일을 업데이트하여 자동으로 수행하도록 하겠습니다.&lt;/p>
&lt;h2 id="bashrc">.bashrc&lt;/h2>
&lt;p>터미널이나 콘솔을 실행하고 프로필 폴더로 이동한 다음 즐겨 사용하는 텍스트 편집기를 사용하여 &lt;code>.bashrc&lt;/code> 파일을 편집하세요.&lt;/p>
&lt;p>마지막 명령 앞의 파일 끝에 &lt;code>cd ~&lt;/code>를 추가합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b601aba59b9e877bc3926ab9ceb2b98c">&lt;img src="https://i.gyazo.com/b601aba59b9e877bc3926ab9ceb2b98c.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>저장하고 터미널을 사용하여 WSL 콘솔을 다시 열면 프로필 폴더에 있어야 합니다.&lt;/p>
&lt;p>단일 WSL에 대해 &lt;code>bashrc&lt;/code> 파일을 업데이트하므로 모든 WSL 설치에 대해 이 작업을 수행해야 한다는 점을 명심하세요.&lt;/p></content:encoded></item><item><title>ExpandoObject를 사용한 동적 개체 생성</title><link>https://emimontesdeoca.github.io/ko/posts/dynamic-object-generation-expando-object/</link><pubDate>Fri, 06 Mar 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/dynamic-object-generation-expando-object/</guid><description>유연한 데이터 내보내기를 위해 ExpandoObject를 사용하면 런타임 정의 속성이 포함된 개체를 동적으로 생성할 수 있습니다.</description><content:encoded>&lt;p>자체적으로 정의된 열이 있는 Excel 파일을 동적 열이 있는 새 파일로 변환해야 했는데 심각한 성능 문제 없이 이를 올바르게 수행하는 방법이 다소 혼란스러웠습니다.&lt;/p>
&lt;p>저는 .NET &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject?view=netframework-4.8">ExpandoObject&lt;/a>를 사용하는 &lt;a href="https://www.oreilly.com/content/building-c-objects-dynamically/">여기&lt;/a>를 찾을 수 있는 튜토리얼을 찾았습니다. 이를 통해 객체를 생성하고 동적 구성원을 추가할 수 있습니다.&lt;/p>
&lt;h2 id="expandoobject">ExpandoObject&lt;/h2>
&lt;p>Microsoft 정의는 다음과 같습니다.&lt;/p>
&lt;blockquote>
&lt;p>런타임 시 동적으로 멤버를 추가하고 제거할 수 있는 개체를 나타냅니다.&lt;/p>&lt;/blockquote>
&lt;p>그리고 여기에는 몇 가지 설명이 있습니다.&lt;/p>
&lt;blockquote>
&lt;p>ExpandoObject 클래스를 사용하면 런타임에 해당 인스턴스의 멤버를 추가 및 삭제할 수 있으며 이러한 멤버의 값을 설정하고 가져올 수도 있습니다. 이 클래스는 동적 바인딩을 지원하므로 SampleObject.GetAttribute(&amp;ldquo;sampleMember&amp;rdquo;)와 같은 더 복잡한 구문 대신 SampleObject.sampleMember와 같은 표준 구문을 사용할 수 있습니다.&lt;/p>&lt;/blockquote>
&lt;h2 id="현재-동작">현재 동작&lt;/h2>
&lt;p>&lt;code>List&amp;lt;T&amp;gt;&lt;/code>를 전달하여 이 경우에는 &lt;code>ExcelItem&lt;/code>를 전달하여 &lt;code>Reflection&lt;/code>를 사용하여 &lt;code>xlsx&lt;/code> 파일을 생성하는 &lt;code>ExcelExportService&lt;/code>가 있습니다.&lt;/p>
&lt;p>지금까지 우리의 코드는 다음과 같습니다:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ExcelItem&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Surname { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Age { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> UpdatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ExcelItem&lt;/code>은 Excel 파일을 생성하는 데 사용되는 모든 속성을 포함하는 개체입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">byte&lt;/span>[] GetExcelBytes() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;ExcelItem&amp;gt; excelItems = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;ExcelItem&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExcelExportService exportService = &lt;span style="color:#ff7b72">new&lt;/span> ExcelExportService();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> items &lt;span style="color:#ff7b72">in&lt;/span> itemsToExcel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> excelItems.Add(&lt;span style="color:#ff7b72">new&lt;/span> ExcelItem()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Id = item.Id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = item.Name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Surname = item.Surname,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Age = item.Age,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreatedAt = item.CreatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UpdatedAt = item.UpdatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> exportService.ExportToExcel(excelItems);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 접근 방식은 완벽하게 작동합니다. &lt;code>xlsx&lt;/code> 파일은 각 열이 각 속성인 문제 없이 생성됩니다. 예를 들어 다음과 같이 &lt;code>GetExcelBytes&lt;/code> 메서드를 사용하여 파일에 저장합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>File.WriteAllBytes(&lt;span style="color:#a5d6ff">&amp;#34;path-to-somewhere/my-file.xlsx&amp;#34;&lt;/span>, GetExcelBytes());
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="문제">문제&lt;/h2>
&lt;p>우리가 하고 있는 일은 &lt;code>ExcelItem&lt;/code> 객체의 모든 속성을 사용하는 것이기 때문에 실제로 모든 속성을 사용하지 않고 2~3개만 사용하는 것이 좋습니다.&lt;/p>
&lt;p>또한 요구 사항은 코드를 변경하고 싶지 않다는 것입니다. 모든 작업은 표시할 열을 결정하는 &lt;code>administrator&lt;/code>에 의해 수행되어야 하며 코드의 속성을 알 수도 모를 수도 있습니다.&lt;/p>
&lt;p>TLDR: 모든 것이 동적이어야 합니다. 속성이 있는 객체가 있고 생성된 파일에 해당 객체의 &lt;code>N&lt;/code> 속성이 있는지 확인해야 하지만 하드코딩되지는 않았습니다.&lt;/p>
&lt;h2 id="동적-열">동적 열&lt;/h2>
&lt;p>변경 사항은 &lt;code>dynamic&lt;/code> 객체를 사용하는 것입니다. 왜냐하면 열 목록을 생성하는 데 사용할 객체의 속성을 설정하려고 하기 때문입니다.&lt;/p>
&lt;p>&lt;em>1톤&lt;/em>과 같은 많은 속성을 가진 객체가 있고 변경할 때마다 &lt;code>ExcelItem&lt;/code> 객체를 변경하고 싶지 않고 &lt;code>ColumnExcelItem&lt;/code> 테이블을 생성하고 해당 &lt;code>ExcelItem&lt;/code>를 생성하는 데 사용된다고 가정해 보겠습니다.&lt;/p>
&lt;h3 id="데이터베이스-구조">데이터베이스 구조&lt;/h3>
&lt;p>우리는 이 정의를 다음과 같이 데이터베이스에 저장합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>Id PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 Age
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ExcelItem&lt;/code> 객체의 기존 속성보다 더 적은 값을 가지며, &lt;code>6&lt;/code> 속성을 가지며 데이터베이스에는 &lt;code>3&lt;/code>만 나열됩니다.&lt;/p>
&lt;p>또한 &lt;code>PropertyName&lt;/code>는 동적 할당에 사용할 객체의 속성 이름과 일치해야 합니다.&lt;/p>
&lt;h2 id="객체-만들기">객체 만들기&lt;/h2>
&lt;p>이제 데이터베이스 테이블이 있으므로 이를 가져오고 코드에서 사용할 수 있도록 저장소를 구축해야 합니다.이 부분에 대한 튜토리얼은 만들지 않을 것이며, 새로운 &lt;code>GetExcelBytes()&lt;/code> 함수 시작 부분에서는 바로 건너뛰겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">byte&lt;/span>[] GetExcelBytes(List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; columns) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;ExcelItem&amp;gt; excelItems = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;ExcelItem&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">dynamic&lt;/span>&amp;gt; objectsToExcel = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">dynamic&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExcelExportService exportService = &lt;span style="color:#ff7b72">new&lt;/span> ExcelExportService();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> itemsToExcel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">dynamic&lt;/span> newObject = &lt;span style="color:#ff7b72">new&lt;/span> ExpandoObject();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> col &lt;span style="color:#ff7b72">in&lt;/span> columns)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.AddProperty(newObject, col, product.GetType().GetProperty(col).GetValue(item, &lt;span style="color:#79c0ff">null&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> objectsToExcel.Add(newObject);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> exportService.ExportToExcel(objectsToExcel);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 우리는 &lt;code>ExcelItem&lt;/code>의 일부 속성을 사용하지만 완전히 동적인 객체를 생성하게 됩니다. 6개의 속성을 모두 사용하는 대신 우리는 그 중 우리가 보내고 싶은 속성 3개만 사용하고 있습니다.&lt;/p>
&lt;p>이제 이 &lt;code>ExcelItem&lt;/code> 객체에 200개의 속성이 있다고 가정해 보겠습니다. &lt;em>미친&lt;/em> 일이지만 그런 일이 일어날 수도 있습니다.&lt;/p>
&lt;p>내가 해야 할 유일한 일은 Excel 파일에 렌더링하려는 속성을 &lt;code>ColumnExcelItem&lt;/code> 테이블에 삽입하는 것뿐입니다.&lt;/p>
&lt;h2 id="사례">사례&lt;/h2>
&lt;p>이 경우에는 단일 사례를 사용했지만 일부 사용자에 대해 다른 보고서를 갖고 싶다고 가정해 보겠습니다. &lt;code>admin&lt;/code> 역할이 모든 속성을 가져와야 한다고 가정하면 데이터베이스에 다음과 같은 구조를 생성할 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>ReportingTemplates
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Id PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 Admin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 Users
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>ReportingTemplateItems
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Id TemplateId PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 1 Id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 1 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 1 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>4 1 Age
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>5 1 CreatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>6 1 UpdatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>7 2 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>8 2 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>9 2 Age
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그런 다음 템플릿을 가져오면 서로 다른 열을 가질 수 있으며 모두 동적으로 코드와 관련이 없습니다.&lt;/p>
&lt;p>&lt;code>Admins&lt;/code> 역할을 가진 사용자가 있는 경우 파일에는 &lt;code>Id&lt;/code>, &lt;code>Name&lt;/code>, &lt;code>Surname&lt;/code>, &lt;code>Age&lt;/code>, &lt;code>CreatedAt&lt;/code>, &lt;code>UpdatedAt&lt;/code> 열이 포함됩니다.&lt;/p>
&lt;p>&lt;code>Users&lt;/code> 역할을 사용하여 생성하는 경우 &lt;code>Name&lt;/code>, &lt;code>Age&lt;/code>를 갖게 됩니다. &lt;code>Surname&lt;/code>.&lt;/p>
&lt;h2 id="결론">결론&lt;/h2>
&lt;p>이는 .NET에서 &lt;code>dynamic&lt;/code>가 어떻게 작동하는지 배울 수 있는 멋진 방법이며, 다양한 사용자를 위해 다양한 역할이나 템플릿이 필요하고 하드코딩에 시간을 투자하는 데 별로 관심이 없을 때 정말 좋은 솔루션입니다.&lt;/p></content:encoded><category>.NET</category></item><item><title>TravisCI를 사용하여 NuGet 패키지를 지속적으로 제공</title><link>https://emimontesdeoca.github.io/ko/posts/automated-nuget-deployment-travis-ci/</link><pubDate>Tue, 21 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/automated-nuget-deployment-travis-ci/</guid><description>Travis CI 지속적 전달 파이프라인을 사용하여 NuGet 패키지 컴파일, 테스트 및 게시를 자동화합니다.</description><content:encoded>&lt;p>﻿최근에 Travis CI를 방금 푸시했거나 &lt;code>master&lt;/code> 브랜치에 푸시하려고 하는 코드를 자동으로 테스트하는 도구로 사용하는 방법에 대한 &lt;a href="https://emimontesdeoca.github.io/2020/ci-dotnet-core-and-travis-ci/">튜토리얼&lt;/a>을 작성했습니다. 컴파일하고 테스트하여 최종적으로 변경 상태를 보고합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e4c3f9019fdb8a8d80b2649f4c4bbbde">&lt;img src="https://i.gyazo.com/e4c3f9019fdb8a8d80b2649f4c4bbbde.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>하지만 계속 &lt;strong>더 좋게&lt;/strong> 만들 수 있습니다. 예를 들어 지난 튜토리얼에서 만든 핵심 라이브러리를 &lt;a href="https://www.nuget.org/">NuGet&lt;/a> 피드를 사용하여 공개하여 모든 사람이 라이브러리에서 쉽게 작업할 수 있도록 하고 싶습니다!&lt;/p>
&lt;p>&lt;strong>하지만 어떻게요?&lt;/strong> 모든 사람이 자신의 프로젝트에서 다운로드할 수 있도록 해당 패키지를 어떻게 컴파일하고 테스트한 다음 피드에 게시할까요?&lt;/p>
&lt;p>자, 이것이 😁에 대한 튜토리얼입니다!&lt;/p>
&lt;h1 id="새로운-소식은-무엇인가요">새로운 소식은 무엇인가요?&lt;/h1>
&lt;p>코드와 관련된 변경 사항은 거의 없습니다. 변경해야 할 사항은 패킹, 게시 및 자동화 방법입니다.&lt;/p>
&lt;h1 id="net-코어-cli">.NET 코어 CLI&lt;/h1>
&lt;p>&lt;a href="https://docs.microsoft.com/en-gb/dotnet/core/tools/?tabs=netcore2x">.NET Core CLI&lt;/a>를 사용할 예정이며, 이전 자습서에서 &lt;code>dotnet restore&lt;/code>, &lt;code>dotnet build&lt;/code> 및 &lt;code>dotnet test&lt;/code>와 같은 명령과 함께 사용했습니다.&lt;/p>
&lt;p>이제 우리는 새로운 명령 세트를 사용할 것입니다!&lt;/p>
&lt;h1 id="dotnet-nuget">&lt;code>dotnet nuget&lt;/code>&lt;/h1>
&lt;p>이 도구 세트에 대한 문서는 바로 &lt;a href="https://docs.microsoft.com/en-gb/nuget/reference/dotnet-commands">여기&lt;/a>에서 확인할 수 있지만 간략한 설명은 다음과 같습니다.&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>dotnet nuget&lt;/code>는 패키지 설치, 복원, 제거 및 게시를 포함하는 필수 도구 세트입니다.&lt;/p>&lt;/blockquote>
&lt;h1 id="nuget-피드">NuGet 피드&lt;/h1>
&lt;p>&lt;a href="https://www.nuget.org/">NuGet은 .NET용 패키지 관리자입니다&lt;/a>. NuGet 클라이언트 도구는 패키지를 생성하고 사용하는 기능을 제공합니다. NuGet 갤러리는 모든 패키지 작성자와 소비자가 사용하는 중앙 패키지 저장소입니다.&lt;/p>
&lt;p>NuGet은 무료이므로 가입하고 API 키를 받아 패키지 업로드를 시작할 수 있습니다. &lt;strong>모든 사람&lt;/strong>에게 업로드되고 확인된 후 나열됩니다.&lt;/p>
&lt;h1 id="api-키를-가져옵니다">API 키를 가져옵니다&lt;/h1>
&lt;p>패키지를 업로드하려면 가입 후 API 키를 받아야 하므로 &lt;a href="https://www.nuget.org/account/apikeys">API 키 페이지&lt;/a>로 이동하여 새 API 키를 만드세요.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/7509632471a35fb6b5b909699dec2520">&lt;img src="https://i.gyazo.com/7509632471a35fb6b5b909699dec2520.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>&lt;strong>API 키의 만료 기간은 365일입니다.&lt;/strong>&lt;/p>
&lt;p>그런 다음 페이지에 설명된 대로 키를 복사하여 어딘가에 저장하면 다시 볼 수 없으며, 저장하지 않으면 다시 생성해야 합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2feb401c84034706d12a59f03bd30136">&lt;img src="https://i.gyazo.com/2feb401c84034706d12a59f03bd30136.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h1 id="nuget-패키지-생성">NuGet 패키지 생성&lt;/h1>
&lt;p>이제 &lt;code>API&lt;/code> 키가 있으므로 다음 단계는 솔루션에서 해당 NuGet 패키지를 생성하는 것입니다. 따라서 클래스 라이브러리가 있는 솔루션을 엽니다.&lt;/p>
&lt;p>저는 &lt;code>CalculatorCLI.Core&lt;/code>라는 .NET Core 라이브러리 클래스가 있는 &lt;a href="https://emimontesdeoca.github.io/2020/ci-dotnet-core-and-travis-ci/">마지막 자습서&lt;/a>의 솔루션을 사용할 것입니다.&lt;/p>
&lt;p>이제 &lt;strong>프로젝트 속성&lt;/strong>으로 이동한 다음 &lt;strong>패키지&lt;/strong>로 이동하면 패키지 버전, 작성자, 설명 등과 같은 패키지에 관한 많은 정보를 볼 수 있습니다.&lt;/p>
&lt;p>다음은 이것을 채우는 것입니다. 실제로 모든 것을 채울 필요는 없지만 중요하고 필수 사항은 &lt;code>Package id&lt;/code>, &lt;code>Package version&lt;/code>, &lt;code>Authors&lt;/code> 및 &lt;code>Description&lt;/code>입니다. 또한 직접 파일을 업로드하려고 하면 &lt;code>License&lt;/code>를 요청하게 되므로 해당 파일도 추가해야 합니다.&lt;a href="https://gyazo.com/6becdace2915c40fa2091ed9916fe517">&lt;img src="https://i.gyazo.com/6becdace2915c40fa2091ed9916fe517.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그런 다음 저장하고 프로젝트를 마우스 오른쪽 버튼으로 클릭한 다음 &lt;code>Pack&lt;/code>를 선택합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;&lt;span style="color:#ff7b72;font-weight:bold">------&lt;/span> Build started: Project: CalculatorCLI.Core, Configuration: Debug Any CPU &lt;span style="color:#ff7b72;font-weight:bold">------&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;CalculatorCLI.Core &lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&amp;gt; D:&lt;span style="color:#f85149">\&lt;/span>Development&lt;span style="color:#f85149">\&lt;/span>Personal&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>demo&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#f85149">\&lt;/span>bin&lt;span style="color:#f85149">\&lt;/span>Debug&lt;span style="color:#f85149">\&lt;/span>netstandard2&lt;span style="color:#a5d6ff">.1&lt;/span>&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core.dll
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;Successfully created &lt;span style="color:#ff7b72">package&lt;/span> &lt;span style="color:#f85149">&amp;#39;&lt;/span>D:&lt;span style="color:#f85149">\&lt;/span>Development&lt;span style="color:#f85149">\&lt;/span>Personal&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>demo&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#f85149">\&lt;/span>bin&lt;span style="color:#f85149">\&lt;/span>Debug&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#a5d6ff">.1.0.0.1&lt;/span>.nupkg&lt;span style="color:#f85149">&amp;#39;&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">==========&lt;/span> Build: &lt;span style="color:#a5d6ff">1&lt;/span> succeeded, &lt;span style="color:#a5d6ff">0&lt;/span> failed, &lt;span style="color:#a5d6ff">0&lt;/span> up&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>to&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>date, &lt;span style="color:#a5d6ff">0&lt;/span> skipped &lt;span style="color:#ff7b72;font-weight:bold">==========&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>출력에는 모든 것이 정상이면 &lt;code>Successfully created package&lt;/code>가 인쇄되는 것으로 표시됩니다.&lt;/p>
&lt;h1 id="dotnet-사용">&lt;code>dotnet&lt;/code> 사용&lt;/h1>
&lt;p>이전 튜토리얼과 마찬가지로 솔루션을 빌드하고 테스트하기 위해 다양한 명령을 사용했습니다. 이제 패키지를 압축하고 게시하는 명령을 더 추가하겠습니다.&lt;/p>
&lt;p>해당 명령은 다음과 같습니다.&lt;/p>
&lt;ol>
&lt;li>&lt;code>dotnet pack -c &amp;lt;configuration&amp;gt;&lt;/code>&lt;/li>
&lt;li>&lt;code>dotnet nuget push &amp;lt;package&amp;gt; &amp;lt;k &amp;lt;apikey&amp;gt; -s &amp;lt;source&amp;gt;&lt;/code>&lt;/li>
&lt;/ol>
&lt;h1 id="배포-스크립트">배포 스크립트&lt;/h1>
&lt;p>빌드 방식을 변경할 예정이므로 환경에 따라 각 파일에 빌드 정의가 포함된 다양한 파일을 갖는 것이 가장 좋은 아이디어라고 생각합니다.&lt;/p>
&lt;p>이제 저장소 루트에 3개의 &lt;code>bash&lt;/code> 스크립트가 포함된 &lt;code>scripts&lt;/code>라는 폴더를 생성해 보겠습니다.&lt;/p>
&lt;p>&lt;code>scripts\compile.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Restoring...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Building...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet build &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span> -c Release
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>scripts\test.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Testing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet test &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span> -c Release -v n
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>scripts\push.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Packing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet pack ./CalculatorCLI/CalculatorCLI.Core/CalculatorCLI.Core.csproj -c Release
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Pushing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet nuget push ./CalculatorCLI/CalculatorCLI.Core/bin/Release/*.nupkg -s &lt;span style="color:#a5d6ff">&amp;#34;https://nuget.org&amp;#34;&lt;/span> -k &lt;span style="color:#79c0ff">$NUGET_API_KEY&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="travisyml-파일-개선">&lt;code>.travis.yml&lt;/code> 파일 개선&lt;/h1>
&lt;p>이제 소스 코드가 업데이트되었으므로 사용해야 하는 명령을 알았으므로 지속적인 통합과 지속적인 제공을 통합해야 합니다. 이를 통해 우리는 코딩, 테스트, 검토 및 푸시 외에는 아무것도 할 필요가 없습니다.&lt;/p>
&lt;p>이제 &lt;code>.travis.yml&lt;/code> 파일에 대해 수행할 작업은 다음과 같습니다.&lt;/p>
&lt;ol>
&lt;li>다른 &lt;code>stages&lt;/code> 추가&lt;/li>
&lt;li>각 &lt;code>stage&lt;/code>는 지점에 따라 다릅니다.&lt;/li>
&lt;li>빌드가 마스터에 있는 경우 패키지를 업데이트해야 함을 의미하므로 패키지를 NuGet 피드에 푸시합니다.&lt;/li>
&lt;li>빌드가 풀 요청인 경우에도 빌드가 컴파일되고 테스트되는지 확인하지만 게시하지는 않을 것입니다.&lt;/li>
&lt;/ol>
&lt;h2 id="스테이지">스테이지&lt;/h2>
&lt;p>해당 문서에서:&lt;/p>
&lt;blockquote>
&lt;p>빌드 구성(.travis.yml 파일)에서 조건을 지정하여 빌드, 단계 및 작업을 필터링하고 거부할 수 있습니다.&lt;/p>&lt;/blockquote>
&lt;p>&lt;a href="https://docs.travis-ci.com/user/conditional-builds-stages-jobs">문서 페이지&lt;/a>에서 TravisCI &lt;code>stages&lt;/code>에 관한 자세한 정보를 찾을 수 있습니다.&lt;/p>
&lt;p>따라서 파일을 변경하고 스테이지를 추가하면 다음과 같이 됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">language&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">csharp&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">sudo&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">required&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">mono&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">none &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">dotnet&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">3.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">os&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">linux&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">include&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">compile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/compile.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">test&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/test.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">deploy-prod&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">if&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">branch = master AND type = push&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Deploy to NuGet&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/push.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>보시다시피 &lt;code>compile&lt;/code>, &lt;code>test&lt;/code> 및 &lt;code>push&lt;/code> 세 가지 단계가 있습니다. 마지막 단계는 &lt;code>branch&lt;/code>가 &lt;code>master&lt;/code>이고 &lt;code>pull request&lt;/code>가 아닌 &lt;code>push&lt;/code>일 때만 실행됩니다.&lt;/p>
&lt;p>이것에 대해 이해하지 못한다면 문서로 가서 아주 쉽게 설명되어 있습니다.&lt;/p>
&lt;p>이를 염두에 두고 모든 &lt;code>pull request&lt;/code>는 NuGet 피드에 아무 것도 푸시하지 않습니다.&lt;/p>
&lt;h1 id="api-키를-환경-변수로-설정">API 키를 환경 변수로 설정&lt;/h1>
&lt;p>리포지토리 빌드 설정으로 이동하여 &lt;code>NUGET_API_KEY&lt;/code>라는 새 환경 변수를 추가합니다. 값은 NuGet 페이지에서 복사한 API 키입니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/68a4bcc4ce57eccd6bb25b7d62901c84">&lt;img src="https://i.gyazo.com/68a4bcc4ce57eccd6bb25b7d62901c84.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="pull-request-생성">&lt;code>pull request&lt;/code> 생성&lt;/h2>
&lt;p>이제 모든 설정이 완료되었으므로 &lt;code>pull request&lt;/code>를 만들고 해당 풀 요청에 대한 컴파일이 마지막 &lt;code>stage&lt;/code>를 무시하는지 확인해야 합니다.&lt;/p>
&lt;h2 id="빌드-확인">빌드 확인&lt;/h2>
&lt;p>끌어오기 요청을 하는 즉시 TravisCI 대시보드에 빌드가 대기열에 추가되며 보시다시피 거기에는 3개가 아닌 2개의 서로 다른 빌드가 있습니다.&lt;a href="https://gyazo.com/11a209e72a121aef82a5b5a80d77a2f0">&lt;img src="https://i.gyazo.com/11a209e72a121aef82a5b5a80d77a2f0.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>이제 &lt;code>pull request&lt;/code>를 수락하고 빌드를 다시 확인하여 이제 두 개가 아닌 세 개의 작업이 있는지 확인하겠습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/46dad20d00defb8c14279332a946e73e">&lt;img src="https://i.gyazo.com/46dad20d00defb8c14279332a946e73e.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그리고 대기열에 있는 빌드를 보자마자 마지막에 &lt;code>Deploy-prod&lt;/code>를 포함한 세 가지 작업을 볼 수 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d24344e42f2b45225acb618ac030fd8f">&lt;img src="https://i.gyazo.com/d24344e42f2b45225acb618ac030fd8f.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>전체 빌드가 완료되자마자 로그에서 패키지가 소스에 푸시되었음을 확인할 수 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/77b1e44b4f9059cb7266fb18fbace8a7">&lt;img src="https://i.gyazo.com/77b1e44b4f9059cb7266fb18fbace8a7.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그리고 분명히 NuGet 페이지에서 볼 수 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/09421094730ea2e087cc5da639069ec4">&lt;img src="https://i.gyazo.com/09421094730ea2e087cc5da639069ec4.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/a7905ddfd6665bb6e8e62e160f21630d">&lt;img src="https://i.gyazo.com/a7905ddfd6665bb6e8e62e160f21630d.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h1 id="그게-다야">그게 다야&lt;/h1>
&lt;p>이 튜토리얼에서는 NuGet 패키지 전달을 자동화하는 방법을 알아냈습니다. 우리는 &lt;code>TravisCI&lt;/code>, &lt;code>stages&lt;/code> 및 &lt;code>dotnet nuget&lt;/code>와 같은 도구를 사용했습니다.&lt;/p>
&lt;p>제 생각에는 제대로 설정하는 데 시간이 걸리고 어떤 경우에는 너무 복잡할 수도 있습니다. 모든 것을 완벽하고 자동화된 상태로 유지하기 위해 이러한 종류의 기술에 시간을 투자합니다.&lt;/p>
&lt;p>&lt;code>.travis.yml&lt;/code> 파일과 함께 소스 코드를 &lt;a href="https://github.com/emimontesdeoca/CalculatorCLI-demo">여기&lt;/a>에서 찾을 수 있습니다. 궁금한 점이 있으면 언제든지 내 &lt;a href="https://twitter.com/emimontesdeocaa">twitter&lt;/a>로 문의하세요!&lt;/p></content:encoded><category>.NET</category><category>NuGet</category><category>CI/CD</category></item><item><title>TravisCI를 사용하여 .NET Core 3.0 프로젝트에 대한 지속적인 통합</title><link>https://emimontesdeoca.github.io/ko/posts/ci-dotnet-core-and-travis-ci/</link><pubDate>Tue, 14 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/ci-dotnet-core-and-travis-ci/</guid><description>자동화된 빌드 및 테스트와 함께 Travis CI를 사용하여 .NET Core 프로젝트에 대한 지속적인 통합을 설정하세요.</description><content:encoded>&lt;p>지난 주말에 저는 다른 저장소에서 해왔던 스크레이퍼-체커-다운로더 프로젝트를 제대로 시작하기로 결정했습니다.&lt;/p>
&lt;p>또 다른 프로젝트를 시작한 후에는 CI/CD, 끌어오기 요청, 문서, Readme의 배지 등 제가 본 모든 것이 멋지고 실제로 모범 사례인 것을 사용하여 &lt;strong>정말 멋지듯이&lt;/strong> 멋져야 했습니다.&lt;/p>
&lt;p>그리고 좋은 주말을 보낸 후 저는 결국 다양한 소스에서 프로그램을 스크랩하고 다운로드하는 도구 세트인 &lt;a href="https://github.com/Dramarr">Dramarr&lt;/a>를 만들었습니다.&lt;/p>
&lt;p>조직에는 다양한 리포지토리가 있으며 대부분은 풀 요청 및 마스터 브랜치에 병합될 때 자체적으로 컴파일, 테스트 및 배포되는 라이브러리입니다.&lt;/p>
&lt;p>이것이 바로 CI/CD 또는 지속적 통합/지속적 전달입니다.&lt;/p>
&lt;p>하지만 이 튜토리얼에서는 CI에 대해서만 이야기하겠습니다.&lt;/p>
&lt;h1 id="지속적인-통합">지속적인 통합&lt;/h1>
&lt;h2 id="그게-뭐죠">그게 뭐죠?&lt;/h2>
&lt;p>Martin Fowler의 &lt;a href="https://martinfowler.com/articles/continuousIntegration.html">블로그&lt;/a>에서 발췌한 내용입니다. 제가 읽은 설명 중 가장 좋은 설명은 다음과 같습니다.&lt;/p>
&lt;blockquote>
&lt;p>지속적 통합은 팀 구성원이 자신의 작업을 자주 통합하는 소프트웨어 개발 방식으로, 일반적으로 각 사람은 적어도 매일 통합하여 하루에 여러 번 통합하게 됩니다.
각 통합은 자동화된 빌드(테스트 포함)를 통해 검증되어 통합 오류를 가능한 한 빨리 감지합니다.&lt;/p>&lt;/blockquote>
&lt;h2 id="도구">도구&lt;/h2>
&lt;p>작업 흐름을 CI/CD와 통합하는 도구는 많지만 이 튜토리얼에서는 &lt;a href="https://github.com/">Github&lt;/a>를 사용하여 코드를 저장하고 &lt;a href="https://travis-ci.org/">TravisCI&lt;/a> 도구를 사용하여 CI를 설정합니다. 언어와 프레임워크는 C#과 new.NET Core 3.0을 사용하겠습니다.&lt;/p>
&lt;h1 id="요구사항">요구사항&lt;/h1>
&lt;p>이 작업을 수행하려면 세 가지 간단한 사항이 필요합니다.&lt;/p>
&lt;ol>
&lt;li>Visual Studio 2019 최신 버전&lt;/li>
&lt;li>Github 계정&lt;/li>
&lt;li>Github 계정에 연결된 Travis-CI 계정&lt;/li>
&lt;/ol>
&lt;h1 id="프로젝트">프로젝트&lt;/h1>
&lt;p>이 튜토리얼에서는 간단한 계산기를 사용하겠습니다. 우리는 모든 것을 테스트하기 위해 라이브러리, 명령줄 도구 및 테스트 프로젝트를 만들 것입니다.&lt;/p>
&lt;p>이 테스트 프로젝트는 CI를 설정할 때도 실행됩니다. 즉, 나중에 코드를 변경하고 처음 만든 테스트가 통과하지 못하는 경우 알림을 받거나 끌어오기 요청을 거부할 수 있습니다.&lt;/p>
&lt;h2 id="github-저장소-생성">Github 저장소 생성&lt;/h2>
&lt;p>먼저 Github 저장소를 생성할 예정이므로 Github에 접속하여 저장소를 생성하고 이를 로컬 환경에 복제하세요. 나는 이 새로운 저장소를 &lt;code>CalculatorCLI-demo&lt;/code>라고 부르기로 결정했습니다.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/0657eb2bdeb3c331b9e4585d7deed5ef.png" >
&lt;h2 id="솔루션-만들기">솔루션 만들기&lt;/h2>
&lt;p>이제 복제된 저장소의 루트 폴더에 &lt;code>CalculatorCLI&lt;/code>라는 빈 솔루션을 만들어 보겠습니다.&lt;/p>
&lt;h2 id="핵심-라이브러리">핵심 라이브러리&lt;/h2>
&lt;p>실제 프로젝트에서와 마찬가지로 라이브러리를 생성하는 별도의 프로젝트에 로직을 저장할 것이므로 라이브러리를 생성해 보겠습니다.&lt;/p>
&lt;p>가서 &lt;code>Class Library (.NET Standard)&lt;/code>를 만들고 이름을 &lt;code>CalculatorCLI.Core&lt;/code>로 지정하세요.&lt;/p>
&lt;h3 id="net-core-버전프로젝트를-생성하자마자-프로젝트-속성으로-이동하여-target-framework를-net-standard-21로-변경하여-net-core-30에-빌드된-프로젝트와-호환되게-만듭니다">NET Core 버전프로젝트를 생성하자마자 프로젝트 속성으로 이동하여 &lt;code>Target framework&lt;/code>를 &lt;code>.NET Standard 2.1&lt;/code>로 변경하여 &lt;code>.NET Core 3.0&lt;/code>에 빌드된 프로젝트와 호환되게 만듭니다.&lt;/h3>
&lt;h3 id="코드">코드&lt;/h3>
&lt;p>튜토리얼을 위해 작업을 처리하는 간단한 클래스를 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.Core&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">enum&lt;/span> OperatorsEnum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ADD,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SUBSTRACT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MULTIPLY,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DIVIDE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Operation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OperatorsEnum OperatorEnum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> LeftValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> RightValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Operation(&lt;span style="color:#ff7b72">string&lt;/span> operatorString, &lt;span style="color:#ff7b72">int&lt;/span> leftValue, &lt;span style="color:#ff7b72">int&lt;/span> rightValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (operatorString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.ADD;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.MULTIPLY;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.DIVIDE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator invalid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LeftValue = leftValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RightValue = rightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> DoOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (OperatorEnum)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.ADD:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue + RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.SUBSTRACT:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue - RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.MULTIPLY:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue * RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.DIVIDE:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue / RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator is not valid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cli">CLI&lt;/h2>
&lt;p>이제 핵심 프로젝트가 있으므로 애플리케이션을 만들어 보겠습니다. 이 경우 인수를 받아들이고 결과를 출력하는 간단한 콘솔 애플리케이션이 됩니다.&lt;/p>
&lt;p>그럼 계속해서 새로운 &lt;code>Console App (.NET Core)&lt;/code>를 만들어 보겠습니다. 저는 이름을 &lt;code>CalculatorCLI.CLI&lt;/code>로 지정했습니다.&lt;/p>
&lt;h3 id="net-core-버전">NET Core 버전&lt;/h3>
&lt;p>이전과 마찬가지로 프로젝트를 생성하자마자 프로젝트 속성으로 이동하여 &lt;code>Target framework&lt;/code>를 &lt;code>.NET Core 3.0&lt;/code>로 변경합니다.&lt;/p>
&lt;p>그런 다음 새로 생성된 프로젝트에 &lt;code>ConsoleCLI.Core&lt;/code>에 대한 참조를 추가합니다.&lt;/p>
&lt;h3 id="코드-1">코드&lt;/h3>
&lt;p>이제 코드의 경우 이전보다 더 간단해졌습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.Core&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text.RegularExpressions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.CLI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Program&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> Main(&lt;span style="color:#ff7b72">string&lt;/span>[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (args.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PrintUsage();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> joinedArgs = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>, args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> regex = &lt;span style="color:#a5d6ff">@&amp;#34;-op [\+\-\*\/] -l [-0-9]+ -r [-0-9]+&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (Regex.IsMatch(joinedArgs, regex))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> _left = Int32.Parse(args[&lt;span style="color:#a5d6ff">3&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> _right = Int32.Parse(args[&lt;span style="color:#a5d6ff">5&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> _operator = args[&lt;span style="color:#a5d6ff">1&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> _operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(_operator, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> _result = _operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Result is: {_result}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PrintUsage();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> PrintUsage()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Welcome to ConsoleCalculator!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-op Operator, it must be +,-,*,/&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-l Left number&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-r Left number&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Example usage: -op + -l 5 -r 6&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>우리는 다음과 같은 명령으로 이 애플리케이션을 사용할 것이므로 작동하게 하려면 일부 매개변수를 전달하여 이를 호출해야 합니다. 예를 들면:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -op + -l &lt;span style="color:#a5d6ff">10&lt;/span> -r &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 다음과 같이 번역됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -operator + -leftValue &lt;span style="color:#a5d6ff">10&lt;/span> -rightValue &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이에 대한 코드는 매우 간단합니다. 특정 정규식 패턴과 일치하지 않으면 잘못된 호출이고 &lt;code>PrintUsage()&lt;/code>를 호출합니다. 이는 숫자가 아닌 다른 것을 입력하면 정규식에 설정되어 있기 때문에 계산을 시도하지도 않는다는 것을 의미합니다.&lt;/p>
&lt;p>즉, 다음과 같이 호출하면 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -operator + -leftValue asdfg -rightValue ghjk
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이는 작업 논리 내부에 입력되지 않으며 값 &lt;code>TryParse&lt;/code>ing과 같은 향후 검사를 저장합니다.&lt;/p>
&lt;h2 id="테스트">테스트&lt;/h2>
&lt;p>핵심 라이브러리와 명령줄이 있지만 지금 테스트가 필요합니다. 왜냐하면 그것이 우리가 CI에서 하고 싶은 일이기 때문입니다.&lt;/p>
&lt;p>이제 새 &lt;code>MSTest Test Project (.NET Core)&lt;/code>를 만들고 이름을 &lt;code>CalculatorCLI.Tests&lt;/code>로 지정하겠습니다.&lt;/p>
&lt;h3 id="net-core-버전-1">NET Core 버전&lt;/h3>
&lt;p>이전과 마찬가지로 프로젝트를 생성하자마자 프로젝트 속성으로 이동하여 &lt;code>Target framework&lt;/code>를 &lt;code>.NET Core 3.0&lt;/code>로 변경합니다.&lt;/p>
&lt;p>그런 다음 새로 생성된 테스트 프로젝트에 &lt;code>ConsoleCLI.Core&lt;/code> 및 &lt;code>ConsoleCLI.Core&lt;/code>에 대한 참조를 추가합니다.&lt;/p>
&lt;h3 id="코드-2">코드&lt;/h3>
&lt;p>테스트를 &lt;code>CoreTests.cs&lt;/code> 및 &lt;code>CLITests.cs&lt;/code> 두 개의 서로 다른 파일로 분할할 예정입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Core&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.VisualStudio.TestTools.UnitTesting&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Tests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestClass]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CoreTests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> _left = &lt;span style="color:#a5d6ff">2&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> _right = &lt;span style="color:#a5d6ff">2&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldAdd()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">4&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldSubstract()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldMultiply()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">4&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldDivide()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ExpectedException(typeof(System.DivideByZeroException))]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldThrowExceptionForDivideByZero()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, _left, &lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ExpectedException(typeof(System.Exception), &amp;#34;Operator invalid&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldThrowExceptionForWrongOperator()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;text&amp;#34;&lt;/span>, _left, &lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.VisualStudio.TestTools.UnitTesting&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Tests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestClass]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CLITests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _left = &lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _right = &lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldAdd()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldSubstract()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldMultiply()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldDivide()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>모든 것이 생성되면 다음과 같은 솔루션이 생성됩니다.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/ffecc23a14d796af9a46dbb390c0d072.png" />
&lt;p>이제 테스트를 실행할 수 있으므로 Visual Studio의 &lt;code>Test Explorer&lt;/code>로 이동하여 실행하세요!&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/17d7ac9b2f1e7fb2666a68fc87882eed.png" />
&lt;h1 id="트래비스-ci">트래비스 CI&lt;/h1>
&lt;p>아직 그렇지 않다면 TravisCI는 호스팅된 지속적인 통합 및 배포 시스템입니다.&lt;/p>
&lt;p>여기서 수행해야 할 몇 가지 단계가 있지만 먼저 프로젝트를 빌드하고 테스트하기 위해 TravisCI 에이전트가 들을 수 있도록 Github 저장소를 연결하겠습니다.&lt;/p>
&lt;h2 id="저장소-활성화">저장소 활성화&lt;/h2>
&lt;p>이렇게 하려면 Travis CI 페이지에 로그인하고 리포지토리로 이동한 다음, 리포지토리 이름 옆에 있는 슬라이더를 클릭하여 생성한 프로젝트를 필터링하고 활성화하세요.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/28b366dddd3f5caa9100ca6b6d200764.png" >
&lt;h2 id="travisyml-생성프로젝트-루트에-travisyml라는-파일을-생성해야-합니다-그-이유는-문서에-명시된-바와-같이">.travis.yml 생성프로젝트 루트에 &lt;code>.travis.yml&lt;/code>라는 파일을 생성해야 합니다. 그 이유는 &lt;a href="https://docs.travis-ci.com/user/tutorial/">문서에 명시된 바와 같이&lt;/a>:&lt;/h2>
&lt;blockquote>
&lt;p>Travis는 .travis.yml 파일을 추가한 후 푸시한 커밋에서만 빌드를 실행합니다.&lt;/p>&lt;/blockquote>
&lt;p>따라서 다음 줄을 사용하여 저장소 루트에 &lt;code>.travis.yml&lt;/code> 파일을 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">language&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">csharp&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">sudo&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">required&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">mono&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">none &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">dotnet&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">3.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">os&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">linux&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">before_script&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet restore &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet build &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34; -c Release&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet test &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34; -c Release -v n&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.travis.yml&lt;/code> 파일이 어떻게 작동하는지에 대한 구문을 다루지는 않겠지만 이것이 무엇을 하는지 검토해 보겠습니다.&lt;/p>
&lt;ol>
&lt;li>언어를 &lt;code>csharp&lt;/code>로 설정했습니다.&lt;/li>
&lt;li>&lt;code>.NET Core 3.0&lt;/code>는 Linux에서 기본적으로 실행되므로 &lt;code>mono&lt;/code>를 사용하지 않습니다.&lt;/li>
&lt;li>&lt;code>dotnet&lt;/code> 버전을 &lt;code>3.0&lt;/code>로 설정했습니다.&lt;/li>
&lt;li>&lt;code>os&lt;/code>를 설정했습니다. 기본적으로 &lt;code>linux&lt;/code>이지만 어쨌든 추가했습니다.&lt;/li>
&lt;li>이제 여기에서 주요 논리보다 먼저 실행될 &lt;code>before_script&lt;/code>가 있으므로 솔루션에 &lt;code>dotnet restore&lt;/code>를 실행하여 나중에 모든 것이 완벽하게 로드되도록 했습니다.&lt;/li>
&lt;li>이제 &lt;code>script&lt;/code>에서 솔루션에 대해 &lt;code>dotnet builld&lt;/code> 및 &lt;code>dotnet test&lt;/code>를 수행할 것입니다. 이는 컴파일되었는지 확인한 다음 테스트를 실행합니다.&lt;/li>
&lt;/ol>
&lt;p>그리고 이제 끝났습니다!&lt;/p>
&lt;h1 id="마스터에-업로드">마스터에 업로드&lt;/h1>
&lt;p>이제 모든 것을 마스터하기만 하면 됩니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-cmd" data-lang="cmd">&lt;span style="display:flex;">&lt;span>git add --all
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git commit -m &lt;span style="color:#a5d6ff">&amp;#34;Initial files&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="지속적인-통합-확인">지속적인 통합 확인&lt;/h1>
&lt;p>저장소 페이지나 TravisCI 대시보드에서 &lt;code>master&lt;/code>에 대한 푸시의 CI 상태를 확인할 수 있습니다.&lt;/p>
&lt;h2 id="진행-중">진행 중&lt;/h2>
&lt;img align="center" src="https://i.gyazo.com/977ea42a90adccf0736464b6603867a5.png" >
&lt;br/>
&lt;br/>
&lt;img align="center" src="https://i.gyazo.com/52a5f9356df5436c862b7df6fe66a9f4.png" >
&lt;h2 id="완료">완료&lt;/h2>
&lt;img align="center" src="https://i.gyazo.com/c3d3521925a3e20bcf55bf5f6a2a711d.png" >
&lt;br/>
&lt;br/>
&lt;img align="center" src="https://i.gyazo.com/8c749c2ce44837a39fc5cd3e8838a798.png" >
&lt;p>#부숴보자&lt;/p>
&lt;p>이제 이것이 얼마나 강력한지 확인하기 위해 코드를 깨고 핵심 라이브러리를 변경하여 실패하게 만들어 보겠습니다.&lt;/p>
&lt;h2 id="코드-변경">코드 변경&lt;/h2>
&lt;p>따라서 &lt;code>Operation.cs&lt;/code>로 이동하여 일부 테스트를 깨뜨릴 수 있는 항목을 변경하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Core&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">enum&lt;/span> OperatorsEnum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ADD,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SUBSTRACT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MULTIPLY,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DIVIDE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Operation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OperatorsEnum OperatorEnum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> LeftValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> RightValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Operation(&lt;span style="color:#ff7b72">string&lt;/span> operatorString, &lt;span style="color:#ff7b72">int&lt;/span> leftValue, &lt;span style="color:#ff7b72">int&lt;/span> rightValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (operatorString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.MULTIPLY;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.DIVIDE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator invalid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LeftValue = leftValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RightValue = rightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> DoOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (OperatorEnum)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.ADD:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue + RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.SUBSTRACT:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue - RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.MULTIPLY:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue * RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.DIVIDE:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue / RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator is not valid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 테스트를 다시 실행하면 사례를 추가로 변경했기 때문에 실패할 것입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>예상대로 &lt;code>ShouldAdd&lt;/code> 사례에서는 실패했습니다.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/a57d0a0f8c07cc1ab6e4f55a8466cbbd.png" >
&lt;p>이제 이 변경 사항을 커밋하고 마스터에 푸시한 후 TravisCI 에이전트의 결과를 기다립니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>git add --all
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git commit -m &lt;span style="color:#a5d6ff">&amp;#34;Breaking changes&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="빌드">빌드&lt;/h2>
&lt;p>이제 TravisCI 로그로 이동하면 통합 테스트가 실패하고 빌드 상태가 오류이기 때문에 프로젝트가 성공적으로 중단되었음을 알 수 있습니다.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/397befe9b7f5a32b6e97511733296b00.png" >
&lt;p>로그 끝에서 오류 자체를 볼 수 있습니다.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/e5cbf32c5dd7a08bf6628d81edff3130.png" >
&lt;h2 id="다시-고쳐보자">다시 고쳐보자!&lt;/h2>
&lt;p>이제 우리가 했던 작업을 되돌리고 코드를 마스터에 푸시하고 새 빌드의 상태를 확인하세요.&lt;/p>
&lt;p>테스트가 성공적으로 통과되었습니다.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/02deb8fcb4fca618ff2d79f1c27c6df5.png" >
&lt;p>그리고 빌드도 성공했습니다.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/38a227f0634c6040c6608f8c51f36cd3.png" >
&lt;h1 id="결론">결론&lt;/h1>
&lt;p>&lt;strong>정말 강력합니다&lt;/strong>, CI와 CD는 오래 전부터 존재했지만 이제는 모든 단일 프로젝트에서 실행하는 것이 매우 간단하며 그것이 얼마나 작거나 단순한지는 중요하지 않습니다.내 관점에서는 모든 사람이 적어도 각 프로젝트에 대해 CI를 설정해야 합니다. 이는 좋은 습관이고 결국 적절한 &lt;code>tests&lt;/code> 및 CI를 설정한 경우 발생해서는 안 되는 오류를 디버깅하고 찾는 시간을 절약할 수 있기 때문입니다.&lt;/p>
&lt;h1 id="그게-다야">그게 다야&lt;/h1>
&lt;p>TravisCI를 사용하고 코드를 Github에 저장하는 모든 빌드에서 지속적으로 통합되는 .NET Core 3.0 솔루션을 만드는 방법에 대한 내용입니다.&lt;/p>
&lt;p>이 프로젝트의 소스 코드는 &lt;a href="https://github.com/emimontesdeoca/CalculatorCLI-demo">여기&lt;/a>에서 찾을 수 있습니다.&lt;/p></content:encoded><category>.NET</category><category>CI/CD</category></item><item><title>인메모리 캐시에 대한 나의 견해</title><link>https://emimontesdeoca.github.io/ko/posts/my-take-on-in-memory-cache/</link><pubDate>Tue, 03 Sep 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/my-take-on-in-memory-cache/</guid><description>C# 및 제네릭을 사용하여 만료 지원이 포함된 사용자 지정 메모리 내 캐시 구현을 구축합니다.</description><content:encoded>&lt;p>저는 엄청난 양의 데이터를 처리하는 작업을 해왔습니다. 그렇게 하는 동안 나는 그것의 큰 덩어리가 결코 변하지 않거나 적어도 어느 정도 고정된 시간 동안은 변하지 않는다는 것을 깨달았습니다.&lt;/p>
&lt;p>그래서 개인 캐시 저장소를 만드는 것이 유용할 것이라고 생각했습니다. 물론 이것은 새로운 것은 아닙니다. 몇 주 전에 &lt;a href="https://nickcraver.com/">Nick Craver&lt;/a>가 작성한 StackOverflow의 &lt;a href="https://nickcraver.com/blog/2019/08/06/stack-overflow-how-we-do-app-caching/#in-memory--redis-cache">포스트&lt;/a>에서 애플리케이션 캐시를 관리하는 방법에 대해 읽었습니다.&lt;/p>
&lt;p>또한 저는 항상 캐시를 사용하여 작업하고 싶었습니다. 캐시가 어떻게 작동하는지, 논리를 어떻게 작동하게 만들 수 있는지, &lt;em>그래서&amp;hellip; 안 될 이유가 없군요!&lt;/em>&lt;/p>
&lt;h2 id="흐름">흐름&lt;/h2>
&lt;p>다음은 사용자가 값을 요청할 때 캐시를 제어하는 클래스가 어떻게 작동하는지에 대한 간단한 흐름입니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/830d5a91089c3344c8b406c66ea547b8">&lt;img src="https://i.gyazo.com/830d5a91089c3344c8b406c66ea547b8.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="구현">구현&lt;/h2>
&lt;h3 id="시작하기-전에">시작하기 전에&lt;/h3>
&lt;p>알아요, 알아요. 메모리 캐시를 처리하는 &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache?view=netframework-4.8">System.Runtime.Caching&lt;/a>가 있습니다. 하지만 제가 직접 만들기로 결정했습니다. 해당 클래스를 사용하려면 &lt;a href="https://stackoverflow.com/search?q=System.Runtime.Caching">여기&lt;/a>에서 방법을 확인하세요.&lt;/p>
&lt;h3 id="캐시항목">캐시항목&lt;/h3>
&lt;p>첫 번째 단계는 객체의 값과 만료 날짜를 저장하는 클래스를 만드는 것입니다. 아마도 더 좋은 방법이 있을 것입니다. 하지만 제 생각에는 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CacheItem&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Identifier { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">object&lt;/span> Value { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime ValidUntil { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CacheItem(&lt;span style="color:#ff7b72">string&lt;/span> identifier, &lt;span style="color:#ff7b72">object&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>, TimeSpan valid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Identifier = identifier;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Value = &lt;span style="color:#ff7b72">value&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ValidUntil = DateTime.UtcNow.Add(valid);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="캐시-저장소">캐시 저장소&lt;/h3>
&lt;p>그런 다음 객체를 처리하고 어딘가에 저장하는 클래스가 필요합니다(&lt;code>CacheItem&lt;/code>). 나는 접미사 &lt;code>Repository&lt;/code>가 있는 클래스의 모든 데이터/모델을 처리하고 싶지만 &lt;em>꼭 그럴 필요는 없으므로&lt;/em> 캐싱을 위해 하나 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CacheRepository&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Cache = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T Set&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key, Func&amp;lt;T&amp;gt; lookup, TimeSpan durationMinutes)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> item = &lt;span style="color:#ff7b72">new&lt;/span> Models.Item(key, lookup(), durationMinutes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Save&amp;lt;T&amp;gt;(key, item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T Save&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key, Item item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Cache[key] = JsonConvert.SerializeObject(item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> (T)item.Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T Get&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> cached = Cache.FirstOrDefault(x =&amp;gt; x.Key == key).Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> item = &lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(cached) ? &lt;span style="color:#79c0ff">null&lt;/span> : JsonConvert.DeserializeObject&amp;lt;Item&amp;gt;(cached);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> (item == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? &lt;span style="color:#ff7b72">default&lt;/span> : (item.ValidUntil &amp;gt; DateTime.UtcNow)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(JsonConvert.SerializeObject(item.Value)) : &lt;span style="color:#ff7b72">default&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T GetOrSet&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key, Func&amp;lt;T&amp;gt; lookup, TimeSpan durationMinutes)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> cache = Get&amp;lt;T&amp;gt;(key);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> cache == &lt;span style="color:#79c0ff">null&lt;/span> ? Set(key, lookup, durationMinutes) : cache;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>지금까지 모든 항목이 저장될 &lt;code>Cache&lt;/code>라는 정적 사전이 있습니다. 이는 애플리케이션이 실행되는 동안에만 지속되므로 이 &lt;em>자습서&lt;/em> 제목에 메모리 내 캐싱이 있는 이유를 기억하세요.&lt;/p>
&lt;p>&lt;em>&lt;code>Cache&lt;/code> 항목은 &lt;code>CacheRepository&lt;/code> 클래스가 로드되면 초기화된다는 점에 유의하세요.&lt;/em>&lt;/p>
&lt;p>CacheRepository 클래스를 호출할 때 사용할 수 있는 유일한 메서드는 세 가지 매개 변수가 필요한 &lt;code>GetOrSet(string key, Func&amp;lt;T&amp;gt; lookup, TimeSpan durationMinutes)&lt;/code>입니다.&lt;/p>
&lt;ol>
&lt;li>&lt;code>key&lt;/code>: 저장할 객체의 식별자입니다.&lt;/li>
&lt;li>&lt;code>lookup&lt;/code>: 캐시가 만료되었거나 null인 경우의 콜백 함수입니다.&lt;/li>
&lt;li>&lt;code>durationMinutes&lt;/code>: 현재 시간(UTC)에 추가될 기간(분)입니다.&lt;/li>
&lt;/ol>
&lt;h2 id="캐시할-시간">캐시할 시간&lt;/h2>
&lt;p>이제 캐싱 저장소를 사용하여 어딘가에서 일부 데이터를 가져옵니다.&lt;/p>
&lt;p>이 모든 것이 이해되도록 하기 위해 몇 가지 속성이 포함된 예제 개체를 만든 다음 해당 개체의 목록을 가져와 채우는 저장소를 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">User&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Guid Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Email { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">UserRepository&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;User&amp;gt; Get()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Code to get users&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>User&lt;/code> 목록을 채울 수 있는 방법이 있으므로 &lt;code>CacheRepository&lt;/code> 클래스를 활용해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> UserRepository _userRepository = &lt;span style="color:#ff7b72">new&lt;/span> UserRepository();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;User&amp;gt; _user;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;User&amp;gt; User
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _user = CacheRepository.GetOrSet(&lt;span style="color:#a5d6ff">$&amp;#34;users&amp;#34;&lt;/span>, usersRepo.Get, TimeSpan.FromMinutes(&lt;span style="color:#a5d6ff">10&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _user;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>마찬가지로 &lt;code>User&lt;/code> 변수에 액세스할 때마다 &lt;code>users&lt;/code> 키가 있는 객체의 값을 &lt;code>CacheRepository&lt;/code>에 요청합니다.&lt;/p>
&lt;p>해당 키가 있으면 만료 날짜를 확인합니다. 이러한 조건 중 하나라도 거짓이면 콜백을 사용하여 객체의 값(&lt;code>usersRepo.Get&lt;/code> 사용)을 설정하고 만료 날짜가 &lt;code>DateTime.UtcNow + TimeSpan.FromMinutes(10)&lt;/code>로 설정된 캐시에 저장한 후 반환합니다.&lt;/p></content:encoded><category>.NET</category></item><item><title>Devtools를 사용하여 페이지 스크린샷 찍기</title><link>https://emimontesdeoca.github.io/ko/posts/google-chrome-screenshot-tool/</link><pubDate>Thu, 09 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/google-chrome-screenshot-tool/</guid><description>Chrome DevTools 내장 스크린샷 명령을 ​​사용하여 전체 페이지 및 영역 스크린샷을 캡처하세요.</description><content:encoded>&lt;p>클라이언트나 팀원에게 웹사이트의 현재 상태 또는 그 일부에 대한 전체 스크린샷을 보여줘야 할 때가 있었습니다. 그래서 저는 &lt;a href="https://chrome.google.com/webstore/detail/full-page-screen-capture/fdpohaocaechififmbbbbbknoalclacl?hl=en">전체 페이지 화면 캡처&lt;/a>를 사용했고, 다른 종류의 스크린샷에는 &lt;a href="https://support.microsoft.com/en-us/help/13776/windows-use-snipping-tool-to-capture-screenshots">Windows의 조각 도구&lt;/a> 또는 &lt;a href="https://www.techsmith.com/store/snagit">Snaggit&lt;/a>를 사용했습니다. 이 게시물 &lt;a href="https://gyazo.com">Gyazo&lt;/a>처럼 온라인으로 호스팅하려면 GIF 메이커도 있습니다.&lt;/p>
&lt;p>최근에 저는 &lt;strong>Devtools&lt;/strong>에 확장 프로그램이 필요 없이 전체 페이지 스크린샷을 찍는 도구가 있다는 것을 발견했습니다!&lt;/p>
&lt;p>이것은 새로운 생각이 아니며 &lt;a href="https://developers.google.com/web/updates/2017/04/devtools-release-notes">2017년 4월 Devtools 업데이트&lt;/a>에 포함되었습니다. &lt;em>하지만 제가 바위 밑에 살고 있어서 지금까지 알지 못한 것 같습니다&amp;hellip;&lt;/em>&lt;/p>
&lt;h2 id="개발자-도구">개발자 도구&lt;/h2>
&lt;p>&lt;a href="https://developers.google.com/web/tools/chrome-devtools/?hl=en">공식 Devtools 페이지&lt;/a>에 명시된 대로:&lt;/p>
&lt;blockquote>
&lt;p>Chrome DevTools는 Google Chrome 브라우저에 직접 내장된 웹 개발자 도구 세트입니다. DevTools는 페이지를 즉석에서 편집하고 문제를 신속하게 진단하는 데 도움이 되며 궁극적으로 더 나은 웹사이트를 더 빠르게 구축하는 데 도움이 됩니다.&lt;/p>&lt;/blockquote>
&lt;h2 id="devtools에서-명령-실행하기">Devtools에서 명령 실행하기&lt;/h2>
&lt;p>Devtools를 여는 방법을 이미 알고 계시지만 &lt;em>이 게시물을 위해&lt;/em> 잊어버린 경우에는 &lt;code>F12&lt;/code> 키 또는 &lt;code>Ctrl + Shift + I&lt;/code> 단축키를 사용하여 여십시오. 그런 다음 Devtools 내부 메뉴에서 &lt;code>Run command&lt;/code>를 열거나 &lt;code>Ctrl + Shift + P&lt;/code>를 사용해야 합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8209d7d3132efba1843a3d51e4ad2183">&lt;img src="https://i.gyazo.com/8209d7d3132efba1843a3d51e4ad2183.gif" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="스크린샷-도구">스크린샷 도구&lt;/h2>
&lt;p>&lt;a href="https://gyazo.com/25ea6c9d258a1117efca5a2d92f715e2">&lt;img src="https://i.gyazo.com/25ea6c9d258a1117efca5a2d92f715e2.gif" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>&lt;code>screenshot&lt;/code>를 입력하면 명령이 필터링되며, 사용할 수 있는 스크린샷 방법에는 4가지가 있습니다.&lt;/p>
&lt;ol>
&lt;li>지역 스크린샷&lt;/li>
&lt;li>전체 크기 스크린샷&lt;/li>
&lt;li>노드 스크린샷&lt;/li>
&lt;li>스크린샷 캡처&lt;/li>
&lt;/ol>
&lt;p>모든 스크린샷 방법을 확인하기 전에 devtools가 이미지 처리를 마친 후 &lt;strong>웹페이지 이름과 함께 자동으로 다운로드됩니다&lt;/strong>.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/89a4935eb0ddb1a06ae997551fd19677">&lt;img src="https://i.gyazo.com/89a4935eb0ddb1a06ae997551fd19677.gif" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e2e29c7fd7e90bb8bfd36ef130793394">&lt;img src="https://i.gyazo.com/e2e29c7fd7e90bb8bfd36ef130793394.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h3 id="지역-스크린샷">지역 스크린샷&lt;/h3>
&lt;p>&lt;code>area screenshot&lt;/code>를 사용하면 웹사이트의 일부를 선택하고 스크린샷을 만들 수 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/f508a479893b6e8af9234d6a77404b26">&lt;img src="https://i.gyazo.com/f508a479893b6e8af9234d6a77404b26.gif" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>결과:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2b096e1ca2974157e478587a4902c1d2">&lt;img src="https://i.gyazo.com/2b096e1ca2974157e478587a4902c1d2.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h3 id="전체-크기-스크린샷">전체 크기 스크린샷&lt;/h3>
&lt;p>&lt;code>full size screenshot&lt;/code>는 처음부터 끝까지 전체 웹페이지의 스크린샷을 찍습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e6c30b8eafbef04091bf66c16429017c">&lt;img src="https://i.gyazo.com/e6c30b8eafbef04091bf66c16429017c.gif" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>결과:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/9519ca32c0029a36432b700268741280">&lt;img src="https://i.gyazo.com/9519ca32c0029a36432b700268741280.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h3 id="노드-스크린샷">노드 스크린샷&lt;/h3>
&lt;p>&lt;code>node screenshot&lt;/code>를 사용하면 검사 요소에서 선택한 노드의 스크린샷을 만들 수 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5b936026735964bb789c4bcae02bb792">&lt;img src="https://i.gyazo.com/5b936026735964bb789c4bcae02bb792.gif" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>결과:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/56089c3d89f9b059dfb018d18572276d">&lt;img src="https://i.gyazo.com/56089c3d89f9b059dfb018d18572276d.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h3 id="스크린샷-캡처">스크린샷 캡처&lt;/h3>
&lt;p>&lt;code>capture screenshot&lt;/code>는 스크롤 없이 현재 페이지와 브라우저 크기의 스크린샷을 찍습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/6dc4f654e8f218e44fcc1fb51ce4c9f5">&lt;img src="https://i.gyazo.com/6dc4f654e8f218e44fcc1fb51ce4c9f5.gif" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>결과:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/3f15d4a004cba6152d1f9606c20540cc">&lt;img src="https://i.gyazo.com/3f15d4a004cba6152d1f9606c20540cc.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p></content:encoded></item><item><title>.NET Framework에 포함된 리소스와 외부 리소스를 사용하는 사용자 지정 언어</title><link>https://emimontesdeoca.github.io/ko/posts/embedded-resources-and-external-resources/</link><pubDate>Mon, 06 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/embedded-resources-and-external-resources/</guid><description>.NET Framework에서 포함된 외부 .resx 리소스 파일을 사용하여 다중 언어 지원을 구현합니다.</description><content:encoded>&lt;h1 id="소개">소개&lt;/h1>
&lt;p>직장에서 우리는 많은 개발자가 처리해야 할 일반적인 문제에 직면했습니다. 이는 클라이언트에게 서비스를 제공하는 클라이언트의 언어 및 사용자 정의 언어입니다.&lt;/p>
&lt;p>예를 들어, 기본 언어가 비공식 언어를 사용하고 클라이언트가 나이가 많은 사람들이기 때문에 클라이언트가 공식적인 언어를 원한다고 가정해 보겠습니다.&lt;/p>
&lt;p>또한 이 솔루션은 여러 언어에 적용되며 영어와 스페인어를 사용합니다.&lt;/p>
&lt;p>&lt;em>제 모국어는 영어가 아닙니다. 튜토리얼의 실수에 대해 사과드립니다. 실수를 발견하고 수정하고 싶다면 &lt;a href="https://github.com/emimontesdeoca/emimontesdeoca.github.io">이 저장소&lt;/a>에서 끌어오기 요청을 열면 기꺼이 승인해 드리겠습니다!&lt;/em>&lt;/p>
&lt;h1 id="리소스">리소스&lt;/h1>
&lt;p>리소스는 키/값 구조를 갖는 확장자가 &lt;code>.resx&lt;/code>인 XML 파일이며 다음과 같습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;root&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:schema&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;root&amp;#34;&lt;/span> xmlns=&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span> xmlns:xsd=&lt;span style="color:#a5d6ff">&amp;#34;http://www.w3.org/2001/XMLSchema&amp;#34;&lt;/span> xmlns:msdata=&lt;span style="color:#a5d6ff">&amp;#34;urn:schemas-microsoft-com:xml-msdata&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:import&lt;/span> namespace=&lt;span style="color:#a5d6ff">&amp;#34;http://www.w3.org/XML/1998/namespace&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;root&amp;#34;&lt;/span> msdata:IsDataSet=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:choice&lt;/span> maxOccurs=&lt;span style="color:#a5d6ff">&amp;#34;unbounded&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;value&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> use=&lt;span style="color:#a5d6ff">&amp;#34;required&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;type&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;mimetype&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> ref=&lt;span style="color:#a5d6ff">&amp;#34;xml:space&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;assembly&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;alias&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;data&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;value&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;1&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;comment&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> use=&lt;span style="color:#a5d6ff">&amp;#34;required&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;1&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;type&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;3&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;mimetype&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;4&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> ref=&lt;span style="color:#a5d6ff">&amp;#34;xml:space&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;resheader&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;value&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;1&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> use=&lt;span style="color:#a5d6ff">&amp;#34;required&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:choice&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:schema&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;resmimetype&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>text/microsoft-resx&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;version&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>2.0&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;reader&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;writer&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;data&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span> xml:space=&lt;span style="color:#a5d6ff">&amp;#34;preserve&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>Finish&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;data&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span> xml:space=&lt;span style="color:#a5d6ff">&amp;#34;preserve&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>Hello&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/root&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>영어&lt;/strong> 및 &lt;strong>스페인어&lt;/strong>라는 두 가지 다른 언어에 대한 리소스를 생성할 예정이므로 파일 명명법은 &lt;code>resources.ISOLANGUAGECODE.resx&lt;/code>입니다. 예를 들어 스페인어의 경우 &lt;code>resource.es.resx&lt;/code>, 영어의 경우 &lt;code>resource.resx&lt;/code>입니다. 나중에 독일어를 추가하려는 경우 파일 이름은 &lt;code>resource.de.resx&lt;/code>가 됩니다.&lt;/p>
&lt;h2 id="임베디드-리소스">임베디드 리소스&lt;/h2>
&lt;p>포함된 리소스는 프로젝트가 컴파일될 때 &lt;code>dll&lt;/code> 내부에 추가됩니다.&lt;/p>
&lt;p>보시다시피 리소스가 포함된 이미지는 다음과 같습니다. &lt;code>Resource.resx&lt;/code> 파일은 편집에서 볼 수 없습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b696d4ff1129634477c0fe3d570a05e8">&lt;img src="https://i.gyazo.com/b696d4ff1129634477c0fe3d570a05e8.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="외부-리소스">외부 리소스&lt;/h2>
&lt;p>반면에 외부 리소스 또는 포함되지 않은 리소스는 컴파일 후 폴더에 추가되는 리소스입니다.&lt;/p>
&lt;p>리소스 파일(&lt;code>.resx&lt;/code>)은 &lt;code>Properties&lt;/code> 폴더 안에 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/db83dc7bb9e3d3fb521cb321eaa8e74a">&lt;img src="https://i.gyazo.com/db83dc7bb9e3d3fb521cb321eaa8e74a.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h1 id="프로젝트-빌드">프로젝트 빌드&lt;/h1>
&lt;p>.NET Framework의 콘솔 프로젝트부터 시작해 보겠습니다.&lt;/p>
&lt;h2 id="내장-리소스-파일-생성">내장 리소스 파일 생성&lt;/h2>
&lt;p>몇 개의 키가 있는 리소스 파일을 추가하고 나중에 외부 리소스로 업데이트하는 데 사용할 내장 리소스인지 확인하세요.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/79af1981a3ee3214089791a3511b94ba">&lt;img src="https://i.gyazo.com/79af1981a3ee3214089791a3511b94ba.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="외부-리소스-파일-생성">외부 리소스 파일 생성&lt;/h2>
&lt;p>외부 리소스와 내장 리소스를 분리하기 위해 나중에 쉽게 액세스할 수 있도록 이름이 있는 폴더 내에 외부 리소스를 추가할 것입니다.&lt;/p>
&lt;p>&lt;em>팁: 속성 폴더 안에 폴더를 추가하고, 외부에 만들고 내부로 이동하려면 Visual Studio에서는 폴더를 만들 수 없습니다.&lt;/em>&lt;/p>
&lt;p>포함된 리소스에 대해 동일한 작업을 수행한 다음 파일의 &lt;code>properties&lt;/code>로 이동하여 &lt;code>Advanced&lt;/code>, &lt;code>Build action&lt;/code> 내부로 이동하여 &lt;code>Content&lt;/code>로 변경하고 &lt;code>Copy to Output Dictionary&lt;/code>를 &lt;code>Copy if newer&lt;/code>로 변경합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/6f7d53c82760b6989d74dc1864bbc2d4">&lt;img src="https://i.gyazo.com/6f7d53c82760b6989d74dc1864bbc2d4.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>다음과 같이 표시됩니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/75b2f06b11fa99b01bca7b5ac64b4e35">&lt;img src="https://i.gyazo.com/75b2f06b11fa99b01bca7b5ac64b4e35.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="appconfig에-키-추가">app.config에 키 추가&lt;/h2>
&lt;p>우리는 멋진 개발자이고 모든 것을 잘 수행하고 각 클라이언트의 코드를 &lt;strong>변경하지 않기&lt;/strong>를 원하기 때문에 &lt;code>appconfig&lt;/code>에 리소스 파일을 찾을 폴더 이름을 갖는 키를 추가해 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34; ?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;startup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;supportedRuntime&lt;/span> version=&lt;span style="color:#a5d6ff">&amp;#34;v4.0&amp;#34;&lt;/span> sku=&lt;span style="color:#a5d6ff">&amp;#34;.NETFramework,Version=v4.7.2&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/startup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;appSettings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;add&lt;/span> key=&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span> value=&lt;span style="color:#a5d6ff">&amp;#34;John&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/appSettings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```따라서 나중에 리소스를 찾을 때 `Properties.Doe` 대신 `Properties.John`를 찾습니다.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>또한 app.config를 쉽게 변경할 수 있으므로 애플리케이션이 이미 배포된 경우에도 쉽게 변경할 수 있습니다.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>## 포함된 리소스 파일에 액세스
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.NET Framework를 사용하는 것은 매우 쉽기 때문에 임베디드 속성에 액세스하려면 `Properties` 개체를 사용해야 합니다. 이 개체는 리소스 파일을 속성으로 가지며 해당 개체 내부에는 `resx` 파일에 있는 모든 키/값이 있습니다.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>static void Main(string[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> var hello = Properties.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> var bye = Properties.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine($&amp;#34;Action_greeting value: {hello}, Action_cancel value: {bye}&amp;#34;);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Read();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>실행하면 다음과 같은 내용이 표시됩니다.&lt;/p>
&lt;p>&lt;code>Action_greeting value: Hello, Action_cancel value: Cancel&lt;/code>&lt;/p>
&lt;h2 id="외부-리소스-파일에-접근하기">외부 리소스 파일에 접근하기&lt;/h2>
&lt;p>이 부분은 거의 동일합니다. &lt;code>Properties&lt;/code> 객체로 리소스 파일에 액세스할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> hello = Properties.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> bye = Properties.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> johnhello = Properties.John.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> johnbye = Properties.John.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doehello = Properties.Doe.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doebye = Properties.Doe.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="문제">문제&lt;/h1>
&lt;p>이 방법을 사용하는 주요 문제점은 모든 키가 객체 내부의 속성이므로 이전에 본 것처럼 호출해야 한다는 것입니다. &lt;code>John&lt;/code> 리소스 파일의 &lt;code>Action_greeting&lt;/code> 키를 호출하려면 다음 &lt;code>Properties.John&lt;/code>를 사용한 다음 &lt;code>Resource.Action_greeting&lt;/code>를 사용해야 합니다.&lt;/p>
&lt;p>&lt;strong>바로 문제가 있습니다.&lt;/strong>&lt;/p>
&lt;p>왜냐하면 많은 클라이언트를 위한 애플리케이션을 개발하는 경우 각 클라이언트에 대한 리소스 파일을 호출하는 방법을 변경하는 것은 좋지 않은 생각이기 때문입니다.&lt;/p>
&lt;p>&lt;em>상상할 수 있습니까?&lt;/em> 각 클라이언트에 대한 애플리케이션을 컴파일하고 &lt;code>John&lt;/code>를 &lt;code>Doe&lt;/code>로 변경한 다음 다른 것으로 변경합니다. &lt;strong>미친 짓이군요!&lt;/strong>&lt;/p>
&lt;h1 id="솔루션">솔루션&lt;/h1>
&lt;p>우리 팀 리더는 폴백 시스템과 같은 매우 좋은 방법을 생각했습니다. 리소스의 기본 모델이 있어야 하고, 각 클라이언트에 대해 해당 리소스로 파일을 업데이트할 리소스 파일이 있어야 하며, 결국 단일 리소스 목록이 생성됩니다.&lt;/p>
&lt;p>클라이언트가 사용자 정의 리소스를 원하지 않으면 우리는 기본 리소스를 사용하고, 클라이언트가 원한다면 우리는 그들의 리소스를 사용합니다.&lt;/p>
&lt;p>이것을 체크리스트에 넣으려면 다음을 수행해야 합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 모든 키/값을 포함된 리소스에 대한 사전에 매핑하는 방법을 찾으세요.&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 모든 키/값을 외부 리소스에 대한 사전에 매핑하는 방법을 찾습니다.&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 두 파일을 혼합하고 각 언어에 대한 단일 사전을 갖습니다.&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 사전에 액세스하고 값을 반환하는 메서드를 만듭니다.&lt;/li>
&lt;/ul>
&lt;h2 id="다이어그램">다이어그램&lt;/h2>
&lt;p>&lt;a href="https://gyazo.com/c7e9ae6c7792c3bace10ff9b3b2b08ee">&lt;img src="https://i.gyazo.com/c7e9ae6c7792c3bace10ff9b3b2b08ee.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="코딩해보자">코딩해보자&lt;/h2>
&lt;p>우선, 리소스 파일을 가져오는 것부터 이를 혼합하고 값을 반환하는 것까지 모든 로직을 포함하는 분리된 클래스를 만들어 보겠습니다. 해당 클래스는 &lt;code>CustomResources&lt;/code>라고 합니다.&lt;/p>
&lt;p>이것은 다음과 같습니다:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomResources&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; OverwriteDictionary(Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; currentDictionary, Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; newDictionary, &lt;span style="color:#ff7b72">bool&lt;/span> addIfDoesntExist = &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromEmbedded(&lt;span style="color:#ff7b72">string&lt;/span> embedded, &lt;span style="color:#ff7b72">string&lt;/span> cultureInfoCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromFile(&lt;span style="color:#ff7b72">string&lt;/span> file)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key, &lt;span style="color:#ff7b72">string&lt;/span> language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>저희는 속성에 대한 지연 로딩을 구현하고 있습니다. 이는 성능을 높이는 데 도움이 되며 사전을 한 번 로드하게 합니다.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>GetDictionaryFromEmbedded&lt;/code>: 포함된 리소스에서 사전을 반환합니다.&lt;/li>
&lt;li>&lt;code>GetDictionaryFromFile&lt;/code>: 외부 리소스에서 사전u를 반환합니다.&lt;/li>
&lt;li>&lt;code>OverwriteDictionary&lt;/code>: 두 개의 사전을 혼합하여 단일 사전을 반환합니다.&lt;/li>
&lt;li>&lt;code>GetText&lt;/code>: 키에 주어진 값을 반환합니다.&lt;/li>
&lt;/ul>
&lt;h2 id="삽입된-리소스에서-사전으로">삽입된 리소스에서 사전으로&lt;/h2>
&lt;p>xml 파일에서 모든 속성을 가져와서 사전을 반환해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromEmbedded(&lt;span style="color:#ff7b72">string&lt;/span> embedded, &lt;span style="color:#ff7b72">string&lt;/span> cultureInfoCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; res = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ResourceManager rm = &lt;span style="color:#ff7b72">new&lt;/span> ResourceManager(embedded, Assembly.GetExecutingAssembly());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> resourceSet = rm.GetResourceSet(&lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(cultureInfoCode), &lt;span style="color:#79c0ff">true&lt;/span>, &lt;span style="color:#79c0ff">true&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> resourceDictionary = resourceSet.Cast&amp;lt;DictionaryEntry&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToDictionary(r =&amp;gt; r.Key.ToString(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r =&amp;gt; r.Value.ToString());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res = resourceDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> a = e.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Error getting resource file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> res;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>두 가지:- &lt;code>embedded&lt;/code>라는 매개변수가 필요합니다. parementers는 디자이너에서 볼 수 있는 파일의 이름이며, 우리의 경우에는 &lt;code>resources-demo.Properties.Resource&lt;/code>입니다.&lt;/p>
&lt;ul>
&lt;li>또한 선택할 언어에 대한 코드인cultreInfoCode라는 매개변수가 있습니다. 운 좋게도 .NET Framework가 작업을 수행하므로 아무 것도 할 필요가 없습니다. 영어나 스페인어 중 하나를 선택하기만 하면 &lt;code>resource.es.resx&lt;/code> 또는 &lt;code>resource.resx&lt;/code> 중에서 선택됩니다.&lt;/li>
&lt;/ul>
&lt;h2 id="외부-리소스에서-사전으로">외부 리소스에서 사전으로&lt;/h2>
&lt;p>파일에서 얻는 것이 약간 해킹적이지만 어렵지는 않습니다. 실행 파일의 현재 위치를 가져와서 리소스 파일의 위치를 연결한 다음 이를 사전으로 구문 분석해야 합니다.&lt;/p>
&lt;p>하지만 &lt;code>ResXResourceReader&lt;/code>에 액세스하려면 먼저 &lt;code>System.Windows.Forms&lt;/code>에 대한 참조를 추가해야 합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5bdf8353acc57b1d95b72acc14af41cd">&lt;img src="https://i.gyazo.com/5bdf8353acc57b1d95b72acc14af41cd.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>이제 &lt;code>GetDictionaryFromFile&lt;/code> 메서드로 이동합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromFile(&lt;span style="color:#ff7b72">string&lt;/span> file)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; res = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> currentPath = (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) + file).Replace(&lt;span style="color:#a5d6ff">&amp;#34;file:\\&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (ResXResourceReader resxReader = &lt;span style="color:#ff7b72">new&lt;/span> ResXResourceReader(currentPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (DictionaryEntry entry &lt;span style="color:#ff7b72">in&lt;/span> resxReader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res.Add((&lt;span style="color:#ff7b72">string&lt;/span>)entry.Key, (&lt;span style="color:#ff7b72">string&lt;/span>)entry.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> a = e.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> res;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>파일 매개변수는 실행 파일에서 리소스 파일까지의 위치로 채워져야 합니다. 이 경우에는 &lt;code>&amp;quot;\\Properties\\John\\Resource.resx&amp;quot;&lt;/code>입니다.&lt;/p>
&lt;h2 id="사전-혼합">사전 혼합&lt;/h2>
&lt;p>거의 끝났습니다. 먼저 &lt;code>app.settings&lt;/code>에 액세스할 수 있도록 참조에 &lt;code>System.Configuration&lt;/code>를 추가하는 것을 기억하세요.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; OverwriteDictionary(Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; currentDictionary, Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; newDictionary, &lt;span style="color:#ff7b72">bool&lt;/span> addIfDoesntExist = &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (String.IsNullOrEmpty(identifier))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> newDictionary)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentDictionary[item.Key] = item.Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span>(addIfDoesntExist)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentDictionary.Add(item.Key, item.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="사전에서-텍스트-가져오기">사전에서 텍스트 가져오기&lt;/h2>
&lt;p>언어를 선택하는 비공개 메서드를 호출하는 공개 메서드를 만듭니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key, &lt;span style="color:#ff7b72">string&lt;/span> language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ResourceSpanish[key];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;en&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ResourceEnglish[key];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;No value with key: {key} and language: {language}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> GetText(key, Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;No value with key: {key}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>언어를 더 추가하려면 스위치를 수정해야 합니다.&lt;/strong>&lt;/p>
&lt;h2 id="속성-getter에-코드-추가">속성 getter에 코드 추가&lt;/h2>
&lt;p>지금은 모든 메서드가 있으므로 공용 속성의 getter를 수정하여 값을 가져올 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesEnglish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (_ResourcesEnglish == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> folderIndentifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>]; ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> baseResources = GetDictionaryFromEmbedded(&lt;span style="color:#a5d6ff">&amp;#34;resources-demo.Properties.Resource&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> customResources = GetDictionaryFromFile(&lt;span style="color:#a5d6ff">$&amp;#34;\\Properties\\{folderIndentifier}\\Resource.resx&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;en&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ResourcesEnglish = OverwriteDictionary(baseResources, customResources);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesSpanish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (_ResourcesSpanish == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> folderIndentifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>]; ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> baseResources = GetDictionaryFromEmbedded(&lt;span style="color:#a5d6ff">&amp;#34;resources-demo.Properties.Resources&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> customResources = GetDictionaryFromFile(&lt;span style="color:#a5d6ff">$&amp;#34;\\Properties\\{folderIndentifier}\\Resources.es.resx&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ResourcesSpanish = OverwriteDictionary(baseResources, customResources);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>먼저 app.settings에서 식별자를 가져옵니다.&lt;/li>
&lt;li>그런 다음 기본 리소스, 즉 내장된 리소스를 얻습니다.&lt;/li>
&lt;li>그런 다음 사용자 정의 리소스를 얻으려면 폴더 이름(식별자)이 필요합니다.&lt;/li>
&lt;li>그런 다음 이를 혼합하고 값을 반환합니다.&lt;/li>
&lt;/ol>
&lt;p>이 모든 작업은 한 번만 수행되므로 지연 로딩이 발생합니다.&lt;/p>
&lt;h1 id="테스트">테스트&lt;/h1>
&lt;p>코드와 관련된 모든 것이 끝났으므로 이제 테스트해 보겠습니다. 사전에서 값을 얻으려면 값을 반환하는 &lt;code>CustomResources.GetText(string key)&lt;/code> 메서드를 호출해야 합니다.&lt;/p>
&lt;h2 id="전체-리소스-파일-업데이트">전체 리소스 파일 업데이트&lt;/h2>
&lt;p>이 테스트는 리소스 파일의 전체 키/값을 업데이트하려는 경우입니다. 그림에서 볼 수 있듯이 키는 동일하지만 값이 다릅니다.&lt;/p>
&lt;p>&lt;code>John&lt;/code>를 테스트하고 이를 설정하기 위해 app.config를 &lt;code>&amp;lt;add key=&amp;quot;CustomResources.Folder&amp;quot; value=&amp;quot;John&amp;quot; /&amp;gt;&lt;/code>로 설정합니다.&lt;/p>
&lt;p>이제 기본 리소스 파일(&lt;code>Properties/Resource.es.resx&lt;/code>)을 확인해 보겠습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/26d6cef147ca3cdd93a1d6cc4c60c212">&lt;img src="https://i.gyazo.com/26d6cef147ca3cdd93a1d6cc4c60c212.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그런 다음 외부 리소스 파일(&lt;code>Properties/John/Resource.es.resx&lt;/code>):&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/fd6582da0d6a427c2b21cc8b1999f9e0">&lt;img src="https://i.gyazo.com/fd6582da0d6a427c2b21cc8b1999f9e0.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>좋습니다. 모든 것이 설정되었으면 콘솔 애플리케이션을 실행하고 속성의 &lt;code>get&lt;/code> 부분으로 이동하여 모든 것을 확인하겠습니다.&lt;/p>
&lt;p>&lt;code>folderIdentifier&lt;/code>에는 앱 설정 값이 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ac30d3deb7abe29a877b368d9300b990">&lt;img src="https://i.gyazo.com/ac30d3deb7abe29a877b368d9300b990.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>&lt;code>baseResources&lt;/code>에는 기본 리소스, 내장된 리소스의 값이 있습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/a61410b365b55d4735f2b2622a18b966">&lt;img src="https://i.gyazo.com/a61410b365b55d4735f2b2622a18b966.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>&lt;code>customResources&lt;/code>에는 &lt;code>John&lt;/code> 폴더 내부에 외부 리소스 값이 있습니다.&lt;a href="https://gyazo.com/0e93733fbd616b274a69cdffbc16afb1">&lt;img src="https://i.gyazo.com/0e93733fbd616b274a69cdffbc16afb1.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>그리고 마지막으로 &lt;code>_ResourcesSpanish&lt;/code>는 기본 자원과 외부 자원이 혼합된 값을 갖는다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/0e25a79023364ea1d5be2109dbf6492c">&lt;img src="https://i.gyazo.com/0e25a79023364ea1d5be2109dbf6492c.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="하나만-업데이트">하나만 업데이트&lt;/h2>
&lt;p>이제 동일하지만 다른 시나리오를 테스트해 보겠습니다. 키 하나를 업데이트하고 다른 키도 동일하게 두십시오.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/9c2a05fb976f1f671b13c9cf77220336">&lt;img src="https://i.gyazo.com/9c2a05fb976f1f671b13c9cf77220336.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>보시다시피 파일은 &lt;code>Action_greeting&lt;/code>에 대해 동일한 값 의미를 가지지만 &lt;code>Action_cancel&lt;/code>에 대해 다른 값을 가지므로 &lt;code>Action_cancel&lt;/code>만 업데이트해야 합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/16938859b05bb1f0fe08751a08b1fcad">&lt;img src="https://i.gyazo.com/16938859b05bb1f0fe08751a08b1fcad.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;h2 id="리소스-파일이-누락되었습니다">리소스 파일이 누락되었습니다.&lt;/h2>
&lt;p>외부 리소스 파일을 제공하지 않으면 파일이 있을 것으로 예상하므로 실패하면 빈 사전을 반환하고 두 사전을 혼합하면 기본 사전으로 끝나기 때문에 전혀 문제가 되지 않습니다.&lt;/p>
&lt;h2 id="포함된-리소스에-쌍이-없습니다">포함된 리소스에 쌍이 없습니다.&lt;/h2>
&lt;p>외부 리소스 파일에 쌍이 있는 경우 기본적으로 최종 사전에 추가되지 않습니다. 두 사전을 혼합할 때 &lt;code>OverwriteDictionary()&lt;/code> 메서드를 호출하고 &lt;code>addIfDoesntExist&lt;/code> 매개변수를 &lt;code>true&lt;/code>로 설정하여 이를 변경할 수 있습니다.&lt;/p>
&lt;h2 id="다른-언어">다른 언어&lt;/h2>
&lt;p>보시다시피 우리는 언어를 지정하지 않았습니다. 모든 작업은 &lt;code>GetText(string key, string language)&lt;/code>를 호출하는 &lt;code>GetText(string key)&lt;/code> 함수에 의해 수행되고 매개변수 &lt;code>language&lt;/code>는 스레드의 현재 언어를 반환하는 &lt;code>TwoLetterISOLanguageName&lt;/code>에 의해 채워지기 때문입니다.&lt;/p>
&lt;p>내 경우에는 기본 언어가 스페인어이므로 항상 스페인어로 표시되지만 영어도 사용해 볼 수 있습니다.&lt;/p>
&lt;p>약간의 코딩을 하고 스페인어와 영어를 모두 확인할 수 있는 것을 만들어 보겠습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> Main(&lt;span style="color:#ff7b72">string&lt;/span>[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(&amp;#34;en-US&amp;#34;);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Threading.Thread.CurrentThread.CurrentCulture = &lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;en-US&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(&amp;#34;es-ES&amp;#34;);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Threading.Thread.CurrentThread.CurrentCulture = &lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;es-ES&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Read();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> WriteText(&lt;span style="color:#ff7b72">string&lt;/span> key) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[Culture: {System.Threading.Thread.CurrentThread.CurrentCulture}] Key: {key} -&amp;gt; value: {CustomResources.GetText(key)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이를 실행하면 출력은 다음과 같아야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>[Culture: en-US] Key: Action_greeting -&amp;gt; value: Hello
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: en-US] Key: Action_cancel -&amp;gt; value: Finish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: es-ES] Key: Action_greeting -&amp;gt; value: Hola
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: es-ES] Key: Action_cancel -&amp;gt; value: Terminar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://gyazo.com/de7e5efcc2ff5e82529749b1188706ff">&lt;img src="https://i.gyazo.com/de7e5efcc2ff5e82529749b1188706ff.png" alt="Gyazo의 이미지">&lt;/a>&lt;/p>
&lt;p>처음 두 값은 영어인 &lt;code>resources.resx&lt;/code>에서 가져오고 마지막 값은 모두 스페인어로 되어 있으며 값은 &lt;code>resources.es.resx&lt;/code>에서 검색됩니다.&lt;/p>
&lt;h1 id="그게-다야">그게 다야&lt;/h1>
&lt;p>이 튜토리얼에서 우리는 언어용 내장 리소스와 외부 리소스를 혼합하는 방법을 찾았습니다. 이는 팀에서 겪었던 문제에 대한 솔루션이었으며 그 이후로 아무런 문제 없이 실행되어 왔습니다.&lt;/p>
&lt;p>소스코드는 &lt;a href="https://github.com/emimontesdeoca/resources-demo">여기&lt;/a>에서 확인할 수 있습니다.&lt;/p>
&lt;p>질문이 있으시면 언제든지 &lt;a href="https://twitter.com/emimontesdeocaa">@emimontesdeocaa&lt;/a>로 트윗해 주세요. 시간이 나면 연락드리겠습니다.&lt;/p></content:encoded><category>.NET</category></item><item><title>흐름 사례에 대해 Bot Framework 및 DirectLine을 사용한 통합 테스트</title><link>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-with-flow-cases/</link><pubDate>Wed, 25 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-with-flow-cases/</guid><description>다중 전환 대화 흐름 시나리오를 지원하도록 Bot Framework 통합 테스트를 확장합니다.</description><content:encoded>&lt;h2 id="소개">소개&lt;/h2>
&lt;p>이전 블로그 게시물에서는 단일 사례에 대한 몇 가지 통합 테스트를 수행했습니다. &amp;ldquo;단일 사례&amp;quot;는 봇에게 무언가를 요청한 후 한 번만 응답하고 결과를 비교하는 경우입니다.&lt;/p>
&lt;p>&lt;strong>이 가이드에서는 인증 및 API 호출이 어떻게 수행되는지 설명하지 않습니다. 확인하려면 단일 사례 가이드를 확인하세요.&lt;/strong>&lt;/p>
&lt;h3 id="흐름-대화는-어떻습니까">흐름 대화는 어떻습니까?&lt;/h3>
&lt;p>현재 방식을 사용하면 대화에 어떤 종류의 흐름도 없습니다. 예를 들어, 도움을 요청하려는 경우 메뉴에 다양한 옵션이 표시되고 그 중 하나를 선택한 후 다른 메뉴가 표시됩니다. 이렇게 하면 2개 이상의 &lt;code>responses&lt;/code>이 됩니다. &lt;strong>즉, 이전에 사용했던 통합 테스트 솔루션을 사용하면 작동하지 않는다는 의미입니다.&lt;/strong>&lt;/p>
&lt;p>다음은 단일 사례에 대한 통합 테스트가 작동하는 방식을 보여주는 다이어그램입니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8915f2653033c1143947ef59196403f4">&lt;img src="https://i.gyazo.com/8915f2653033c1143947ef59196403f4.png" alt="https://gyazo.com/8915f2653033c1143947ef59196403f4">&lt;/a>&lt;/p>
&lt;p>다음은 현재 통합 테스트 솔루션을 흐름 사례에 어떻게 적용할 것인지에 대한 다이어그램입니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;img src="https://i.gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83.png" alt="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;/a>&lt;/p>
&lt;p>&lt;strong>다음 설명에서는 Bot Framework 작동 방식에 대한 모든 기본 정보를 다루지 않습니다. 이해가 되지 않는 경우 공식 문서를 확인하세요.&lt;/strong>&lt;/p>
&lt;h2 id="예시-사례">예시 사례&lt;/h2>
&lt;p>다음 가이드에서는 도움을 요청하고 다른 옵션을 선택하는 흐름 대화가 포함된 봇을 사용하겠습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2">&lt;img src="https://i.gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2.gif" alt="https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2">&lt;/a>&lt;/p>
&lt;h2 id="새로운-json-구조">새로운 JSON 구조&lt;/h2>
&lt;p>이제 봇과 대화할 때 하나 이상의 &lt;code>request&lt;/code>가 있으므로 json 구조를 수정하여 수행할 모든 &lt;code>request&lt;/code>를 추가해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;secret&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;direct-line-secret&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineGenerateTokenEndpoint&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/tokens/generate&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineConversationEndpoint&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/conversations/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entries&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;PedirAyuda&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requests&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Ayuda&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Telefono&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Oficina&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Tenerife&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;response&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.901Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;localTimestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T09:04:37+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;serviceUrl&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;http://localhost:50629&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;emulator&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;j98bbdf097a&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Bot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;conversation&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;eabcie4be8ak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;recipient&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;922920252&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;attachments&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;replyToId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;47me557ikbf7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;assert&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Request.Text == Response.Text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>보시다시피 중요한 변경 사항이 있습니다. &lt;code>request&lt;/code>는 이제 &lt;code>requests&lt;/code>입니다. 이는 이제 단일 &lt;code>activity&lt;/code> 대신 &lt;code>List&amp;lt;Activity&amp;gt;&lt;/code>가 있음을 의미합니다.&lt;/p>
&lt;h2 id="새로운-객체">새로운 객체&lt;/h2>
&lt;p>이전에는 &lt;code>TestEntry&lt;/code> 및 &lt;code>TestEntryCollection&lt;/code>라는 단일 사례에 대해 개체를 설정했습니다. 흐름 사례의 경우 &lt;code>TestEntryFlow&lt;/code> 및 &lt;code>TestEntryFlowCollection&lt;/code>라는 새 객체를 생성합니다.&lt;/p>
&lt;h3 id="testentryflow">&lt;code>TestEntryFlow&lt;/code>&lt;/h3>
&lt;p>이 객체는 컬렉션에 있는 모든 항목에 대한 것입니다. 앞서 언급한 것처럼 &lt;code>Requests&lt;/code> 객체는 이제 단일 &lt;code>Activity&lt;/code> 대신 &lt;code>List&amp;lt;Activity&amp;gt;&lt;/code>입니다.&lt;/p>
&lt;p>봇에게 여러 번 요청할 것이므로 대화에 전송될 &lt;code>activities&lt;/code>가 여러 개 필요합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntryFlow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entry name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;name&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity requested by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;requests&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;Activity&amp;gt; Requests { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity response expected by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;response&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Response { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert value in string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;assert&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Assert { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="testentirescollection">&lt;code>TestEntiresCollection&lt;/code>&lt;/h3>
&lt;p>이 객체에는 &lt;code>secret&lt;/code> 및 엔드포인트와 같은 &lt;code>DirectLine&lt;/code>에 대한 관련 정보와 테스트할 &lt;code>Entries&lt;/code> 목록이 포함됩니다.&lt;/p>
&lt;p>&lt;code>Entries&lt;/code>는 이제 &lt;code>TestEntry&lt;/code>가 아니라 &lt;code>TestEntryFlow&lt;/code>의 목록입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntryFlowCollection&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// DirectLine Secret&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;secret&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Secret { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint to get the token using the secret for DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineGenerateTokenEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineGenerateTokenEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint for a conversation in DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineConversationEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineConversationEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entries list&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;entries&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;TestEntryFlow&amp;gt; Entries { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="흐름-사례를-위한-testmethod-생성">흐름 사례를 위한 &lt;code>TestMethod&lt;/code> 생성&lt;/h2>
&lt;h3 id="새로운-흐름">새로운 흐름&lt;/h3>
&lt;p>우선 다이어그램을 다시 한 번 살펴보세요(위에 게시한 것과 동일합니다).&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;img src="https://i.gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83.png" alt="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;/a>&lt;/p>
&lt;p>보시다시피, 테스트를 수행하기 위한 흐름 구조는 거의 동일합니다.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>정보 얻기&lt;/p>
&lt;/li>
&lt;li>
&lt;p>인증&lt;/p>
&lt;/li>
&lt;li>
&lt;p>대화 만들기&lt;strong>여기에 변경 사항이 있습니다. 이제 대화에 대한 모든 요청을 여러 번 보내야 합니다. 이를 위해서는 각 요청을 반복하여 봇에 보낸 다음 최신 응답을 예상 응답과 비교해야 합니다.&lt;/strong>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>모든 요청 보내기&lt;/p>
&lt;/li>
&lt;li>
&lt;p>모든 메시지 받기&lt;/p>
&lt;/li>
&lt;li>
&lt;p>최신 답변 받기&lt;/p>
&lt;/li>
&lt;li>
&lt;p>예상 응답과 비교&lt;/p>
&lt;/li>
&lt;li>
&lt;p>결과 어설션&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="코드">코드&lt;/h3>
&lt;p>우선 파일에서 정보를 가져와야 합니다. 이는 이전에 단일 사례에서 했던 것과 동일합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\dataFlow.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntryFlowCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 &lt;code>data.entries&lt;/code>의 각 &lt;code>TestEntryFlow&lt;/code>에 대해 반복해야 합니다. 이를 통해 &lt;code>requests&lt;/code>에서 반복하는 새 부분까지 단일 사례에서 수행한 것과 동일한 흐름을 따를 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Activity latestResponse = &lt;span style="color:#ff7b72">new&lt;/span> Activity();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act for step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 - Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>다음 단계는 매우 간단합니다. &lt;code>entry.requests&lt;/code>를 반복하고 모든 &lt;code>activity&lt;/code>을 대화로 보내야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">foreach&lt;/span> (Activity step &lt;span style="color:#ff7b72">in&lt;/span> entry.Requests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (step.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>watermark&lt;/code>를 사용하여 최신 메시지를 가져오며, &lt;code>watermark&lt;/code>는 DirectLine API가 대화 정보를 요청할 때 반환하는 값입니다.&lt;/p>
&lt;p>그런 다음 &lt;code>globals&lt;/code>를 &lt;code>latestReponse&lt;/code> 및 &lt;code>expectedResponse&lt;/code>로 채워야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 사례를 마무리하기 위해 &lt;code>entry&lt;/code>에서 &lt;code>assert&lt;/code> 문자열을 평가합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="최종-코드">최종 코드&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ShouldTestFlowCases()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\dataFlow.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntryFlowCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Flow: Arrange -&amp;gt; Act -&amp;gt; arrange -&amp;gt; assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntryFlow entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Activity latestResponse = &lt;span style="color:#ff7b72">new&lt;/span> Activity();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act for step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (Activity step &lt;span style="color:#ff7b72">in&lt;/span> entry.Requests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (step.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="개선-사항">개선 사항&lt;/h2>
&lt;p>나는 더 나은 흐름이 가능할 수 있다고 믿습니다. 그러나 이러한 개선은 JSON 구조도 변경되어야 함을 의미합니다. 또한 이를 실현하려면 json을 더 많이 채워야 합니다.&lt;/p>
&lt;p>이 테스트를 더 좋게 만들기 위해서는 각 &lt;code>request&lt;/code>에 대해 &lt;code>response&lt;/code>가 있어야 하며 메시지를 보낼 때마다 주장해야 합니다. 지금 우리가 하고 있는 방법은 모든 &lt;code>requests&lt;/code> 와 &lt;strong>최종 &lt;code>response&lt;/code>&lt;/strong> 을 저장하는 것입니다.&lt;/p>
&lt;p>이것이 어떻게 보이는지 보여주기 위해 다이어그램을 만들었습니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc">&lt;img src="https://i.gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc.png" alt="https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc">&lt;/a>&lt;/p>
&lt;p>나는 최종 응답을 테스트하는 대신 흐름의 거의 모든 동작을 테스트하기 때문에 이 방법이 테스트의 무결성을 위해 전반적으로 훨씬 더 낫다고 생각합니다.&lt;/p>
&lt;hr>
&lt;p>&lt;strong>이 가이드는 이것이 전부입니다. 이 가이드는 단일 사례 가이드의 연속이라는 점을 기억하세요. 길을 잃은 것 같으면 모든 것에 대해 더 길고 더 많은 설명이 포함된 가이드를 확인하세요.&lt;/strong>&lt;/p>
&lt;p>모든 코드는 내 github의 &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> 저장소에 저장되어 있습니다.&lt;/p>
&lt;p>좋은 하루 보내세요!&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Bot Framework와 DirectLine을 이용한 통합 테스트 (1)</title><link>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-1/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-1/</guid><description>DirectLine API 및 JSON 테스트 사례를 사용하여 Bot Framework 챗봇에 대한 통합 테스트를 설정합니다.</description><content:encoded>&lt;p>저는 회사에 잠시 입사한 이후 Bot Framework 통합 테스트 업무를 맡게 되었습니다.&lt;/p>
&lt;p>그룹에서 제가 하는 일은 봇을 최대한 안정적으로 만드는 것인데, 먼저 Bot Framework가 무엇인가요?&lt;/p>
&lt;blockquote>
&lt;p>지능형 봇을 구축, 연결, 배포 및 관리하여 웹사이트, 앱, Cortana, Microsoft Teams, Skype, Slack, Facebook Messenger 등에서 사용자와 자연스럽게 상호 작용합니다. 사용한 만큼만 비용을 지불하면서 완전한 봇 구축 환경을 빠르게 시작하세요.&lt;/p>&lt;/blockquote>
&lt;p>Bot Framework에 대한 자세한 내용은 &lt;a href="https://dev.botframework.com/">여기&lt;/a>에서 확인할 수 있습니다.&lt;/p>
&lt;p>&lt;strong>다음 설명에서는 Bot Framework 작동 방식에 대한 모든 기본 정보를 다루지 않습니다. 이해가 되지 않는 경우 공식 문서를 확인하세요.&lt;/strong>&lt;/p>
&lt;h2 id="통합-테스트는-왜-필요한가요">통합 테스트는 왜 필요한가요?&lt;/h2>
&lt;p>통합 테스트가 필요한 이유는 동료 중 한 명이 수정 사항, 새 기능 또는 새 버그를 푸시할 때마다 이 테스트가 코드를 프로덕션 환경으로 푸시하기 전에 실행되며, 테스트 중 하나라도 실패하면 코드가 프로덕션 환경으로 이동하지 않기 때문입니다. 즉, 최종 사용자에게는 버그가 발생하지 않습니다.&lt;/p>
&lt;h2 id="봇에서의-통합-테스트">봇에서의 통합 테스트?&lt;/h2>
&lt;p>저는 봇에서는 통합 테스트가 매우 중요하다고 믿습니다. 일부 메뉴가 작동하지 않거나 일부 기능이 아무것도 반환하지 않는 봇을 가질 수 없습니다.&lt;/p>
&lt;p>회사는 사람들이 문제로 바쁘게 지내는 것을 원하지 않기 때문에 고객을 위해 봇을 사용하고 있습니다. 봇이 사용자를 도울 수 있다면 다른 직원은 자신의 시간을 더 중요한 일에 사용할 수 있습니다.&lt;/p>
&lt;h2 id="솔루션-개요">솔루션 개요.&lt;/h2>
&lt;p>이 작업을 수행하기 위해 Visual Studio에서 API Rest용 WebClient와 케이스를 저장할 Json 파일을 사용하는 테스트 프로젝트를 사용했습니다.&lt;/p>
&lt;h2 id="json-파일">JSON 파일&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;secret&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;direct-line-secret&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineGenerateTokenEndpoint&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/tokens/generate&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineConversationEndpoint&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/conversations/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entries&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;DecirHola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;request&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Hola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;response&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.901Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;localTimestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T09:04:37+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;serviceUrl&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;http://localhost:50629&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;emulator&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;j98bbdf097a&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Bot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;conversation&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;eabcie4be8ak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;recipient&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;No tengo respuesta para eso.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;attachments&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;replyToId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;47me557ikbf7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;assert&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Request.Text == Response.Text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>보시다시피 우리는 다음을 가지고 있습니다:&lt;/p>
&lt;ul>
&lt;li>다이렉트라인 비밀 -&amp;gt; 게시된 봇의 비밀&lt;/li>
&lt;li>Directline 토큰 생성 엔드포인트 -&amp;gt; 시크릿을 사용하여 토큰을 가져오는 엔드포인트&lt;/li>
&lt;li>직접 대화 끝점 -&amp;gt; 대화를 플레이하기 위한 끝점&lt;/li>
&lt;li>항목 -&amp;gt; 테스트 케이스
&lt;ul>
&lt;li>요청 -&amp;gt; 대화에 보내는 내용&lt;/li>
&lt;li>응답 -&amp;gt; 우리가 얻기를 기대하는 것&lt;/li>
&lt;li>주장 -&amp;gt; 무엇을 비교하고 있나요?&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="역직렬화">역직렬화&lt;/h2>
&lt;p>완벽하게 형식화된 json 파일이 있으므로 이제 이를 솔루션에 로드해야 하므로 JSON.NET과 일부 클래스를 사용하게 됩니다. 먼저 모든 것을 포함하는 항목 컬렉션이 있고 각 컬렉션에 대한 항목 목록이 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Object to parse from the file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntriesCollection&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// DirectLine Secret&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;secret&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Secret { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint to get the token using the secret for DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineGenerateTokenEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineGenerateTokenEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint for a conversation in DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineConversationEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineConversationEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entries list&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;entries&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;TestEntry&amp;gt; Entries { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>그리고 이것은 테스트 케이스에 대한 이름, 요청, 응답 및 주장을 포함하는 객체입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entry name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;name&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity requested by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;request&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Request { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity response expected by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;response&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Response { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert value in string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;assert&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Assert { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="테스트-케이스의-json에서-객체로-구문-분석">테스트 케이스의 json에서 객체로 구문 분석&lt;/h2>
&lt;p>구문 분석 개체에 대한 클래스가 있으면 개체로 읽어야 하므로 매우 쉽습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\data.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntriesCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 이 컬렉션을 사용하면 컬렉션을 반복하여 foreach 등을 사용하여 정보를 얻을 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntry entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ....
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것이 이 부분의 전부입니다. 다음 부분에는 DirectLine에 대한 인증이 포함됩니다. 모든 코드는 내 github의 &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> 저장소에 저장되어 있습니다.&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Bot Framework와 DirectLine을 이용한 통합 테스트 (2)</title><link>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-2/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-2/</guid><description>Bot Framework 통합 테스트를 위한 DirectLine 인증 및 API 호출을 구현합니다(2부).</description><content:encoded>&lt;h4 id="이-부분에서는-directline-인증을-수행하고-봇의-응답에서-값을-가져옵니다">이 부분에서는 DirectLine 인증을 수행하고 봇의 응답에서 값을 가져옵니다.&lt;/h4>
&lt;p>이제 역직렬화를 완료했으므로 인증을 받고 API를 호출하고 결과를 확인하는 데 사용할 컬렉션과 전체에서 정보를 가져올 차례입니다.&lt;/p>
&lt;p>&lt;strong>다음 설명에서는 Bot Framework 작동 방식에 대한 모든 기본 정보를 다루지 않습니다. 이해가 되지 않는 경우 공식 문서를 확인하세요.&lt;/strong>&lt;/p>
&lt;h2 id="webclient를-사용하여-api-호출하기">WebClient를 사용하여 API 호출하기&lt;/h2>
&lt;p>API를 더 쉽게 호출할 수 있도록 &lt;code>utils&lt;/code> 클래스를 만들었습니다. 여기에 몇 번 사용할 함수를 저장합니다. 이 클래스에는 POST용 &lt;code>uploadString&lt;/code>과 GET용 &lt;code>downloadString&lt;/code>가 포함되어 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Utils&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Uploads to an URL and gets result&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;typeparam name=&amp;#34;T&amp;#34;&amp;gt;Type of object you are receiving&amp;lt;/typeparam&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;bearer&amp;#34;&amp;gt;Token&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;url&amp;#34;&amp;gt;Url&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;serializedJson&amp;#34;&amp;gt;Serialized JSON to send&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T uploadString&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> bearer, &lt;span style="color:#ff7b72">string&lt;/span> url, &lt;span style="color:#ff7b72">string&lt;/span> serializedJson)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> serializedResult = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Webclient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> client = &lt;span style="color:#ff7b72">new&lt;/span> WebClient())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Add headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Content-Type&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Authorization&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">$&amp;#34;Bearer {bearer}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Upload string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializedResult = client.UploadString(url, serializedJson);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Get result and return it as an object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(serializedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Downloads from URL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;typeparam name=&amp;#34;T&amp;#34;&amp;gt;Type of object you are receiving&amp;lt;/typeparam&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;bearer&amp;#34;&amp;gt;Token&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;url&amp;#34;&amp;gt;Url&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T downloadString&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> bearer, &lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> serializedResult = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Webclient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> client = &lt;span style="color:#ff7b72">new&lt;/span> WebClient())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Add headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Content-Type&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Authorization&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">$&amp;#34;Bearer {bearer}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Download string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializedResult = client.DownloadString(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Get result and return it as an object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(serializedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="directline-인증">DirectLine 인증&lt;/h2>
&lt;p>공식 문서를 읽으면 이를 수행하는 방법을 찾을 수 있으며 매우 쉽습니다. 우리 기능을 사용하면 훨씬 더 쉽습니다. 우선 우리가 foreach 문 안에 있다는 점을 기억하세요. 시간이 부족할 경우를 대비해 각 사례에 대해 인증을 수행하고 있기 때문에 테스트가 실패한다는 의미입니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이제 대화 끝점에 대한 다음 호출을 모두 수행하는 데 사용되는 토큰이 있습니다.&lt;/p>
&lt;h2 id="대화를-만드는-중입니다">대화를 만드는 중입니다.&lt;/h2>
&lt;p>봇과 대화하려면 먼저 대화를 만들어야 합니다. 이 대화는 대화 ID가 포함된 새 토큰을 반환합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>또한 &lt;code>newToken&lt;/code> 및 &lt;code>conversationId&lt;/code>를 저장합니다. 둘 다 사용자가 봇에 메시지를 보내는 데 필요합니다.&lt;/p>
&lt;h2 id="활동을-대화로-보내기">활동을 대화로 보내기&lt;/h2>
&lt;p>이제 &lt;code>conversationId&lt;/code> 및 &lt;code>conversationEndpoint&lt;/code>를 사용하여 json 파일에서 &lt;code>request&lt;/code>인 &lt;code>Activity&lt;/code>를 보내는 최종 엔드포인트를 생성할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(entry.Request));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="최신-메시지-받기">최신 메시지 받기&lt;/h2>
&lt;p>메시지 기록에서 활동을 보낸 후 봇이 이미 응답했어야 하므로 워터마크가 있는 모든 메시지를 가져온 다음 해당 워터마크를 사용하여 최신 메시지/활동을 필터링해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이것이 이 부분의 전부입니다. 다음 부분에는 json의 &lt;code>assert&lt;/code>에서 텍스트를 가져와서 Javascript에서는 &lt;code>eval()&lt;/code>를 사용하고 C#에서는 &lt;code>Assert.isTrue()&lt;/code>를 사용하여 최종 테스트 결과를 얻는 것과 같은 코드로 변환하는 부분이 포함됩니다.&lt;/p>
&lt;p>모든 코드는 내 github의 &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> 저장소에 저장되어 있습니다.&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Bot Framework와 DirectLine을 이용한 통합 테스트 (3)</title><link>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-3/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/posts/integration-test-bot-framework-3/</guid><description>Bot Framework 통합 테스트(3부)에서 Roslyn CodeAnalytic을 사용하여 봇 응답을 평가합니다.</description><content:encoded>&lt;h4 id="이것은-가이드의-최신-부분입니다-이-부분에서는-json에-있는-어설션의-텍스트를-평가할-것입니다">이것은 가이드의 최신 부분입니다. 이 부분에서는 json에 있는 어설션의 텍스트를 평가할 것입니다.&lt;/h4>
&lt;p>이제 &lt;code>request&lt;/code>를 사용하고 &lt;code>activity&lt;/code>를 봇에 보냈으므로 &lt;code>response&lt;/code>를 얻었고 json의 &lt;code>assert&lt;/code>(json에 저장한 응답, 예상 응답)과 봇에서 얻은 응답을 비교해야 합니다.&lt;/p>
&lt;p>&lt;strong>다음 설명에서는 Bot Framework 작동 방식에 대한 모든 기본 정보를 다루지 않습니다. 이해가 되지 않는 경우 공식 문서를 확인하세요.&lt;/strong>&lt;/p>
&lt;h2 id="솔루션에-microsoftcodeanalytic-추가">솔루션에 Microsoft.CodeAnalytic 추가&lt;/h2>
&lt;p>우선 CodeAnalytic을 NuGet 패키지로 포함해야 합니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ce97a60ecab998f162f6ac7a2ab2c9a7">&lt;img src="https://i.gyazo.com/ce97a60ecab998f162f6ac7a2ab2c9a7.png" alt="https://gyazo.com/ce97a60ecab998f162f6ac7a2ab2c9a7">&lt;/a>&lt;/p>
&lt;p>설치 후에는 &lt;code>.cs&lt;/code> 파일에 패키지를 추가해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.CodeAnalysis.CSharp.Scripting&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="globals-객체-생성">&lt;code>Globals&lt;/code> 객체 생성&lt;/h2>
&lt;p>평가하려면 매개변수를 평가자에게 전달해야 합니다. 이 평가자에는 구성 파일이 필요합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Object to pass parameters to Roslyn compiler&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Globals&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// ExpectedResponse&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Request;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// ReceivedResponse&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 부분은 매우 중요합니다. 해당 구성 파일에는 비교할 객체가 포함되므로 &lt;strong>예상 응답&lt;/strong>과 &lt;strong>응답&lt;/strong>을 전달해야 합니다.&lt;/p>
&lt;p>해당 정보를 사용하여 json 파일의 &lt;code>assert&lt;/code>을 사용하고 내용을 평가합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>&lt;code>EvaluateAsync&amp;lt;T&amp;gt;&lt;/code>는 T를 평가하고 반환합니다. 이 경우 평가할 &lt;code>string&lt;/code>와 평가할 데이터가 있는 &lt;code>globals&lt;/code>를 전달합니다.&lt;/strong>&lt;/p>
&lt;p>나는 항목(이름, 요청, 응답 및 주장이 있음)을 사용하여 예를 들어 이것을 설명하려고 합니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;DecirHola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;request&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Hola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;response&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.901Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;localTimestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T09:04:37+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;serviceUrl&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;http://localhost:50629&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;emulator&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;j98bbdf097a&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Bot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;conversation&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;eabcie4be8ak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;recipient&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;No tengo respuesta para eso.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;attachments&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;replyToId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;47me557ikbf7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;assert&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Request.Text == Response.Text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 항목에서 중요한 사항을 알아봅시다. 먼저 **&lt;code>&amp;quot;assert&amp;quot;: &amp;quot;Request.Text == Response.Text&amp;quot;&lt;/code>**를 주장합니다. 이는 &lt;code>Request.Text&lt;/code>를 &lt;code>Response.Text&lt;/code>와 비교하고 값을 부울로 반환한다는 의미입니다.&lt;/p>
&lt;p>그러나 &lt;code>await CSharpScript.EvaluateAsync&amp;lt;bool&amp;gt;(entry.Assert, globals: globals)&lt;/code> 함수를 호출할 때 2개의 매개변수를 전달합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>string&lt;/code> 평가할 문자열 -&amp;gt; &lt;code>&amp;quot;Request.Text == Response.Text&amp;quot;&lt;/code>&lt;/li>
&lt;li>평가자를 위한 &lt;code>globals&lt;/code> 데이터 -&amp;gt; 이 경우 &lt;code>Request&lt;/code> 및 &lt;code>Response&lt;/code>를 제공해야 합니다. 요청은 &lt;strong>예상 응답&lt;/strong>이고 응답은 &lt;strong>수신된 응답&lt;/strong>입니다.&lt;/li>
&lt;/ul>
&lt;p>평가자에 데이터를 채우는 동안 이제 문자열을 사용하여 평가할 수 있으므로 &lt;code>true&lt;/code> 또는 &lt;code>false&lt;/code>가 반환됩니다.&lt;/p>
&lt;h2 id="완료">완료&lt;/h2>
&lt;p>끝났습니다. 여기서 완성된 &lt;code>TestMethod&lt;/code>를 볼 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ShouldTestSingleCases()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\data.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntriesCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Flow: Arrange -&amp;gt; Act -&amp;gt; arrange -&amp;gt; assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntry entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (entry.Request.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(entry.Request));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="다이어그램">다이어그램&lt;/h2>
&lt;p>다음은 이 지점을 얻기 위해 우리가 따른 모든 흐름에 대한 다이어그램입니다. 이해하지 못한 부분이 있으면 이를 통해 문제가 해결되기를 바랍니다.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d">&lt;img src="https://i.gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d.png" alt="https://gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d">&lt;/a>&lt;/p>
&lt;p>이것이 전부입니다. 이는 사례가 1:1인 단일 사례에 대해 수행되며 사용자는 &lt;code>activity&lt;/code>를 보내고 봇은 또 다른 단일 &lt;code>activity&lt;/code>를 반환합니다.&lt;/p>
&lt;p>마음에 드셨기를 바랍니다. &lt;strong>다음은 응답이 두 개 이상인 테스트 흐름 사례입니다.&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d964cfac395ed438a5282e60614863e7">&lt;img src="https://i.gyazo.com/d964cfac395ed438a5282e60614863e7.png" alt="https://gyazo.com/d964cfac395ed438a5282e60614863e7">&lt;/a>&lt;/p>
&lt;p>모든 코드는 내 github의 &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> 저장소에 저장되어 있습니다.&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category><category>NuGet</category></item><item><title>발표</title><link>https://emimontesdeoca.github.io/ko/speaking/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/speaking/</guid><description>Emiliano Montesdeoca의 컨퍼런스 발표 및 세션 — Microsoft MVP이자 국제 발표자.</description><content:encoded>&lt;p>저는 &lt;strong>.NET&lt;/strong>, &lt;strong>Azure&lt;/strong>, &lt;strong>AI&lt;/strong>, &lt;strong>클라우드 네이티브 개발&lt;/strong>에 초점을 맞춘 국제 기술 컨퍼런스의 자주 발표자입니다. 2023, 2024, 2025년에 &lt;strong>Sessionize 가장 활발한 발표자&lt;/strong>로 선정되었으며, 개발자 기술 분야의 &lt;strong>Microsoft MVP&lt;/strong> 자격을 보유하고 있습니다.&lt;/p>
&lt;p>제 이벤트에서 발표하기를 원하신다면, &lt;a href="mailto:emimontesdeoca@outlook.es">연락해 주세요&lt;/a>!&lt;/p>
&lt;hr>
&lt;h2 id="2026">2026&lt;/h2>
&lt;h3 id="2026년의-blazor">2026년의 Blazor&lt;/h3>
&lt;p>2026년 Blazor의 현재 상태와 .NET 10과 함께 어떻게 발전했는지 살펴봅니다. 최신 변경 사항과 개선 사항, 실제 애플리케이션 개발에 미치는 영향, 그리고 각 시나리오에 가장 적합한 Blazor 호스팅 모델을 검토합니다.&lt;/p>
&lt;h3 id="ai로-level-3-지원-강화-azure-functions--openai-실전">AI로 Level 3 지원 강화: Azure Functions + OpenAI 실전&lt;/h3>
&lt;p>Azure Functions, Azure Monitor 알림, Azure OpenAI가 함께 작동하여 Level 3 지원을 가속화하는 방법. 자동 알림 분류에서 지능형 로그 분석까지 — AI를 실제 지원 워크플로에 추가하는 실용적인 방법.&lt;/p>
&lt;h3 id="대시보드에서-에이전트로-관찰-가능성의-다음-단계">대시보드에서 에이전트로: 관찰 가능성의 다음 단계&lt;/h3>
&lt;p>AI 에이전트와 프로덕션 관찰 가능성의 세계를 연결하는 실습 세션. Semantic Kernel에서 OpenTelemetry 데이터를 쿼리하고, MCP를 통해 Grafana와 상호 작용하며, 실제 시스템 운영과 최적화를 돕는 자율 에이전트까지.&lt;/p>
&lt;p>&lt;strong>예정&lt;/strong>: &lt;a href="https://globalazure.es">Global Azure 2026&lt;/a> — 2026년 4월, 스페인 마드리드&lt;/p>
&lt;hr>
&lt;h2 id="2025">2025&lt;/h2>
&lt;h3 id="크리스마스를-구하는-microsoft-agent-framework">크리스마스를 구하는 Microsoft Agent Framework&lt;/h3>
&lt;p>완벽한 크리스마스 선물을 찾기 위해 Microsoft Agent Framework를 사용하여 여러 AI 에이전트를 구축하고 조정합니다. 각 에이전트는 전문화되어 — 선물 아이디어 생성에서 가격 비교까지 — 더 스마트한 연말 쇼핑을 위해 함께 작동합니다.&lt;/p>
&lt;h3 id="ai로-level-3-지원-강화-azure-functions--semantic-kernel-실전">AI로 Level 3 지원 강화: Azure Functions + Semantic Kernel 실전&lt;/h3>
&lt;p>Azure Functions, Azure Monitor, Semantic Kernel이 함께 작동하여 Level 3 지원을 더 빠르고 효율적으로 만듭니다 — 자동 알림 감지에서 지능형 로그 분석까지.&lt;/p>
&lt;h3 id="net-10으로-blazor에-무엇이-새로워졌나">.NET 10으로 Blazor에 무엇이 새로워졌나?&lt;/h3>
&lt;p>성능 향상, 양식, 영구 상태, 더 강력한 JavaScript 상호 운용성 등 .NET 10을 위한 Blazor의 주요 새 기능 탐색.&lt;/p>
&lt;h3 id="c-semantic-kernel-gemini를-활용한-하이브리드-ai-스마트한-엔터프라이즈-앱-구축">C#, Semantic Kernel, Gemini를 활용한 하이브리드 AI: 스마트한 엔터프라이즈 앱 구축&lt;/h3>
&lt;p>C#에서 하이브리드 AI 워크플로 오케스트레이션 — 로컬 모델(Phi-3)을 Vertex AI를 통한 Gemini Pro/Ultra와 결합하고, Semantic Kernel 플러그인으로 인지 에이전트를 구축하며, 적응형 아키텍처를 설계합니다.&lt;/p>
&lt;h3 id="net-aspire-두통-없이-cloud-native-앱-구축">.NET Aspire: 두통 없이 Cloud-Native 앱 구축&lt;/h3>
&lt;p>태생적으로 확장 가능하고, 탄력적이며, 관찰 가능한 클라우드 퍼스트 마이크로서비스를 구축하는 실습 세션. .NET Aspire가 클라우드 보일러플레이트의 70%를 어떻게 제거하는지 탐색.&lt;/p>
&lt;h3 id="net-aspire-혼돈-없는-cloud-native">.NET Aspire: 혼돈 없는 Cloud-Native&lt;/h3>
&lt;p>첫 번째 커밋부터 관찰 가능성과 자동 탄력성이 내장된, 태생적으로 확장 가능한 앱 설계의 실제 사례.&lt;/p>
&lt;hr>
&lt;h2 id="2024">2024&lt;/h2>
&lt;h3 id="ai와-sql의-만남-net과-semantic-kernel로-스마트-피자-가게-구축">AI와 SQL의 만남: .NET과 Semantic Kernel로 스마트 피자 가게 구축&lt;/h3>
&lt;p>코드와 데이터를 이해하는 자연어 AI 상호 작용 — .NET과 Semantic Kernel로 구동되는 지능형 피자 가게 구축.&lt;/p>
&lt;h3 id="net-aspire-복잡함-없는-cloud-native-애플리케이션">.NET Aspire: 복잡함 없는 Cloud-Native 애플리케이션&lt;/h3>
&lt;p>두통 없이 클라우드 퍼스트 마이크로서비스 구축 — .NET Aspire가 클라우드 네이티브 개발을 어떻게 단순화하는지 보여주는 실용적인 데모 중심 세션.&lt;/p>
&lt;h3 id="power-platform-low-code에서-pro-code-기술로의-여정">Power Platform: Low-code에서 Pro-code 기술로의 여정&lt;/h3>
&lt;p>같은 플랫폼 내에서 TypeScript를 사용하여 Low-code Power Apps에서 고급 Pro-code 컴포넌트로 전환.&lt;/p>
&lt;h3 id="지능형-자동화-ai-통합으로-엔터프라이즈-애플리케이션-혁신">지능형 자동화: AI 통합으로 엔터프라이즈 애플리케이션 혁신&lt;/h3>
&lt;p>.NET 애플리케이션을 다음 단계로 — Semantic Kernel과 .NET Aspire를 사용하여 전통적인 로직을 AI 기반 추론과 결합.&lt;/p>
&lt;h3 id="low-code에서-pro-code로-엔터프라이즈-솔루션에서의-ai-개발-접근-방식">Low-Code에서 Pro-Code로: 엔터프라이즈 솔루션에서의 AI 개발 접근 방식&lt;/h3>
&lt;p>엔터프라이즈 솔루션의 다양한 접근 방식 탐색 — AI가 포함된 Power Platform에서 Semantic Kernel을 사용한 전통적인 개발까지.&lt;/p>
&lt;h3 id="azure로-출장-비용-관리">Azure로 출장 비용 관리&lt;/h3>
&lt;p>Azure AI Vision, Document Intelligence, Semantic Kernel과 OpenAI를 사용하여 출장 비용 관리를 자동화.&lt;/p>
&lt;hr>
&lt;h2 id="배지-및-인정">배지 및 인정&lt;/h2>
&lt;ul>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2025&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2024&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2023&lt;/strong>&lt;/li>
&lt;li>🏅 &lt;strong>개발자 기술 분야 Microsoft MVP&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://sessionize.com/emimontesdeoca/">Sessionize에서 모든 세션 보기 →&lt;/a>&lt;/p></content:encoded></item><item><title>소개</title><link>https://emimontesdeoca.github.io/ko/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ko/about/</guid><description>Emiliano Montesdeoca 소개 — Microsoft MVP, 클라우드 솔루션 팀 리드, 커뮤니티 옹호자.</description><content:encoded>&lt;h2 id="저에-대해">저에 대해&lt;/h2>
&lt;p>저는 **에밀리아노 몬테스데오카(Emiliano Montesdeoca)**입니다. 우루과이-스페인 출신 소프트웨어 개발자이며, &lt;strong>개발자 기술 분야 Microsoft MVP&lt;/strong>이고, &lt;strong>테네리페, 카나리아 제도&lt;/strong>에 거주하는 자랑스러운 아빠입니다.&lt;/p>
&lt;p>복잡한 기술적 도전을 극복하고 Microsoft 기술로 확장 가능한 클라우드 솔루션을 구축하는 것을 좋아합니다. 일상적인 도구로는 &lt;strong>.NET&lt;/strong>, &lt;strong>Azure&lt;/strong>, &lt;strong>Semantic Kernel을 활용한 AI&lt;/strong>, 그리고 &lt;strong>.NET Aspire&lt;/strong>와 같은 현대적인 아키텍처를 사용합니다.&lt;/p>
&lt;h2 id="제가-하는-일">제가 하는 일&lt;/h2>
&lt;p>&lt;a href="https://intelequia.com">Intelequia Technologies&lt;/a>의 &lt;strong>클라우드 솔루션 팀 리드&lt;/strong>로서, 클라우드 네이티브 애플리케이션의 설계와 제공을 이끌고 있습니다 — 전략적 비전과 실질적 실행을 결합하여. 저는 아키텍처와 코드의 교차점에서 번창하며, 팀이 우아하고 프로덕션 준비가 된 솔루션을 제공하도록 보장합니다.&lt;/p>
&lt;h2 id="커뮤니티">커뮤니티&lt;/h2>
&lt;p>저는 자주 국제 발표자로 활동하며, &lt;a href="https://sessionize.com/emimontesdeoca/">2023&lt;/a>, &lt;a href="https://sessionize.com/emimontesdeoca/">2024&lt;/a>, &lt;a href="https://sessionize.com/emimontesdeoca/">2025&lt;/a>에 &lt;strong>Sessionize 가장 활발한 발표자&lt;/strong>로 인정받았습니다. 전 세계 컨퍼런스 발표와 블로그를 통해 실용적인 실세계 인사이트를 공유합니다.&lt;/p>
&lt;p>또한 .NET 커뮤니티를 위한 리소스인 &lt;a href="https://thedotnetblog.com">&lt;strong>The .NET Blog&lt;/strong>&lt;/a>의 창시자이며, 배우고 만드는 것들에 대해 이 블로그에 정기적으로 글을 씁니다.&lt;/p>
&lt;p>멘토링과 커뮤니티 구축은 제 정체성의 핵심 부분입니다. 제 커리어를 형성한 생태계에 보답하는 것은 진지하게 임하는 일입니다.&lt;/p>
&lt;h2 id="연락해요">연락해요&lt;/h2>
&lt;p>이벤트 발표, 프로젝트 협업, 또는 클라우드 아키텍처와 AI에 대한 대화에 언제나 열려 있습니다. 편하게 연락해 주세요!&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Email&lt;/strong>: &lt;a href="mailto:emimontesdeoca@outlook.es">emimontesdeoca@outlook.es&lt;/a>&lt;/li>
&lt;li>&lt;strong>GitHub&lt;/strong>: &lt;a href="https://github.com/emimontesdeoca">@emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>X / Twitter&lt;/strong>: &lt;a href="https://twitter.com/emimontesdeocaa">@emimontesdeocaa&lt;/a>&lt;/li>
&lt;li>&lt;strong>LinkedIn&lt;/strong>: &lt;a href="https://www.linkedin.com/in/emimontesdeoca/">emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>Instagram&lt;/strong>: &lt;a href="https://www.instagram.com/emimontesdeoca/">@emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>Sessionize&lt;/strong>: &lt;a href="https://sessionize.com/emimontesdeoca/">emimontesdeoca&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item></channel></rss>