ASP.NET Core는 일련의 내장된 tag helper들을 제공함으로써 일반적인 상황에서 필요한 요소의 변환을 수행할 수 있습니다. 이번 글을 통해 anchor, script, link, image요소등을 처리하는 tag helper와 content를 caching 하거나 환경에 기반한 content를 선택하는 등의 기능에 관해서도 함께 알아보고자 합니다.
1. Project 준비하기
해당 예제 Project는 역시 이전이 Project를 그대로 사용할 것이지만 Program.cs를 아래와 같이 변경하여 이전에 tag helper를 등록하기 위한 구문을 주석처리합니다.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddSingleton<CitiesData>();
//builder.Services.AddTransient<ITagHelperComponent, TimeTagHelperComponent>();
//builder.Services.AddTransient<ITagHelperComponent, TableFooterTagHelperComponent>();
다음으로 Views/Home folder에 있는 _RowPartial.cshtml 부분를 아래와 같이 변경합니다.
<tr>
<td>@Model?.ProductName</td>
<td>@Model?.UnitPrice.Value.ToString("c")</td>
<td>@Model?.CategoryId</td>
<td>@Model?.SupplierId</td>
<td></td>
</tr>
또한 같은 folder에 있는 List.cshtml file또한 아래와 같이 변경하여 Layout을 적용하고 View에서 Render 될 Table에 Column을 추가합니다.
@model IEnumerable<Products>
@{
Layout = "_SimpleLayout";
}
<h6 class="bg-secondary text-white text-center m-2 p-2">Products</h6>
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<thead>
<tr>
<th>Name</th><th>Price</th>
<th>Category</th><th>Supplier</th><th></th>
</tr>
</thead>
<tbody>
@foreach (Products p in Model ?? Enumerable.Empty<Products>()) {
<partial name="_RowPartial" model="p" />
}
</tbody>
</table>
</div>
(1) Image file 추가하기
곧 알아볼 tag helper중 하나는 image를 위한 serivce를 제공하므로 project의 wwwroot/images folder안에 다음과 같은 image를 exImage.jpg이름으로 저장해 놓았습니다.
위 image는 예제용 image이므로 개인별로 원하는 다른 image로 얼마든지 대체할 수 있습니다.
(2) Client-Side Package 설치
앞으로 보게될 예제 중 일부는 jquery package인 javascript file을 사용하기 위한 tag helper를 포함하고 있습니다. 따라서 Visual Studio의 Add->Client-Side Library기능을 통해 jquery package를 설치합니다.
Project를 실행하고 /home/list URL을 요청하여 다음과 같은 응답이 생성되는지 확인합니다.
2. 내장 Tag Helper 사용준비
내장 tag helper는 모두 Microsoft.AspNetCore.Mvc.TagHelpers namespace에 정의되어 있으며 각각의 view나 page에 또는 예제에서와 같이 imports file에 @addTagHelpers지시자를 추가함으로써 사용할 수 있습니다. Views folder에 있는 _ViewImports.cshtml file에서 필요한 지시자는 controller view를 위한 내장 tag helper를 사용하기 위한 것이며
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using MyWebApp.Components
@addTagHelper *, MyWebApp
Pages folder에 있는 _ViewImports.cshtml file의 지시자는 Razor Page를 위한 내장 tag helper를 사용하기 위한 것입니다.
@namespace MyWebApp.Pages
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebApp
위의 지시자들은 view component기능을 사용하기 위해 이전 예제부터 이미 추가된 지시자들입니다.
3. Anchor 요소 변환
a요소는 application을 탐색하고 GET요청을 보내기 위한 기본적인 도구입니다. AnchorTagHelper class는 a요소의 href attribute를 변환하는데 사용되며 routing system을 통해 생성된 해당 대상 URL을 생성할 수 있습니다. 따라서 URL의 hard coding이 필요하지 않고 routing 설정을 통한 변경사항이 자동적으로 application의 anchor요소에 반영될 수 있습니다. 아래 표는 AnchorTagHelper class에서 지원하는 속성들을 나열한 것입니다.
asp-action | 이 속성은 URL의 대상이될 action method를 특정합니다. |
asp-controller | 이 속성은 URL의 대상이될 controller를 특정합니다. 이 속성이 생략되면 URL은 현재 view에 rendering된 controller나 page가 대상이 됩니다. |
asp-page | 이 속성은 URL의 대상이될 Razor Page를 특정합니다. |
asp-page-handler | 이 속성은 요청을 처리하게될 razor page handler 기능을 특정합니다. |
asp-fragment | 이 속성은 URL flagment(#문자 이후에 나타나는)를 지정하는데 사용됩니다. |
asp-host | 이 속성은 URL의 대상이 될 host의 이름을 특정합니다. |
asp-protocol | 이 속성은 URL이 사용할 protocol을 특정합니다. |
asp-route | 이 속성은 URL을 생성하는데 사용하게될 route의 이름을 특정하는데 사용됩니다. |
asp-route-* | asp-route-로 시작하는 이름의 attribute는 URL에서의 추가적인 값을 특정합니다. 따라서 만약 asp-route-id라고 한다면 id segment값을 routing system에 제공하기 위해 사용됩니다. |
asp-all-route-data | 이 속성은 각각 분리된 속성을 사용하기 보다는 단일 값으로 routing에 사용될 값을 제공합니다. |
AnchorTagHelper는 단순하며 예측가능하고 application의 routing설정을 사용하는 a요소의 URL을 쉽게 생성할 수 있도록 합니다. 아래 예제는 View/Home folder의 _RowPartial.cshtml을 변경하여 간단히 anchor요소를 추가한 것인데 이 요소는 table의 속성을 사용하여 Home Controller에서 정의된 다른 action을 대상으로 하는 URL을 생성합니다.
<tr>
<td>@Model?.ProductName</td>
<td>@Model?.UnitPrice.Value.ToString("c")</td>
<td>@Model?.CategoryId</td>
<td>@Model?.SupplierId</td>
<td>
<a asp-action="index" asp-controller="home" asp-route-id="@Model?.ProductId" class="btn btn-sm btn-info text-white">선택</a>
</td>
</tr>
asp-action과 asp-controller attribute는 action method의 이름과 이것이 정의된 controller를 특정하고 있습니다. segment변수의 값은 asp-route-[name] attribute를 통해 정의되었으므로 asp-route-id attribute는 asp-action attribute에 의해 선택된 action method로 인수를 전달하는데 사용되는 id segment변수의 값을 제공하게 됩니다.
위 예제에서 anchor요소에 추가된 class속성은 button에서 나타나는 요소를 주기위해 Bootstrap CSS Framework style을 적용한 것으로서 tag helper사용과는 무관합니다.
요소가 어떻게 변하는지를 확인하기 위해 project를 실행하고 /home/list URL을 호출합니다. 그러면 응답은 아래와 같이 표시될 것입니다.
위 결과에서 select요소를 확인해 보면 각 href attribute가 Product개체의 ProductId값을 다음과 같이 포함한다는 것을 알 수 있습니다.
<a class="btn btn-sm btn-info text-white" href="/home/index/1">선택</a>
예제의 경우 asp-route-id attribute를 통해 제공된 값은 기본 URL을 사용할 수 없음을 의미하며 따라서 routing system은 controller와 action이름의 segment와 action method로 매개변수를 제공하는 데 사용되는 segment를 포함하는 URL을 생성하게 됩니다. 이제 응답에서 '선택'이라는 anchor요소를 click 하게 되면 Home controller의 index method를 대상으로 하는 HTTP Get 요청을 보낼 것입니다.
(1) Razor Page에서의 anchor요소 사용하기
asp-page attribute는 anchor요소에서 href속성의 대상으로 Razor Page를 설정하는데에도 사용될 수 있습니다. 이때 page의 경로는 /문자로 시작되며 @page지시자에 의한 route segment의 값은 asp-route-[name] attribute를 사용해 정의됩니다. 아래 예제는 Views/Home foler의 List.cshtml을 변경하여 Pages/Suppliers folder에서 정의된 List page의 대상인 anchor요소를 추가한 것입니다.
asp-page-handler attribute는 요청을 처리할 page model handler method의 이름을 특정하는 데에도 사용될 수 있습니다.
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<thead>
<tr>
<th>Name</th><th>Price</th>
<th>Category</th><th>Supplier</th><th></th>
</tr>
</thead>
<tbody>
@foreach (Products p in Model ?? Enumerable.Empty<Products>()) {
<partial name="_RowPartial" model="p" />
}
</tbody>
</table>
<a asp-page="/suppliers/list" class="btn btn-secondary">Suppliers</a>
</div>
Project를 실행하여 /home/list URL을 요청하면 다음과 같이 anchor요소가 button으로 표시되었으며
해당 요소를 확인해 보면 다음과 같이 HTML이 변환된 것을 알 수 있습니다.
<a class="btn btn-secondary" href="/lists/suppliers">Suppliers</a>
href attribute에서 사용된 URL은 해당 page에서 기본적인 routing규칙을 재정의하기 위해 사용된 @page지시자를 반영하고 있습니다. 따라서 해당 요소를 click 하면 Browser는 Razor Page를 다음과 같이 표시할 것입니다.
Link가 아닌 URL생성하기
tag helper는 anchor요소에서만 URL을 생성합니다. 따라서 Link를 만드는 것이 아닌 단순히 URL만을 생성하고자 한다면 Controller나 Page Model, View등에서 가능한 Url 속성을 사용하면 됩니다. 이 속성은 URL을 생성하기 위한 일련의 method와 확장 method를 제공하는 IUrlHelper interface구현체를 반환합니다. 아래 예제는 View에서 URL을 생성하는 Razor 구문을 나타내고 있습니다.
<div>@Url.Page("/suppliers/list")</div>
예제는 div와 함께 /suppliers/list razor page를 목표로 하는 URL을 생성합니다. controller나 page model class에서도 아래와 같이 같은 interface를 사용할 수 있습니다.
string url = Url.Action("List", "Home");
위 예제는 Home Controller의 List action을 목표로 하는 URL을 생성하고 url이름의 문자열 변수에 할당합니다.
4. Javascript와 CSS Tag Helper사용하기
ASP.NET Core는 script와 link요소를 통해 JavaScript file과 CSS Stylesheet를 관리하기 위한 tag helper를 제공하고 있습니다. 이들 tag helper는 강력하면서도 유연하지만 예상치 못한 결과를 만들 수 있으므로 주의해야 합니다.
(1) Javascript file 관리
ScriptTagHelper class는 script요소를 위한 내장 tag helper이며 아래 표의 attribute를 사용하는 View에서 Javascript file 포함을 관리하기 위해 사용됩니다.
asp-src-include | 이 속성은 view에 포함될 Javascript file을 지정합니다. |
asp-src-exclude | 이 속성은 view로 부터 제외될 Javascript file을 지정합니다. |
asp-append-version | 이 속성은 곧 설명하게될 cache busting을 위해 사용됩니다. |
asp-fallback-src | 이 속성은 network문제로 본래 Javascript를 받지 못했을때 대체적인 Javascript file을 지정하는데 사용됩니다. |
asp-fallback-src-include | 이 속성은 network문제로 본래 Javascript를 받지 못했을때 사용할 Javascript file을 선택하는데 사용됩니다. |
asp-fallback-src-exclude | 이 속성은 network문제로 본래 Javascript를 받지 못했을때 Javascript file을 제외하여 사용을 표시하는데 사용됩니다. |
asp-fallback-test | 이 속성은 network로 부터 Javascript code가 정확하게 Load되었는지의 여부를 판단하기 위해 사용되는 Javascript의 조각을 지정하는데 사용됩니다. |
● JavaScript File 선택
asp-src-include attribute는 globbing pattern을 사용하는 view에서 JavaScript file을 포함시키기 위해 사용됩니다. 여기서 globbing pattern은 file match에 사용되는 wildcard의 설정을 지원하는데 아래 표는 가장 일반적인 globbing pattern을 나열한 것입니다.
pattern | example | |
? | js/src?.js | 이 pattern은 /를 제외하고 모든 단일문자와 일치합니다. 따라서 이 예제는 js directory안에서 이름이 src이고 .js로 끝나는 모든 file과 일치합니다. 그래서 js/src1.js과 js/srcX.js와는 일치하지만 js/src123.js혹은 js/mydir/src1.js와는 일치하지 않습니다. |
* | js/*.js | 이 pattern은 /를 제외하고 모든수의 문자와 일치합니다. 따라서 예제는 js directory안에서 .js확장자를 가진 모든 file과 일치됩니다. 그래서 js/ src1.js나 js/src123.js와는 일치하지만 js/mydir/src1.js와는 일치하지 않습니다. |
** | js/**/*.js | 이 pattern은 /문자를 포함한 모든 수의 문자와 일치합니다. 따라서 예제는 js directory또는 그 하위 모든 directory이내에 ..js확장자를 가진 모든 file과 일치합니다. 그래서 /js/src1.js혹은 /js/mydir/src1.js와도 일치할 수 있습니다. |
Globbing은 view에서 application이 필요로 하는 Javascript file을 포함시키기에 유용한 방법이며 심지어는 file이름에 version번호가 포함되거나 package가 다른 file을 추가하는 경우처럼 file의 정확한 경로가 변경되는 경우에도 유효하게 작동할 수 있습니다.
아래 예제는 Views/Shared folder에 있는 _SimpleLayout.cshtml file을 변경하여 JQuery package가 설치된 위치인 wwwroot/lib/ jquery folder에서 포함된 모든 javascript file을 include 하기 위해 asp-src-include attribute를 사용한 것을 나타내고 있습니다.
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<script asp-src-include="lib/jquery/**/*.js"></script>
Pattern은 wwwroot folder안에서만 평가되며 예제에서의 pattern은 wwwroot folder이내의 위치와는 관계없이 js확장자를 가진 모든 file을 찾게됩니다. 이는 project에 추가된 모든 Javascript package가 client로 전송할 HTML안에 포함될 수 있음을 의미합니다.
project를 실행한 후 /home/list로 URL을 요청하고 client로 전송된 HTML을 확인해 보면 다음과 같이 Layout에 있는 단일 script요소 하나가 각각의 javascript를 위한 script요소로 바뀌어 있음을 확인할 수 있습니다.
<script src="/lib/jquery/jquery.js"></script>
<script src="/lib/jquery/jquery.min.js"></script>
<script src="/lib/jquery/jquery.slim.js"></script>
<script src="/lib/jquery/jquery.slim.min.js"></script>
<script src="/lib/jquery/dist/jquery.js"></script>
<script src="/lib/jquery/dist/jquery.min.js"></script>
만약 Visual Studio를 사용중인 경우라면 JQuery package가 많은 Javascript file을 포함하고 있음을 알지 못할 수 있습니다. 왜냐하면 Visual Studio는 Solution Exprorer에서 몇몇 항목을 감춰놓기 때문입니다. client-side package folder에서 전체 content를 표시하기 위해 Solution Explorer창에서 각각의 중첩된 요소를 확장하거나 Soluition Explorer Window창 상단에 있는 button을 click 함으로써 file중첩을 비활성화합니다.
Source Map file
대부분의 Javascript file은 되도록 크기를 줄이기 위해 최소화되는 경향이 있습니다. 이것은 client로 더 빠르게 전달될 수 있도록 하며 대역폭의 낭비도 줄일 수 있습니다. 최소화의 과정은 code에서 모든 여백을 제거하고 함수와 변수의 이름도 짧게 변경하는 것으로 처리됩니다. 따라서 myHelpfullyNamedFunction와 같은 의미를 가진 이름은 x1처럼 최소화된 문자의 수로만 표현됩니다. 따라서 문제가 발생했을 때 최소화된 code안에서 해당 문제점을 추적하고자 Browser의 Debugger를 사용할 때 x1과 같은 이름은 code를 통해 진행과정을 추적하는 것이 거의 불가능할 수 있습니다.
여기서 map확장자를 가진 file은 압축된(최소화된) code와 개발자가 읽을 수 있는 비압축 source file사이에 map을 제공함으로써 압축된 code안에서 debug 할 수 있도록 browser가 사용하는 source map file입니다. debug를 위해 browser의 개발도구(F12)를 열면 browser는 자동적으로 source map file을 요청하고 application의 client-side code debug를 돕기 위해 이들을 사용하게 됩니다.
● Globbing Pattern 좁히기
위 예제에서 처럼 pattern에 의해 선택된 모든 file을 필요로 하는 Application은 없습니다. 많은 package들은 비슷한 content를 가진 여러 javascript file을 포함하고 있으며 종종 대역폭을 절약하기 위해 흔히 사용되는 기능만을 포함하기도 합니다. 같은 맥락에서 JQuery package는 jquery.slim.js file을 포함하고 있는데 이 file은 jquery.js와 같은 code를 가지고 있지만 동기 HTTP요청과 animation효과를 처리하는 기능이 없습니다.
또한 이들 각각의 file은 저마다의 min.js확장자를 가지고 있는데 이는 압축된 file임을 의미합니다. 이러한 file들은 가능한 모든 공백을 제거하고 함수나 변수의 이름을 더 짧은 이름으로 바꾸는 방식을 통해 file의 size를 줄인 것입니다.
각 package에서는 단지 하나의 javascript file만이 필요로 하는데 대부분의 project에서와 같이 압축된 file만을 필요로 하는 경우라면 아래 예제에서와 같이 globbing pattern을 통해 해당 file의 설정을 제한할 수 있습니다.
<script asp-src-include="lib/jquery**/*.min.js"></script>
project를 실행하고 /home/list URL을 요청한 후 돌아오는 응답에서 HTML을 확인해 보면 다음과 같이 압축된 file만이 선택되어 있음을 확인할 수 있습니다.
<script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script src="/lib/jquery/jquery.min.js"></script>
<script src="/lib/jquery/jquery.slim.min.js"></script>
또 다르게는 pattern을 통해 slim version의 file만을 포함할 수 있도록 처리할 수도 있습니다.
<script asp-src-include="lib/jquery**/*slim.min.js"></script>
당연하지만 위 예제는 다음과 같은 file만을 선택하도록 합니다.
<script src="/lib/jquery/jquery.slim.min.js"></script>
이와 같은 방법은 file위치에 대한 유연성을 유지하면서 JQuery file 중 하나의 version만이 browser로 전송하게 됩니다.
이렇게 JavaScript file을 선택하기 위한 pattern에서 범위를 좁히는 것은 file명이 'slim'과 같이 특정한 용어를 규칙적으로 포함하고 있는 경우에만 유용하다고 할 수 있습니다. 그런데 이와 반대의 경우에는 예를 들어 압축된 file 중에서도 완전한 version의 file을 가져오고자 하는 경우에는 좀 더 다른 방법을 생각해야 합니다. 다행히도 asp-src-exclude attribute를 사용하면 asp-src-include attribute에 의해 일치되는 list 중에서 특정한 file을 제외시킬 수 있습니다. 아래 예제는 Views/Shared folder의 _SimpleLayout.cshtml에서 asp-src-exclude attribute를 어떻게 사용했는지를 나타내고 있습니다.
<script asp-src-include="/lib/jquery/**/*.min.js" asp-src-exclude="**.slim.**"></script>
project를 실행하고 /home/list로 URL을 요청하고 응답된 HTML을 확인해 보면 다음과 같이 script요소에서 JQuery Library의 압축된 file이면서 full version의 file을 선택하고 있음을 알 수 있습니다.
<script src="/lib/jquery/jquery.min.js"></script>
CACHE BUSTING
image나 CSS StyleSheet, JavaScript file 등 정적 content는 종종 변화가 흔하지 않은 content의 요청이 server에 도달하는 것을 줄이기 위해 cache 되는 경우가 있습니다. Browser는 server에 의해 content를 cache 해야 함을 알 수 있고 application은 application server를 보완하기 위해 cache server를 사용하거나 content delivery network를 통해 content를 배포할 수 있습니다. 물론 모든 caching을 제어할 수는 없습니다. 규모가 큰 일부 대형회사의 경우 대부분의 요청이 같은 site나 application으로 전송되는 경향이 있기 때문에 그들의 대역폭 요구를 줄이기 위해 cache를 설치하기도 합니다.
caching의 한 가지 문제는 일부 정적 file을 변경한다고 해도 client는 요청이 이전에 cache 된 content로 service되기 때문에 즉각적으로 전달받지 못한다는 것입니다. 언젠가는 cache된 content가 만료되고 새로운 content가 사용될 테지만 application controller에 의해 생성된 동적 content는 cache에 의해 전달되는 정적 content와 보조를 맞추지 못하는 기간을 남기게 됩니다. 이것은 view상에서 layout이 틀어지는 문제나 심지어 update 된 content에 의존하는 application 동작에서 예상치 못한 예외를 일으킬 수도 있습니다.
이 문제를 해결하는 것을 cache busting이라고 합니다. 기본적인 개념은 cache가 정적 content를 처리하도록 하는 것이지만 대신 server에 의해 변경되는 모든 사항에서 즉각적인 반영을 시도할 수 있습니다. 이를 위해 tag helper class는 version 번호의 역할을 하는 checksum을 포함한 정적 content를 위해 URL에 query 문자열을 추가함으로써 cache busting을 지원합니다. 예를 들어 javascript file의 경우 ScriptTagHelper class는 asp-append-version attribute를 통해 다음과 같이 cache busting을 지원하고 있습니다.
<script asp-src-include="/lib/jquery/**/*.min.js" asp-src-exclude="**.slim.**" asp-append-version="true"></script>
이와 같이 cache busting기능을 사용하게 되면 browser로 전달되는 html에서는 다음과 같은 요소를 생성하게 됩니다.
<script src="/lib/jquery/jquery.min.js?v=_xUj-3OJU5yExlq6GSYGSHk7tPXikyn"></script>
이러한 방법은 다른 checksum이 계산되는 시점이 되는 JavaScript library update 등의 file content변경이 발생하기 전까지는 tag helper에 의해 같은 version번호가 사용될 것입니다. version번호를 추가한다는 것은 file에 대한 변경사항이 발생할 때마다 client는 application server로 전달되는 새로운 요청으로서 다뤄지게 될 다른 URL을 사용하게 된다는 것을 의미합니다. 그리고 content는 다음 update까지 cache 되어 다른 version의 URL을 생성하게 됩니다.
● content delivery network 사용하기
Content delivery networks (CDNs)은 application content에 대한 요청을 client와 가장 근접한 server로 넘기기 위해 사용됩니다. 어떤 경우에 browser는 Server에게서 javascript file을 직접적으로 요청하기보다는 지리적인 지역 server로 확인되는 hostname을 통해 요청하게 되는데 이것은 file을 불러오기 위한 요청시간을 줄이고 application에서 지원되어야 하는 대역폭을 절약할 수 있게 합니다. 지역적으로 대규모 사용자가 존재하는 경우 가능하다면 전문적인 CDN service를 활용하는 것이 경제적으로 타당할 수 있지만 그렇지 않은 경우라면 JQuery 같은 일반적인 경우에서 javascript package를 전달하기 위한 무료 CDN을 활용하는 것도 방법이 될 수 있습니다.
아래 URL들은 CDNJS로서 ASP.NET Core project에서 client-side package를 설치하는 데 사용되는 Library Manager tool의 CDN과 같은 것입니다. 참고로 예제 project에는 JQuery 3.6.3이 설치되어 있는데 해당 package는 https://cdnjs.com에서 다음과 같은 URL들로 검색될 수 있습니다.
위의 URL에서는 일반적인 javascript file을 비롯해 간소화된 javascript file, 그리고 간소화된 javascript를 위한 source map file등과 함께 JQuery의 full version 그리고 slim version의 javascript file도 제공하고 있습니다.
하지만 이러한 CDN은 file을 받는데 실패하는 경우도 있어서 application이 동작중인 상황이라 하더라도 CDN content가 사용이 불가능하게 되면 application은 예상한 대로 동작하지 못할 수 있습니다. 여기서 ScriptTagHelper class는 CDN content를 client에서 받을 수 없게 되는 상황인 경우라면 이를 local file로 대체할 수 있는 방안을 제공하고 있습니다. 아래 예제는 실제 Views/Shared folder에 있는 _SimpleLayout.cshtml file을 변경하여 이러한 방안을 어떻게 구현할 수 있는지를 나타내고 있습니다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" asp-fallback-src="/lib/jquery/jquery.min.js" asp-fallback-test="window.jQuery"></script>
예제에서 src attribute는 CDN URL을 지정하는 데 사용되며 asp-fallback-src attribute는 src attribute에서 지정한 CDN을 통해 file을 받을 수 없는 상황이 발생한다면 대안으로 사용가능한 local file의 위치를 특정하는 데 사용됩니다. 또한 CDN이 작동 중인지를 확인할 수 있는 asp-fallback-test attribute도 사용되었는데 browser에서 실행할 수 있는 일부 javascript구문을 정의하고 해당 구문의 실행결과가 false가 된다면 fallback file이 대신 요청될 것입니다.
asp-fallback-src-include attribute와 asp-fallback-src-exclude attribute 또한 globbing pattern을 통해 local file을 선택하는 데 사용될 수 있습니다. 하지만 CDN script요소에서 하나의 file만을 선택하고 있다면 해당 file의 local file을 지정하는데 asp-fallback-src attribute를 사용하는 것만으로도 충분할 것입니다.
project를 실행하고 /home/list URL을 요청한 뒤 HTML 응답을 살펴보면 다음과 같은 2개의 script요소가 존재함을 알 수 있습니다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>(window.jQuery||document.write("\u003Cscript src=\u0022/lib/jquery/jquery.min.js\u0022\u003E\u003C/script\u003E"));</script>
첫 번째 script요소는 CDN으로 부터 javascript file을 요청하고 있으며 두번째 script요소는 asp-fallback-test attribute에서 지정한 javascript구문을 실행하여 첫번째 script요소가 잘 처리되었는지를 확인하고 있습니다. 해당 실행 결과가 true라면 CDN이 잘 작동되었다는 뜻이 되므로 어떠한 action도 일어나지 않습니다. 그러나 실행결과가 false가 된다면 HTML Document에 새로운 script요소가 추가되어 browser가 fallback url로부터 javascript file을 불러오도록 할 것입니다.
이와 같이 CDN 이외의 대안을 설정하고 실제 이것을 test 하는 것은 CDN이 작동을 멈추는 경우 곧장 이를 파악하지 못할 수 있고 최악의 경우 client가 application에 접속하지 못하는 상황이 발생할때까지 이를 확인하지 못할 수 있기 때문에 매우 중요합니다. 대안(fallback)설정을 test하는 가장 간단한 방법은 src attribute에 지정된 file의 이름을 현재 존재하지 않는 file명으로 변경하는 것입니다. 그리고 chrome browser의 경우 F12 key를 통해 개발자 mode를 열고 CDN으로의 요청이 실패할 때 fallback file요청이 다시 들어가는지를 확인해 보면 됩니다.
주의
CDN fallback기능은 정의된 순서대로 browser가 script요소의 content를 동기적으로 불러들이고 실행하는데 의존합니다. 물론 비동기적인 방법을 통해 javascript의 loading속도를 올릴 수 있는 여러 방법이 있을 수 있지만 이것은 browser가 CDN으로부터 file을 가져와 content를 실행하기도 전에 fallback test를 실행시키고 CDN이 완벽히 동작중인 상황에서도 fallback file을 요청하게 되는 결과를 가져올 수 있습니다. 따라서 CDN fallback기능을 비동기 script loading과 혼용해서는 안됩니다.
(2) CSS Stylesheet 관리
LinkTagHelper class는 link 요소를 위해 사용되는 내장 tag helper로서 view에서 CSS Stylesheet를 포함시키기 위해 사용되며 아래 표에 있는 attribute를 사용할 수 있습니다.
asp-href-include | 이 속성은 href attribute에 사용되는 file을 설정합니다. |
asp-href-exclude | 이 속성은 href attribute에서 제외될 특정 file을 설정합니다. |
asp-append-version | 이 속성은 cache busting기능을 사용하기 위한 속성입니다. |
asp-fallback-href | 이 속성은 CDN이 작동하지 않는 경우 이를 대체할 file을 지정합니다. |
asp-fallback-href-include | 이 속성은 CDN이 작동하지 않는 경우 사용될 file들을 설정합니다. |
sp-fallback-href-exclude | 이 속성은 CDN이 작동하지 않는 경우 설정으로 부터 file을 제외하는데 사용됩니다. |
sp-fallback-href-test-class | 이 속성은 CDN을 test하기 위해 사용될 수 있는 CSS class를 지정하는데 사용됩니다. |
asp-fallback-href-test-property | 이 속성은 CDN을 test하기 위해 사용될 수 있는 CSS property를 지정하는데 사용됩니다. |
sp-fallback-href-test-value | 이 속성은 CDN을 test하기 위해 사용될 수 있는 CSS 값을 특정하는데 사용됩니다. |
● Stylesheet 지정
LinkTagHelper는 CSS file을 제외하거나 선택하기 위한 globbing patterns 등을 포함 많은 기능을 ScriptTagHelper와 공유하고 있습니다. 따라서 이 둘을 개별적으로 취급할 필요는 없습니다. 정확하게 CSS file을 선택하는 것은 javascript file만큼이나 중요한데 stylesheet는 또한 일반 version과 최소화 version이 있을 수 있으며 source map file 또한 지원할 수 있기 때문입니다. HTML요소의 style을 위해 예제에서 사용해온 보통의 Bootstrap package는 wwwroot/lib/bootstrap/ css folder에서 stylesheet file을 포함하고 있으며 visual studio의 solution explorer에서 다음과 같이 확인할 수 있습니다.
여기서 bootstrap.css file이 일반적인 CSS file이며 bootstrap.min.css이 최소화 version, bootstrap.css.map이 source map file이 됩니다. 나머지 file들은 application에서 대역폭을 줄이기 위해 하위 CSS기능을 포함하고 있는 것들이며 아직 예제에서는 사용하지 않고 있습니다.
아래 예제는 asp-href-include와 asp-href-exclude attribute를 사용해 Views/Shared folder에 있는 _SimpleLayout.cshtml file을 변경하여 layout에 있는 일반적인 link 요소를 바꾼 것입니다.(script 요소는 더 이상 다루지 않을 것이므로 삭제하였습니다.)
<link asp-href-include="/lib/bootstrap/css/*.min.css" asp-href-exclude="**/*-reboot*,**/*-grid*,**/*-utilities*, **/*.rtl.*" rel="stylesheet" />
stylesheet file 역시 javascript file을 위와 같이 선택하는 경우와 동일한 주의가 필요합니다. 왜냐하면 stylesheet 역시도 같은 file에 대해서 여러 version을 선택하기 위한 link요소를 쉽게 생성할 수 있고 이 과정에서 필요하지 않은 file까지 선택할 수 있기 때문입니다.
● Content Delivery Network 사용
LinkTag helper 역시 stylesheet를 정상적으로 불러왔는지를 testing 하는 과정을 통해 CDN이 가능하지 않은 경우 local content로 대체하기 위한 몇가지 attribute를 제공하고 있습니다. 다만 stylesheet의 경우 javascript를 testing하는 경우보다 약간은 더 복잡할 수 있습니다. 아래 예제는 Bootstrap CSS Stylesheet를 위해 CDNJS URL을 사용한 것으로 Views/Shared foler에 있는 _SimpleLayout.cshtml file에 적용한 것입니다.
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" asp-fallback-href="/lib/bootstrap/css/bootstrap.min.css" asp-fallback-test-class="btn" asp-fallback-test-property="display" asp-fallback-test-value="inline-block" rel="stylesheet" />
href attribute는 CDN URL을 설정하고 있으며 asp-fallback-href속성을 통해 CDN을 사용할 수 없을 때 대체할 수 있는 file을 지정하고 있습니다. 특히 CDN을 사용할 수 있는지의 여부를 확인하기 위해 세 개의 다른 attribute를 사용하고 있으며 사용중인 CSS stylesheet에 의해 정의된 CSS class의 이해가 필요합니다.
project를 실행하고 /home/list URL을 요청하여 응답되는 HTML을 확인해 보면 link요소가 다음과 같이 세개의 다른 요소로 변환되어 있음을 확인할 수 있습니다.
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet" />
<meta name="x-stylesheet-fallback-test" content="" class="btn" /><script>!function(a,b,c,d){var e,f=document,g=f.getElementsByTagName("SCRIPT"),h=g[g.length-1].previousElementSibling,i=f.defaultView&&f.defaultView.getComputedStyle?f.defaultView.getComputedStyle(h):h.currentStyle;if(i&&i[a]!==b)for(e=0;e<c.length;e++)f.write('<link href="'+c[e]+'" '+d+"/>")}("display","inline-block",["/lib/bootstrap/css/bootstrap.min.css"], "rel=\u0022stylesheet\u0022 ");</script>
첫 번째 요소는 일반적인 link요소이며 CDN주소를 href attribute를 통해 지정하고 있습니다. 두 번째 요소는 meta tag인데 view의 asp-fallback-test-class로부터 clsss를 지정하고 있습니다. 예제에서는 이 속성의 값에 listing 되는 btn class가 지정되었습니다.
지정된 CSS class는 CDN에서 불러올 stylesheet에서 정의되어야 하는데 예제에서의 btn class는 Bootstrap button요소에 기본적인 형식을 제공합니다.
asp-fallback-test-property attribute는 CSS속성을 지정하는 데 사용되며 CSS class가 요소에 적용될 때 설정되고 asp-fallback-test-value attribute를 통해 설정될 값을 지정하게 됩니다.
tag helper에 의해 생성된 script요소는 javascript code를 포함하고 있는데 지정된 class에 요소를 추가하고 CSS 속성의 값을 test 하여 CDN stylesheet가 정상적으로 불러와졌는지를 확인하게 됩니다. 만약 확인결과가 false라면 a link요소는 fallback file을 통해 생성될 것입니다. Bootstrap btn class는 display속성에 'inline-block'을 설정하고 browser가 CDN으로부터 Bootstrap stylesheet를 불러올 수 있는지를 확인하기 위한 test를 제공하게 됩니다.
Boostrap과 같은 third-party package가 어떻게 test를 수행하는지를 알아내는 쉬운 방법은 browser의 F12 개발자 도구를 사용하는 것입니다. 예제에서는 test여부를 결정하기 위해 요소에 btn class를 할당한 다음 browser에서 이를 확인하여 class가 변경된 개별적인 CSS 속성을 보게 되었습니다. 이는 길고 복잡한 style sheet를 읽는 것보다 더 쉬운 방식이 될 수 있을 것입니다.
5. Image 요소 사용하기
ImageTagHelper class는 img요소의 src attribute를 통해 image를 위한 cache busting을 제공하여 application이 image의 변경이 즉각적으로 반영될 수 있음을 보장하는 caching의 이점을 활용할 수 있도록 해줍니다. ImageTagHelper class는 아래 표에서 설명된 asp-append-version attribute를 정의하는 img요소에서 동작합니다.
asp-append-version | 이 속성은 cache busting기능을 사용할 수 있도록 설정하는데 사용됩니다. |
아래 예제는 img요소를 위에서 미리 마련해 놓은 image를 위해 shared layout에 추가하였으며 간결함을 위해 link요소를 local file을 사용하도록 재설정하였습니다.
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="m-2">
<img src="/images/city.png" asp-append-version="true" class="m-2" />
@RenderBody()
</div>
</body>
project를 실행하여 /home/list URL을 요청하면 다음과 같은 응답을 보게 될 것입니다.
해당 결과의 HTML을 확인해 보면 version checksum을 포함해 image를 요청하기 위해 사용된 URL을 다음과 같이 확인할 수 있습니다.
<img src="/images/exImage.jpg?v=1-C8RfX-VClBD1Rnb2K_v4S15PlCedkMwCznkDRvEEk" class="m-2">
위와 같이 추가된 checksum은 file의 모든 변경사항이 cache를 통과하도록 함으로써 변경이전의 content가 표시되는 것을 방지할 수 있도록 합니다.
6. Data cache 사용
CacheTagHelper class는 content의 일부를 caching 할 수 있도록 함으로써 view나 page에서의 rendering속도를 높일 수 있습니다. cache 될 content는 cache요소를 사용해 표시하며 아래 표의 attribute를 사용해 설정됩니다.
cache는 content의 일부 section을 재사용하는 것에서 유용한 도구가 될 수 있으며 모든 요청에 section을 생성할 필요가 없습니다. 하지만 caching의 효과적인 사용을 위해서는 신중한 생각과 계획이 필요합니다. caching은 application의 성능을 향상할 수 있지만 사용자가 오래된 version의 content 혹은 content의 다른 version들을 포함하고 있는 여러 cache를 받게 될 수 있고 application의 이전 version이 cache 된 content가 새로운 version과 섞인 채로 배포가 update 될 수 있는 등 부정적인 효과를 가져올 수도 있습니다. 따라서 명확하게 성능적인 문제를 해결할 수 있음이 확실하지 않은 상태라면 cache기능을 무조건 사용하는 것은 권장하지 않으며 caching으로 가질 수 있는 효과를 명확하게 이해하고 있어야 합니다.
enabled | bool형식의 attribute이며 cache요소의 content가 cache될지의 여부를 제어하는데 사용됩니다. 해당 attribute를 생략하면 caching이 활성화됩니다. |
expires-on | 해당 attribute는 cache된 content가 만료될 완전한 시간을 설정하며 DateTime값으로 표현됩니다. |
expires-after | 이 attribute는 content가 만료될 상대적인 시간을 설정하며 TimeSpan값으로 표현됩니다. |
expires-sliding | 이 attribute는 cache된 contnet가 만료될 마지막으로 사용된 이후의 기간을 설정하며 TimeSpan값으로 표현됩니다. |
vary-by-header | 이 attribute는 cache된 content의 다른 version을 관리하기 위해 사용되는 요청 header의 이름을 특정합니다. |
vary-by-query | 이 attribute는 cache된 content의 다른 version을 관리하기 위해 사용되는 query string key의 이름을 특정합니다. |
vary-by-route | 이 attribute는 cache된 content의 다른 version을 관리하기 위해 사용되는 routing 변수의 이름을 특정합니다. |
vary-by-cookie | 이 attribute는 cache된 content의 다른 version을 관리하기 위해 사용되는 cookie의 이름을 특정합니다. |
vary-by-user | bool형식의 attribute이며 인증된 사용자의 이름을 cache된 content의 다른 version을 관리하기 위해 사용할지를 지정합니다. |
vary-by | 해당 attribute는 content의 다른 version을 관리하기 위해 사용되는 key를 제공하기 위해 실행됩니다. |
priority | 이 attribute는 cache된 하나의 거대한 content가 만료되지 않고 cache할 memory공간이 부족해지는 경 고려할 상대적 우선순위를 특정하는데 사용됩니다. |
아래 예제는 Views/Shared folder의 _SimpleLayout.cshtml file에서 timestamp를 포함하는 content로 img요소를 변경한 것입니다. (이후에 예제에서도 특별한 언급이 없으면 _SimpleLayout.cshtml을 수정한 것입니다.)
<div class="m-2">
<h6 class="bg-primary text-white m-2 p-2">
Uncached timestamp: @DateTime.Now.ToLongTimeString()
</h6>
<cache>
<h6 class="bg-primary text-white m-2 p-2">
Cached timestamp: @DateTime.Now.ToLongTimeString()
</h6>
</cache>
@RenderBody()
</div>
cache요소는 cache 될 content의 영역을 표시하는 데 사용되며 timestamp를 포함하는 h6요소에 적용되었습니다. project를 실행하여 /home/list URL을 요청하고 예제에서의 두 timestamp가 같은지를 확인합니다.
그다음 browser를 새로고침 하면 h6요소에 cache 된 content가 사용되어 다음과 같이 timestamp가 변경되지 않음을 확인할 수 있습니다.
content에 분산 cache사용
CacheTagHelper class에 의해 사용된 cache는 memory를 기반으로 합니다. 다시 말해 cache는 RAM에 한정되며 각각의 application에서 분리된 cache를 관리해야 함을 의미합니다. 가용용량이 부족해지면 cache로부터 content가 나오게 되며 application이 멈추거나 재시작되면 전체 content는 손실됩니다.
distributed-cache요소는 공유된 cache게 content를 저장하는 데 사용되며 이로 인해 모든 application server가 같은 data를 사용할 수 있도록 하고 server가 재시작하는 경우가 발생해도 여전히 이전의 cache를 그대로 사용할 수 있도록 합니다. distributed-cache요소는 cache요소로서 위 표에서 나타낸 같은 attribute를 사용해 설정되는데 아래 글을 통해 distributed cache의 상세설정을 확인해 볼 수 있습니다.
[.NET/ASP.NET] - ASP.NET Core - 6. Data Caching
(1) Cache 만료 설정
expires-* attribute는 cache 된 content가 특정 시간이나 현재로부터 상대적인 시간으로 표현되는 값 또는 더 이상 요청되지 않을 때까지의 시간값을 통해 만료될 때를 지정할 수 있습니다. 아래 예제에서는 expires-after attribute를 사용해 content가 15초까지만 cache 되도록 지정하였습니다.
<cache expires-after="@TimeSpan.FromSeconds(15)">
<h6 class="bg-primary text-white m-2 p-2">
Cached timestamp: @DateTime.Now.ToLongTimeString()
</h6>
</cache>
project를 실행하고 /home/list URL을 요청한 뒤 응답된 page를 새로고침합니다. 15초 후 cache 된 content는 만료될 것이며 content의 새로운 section이 생성될 것입니다.
● 만료시점 지정
expires-on attribute에서는 특정 시간을 통해 cache된 content가 만료되는 시점을 명확하게 지정할 수 있으며 값은 DateTime형식으로 전달됩니다.
<cache expires-on="@DateTime.Parse("2100-01-01")">
<h6 class="bg-primary text-white m-2 p-2">
Cached timestamp: @DateTime.Now.ToLongTimeString()
</h6>
</cache>
예제에서는 data가 2100년까지 cache 되도록 지정하였습니다. 물론 application이 2100년이 되기 전까지 정상적으로 작동한다는 보장이 없으므로 그다지 유용한 caching전략이라고 보기는 힘들지만 content가 cache 되는 순간에 상대적인 만료시점이 아닌 어떻게 미래의 특정 시점을 지정할 수 있는지를 확인할 수 있습니다.
● 마지막으로 사용된 기준의 만료 기간 설정
expires-sliding attribute는 content가 cache로부터 검색된 것이 아닌 경우 content가 만료되는 기간을 설정합니다. 아래 예제에서는 sliding만료값을 10초로 설정하였습니다.
<cache expires-sliding="@TimeSpan.FromSeconds(10)">
<h6 class="bg-primary text-white m-2 p-2">
Cached timestamp: @DateTime.Now.ToLongTimeString()
</h6>
</cache>
express-sliding attribute의 효과를 확인해 보려면 project를 실행하고 /home/list로 URL을 요청합니다. 만약 10초 안에 browser를 새로고침한다면 cache 된 content가 사용될 것이며 10초가 지난 이후에 새로고침을 시도한다면 cache된 content는 만료되고 view component는 새로운 content를 생성한뒤 만료설정이 새롭게 적용될 것입니다.
● cache 변형 사용
기본적으로 모든 요청은 cache 된 같은 content를 전달받게 됩니다. 이때 CacheTagHelper class는 cache된 content의 다른version을 관리할 수 있으며 이들을 이름이 vary-by로 시작되는 attribute를 사용하여 다양한 유형의 HTTP요청에 대응할 수 있습니다. 아래 예제에서는 routing system과 일치되는 action값에 기반하여 cache의 다양성을 생성하기 위해 vary-by-route attribute를 사용한 것입니다.
<cache expires-sliding="@TimeSpan.FromSeconds(10)" vary-by-route="action">
project를 실행하고 browser에서 새로운 tab을 열어 하나는 /home/index URL을 요청하고 다른 하나는 /home/list URL을 요청합니다. 그러면 각 window에서는 각 요청이 다른 action값을 생성하게 되므로 각자의 만료시점과 함께 각자 cache된 content가 전달됨을 알 수 있습니다.
만약 razor page를 사용한다면 routing system과 일치되는 값으로 page를 사용해 같은 효과를 달성할 수 있습니다.
(2) Hosting Environment Tag Helper 사용
EnvironmentTagHelper class는 사용자 environment 요소에 적용되며 hosting기반하에서 browser로 전송되는 HTML에 content의 영역이 포함되는지의 여부를 결정합니다. 이때 environment요소는 아래 표에 나타낸 names attribute에 의존합니다.
names | 해당 attribute에는 environment요소에 포함된 content가 client로 전송되는 HTML에 포함될 hosting environment name을 지정합니다. |
아래 예제는 development와 production환경변수를 위해 vew에서 다른 content를 포함하도록 하고 해당 layout에 environment요소를 추가한 것입니다.
<div class="m-2">
<environment names="development">
<h2 class="bg-info text-white m-2 p-2">Development 환경</h2>
</environment>
<environment names="production">
<h2 class="bg-danger text-white m-2 p-2">Production 환경</h2>
</environment>
@RenderBody()
</div>
environment요소는 현재의 hosting environment name을 확인하여 여기에 content를 포함하거나 생략하게 됩니다.(environment 요소자체는 client로 전송되는 HTML에서 항상 생략됩니다.) 다음 결과는 현재 설정된 hosting environment name에 따라 다르게 보일 것입니다.
'.NET > ASP.NET' 카테고리의 다른 글
ASP.NET Core - 17. Model Binding (0) | 2023.02.08 |
---|---|
ASP.NET Core - 16. Forms Tag Helper (0) | 2023.01.27 |
ASP.NET Core - 14. Tag Helper (0) | 2023.01.08 |
ASP.NET Core - 13. View Component (0) | 2023.01.01 |
ASP.NET Core - 12. Razor Page (0) | 2022.12.21 |