ASP.NET Core - 10. View와 Controller - 1
이번 글에서는 HTML응답을 생성하여 사용자(XML 혹은 JSON을 응답함으로써 다른 Application에서 사용될 수 있도록 하는 것과는 다른)에게 직접적으로 표시할 수 있도록 하는 Razor View Engine에 대해 알아볼 것입니다. View는 C#표현식을 사용하고 HTML응답을 생성하기 위해 view engine에서 처리될 수 있는 HTML조각을 포함하는 file입니다. 여기서는 View가 어떤 식으로 동작하는지를 확인하고 이들이 action method에서 어떻게 사용되는지를 알아보고 또한 여기에 포함되는 다양한 C#표현식에 대해서도 다뤄볼 것입니다.
1. Project 준비하기
예제로 사용될 Project는 이전에 사용하던 Project를 그대로 재사용할 것입니다. 다만 Program.cs file에서는 아래와 같이 이전에 사용되던 code를 삭제하고 여기서 필요한 최소한의 것만 남겨둘 수 있도록 합니다.
using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<NorthwindContext>(opts => {
opts.UseSqlServer(builder.Configuration["ConnectionStrings:DefaultConnection"]);
opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseStaticFiles();
app.MapControllers();
app.Run();
2. View 시작하기
Web Service와 View Controller와는 서로 다른 것이라 생각할 수 있지만 2가지 모두 무엇인가를 응답하기 위해 사용된다는 것은 같습니다. 다만 이전과 달리 이번에는 Application이 HTML을 지원하도록 설정할 것이며 곧 추가할 Home Controller에서도 역시 HTML응답이 가능하도록 생성할 것입니다.
(1) Application 설정하기
우선 Program.cs에서 아래와 같이 HTML응답에 필요한 설정을 추가하도록 합니다.
builder.Services.AddDbContext<NorthwindContext>(opts => {
opts.UseSqlServer(builder.Configuration["ConnectionStrings:DefaultConnection"]);
opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllersWithViews()
var app = builder.Build();
app.UseStaticFiles();
app.MapControllers();
app.MapControllerRoute("Default", "{controller=Home}/{action=Index}/{id?}");
app.Run();
HTML 응답은 View를 통해서 이루어 지는데 이 것은 위에서 언급한 것처럼 C#표현식과 HTML요소가 섞여 있는 file에 해당합니다. AddControllers method는 MVC Framework가 이전에 진행하던 Web Service Controller만을 지원하기 때문에 View를 사용하기 위해 AddControllersWithViews method로 대체하였습니다.
Program.cs에서 두번째 변화는 MapControllerRoute라는 method가 endpoint routing설정에 추가된다는 것입니다. HTML응답을 생성하는 Controller는 Web Service에서 적용했던 것과 같은 routing attribute를 사용하지 않으며 추후에 같이 알아볼 convention routing이라는 이름의 기능에 의존합니다.
(2) HTML Controller 생성하기
HTML Application의 Controller는 Web Service에서 사용하던것과 유사하지만 중요한 몇 가지 차이가 있습니다. HTML Controller를 추가하기 위해 Project에서 HomeControllers.cs이름의 file을 Controllers folder안에 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
namespace MyWebApp.Controllers
{
public class HomeController : Controller
{
readonly NorthwindContext _context;
public HomeController(NorthwindContext context)
{
_context= context;
}
public async Task<IActionResult> Index(int id = 1)
{
var result = await _context.Products.FindAsync(id);
return View(result);
}
}
}
HTML Controller의 기반 class인 Controller class는 Web Service를 위해 사용되는 ControllerBase class로부터 파생된 것으로 View를 통해 동작할 수 있는 몇 가지 추가적인 Method를 제공하고 있습니다.
HTML Controller의 Action Method는 IActionResult interface를 구현하는 객체를 반환하는데 이전에 사용하던 특정 상태 code를 반환하기 위해 사용한 result유형과 같은 것입니다.Controller기반 class는 View Method를 제공하여 응답에 사용될 수 있는 View를 선택하기 위해 사용됩니다.
예제의 Controller에는 어떠한 attributes도 사용되지 않았음에 주의하시기 바랍니다. ApiController attribute는 단지 Web Sewrvice를 위한 Controller에만 사용될뿐 HTML Controller에는 사용될 수 없습니다. HTML Controller는 이전 예제에서 설정 시 간략하게 소개한 규칙 기반 routing에 의존하므로 Route와 HTTP method attribute 또한 필요하지 않습니다.
View Method는 IActionResult의 Interface를 구현하는 ViewResult class의 Instance를 생성하는데 MVC Framework에게 View가 client로의 응답을 생성하는데 사용될 수 있음을 알려주게 됩니다. View Method의 인수를 View Model이라고 하며 응답을 생성하는데 필요한 Data와 함께 VIew를 제공하게 됩니다.
아직까지는 MVC Framework에서 사용할 수 있는 View는 존재하지 않기 때문에 Project를 실행하게 되면 다음과 같은 오류를 보여주게 되는데 이를 통해 MVC Framework가 Index Action Method로부터 전달받은 ViewResult로 어떻게 응답하는지를 알 수 있습니다.
Project가 Razor Page를 사용한다면 /Pages/Shared folder를 포함할 수 있는 것처럼 검색의 범위는 확장될 수 있습니다.
● Routing 규칙
HTML Controller는 Route Attribute대신 규칙기반 Routing에 의존합니다. 여기서 '규칙'이라 함은 Controller의 이름과 Action Method의 이름이 Routing System의 설정을 위해 사용된다는 것을 의미하는데 위 예제에서 Program.cs에 Endpoint Routing설정 구문을 추가함으로써 이미 해당 설정을 적용하였습니다.
app.MapControllerRoute("Default", "{controller=Home}/{action=Index}/{id?}");
해당 구문을 통해 설정된 Route는 2~3개정도의 URL segment과 일치될 수 있습니다. 첫 번째 Segment의 값은 Controller라는 접미사가 빠진 Controller class의 이름으로서 사용되므로 Home은 곧 HomeController class임을 의미하게 됩니다. 두 번째 Segment는 Action Method의 이름이 되고 선택적으로 추가할 수 있는 세 번째 Segment는 Action Method가 id라는 매개변수의 값을 전달받을 수 있음을 나타낸 것입니다. 위와 같은 방식 이외에 기본 값을 통한 설정은 Home Controller에서 전체 Segment를 포함하지 않는 URL에 대한 Action Method를 선택하는 데 사용될 수 있습니다.
app.MapDefaultControllerRoute();
//app.MapControllerRoute("Default", "{controller=Home}/{action=Index}/{id?}");
MapDefaultControllerRoute Method는 URL Pattern을 직접 typing하는데 대한 실수를 방지할 수 있으면서도 규칙 기반 Routing을 설정할 수 있도록 합니다. 예제에서는 하나의 Routing만을 설정하지만 실제 Application에서는 필요한 만큼 다수의 Routing을 설정할 수도 있습니다.
MVC Framework는 HTML Controller안에 있는 모든 Public Method가 Action Method이며 Action Method는 모든 HTTP Method를 지원할 것이라고 추정하게 됩니다. 따라서 만약 Controller에 Action Method가 아닌 일반적인 Method를 정의해야 한다면 private로 만들거나 그것이 불가능하다면 Method에 NonAction attribute를 적용해야 합니다. 또한 Action Method는 attribute를 통해 특정한 HTTP Method만을 지원할 수 있도록 제한할 수 있는데 예를 들어 HttpGet attribute의 경우 해당 Action은 GET 요청을 처리할 수 있다는 것을 의미하는 것이며 HttpPost는 Action이 POST 요청을 처리할 수 있다는 것을 의미하는 것입니다.
● Razor View 규칙
Home Controller에 정의된 Index Action Method가 호출될때 id매개변수는 Database로부터 개체를 가져오기 위해 사용되고 해당 개체는 곧 View Method로 전달됩니다.
public async Task<IActionResult> Index(int id = 1)
{
var result = await _context.Products.FindAsync(id);
return View(result);
}
Action Method가 View Method를 호출할때는 ViewResult를 생성하며 MVC Framework가 View의 위치를 파악하기 위해 기본규칙을 사용하도록 합니다. Razor View Engine은 Action Method와 같은 이름을 통해 View를 찾게 되며 이때 View는 cshtml확장자가 추가된 file로서 Razor View Engine에 의해 사용되는 File유형에 해당합니다. View는 Views folder에 저장되어 있고 관련된 Controller에 의해 group화 될 수 있습니다. 첫 번째 위치는 Action Method가 HomeController(이때 이름은 Controller class의 이름에서 Controller만 제거된 것입니다.)에서 정의되었으므로 Views/Home folder에서 검색됩니다. 만약 Index.cshtml file이 Views/Home folder에서 발견되지 않으면 Controller사이에 공유되는 View가 존재하는 Views/Shared folder를 다시 확인하게 됩니다.
대부분의 Controller가 자신만의 View를 가질 수도 있지만 View는 또한 공유가 가능하므로 공통기능이 중복될 필요가 없습니다.
따라서 위에서 보인 에러 화면은 이 2가지 규칙을 모두 적용한 결과로서 Razor view engine에게 View검색 규칙을 사용하여 View를 검색하도록 하는 Routing규칙이 Home Controller에 정의된 Index Action Method의 사용 요청을 처리하기 위해 사용된 것입니다. 이때 View Engine은 검색 pattern을 구축하기 위해 사용된 Action Method와 Controller의 이름을 통해 Views/Home/Index.cshtml과 Views/Shared/Index.cshtml file을 확인하는 것입니다.
(3) Razor View 만들기
MVC Framework에게 무엇인가를 보여주기 위한 View를 제공하려면 Views/Home folder를 생성하고 그 안에 Index.cshtml이름의 file을 아래 내용으로 추가합니다.
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h6 class="bg-primary text-white text-center m-2 p-2">제품 목록</h6>
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice.ToString("c")</td></tr>
</tbody>
</table>
</div>
</body>
</html>
Visual Studio에서 View를 추가하는 방법은 다음과 같습니다. Project에 Views/Home folder를 만들고 나면 Home folder에서 Mouse오른쪽 button을 누른뒤 Menu에서 'Add -> View'를 선택합니다. 그리고 이어진 'Add New Scaffolded Itrem'화면에서 'Razor View - Empty'항목을 선택합니다.
'Add New Item'화면에서 다시 'Razor View -Empty'항목을 선택하고 Name을 Index.cshtml로 지정한뒤 'Add' button을 눌러줍니다.
'Add popup menu'에는 View를 생성하기 위한 menu항목이 존재하며 View의 다양한 유형을 생성하기 위한 template content를 추가하는 Visual Studio scaffolding기능을 구현하고 있습니다.
View file은 class attribute를 통해 style을 구현하는 Bootstrap CSS framework와 함께 기본적인 HTML요소를 포함하고 있습니다. 여기서 주목해야 하는 부분은 아래와 같이 C# 표현식을 통해 content를 생성하고 있다는 것입니다.
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice.ToString("c")</td></tr>
Razor구문에 관한 자세한 내용은 곧 자세히 알아볼 테지만 지금은 위의 표현식이 Action Method에 의해 View로 전달된 Product View Model로부터 UnitPrice와 ProductName속성의 값을 추가한다는 것만 이해하면 충분합니다.
● Razor View 변경하기
Project를 실행할때 Ctrl + F5 key 또는 'Start Without Debugging'으로 실행한 뒤
'Hot Reload on File Save'를 설정해 두면 File이 편집되고 저장되는 순간마다 실행 중인 Web Browser를 통해 그대로 반영될 수 있습니다. 이것은 File을 수정할 때마다 일일 Project를 종료했다가 다시 실행시켜 확인하는 번거로움을 줄여줄 수 있습니다.
예를 들어 Index.csthml을 아래와 같이 변경하고 File을 저장하게 되면
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice.ToString("c")</td></tr>
<tr><th>카테고리 ID</th><td>@Model?.CategoryId</td></tr>
Web Browser에는 위의 변경사항이 자동으로 반영된 결과를 표시하게 될 것입니다.
(4) 이름을 통한 View 지정하기
Action method는 전적으로 규칙에 의존하지만 필요한 경우 응답을 생성하는데 사용되는 View를 지정할 수 있으며 이때 Action Method는 View Method에서 View의 이름을 제공함으로써 View를 지정하게 됩니다.
public async Task<IActionResult> Index(int id = 1)
{
var result = await _context.Products.FindAsync(id);
if (result?.CategoryId == 1)
return View("Watersports", result);
else
return View(result);
}
예제에서 Action Method는 Database로 부터 가져온 Product개체의 CategoryId에 기반하여 View를 지정하고 있습니다. 따라서 CategoryId가 만약 1이라면 Action Method는 Watersports라는 이름의 View를 선택하기 위한 매개변수와 함께 View Method를 호출하게 됩니다.
다만 Action Method에서는 File에 대한 확장자나 위치를 특정하지 않음에 주의해야 합니다. Watersports를 View File로서 처리하는 역활은 View engine의 역할입니다.
HomeController.cs file을 저장하고 나서 Project를 실행하면 Web Browser는 해당 Page를 읽어 들이기를 시도하지만 해당 View file은 존재하지 않으므로 곧 오류를 발생시키게 됩니다. Watersports라는 View를 생성하기 위해 Views/Home folder에 Watersports.cshtml이름의 file을 아래 내용으로 추가합니다.
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h6 class="bg-secondary text-white text-center m-2 p-2">수상 스포츠</h6>
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice.ToString("c")</td></tr>
<tr><th>카테고리 ID</th><td>@Model?.CategoryId</td></tr>
</tbody>
</table>
</div>
</body>
</html>
새롭게 추가된 View는 Index와 매우 유사합니다. Project를 실행하여 /home/index/1 URL을 호출하여 다음과 같은 결과가 생성되는지 확인합니다. Action Method는 1의 값을 통해 Watersports라는 View를 통해 응답할 것이며 그 외 모든 값은 Index View를 통해 응답하게 됩니다.
참고로 URL을 따로 지정하지 않아도 Action Method의 id 매개변수 기본값은 1이기에 위의 결과를 바로 확인할 수 있습니다.
● Shared View의 사용
Razor view engine이 View를 찾을 때는 Views하위의 Controller명 folder를 찾고 그다음 Shared folder를 찾게 됩니다. 이러한 검색 pattern은 공용 content를 포함하고 있는 View가 Controller사이에서 공유될 수 있음을 의미하며 곧 content를 중복시킬 필요가 없음을 알 수 있습니다. 이러한 처리과정이 어떻게 진행되는지를 확인해 보기 위해 Views하위에 Shared folder를 추가하고 다시 Shared folder에 Common.cshtml이름의 file을 아래 내용으로 추가해 줍니다.
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h6 class="bg-secondary text-white text-center m-2 p-2">Shared View</h6>
</body>
</html>
그 다음 HomeController.cs에 위에서 추가한 View에 사용되는 Action Method를 추가합니다.
public IActionResult Common()
{
return View();
}
예제의 Action Method는 View의 이름을 Method의 이름으로서 사용하는 규칙에 의존하고 있습니다. View가 사용자에게 표시될 때 어떠한 Data도 필요로 하지 않을 수 있고 View Method 또한 어떠한 인수도 없이 호출될 수 있습니다. 이제 Controller folder에 SecondController.cs이름으로 아래와 같은 새로운 Controller를 추가합니다.
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Controllers
{
public class SecondController : Controller
{
public IActionResult Index()
{
return View("Common");
}
}
}
예제의 Controller는 Index라는 하나의 Action Method만을 정의하고 있으며 이 Method는 Common View를 지정하는 View Method를 호출하고 있습니다. Project를 실행하고 /home/common과 /second라는 2개의 URL을 요청하여 다음과 같이 둘 다 Common View가 응답으로 생성되는지를 확인합니다.
Razor view engine은 Shared보다 먼저 Controller별 View를 찾게 되는데 필요하다면 이러한 동작은 View file이 위치한 완전한 경로를 지정함으로써 바꿀 수 있습니다. View를 지정하기 위한 이러한 방법은 Controller별 View와 동일한 이름일 경우 무시될 수 있는 Shared에서의 View를 선택할 때 유용한 방법이 될 수 있습니다.
public IActionResult Index() {
return View("/Views/Shared/Common.cshtml");
}
View를 특정할 때는 Project folder에 대한 상대적인 경로가 지정되어야 하며 / 문자로 시작해야 하고 확장자를 포함한 완전한 file명이 사용되어야 합니다.
다만 이러한 View의 지정방식은 Razor view engine이 file을 선택하도록 하기보다는 특정한 file에 의존성이 만들어지게 되므로 되도록이면 사용하지 않는 것이 좋습니다.
3. Razor View 사용
Razor View는 HTML 요소와 C# 구문을 같이 포함할 수 있습니다. 이때 C#구문은 HTML요소와 섞일 수 있으므로 C#구문을 따로 구분하기 위해 @문자를 사용합니다.
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
View가 응답을 생성하는 데 사용되는 순간 C#구문이 실행되고 그 결과가 Client에 보낼 content에 포함됩니다. 따라서 위 예제는 Action Method로부터 제공된 Product View Model 개체의 이름을 가져오게 되므로 다음과 같은 응답을 생성할 것입니다.
<tr><th>제품명</th><td>Chai</td></tr>
Razor View는 RazorPage class로부터 상속된 C# class로 변환되어 다른 C# class와 마찬가지로 compile과정을 거친 후 동작합니다.
기본적으로 Razor View는 직접 DLL안으로 compile 되지만 생성된 C# class는 build 처리동안은 file로 저장되지 않습니다. 그러나 만약 생성된 clsss의 결과물을 확인해 보고자 한다면 Project file(csproj)에서 아래와 같은 설정을 추가하고
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
Project를 build 하면 '\obj\Debug\net6.0\generated\' 위치에 compile 된 결과가 file로 저장되므로 이를 통해 결과를 확인할 수 있습니다.
#pragma checksum "C:\Users\Administrator\source\repos\MyWebApp\MyWebApp\Views\Home\Index.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "a5b8a805f1040a889eb3c40d0de24b405d7daf99f1026ebd394bc4501cbbd429"
// <auto-generated/>
#pragma warning disable 1591
[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.Views_Home_Index), @"mvc.1.0.view", @"/Views/Home/Index.cshtml")]
namespace AspNetCoreGeneratedDocument
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
[global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/Views/Home/Index.cshtml")]
[global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute]
#nullable restore
internal sealed class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<dynamic>
#nullable disable
{
#line hidden
#pragma warning disable 0649
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext;
#pragma warning restore 0649
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner();
#pragma warning disable 0169
private string __tagHelperStringValueBuffer;
#pragma warning restore 0169
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __backed__tagHelperScopeManager = null;
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __tagHelperScopeManager
{
get
{
if (__backed__tagHelperScopeManager == null)
{
__backed__tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager(StartTagHelperWritingScope, EndTagHelperWritingScope);
}
return __backed__tagHelperScopeManager;
}
}
...이하 생략
위 예제의 모든 Oode를 다 살펴볼 수 없지만 중요한 부분 몇 가지를 우선 살펴보자면 우선 다음 code에 따라
internal sealed class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<dynamic>
위 예제를 통해 생성된 class는 RazorPage<T>로 부터 상속받고 있음을 알 수 있습니다. 아래 표를 통해서 RazorPage<T>에서 정의된 가장 중요한 속성과 Method를 확인해 볼 수 있습니다.
View를 통해 생성된 응답은 Controller class의 Action Method(또는 모든 Action Method의 응답을 Cache 할 수 있도록 Controller class)에 ResponseCache attribute를 적용함으로써 Cache 될 수 있습니다.
Context | 이 속성은 현재 요청에서 HttpContext 개체를 반환합니다. |
ExecuteAsync() | 이 Method는 View에서 출력을 생성하는데 사용됩니다. |
Layout | 이 속성은 View의 Layout을 설정하는데 사용됩니다. |
Model | 이 속성은 Action Method에 의해 View로 전달된 View Model를 반환합니다. |
RenderBody() | 이 Method는 View에서의 content를 포함하기 위해 Layout에서 사용됩니다. |
RenderSection() | 이 Method는 View의 Section에 있는 content를 포함하기 위해 Layout에서 사용됩니다. |
TempData | 이 속성은 Temp data기능에 접근하기 위해 사용됩니다. |
ViewBag | 이 속성은 View Bag에 접근하기 위해 사용됩니다. |
ViewContext | 이 속성은 context data를 제공하는 ViewContext 개체를 반환합니다. |
ViewData | 이 속성은 View Data를 반환합니다. |
Write() | 이 Method는 지정한 HTML에서의 사용을 위해 안전하게 encode되는 문자열을 표시합니다. |
WriteLiteral() | 이 Method는 별도의 encode과정없이 지정한 문자열을 표시합니다. |
View의 C# 구문은 Write Method의 호출로 변환되고 구문의 실행결과를 encode 함으로써 안전하게 HTML안으로 포함될 수 있습니다. WriteLiteral Method는 View에 있는 정적 HTML을 처리하기 위해 사용되며 이때는 encode과정을 필요로 하지 않습니다. 예를 들어 아래와 같은 HTML문은
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
ExecuteAsync Method에서 아래와 같은 일련의 C#구문으로 변환될 수 있습니다.
WriteLiteral("<th>제품명</th><td>");
Write(Model?.ProductName);
WriteLiteral("</td></tr>");
ExecuteAsync Method가 호출될 때 응답은 View에 포함된 정적 HTML과 구문이 혼합되어 생성됩니다. 따라서 구문이 실행된 결과 아래 HTML과 같은 형태의 응답이 될 수 있습니다.
<tr><th>제품명</th><td>Chai</td></tr>
RazorPage<T> class로 부터 상속되어 생성된 View class는 아래 표와 같은 속성과 응답이 추가적으로 정의됩니다.
Component | 이 속성은 View component와의 동작을 위한 Helper를 반환하며 vc tag helper를 통해 접근할 수 있습니다. |
Html | 이 속성은 IHtmlHelper interface의 구현체를 반환하며 HTML encoding를 관리하기 위해 사용됩니다. |
Json | 이 속성은 IJsonHelper interface의 구현체를 반환하며 JSON data encode에 사용됩니다. |
ModelExpression provider | 이 속성은 tag helper를 통해 사용될 수 있 Model로 부터 속성을 선택하는 표현식의 접근을 제공합니다. |
Url | 이 속성은 URL에 대한 동작과 관련된 helper를 제공합니다. |
(1) View Model Type 설정
Watersports.cshtml file에서 생성된 class는 RazorPage<T> class로 부터 상속받았지만 Razor는 Action Method에서 View Model로 어떠한 Type이 사용될지 알 수 없습니다. 따라서 generic type매개변수로서 dynamic이 선택되는 것이고 이 것은 응답이 생성될 때 runtime에서 실행되는 @Model 표현식이 모든 속성 또는 Model의 이름으로 사용될 수 있음을 의미합니다. 하지만 아래와 같이 존재하지 않는 Member를 사용하게 될 때는
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice.ToString("c")</td></tr>
<tr><th>카테고리 ID</th><td>@Model?.CategoryId</td></tr>
<tr><th>제폼코드</th><td>@Model?.Code</td></tr>
다음과 같은 예외를 표시하게 됩니다.
위와 같은 오류를 피하려면 model keyword를 통해 Model 개체의 유형을 특정해야 합니다.
Model과 model에서 약간의 혼란이 발생할 수 있는데 대문자 M으로 시작하는 건 Action Method에서 전달된 model 개체에 접근하기 위한 표현식이며 반면 소문자 m으로 시작하는건 View Model의 Type을 특정하기 위해 사용됩니다.
@model MyWebApp.Models.Products
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h6 class="bg-secondary text-white text-center m-2 p-2">수상 스포츠</h6>
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice.ToString("c")</td></tr>
<tr><th>카테고리 ID</th><td>@Model?.CategoryId</td></tr>
<tr><th>제폼코드</th><td>@Model?.Code</td></tr>
</tbody>
</table>
</div>
</body>
</html>
따라서 위와 같이 Model을 지정하고 나서 몇 초가 지나게 되면 Visual Studio는 Background에서 View를 확인하여 오류사항을 Edit상으로 표시하게 됩니다.
View에 대한 C# class를 생성할 때 base class의 generic type인수로 view model type이 아래와 같이 사용됩니다.
internal sealed class Views_Home_Watersports : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<MyWebApp.Models.Products>
view model type을 특정하여 Visual Studio Editor상에서 속성과 method의 이름을 유추할 수 있도록 함으로써 사전에 아래와 같이 존재하지 않는 속성을 삭제 허거나 변경할 수 있는 기회를 가질 수 있습니다.
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice?.ToString("c")</td></tr>
<tr><th>카테고리 ID</th><td>@Model?.CategoryId</td></tr>
<tr><th>제폼코드</th><td>@Model?.ProductId</td></tr>
view model type을 특정함으로써 생길 수 있는 또 다른 이점은 자동완성 기능을 통해 해당 개체가 제공하는 속성 또는 Method를 자동으로 표시함으로써 오타를 줄이고 빠르게 code입력이 가능해지는 것입니다.
● View Imports File 사용
Watersports.cshtml 시작점에 View model 개체를 정의할 때 아래와 같이 class를 포함하고 있는 namespace를 같이 사용하였습니다.
@model MyWebApp.Models.Products
기본적으로 Razor View에서 참조되는 모든 type은 namespace가 사용되어야 합니다. 이러한 방식은 Model 개체에 대한 참조에서는 큰 문제 될 것이 없지만 다소 복잡한 Razor구문이 사용되는 경우에는 view자체를 읽기 어렵게 만들 수 있습니다.
이러한 문제는 project에 view import file을 추가함으로써 검색되어야 하는 type에 대한 일련의 namespace를 특정함으로써 해결할 수 있습니다. view import file은 View folder에서 _ViewImports.cshtml이름으로 위치해 있습니다.
View folder에 이름이 밑줄(underscore(_))로 시작하는 file은 사용자에게 반환되지 않습니다. 이는 file이름을 통해 사용자에게 표시되는 View와 이들을 지원하는 file을 구별할 수 있도록 하는데 예제에서 사용되는 import file과 Layout file도 밑 즐로 file이름이 시작됩니다.
Visual Studio의 Solution Explorer에서 View folder를 선택하고 mouse오른쪽 button을 눌러 Add -> New항목을 선택한 뒤 다시 ASP.NET Core Category에서 'Razor View Imports'항목을 선택합니다.
Visual Studio는 자동적으로 file의 이름을 _ViewImports.cshtml로 설정하게 되고 이 상태에서 'Add' button을 누르면 해당 이름의 빈 file을 생성할 것입니다. 그리고 해당 file에 아래와 같은 내용을 입력하고 저장합니다.
@using MyWebApp.Models
Razor View에서 class사용을 위해 검색되어야 하는 Namespace는 @using구문을 통해 특정됩니다. 예제에서는 Watersports.cshtml view에서 사용되는 View model class를 포함하는 MyWebApp.Models을 추가하였습니다.
이제 namespace는 View import file에 추가되었으므로 View에서는 해당 Namespace를 제거할 수 있습니다.
각각의 View file에서도 @using 구문을 사용할 수 있으며 단일 View에서 Namespace 없이 type을 사용할 수 있습니다.
@model Products
<!DOCTYPE html>
<html>
해당 view file을 저장하고 project를 실행하면 이전과 동일한 결과가 생성될 것입니다.
4. Razor 구문의 이해
Razor compiler는 C# 표현식으로부터 정적 HTML요소를 분리하고 생성된 class file에서 별도로 처리합니다. View에서는 포함될 수 있는 몇몇 표현식이 존재하는데 이에 관하여 하나씩 살펴보도록 하겠습니다.
(1) Directive (지시자)
Directive는 Razor View Engine에 일종의 지침을 제공하는 표현식입니다. 예컨대 @model 표현식은 directive중의 하나로서 View Engine에게 View Model로 사용할 특정한 type을 알리는 데 사용합니다. 또한 @using directive는 특정 namespace를 import 할 것임을 알려줍니다. 아래 표는 가장 대표적인 몇몇 directive와 그에 대한 설명을 나타내고 있습니다.
@model | View Model에 사용할 type을 명시합니다. |
@using | Namespace를 Import합니다. |
@page | Razor Page임을 표시합니다. |
@section | Layout section임을 표시합니다. |
@addTagHelper | View에 Tag helper를 추가합니다. |
@namespace | View에서 생성된 C# class의 Namespace를 설정합니다. |
@functions | View에서 생성된 C# class에 Method와 속성을 추가하며 보통 Razor Page에서 사용됩니다. |
@attribute | View에서 생성된 C# class에서 attribute를 추가합니다. |
@implements | View에서 생성된 C# class가 interface를 구현함을 선언합니다. |
@inherits | View에서 생성된 C# class의 기반(base) class를 설정합니다. |
@inject | 의존성 주입을 통해 service에 직접적으로 접근할 수 있는 View를 제공합니다. |
(2) Content 표현식(Expression)
Razor content 표현식은 VIew에 의해 만들어진 결과물에 포함되는 content를 생성합니다. 아래 표에서는 가장 대표적인 Content 표현식을 나열하였습니다.
@<expression> | 기본적인 Razor 표현식으로 평가되며 Razor 표현식이 생성한 결과가 응답에 삽입됩니다. |
@if | if 조건문을 구현합니다. |
@switch | switch 조건문을 구현합니다. |
@foreach | foreach 반복문을 구현합니다. |
@{...} | code block을 정의합니다. |
@: | HTML요소를 포함하지 않는 content의 영역을 표시합니다. |
@try | 예외처리를 구현합니다. |
@await | 비동기 동작을 구현하는데 사용되며 해당 결과는 응답에 추가됩니다. |
(3) Element Content
상기 예제에서 사용된 가장 간단한 표현식은 client로 전송된 응답에서 HTML요소의 content로 사용되는 단일 값을 생성하는 것입니다. 이러한 방법은 표현식의 가장 일반적인 유형으로 View Model 개체로부터 값을 아래와 같이 추가하는 것입니다.
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice?.ToString("c")</td></tr>
위와 같은 표현식은 속성 값을 읽거나 Method를 호출할 수 있는데 View는 괄호로 감싸는 방법 등을 통해 더 복잡한 표현식도 포함할 수 있습니다. 물론 이런 경우에도 Razor compiler는 정적 content와 code를 명확히 구분하여 처리할 수 있습니다.
View에서 NULL조건 연산자 사용
@Model 표현식이 사용될 때 Null 조건 연산자(?)는 NULL Model 값으로 예외가 발생하지 않도록 보호하는데 필요합니다. 하지만 @model이 Model의 Type을 정의하는 데 사용되는 경우에는 아래와 같이 non-nullable type을 사용해야 합니다.
@model Products
그렇지 않고 nullable 참조 type을 @model에 사용하게 된다면 Compiler는 경고를 표시할 것입니다. 생성된 View class가 퍄생되는 RazorPage<T> class는 View 표현식에 사용되는 Model 속성을 정의하는데 이때 RazorPage<T>의 Model 속성은 다음과 같이 정의되어 있습니다.
public T? Model => ViewData == null ? default(T) : ViewData.Model;
비록 non-nullable type이 @model 표현식에 사용되어야 하지만 @Model 표현식에 사용되는 Model 속성의 type은 nullable type이 될 수 있으므로 결국 View 표현식에서 null 조건 연산자가 필요하게 되는 것입니다.
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice?.ToString("c")</td></tr>
<tr><th>부가세</th><td>@Model?.UnitPrice * 0.2m</td></tr>
<tr><th>부가세</th><td>@(Model?.UnitPrice * 0.2m)</td></tr>
위와 같이 예제를 변경한 후 Project를 실행해 보면 왜 괄호가 중요한지를 곧 알 수 있게 됩니다.
Razor View compiler는 표현식을 다수 보수적으로 일치시키기 때문에 첫 번째 표현식에서의 *과 숫자 값은 정적 content라고 판단하게 됩니다. 따라서 이러한 문제는 두 번째 표현식처럼 괄호를 통해 해결할 수 있습니다.
(4) 속성 값 설정
표현식은 HTML 요소의 속성 값을 설정하는 데에도 사용될 수 있습니다.
<table class="table table-sm table-striped table-bordered" data-id="@Model?.ProductId">
<tbody>
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice?.ToString("c")</td></tr>
<tr><th>부가세</th><td>@Model?.UnitPrice * 0.2m</td></tr>
<tr><th>부가세</th><td>@(Model?.UnitPrice * 0.2m)</td></tr>
</tbody>
</table>
예제에서는 Razor표현식을 통해 table요소의 data속성 값을 설정하고 있습니다.
data-속성
Data 속성은 data- 접두사로 시작하는 이름의 속성을 말하며 수년간 사용자 정의 속성을 만들기 위한 비공식적인 방법이었지만 HTML5에 들어서면서 공식적인 방법으로 인정되었습니다. 이러한 방법은 이제 아주 흔하게 사용되는 것으로 Javascript code에서 특정 요소를 찾아내는데도 도움이 될 수 있습니다.
project를 실행하고 결과에 대한 HTML구조를 살펴보면 예제에서 사용된 속성 값이 아래와 같이 설정되어 있음을 확인할 수 있습니다.
<table class="table table-sm table-striped table-bordered" data-id="1">
<tbody>
<tr><th>제품명</th><td>Chai</td></tr>
<tr><th>단가</th><td>₩18</td></tr>
<tr><th>부가세</th><td>18.0000 * 0.2m</td></tr>
<tr><th>부가세</th><td>3.60000</td></tr>
</tbody>
</table>
(5) 조건부 표현식
Razor는 조건부 표현식을 지원하는데 이를 통해서 View Model기반하에 결과가 조정될 수 있습니다. 이 것은 Razor의 가장 핵심이며 쉽게 읽고 관리하기 쉬운 View로부터 복합적이고 유동적인 응답을 생성할 수 있도록 합니다.
<tbody>
@if (Model?.UnitPrice > 18)
{
<tr><th>제품명</th><td>고가 : @Model?.ProductName</td></tr>
}
else
{
<tr><th>저가</th><td>저가 : @Model?.ProductName</td></tr>
}
<tr><th>단가</th><td>@Model?.UnitPrice?.ToString("c")</td></tr>
<tr><th>부가세</th><td>@Model?.UnitPrice * 0.2m</td></tr>
<tr><th>부가세</th><td>@(Model?.UnitPrice * 0.2m)</td></tr>
</tbody>
@문자는 if keyword와 runtime에서 실행될 수 있는 조건의 사용을 가능하게 합니다. 특히 if표현식은 C#에서의 if와 다를 바 없이 else와 elseif 모두를 지원하고 동일하게 {로 시작해 }로 조건이 종료될 수 있습니다. 예제에서 조건이 일치한다면 if절이 응답에 삽입되며 그렇지 않으면 else절의 content가 응답에 삽입될 것입니다.
@ 접두사는 조건절 안에서는 Model 속성의 접근 시 필요하지 않지만
@if (Model?.UnitPrice > 200)
if와 else안에서는 사용되어야 합니다.
<tr><th>제품명</th><td>고가 : @Model?.ProductName</td></tr>
조건 구문의 효과를 확인해 보기 위해 Project를 실행하고 /home/index/1과 /home/index/2 URL을 차례로 요청합니다. 조건 표현식은 이들 URL에 따라 다른 HTML요소를 생성하게 될 것입니다.
Razor는 또한 @switch 표현식을 지원함으로써 여러 조건에 대한 처리를 더욱 간략하게 표현할 수 있습니다.
<tbody>
@switch (Model?.ProductName) {
case "Chai":
<tr><th>Name</th><td>1. Chai</td></tr>
break;
case "Chang":
<tr><th>Name</th><td>2. Chang</td></tr>
break;
default:
<tr><th>Name</th><td>Etc. @Model?.ProductName</td></tr>
break;
}
<tr><th>단가</th><td>@Model?.UnitPrice?.ToString("c")</td></tr>
<tr><th>부가세</th><td>@Model?.UnitPrice * 0.2m</td></tr>
<tr><th>부가세</th><td>@(Model?.UnitPrice * 0.2m)</td></tr>
</tbody>
조건 표현식에서는 각 결과 절에 대해서 content의 중복 사용을 유발할 수 있는데 예제의 switch 표현식 안에서도 각 case절에서는 tr과 th요소가 동일하게 유지되고 있으면서도 td요소의 content만을 달리하고 있습니다. 이러한 중복성을 제거하기 위해 조건 표현식은 요소 내부에서만 사용될 수 있습니다.
<table class="table table-sm table-striped table-bordered" data-id="@Model?.ProductId">
<tbody>
<tr><th>Name</th><td>
@switch (Model?.ProductName) {
case "Chai":
@:1. Chai
break;
case "Chang":
@:2, Chang
break;
default:
@:Etc. @Model?.ProductName
break;
}
</td></tr>
<tr><th>단가</th><td>@Model?.UnitPrice?.ToString("c")</td></tr>
<tr><th>부가세</th><td>@Model?.UnitPrice * 0.2m</td></tr>
<tr><th>부가세</th><td>@(Model?.UnitPrice * 0.2m)</td></tr>
</tbody>
</table>
Razor compiler는 HTML 요소로 변환되지 말아야 할 Literal 값에 대해서는 접두사 @:을 붙임으로써 예외로 인식하게 됩니다.
@:1. Chai
Compiler는 Tag가 시작됨을 감지하여 HTML요소로서 대응하게 되는데 단순한 문자열 content의 경우에는 위 예제처럼 특별한 처리를 추가해야 합니다.
Project를 실행하여 /home/index/2 URL을 요청하고 이전과 동일한 결과가 표시되는지를 확인합니다.
(6) 배열 열거하기
Razor의 @foreach 표현식은 Data를 처리할 때 흔히 사용되는 것으로 Array나 Collection에서 각 개체에 대한 content를 생성합니다. 아래 예제는 Home Controller에 추가한 Action Method로서 개체에 대한 배열을 생성하도록 하였습니다.
public IActionResult List()
{
return View(_context.Products);
}
위 Action Method는 Entity Framework Data Context로부터 가져온 Product 개체의 배열을 View에 제공하고 있습니다. 따라서 View/Home folder에 List.cshtml이라는 file을 아래 내용으로 추가합니다.
@model IEnumerable<Products>
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<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></tr>
</thead>
<tbody>
@foreach (Products p in Model ?? Enumerable.Empty<Products>())
{
<tr>
<td>@p.ProductName</td><td>@p.UnitPrice</td>
</tr>
}
</tbody>
</table>
</div>
</body>
</html>
예제의 @foreach 형식 역시 C#의 foreach와 다르지 않으며 ?? 연산자를 통해 Model이 null인 경우 빈 Collection으로 대체될 수 있도록 하였습니다. 예제에서 변수 p에는 Action method에서 제공된 배열의 각 개체가 할당됩니다. 표현식 내부에서 각 개체에 대한 content는 반복적으로 중복된 content를 만들어 내는데 이 것은 포함된 표현식을 실행한 후 응답에 삽입될 것입니다. 해당 예제의 경우 foreach 표현식의 content는 cell과 함께 자신만의 표현식을 가진 table의 row를 생성합니다.
<td>@p.ProductName</td><td>@p.UnitPrice</td>
Project를 실행하고 /home/list URL을 호출하여 다음과 같은 응답이 생성되는지 확인합니다.
(7) Razor Code Block
Code block은 C# content의 영역을 의미하며 contnet를 직접적으로 생성하지 않지만 필요한 특정 작업을 수행할 수 있습니다. 아래 예제는 평균값을 계산하도록 하는 code block을 표현하고 있습니다.
@{
decimal avr = Model?.Average(p => p.UnitPrice) ?? 0;
}
@model IEnumerable<Products>
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<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></tr>
</thead>
<tbody>
@foreach (Products p in Model ?? Enumerable.Empty<Products>())
{
<tr>
<td>@p.ProductName</td><td>@p.UnitPrice</td>
<td>@((p.UnitPrice ?? 0 / avr * 100).ToString("F1")) % of average</td>
</tr>
}
</tbody>
</table>
</div>
</body>
</html>
code block은 @{ 와 } 사이를 의미하는 것으로 기본적인 C# 구문이 포함될 수 있습니다. 예제는 arv이라는 이름의 변수에 LINQ에서 계산된 값을 할당하도록 되어 있으며 표현식에서 table cell의 content를 설정하는 데 사용하였습니다. 또한 View Model 배열의 각 개체에 대한 평균값 계산에서 반복적인 부분을 피할 수 있게 되었습니다. Project를 실행하여 /home/list로 URL을 요청하고 변경 부분이 잘 반영되었는지를 확인합니다.
Code block는 구문의 내용이 많았지만 오히려 관리하기가 어려워질 수 있습니다. 복잡한 작업을 수행하려면 View Bag사용을 고려하거나 non-action method를 Controller에 추가하는 것이 좋습니다.