이제까지 ASP.NET Core 개발에 필요한 여러 사항들을 알아보았으므로 이를 토대로 간략한 Project를 하나 진행해 볼 것입니다. Project는 Comuter 부품을 판매하는 Shopping mall로 사용자가 찾아볼 수 있는 제품과 제품의 category 그리고 특정 상품을 담아 둘 수 있는 Cart, 주문현황의 상세를 볼 수 있는 Checkout 기능을 만들어 볼 것입니다. 또한 Shopping mail자체를 관리하는 관리자기능도 같이 추가해 보고자 합니다.
ASP.NET Core를 사용하여 가능한한 실질적인 Application을 만들어 봄으로서 Application이 만들어지는 전체적인 개념을 이해하고자 하지만 ASP.NET Core가 주된 주제이므로 Database와 같은 외부 system사용을 최소화하고 결제처리와 같은 부수적인 기능은 제외하였습니다.
필요한 수준의 기능을 만들어 감으로서 다소 진행이 느려질 수 있지만 초기 이러한 투자는 관리/확장성이 높고 잘 구조화된 code의 결과를 만들어낼 것입니다.
해당 Project의 진행과정에서는 ASP.NET Core의 다양한 구성요소를 어떻게 격리하고 test할 수 있는지를 나타내기 위해 단위 test를 도입할 것입니다. 물론 단위 test가 모두에게 유용한 것은 아니므로 굳이 단위 test를 원하지 않는다면 단위 test를 설명하는 부분을 지나칠 수 있습니다. ASP.NET Core의 기술적인 면을 익히기 위해서 단위 test가 필요한 것은 아닙니다.
Project를 만드는데 사용한 대부분의 기능은 추후 개별적인 글을 통해 좀 더 상세하게 알아볼 것입니다. 따라서 중복설명을 피하기 위해 Project가 진행되는 동안에는 개념적인 이해가 가능할 정도만 짚고 넘어갈 테지만 Application build에 필요한 각 단계를 설명함으로써 ASP.NET Core 기능이 어떻게 서로 결합하여 사용할 수 있는지 확인할 수 있습니다.
1. Project 생성
Project는 최소한의 설정으로 시작하고 점진적으로 기능을 추가해 나갈 것입니다. 아래 명령으로 Project를 생성합니다.
dotnet new globaljson --sdk-version 8.0.202 --output CompuMallSln/CompuMallStore dotnet new web --no-https --output CompuMallSln/CompuMallStore --framework net8.0 dotnet new sln -o CompuMallSln dotnet sln CompuMallSln add CompuMallSln/CompuMallStore |
위 명령은 Project template을 통해 CompuMallStore라는 Project를 생성하고 이를 포함하는 CompuMallSln이름의 Solution folder를 생성합니다. CompuMallSln에는 또한 CompuMallStore project가 추가된 Solution file이 존재합니다.
예제는 의도적으로 Solution과 Project에 다른 이름을 적용하여 이들이 명확히 구분될 수 있도록 하였습니다. 다만 Visual Studio를 사용한다면 이때는 기본적으로 같은 이름이 사용되는 것이 기본입니다. 정답은 없으므로 이들 이름은 자유롭게 정할 수 있습니다.
(1) 단위 test project 생성
단위 test project는 아래 명령을 사용해 생성합니다.
dotnet new xunit -o CompuMallSln/CompuMallStore.Tests --framework net8.0 dotnet sln CompuMallSln add CompuMallSln/CompuMallStore.Tests dotnet add CompuMallSln/CompuMallStore.Tests reference CompuMallSln/CompuMallStore |
Mock 개체를 생성하기 위해 Moq package를 사용할 것이므로 아래 명령을 사용해 Moq package를 단위 test project에 설치합니다.
dotnet add CompuMallSln/CompuMallStore.tests package Moq |
(2) Project 열기
Visual Studio Code를 사용하고자 한다면 File > Open Folder 를 선택하고 CompuMallSln folder를 찾아 'Select Folder' button을 눌러줍니다.
Visual Studio를 사용한다면 Visual Studio를 실행하고 'Open a project or solution'을 눌러 CompuMallSln folder안에서 CompuMallSln.sln file을 찾아 Open button을 눌러줍니다
(3) Application folder 추가하기
다음으로 Application의 구성요소를 포함할 folder를 Project에 추가합니다. Visual Studio Code의 EXPLORER에서 mouse 오른쪽 button을 눌러 New Folder를 선택하거나 Visual Studio의 Solution Explorer에서 Store Project를 선택한 뒤 mouse 오른쪽 button을 눌러 Add > New Folder를 선택해 아래 표의 folder들을 추가해 줍니다.
Models | 필요한 Data Model과 Application의 Database에 접근하는데 필요한 Class file이 저장됩니다. |
Controllers | HTTP 요청을 처리할 Controller class가 저장됩니다. |
Views | 별도의 하위 folder로 group화된 모든 Razor file이 저장됩니다. |
Views/Home | HomeController로 연결된 Razor file이 포함되는 folder입니다. |
Views/Shared | 모든 Controller에서 공통적으로 사용되는 Razor file이 포함되는 folder입니다. |
(4) Service와 요청 pipeline준비하기
Project의 Program.cs는 ASP.NET Core application을 여러 구성과 설정에 사용됩니다. 아래 Program.cs는 CompuMallStore Project를 위해 기본적인 Application기능을 적용하고 있습니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
//app.MapGet("/", () => "Hello World!");
app.UseStaticFiles();
app.MapDefaultControllerRoute();
app.Run();
builder.Services는 Application전역에 사용되며 의존성주입(dependency injection)을 통해 접근되는 Service라는 개체의 설정에 사용됩니다. AddControllersWithViews method는 MVC Framework와 Razor view engine을 Application에서 사용하기 위해 필요한 공유개체를 설정합니다.
ASP.NET Core는 HTTP요청을 받으면 이를 app속성을 사용해 등록된 middleware 구성요소로 구성된 요청 pipeline을 따라 전달합니다. 각 middleware 구성요소는 요청을 검사하고 이에 따른 응답을 생성합니다. 요청 pipeline은 ASP.NET Core의 가장 핵심적인 부분에 해당합니다.
UseStaticFiles method는 wwwroot folder에 존재하는 정적 content를 client에 service 할 수 있도록 합니다.
Middleware component중에서 가장 중요한 것 중 하나는 HTTP요청을 end point라고 하는 application기능과 일치시키는 것이며 여기에서 요청에 대한 응답을 생성합니다. Endpoint routing기능은 request pipeline에 자동적으로 추가되며 MapDefaultControllerRoute method는 요청을 class와 method로 일치시키는 방식을 사용하여 MVC Framework를 endpoint의 source로서 등록하게 됩니다.
(5) Razor view engine 설정
Razor view engine은 .cshtml확장자를 가진 View file을 직접적으로 처리하고 HTML응답을 생성하는 역할을 수행합니다. Application에 대한 view를 쉽게 만들려면 Razor를 구성하기 위한 일부 사전준비가 필요합니다. Views folder에 _ViewImports.cshtml이름의 Razor View Imports file을 아래와 같이 추가합니다.
@using CompuMallStore.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
예제에서 사용된 @using은 CompuMallStore.Models의 type을 해당 namespace를 선언하지 않고도 view에서 사용할 수 있는 합니다. @addTagHelper는 기본적인 tag helper를 사용하기 위한 것으로 응답에 필요한 HTML요소를 만들 때 CompuMallStore의 구성을 반영할 수 있도록 합니다. 이에 대한 자세한 사항은 추후에 계속 알아볼 것입니다.(이 상태에서 Editor상으로 Compiler관련 오류가 발생할 수 있지만 곧 해결할 것이므로 무시해도 됩니다.)
_ViewStart.cshtml이름의 Razor View Start file을 View folder에 아래와 같이 추가합니다. (Visual Studio의 item template을 사용해 file을 추가하는 경우라면 아래 content가 미리 작성되어 있을 수 있습니다.)
@{
Layout = "_Layout";
}
예제의 Razor View Start file은 HTML에서 Razor에게 지정한 layout file을 사용하도록 함으로서 view에서 공통적으로 사용되는 content를 반복할 필요가 없도록 합니다. 해당 Layout file을 Views > Shared folder에 _Layout.cshtml이름으로 아래와 같이 추가합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>CompuMall</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
해당 예제에서 정의된 HTML은 다른 View의 content에서 @RenderBody표현식을 통해 삽입될 것입니다.
(6) Controller와 View 만들기
Project에 Controllers folder를 만들고 HomeController.cs이름의 file을 아래와 같이 추가합니다. 예제의 Controller는 응답을 생성하는데 필요한 최소한의 요건만 갖추고 있습니다.
using Microsoft.AspNetCore.Mvc;
namespace CompuMallStore.Coantrollers;
public class HomeController : Controller
{
public IActionResult Index() => View();
}
이전 예제에서 사용된 MapDefaultControllerRoute method는 URL을 Controller class와 어떻게 연결시킬지를 설정하는 것으로 해당 method에 의해 적용되는 설정은 HomeController의 Index action method가 요청을 처리하는 데 사용되도록 합니다.
예제상의 Index action method는 Controller 기반 class로 부터 상속된 View method의 호출결과를 반환하도록 하고 있는데 이는 ASP.NET Core가 action method와 연결된 기본 View를 render 하도록 합니다. View를 만들기 위해 Index.cshtml이름의 Razor View file을 Views > Home folder에 아래와 같이 추가합니다.
<h4>Welcome to CompuMall</h4>
(7) Data model 구성하기
대부분의 ASP.NET Core Project는 자체적으로 필요한 data model을 가집니다. 해당 예제 Project도 data처리를 위해 일부 Data model을 추가할 것입니다. Product.cs이름의 file의 file을 Models folder에 아래와 같이 추가합니다.
using System.ComponentModel.DataAnnotations.Schema;
namespace CompuMallStore;
public class Product
{
public long? ProductID { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
[Column(TypeName = "decimal(8, 2)")]
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
}
Price속성에는 Column attribute가 적용하여 SQL data type을 지정하고 있고 속성의 값을 저장하는데도 사용되는 속성입니다. 모든 C# type이 실제 SQL type과 일치하는 것은 아니므로 해당 attribute를 통해 application data에 대한 적절한 type을 사용할 수 있도록 합니다.
(8) Application 실행하기
위의 모든 절차를 완료하고 나면 아래 명령을 통해 예제가 제대로 동작하는지 확인합니다.
dotnet run |
2. Application에서 Data다루기
CompuMall Project에 대한 기본적인 설정이 모두 완료되었으므로 이제 Application에서 실제 Data를 다룰 수 있도록 구성해 보고자 합니다. 기본적인 DB로는 SQL Server LocalDB를 사용할 것이며 Application과의 상호작용을 위해 Entity Framework Core를 사용할 것입니다. Entity Framework Core는 Microsoft의 개체-관계 mapping(ORM) framework로 ASP.NET Core project에서 가장 광범위하게 사용되는 Database접근 방식입니다.
(1) Entity Framework Core package 설치
Entity Framework Core package를 설치하기 위해 *.csproj file이 있는 folder에서 아래 명령을 사용합니다.
dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.EntityFrameworkCore.SqlServer |
위 명령은 Entity Framework Core 및 SQL Server사용을 위한 package를 설치합니다. Entity Framework Core는 또한 ASP.NET Core application의 Database를 위한 명령어 도구를 포함하는 도구 package를 필요로 하므로 아래 명령을 통해 해당 도구를 설치해 줍니다.
dotnet tool install --global dotnet-ef |
(2) 연결 문자열 정의
Database연결 문자열과 같은 설정값은 appsettings.json file에 저장되므로 Project에서 사용할 Database연결문자열을 CompuMallStore folder의 해당 JSON file에 필요한 entry를 추가해 줘야 합니다.
Project folder에는 또한 appsettings.Development.json file도 존재하는데 이는 개발단계전용 설정 file로서 Visual Studio를 사용한다면 Solution Explorer에 appsettings.json로 중첩되어 표시되지만 Visual Studio Code라면 해당 file 2개의 항상 표시됩니다. 예제에서는 appsettings.json만을 사용할 것이지만 이들 file을 사용할 수 있는지에 대한 방법은 추후에 자세히 알아볼 것입니다.
연결문자열 설정은 단 하나의 line으로 설정되어야 합니다. 따라서 설정값을 지정할 때는 줄 바꿈을 하지 말아야 합니다.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"CompuMallStoreConnection": "Server=(localdb)\\MSSQLLocalDB;Database=CompuMallStore;MultipleActiveResultSets=true"
}
}
위 예제에서 CompuMallStoreConnection값은 CompuMallStore라는 LocalDB를 지정하고 있으며 multiple active result set(MARS) 기능을 사용하도록 하고 있습니다. 특히 MARS는 Entity Framework Core를 사용할 때 만들어지는 일부 database query를 위해 필요한 설정입니다.
사용하는 database마다 연결문자열의 설정은 다를 수 있습니다. 아래 link에서 database마다 사용하는 연결문자열 형식을 확인할 수 있습니다.
ConnectionStrings.com - Forgot that connection string? Get it here!
(3) Database context class 만들기
Entity Framework Core는 context class를 통해 database로의 접근을 제공합니다. 따라서 StoreDbContext.cs이름의 file을 Models folder에 아래와 같이 추가합니다.
using Microsoft.EntityFrameworkCore;
namespace CompuMallStore.Models;
public class StoreDbContext : DbContext
{
public StoreDbContext (DbContextOptions<StoreDbContext> options) : base(options)
{
}
public DbSet<Product> Products => Set<Product>();
}
DbContext base class는 Entity Framework Core의 기본기능에 대한 접근을 제공하며 Products속성은 Database안에서 Product개체에 대한 접근을 제공합니다. StoreDbContext class는 DbContext로부터 파생되는데 여기에서 Application의 data를 읽거나 쓰는 것에 대한 속성을 추가합니다. 위 예제는 현재 Product개체의 접근을 위한 하나의 속성만을 정의하고 있습니다.
(4) Entity Framework Core 설정
Entity Framework Core는 연결할 Database의 유형과 연결에 관한 정보를 가진 연결문자열, Database에서 data를 표현할 context class를 알 수 있도록 구성되어야 합니다. 이를 위해 Program.cs를 아래와 같이 변경합니다.
using Microsoft.EntityFrameworkCore;
using CompuMallStore.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<StoreDbContext>(opts => {
opts.UseSqlServer(
builder.Configuration["ConnectionStrings:CompuMallStoreConnection"]);
});
var app = builder.Build();
//app.MapGet("/", () => "Hello World!");
app.UseStaticFiles();
app.MapDefaultControllerRoute();
app.Run();
IConfiguration interface는 appsettings.json file의 content를 포함하는 ASP.NET Core 구성 system으로의 접근을 제공하며 builder.Configuration속성을 통해 구성 data로의 접근이 가능합니다. 예제에서는 이를 통해 Database연결문자열을 가져오고 있습니다. Entity Framework Core는 AddDbContext method를 통해 구성되는데 여기서 database context class가 등록되고 Database의 관계가 구성됩니다. UseSQLServer method는 Database로 SQL Server가 사용될 것임을 선언합니다.
(5) Repository 생성
다음 절차로 Repository interface를 생성하는 것입니다. Repository pattern은 가장 광범위하게 사용되는 방법 중 하나로 database context class에서 제공되는 기능에 일관성 있는 접근 방식을 제공합니다. 모든 사람이 Repository가 유용하다고 생각하는 건 아니지만 Database를 사용하기 위한 code의 중복을 최소화하고 일관성 있는 작업을 수행할 수 있도록 합니다. IStoreRepository.cs라는 이름의 file을 Models folder에 아래와 같이 추가합니다.
namespace CompuMallStore.Models;
public interface IStoreRepository
{
IQueryable<Product> Products { get; }
}
Interface는 IQueryable<T>를 사용하여 일련의 Product개체를 가져올 수 있도록 하고 있습니다. IQueryable<T> interface는 좀 더 친숙한 IEnumerable<T> interface로 부터 파생되며 Database에서와 같이 질의되는 개체의 collection을 나타냅니다.
IStoreRepository interface를 구현하는 class는 어떤 방식으로 저장되는지, 구현 class가 이들을 어떻게 가져오는지에 대한 상세한 부분에 신경 쓰지 않고도 Product object를 확인할 수 있습니다.
IEnumerable<T>와 IQueryable<T> interface
IQueryable<T> interface는 개체의 collection을 효휼적으로 질의하는데 유용하게 사용될 수 있습니다. 즉, 어떤 Database가 data를 저장하고 있는지 또는 질의를 어떻게 처리하는지에 대해 상세한 부분을 모르고 있어도 표준 LINQ문을 사용해 원하는 개체만을 가져올 수 있도록 요청할 수 있습니다. IQueryable<T> interface가 없다면 Database로부터 모든 Product개체를 가져온 뒤 원하지 않는 것을 걸러낼 수 있지만 일단 모든 개체를 가져와야 하므로 요청수가 증가하면 그만큼 운용에 필요한 비용이 증가하게 됩니다. 이러한 이유 때문에 IEnumerable<T> interface보다는 IQueryable<T>가 Database를 상대로 하는 작업에서는 더 널리 사용되고 있습니다.
단 IQueryable<T>는 매번 개체의 collection을 열거해야 하므로 질의가 반복적으로 실행될 수 있기 때문에 주의가 필요합니다. 이는 IQueryable<T>의 사용으로 얻어지는 효휼성에 대한 이점을 약화시킬 수 있습니다. 이런 경우
IQueryable<T> interface에서 ToList 혹은 ToArray확장 method를 사용함으로써 더 예측 가능한 상태로 변환시킬 수 있습니다.
Repository interface를 구현을 생성하기 위해 Models folder에 EFStoreRepository.cs이름의 file을 아래와 같이 추가합니다.
namespace CompuMallStore.Models;
public class EFStoreRepository : IStoreRepository
{
private StoreDbContext context;
public EFStoreRepository(StoreDbContext ctx)
{
context = ctx;
}
public IQueryable<Product> Products => context.Products;
}
점차 Application을 보완해 가면서 기능이 추가될 테지만 지금 Repository는 StoreDbContext class에 정의된 Products속성을 통해 IStoreRepository interface에 따른 Products속성만을 정의하고 있습니다. context class의 Products속성은 DbSet<Product>개체를 반환하고 있는데 이는 IQueryable<T> interface를 구현하고 있고 또한 Entity Framework Core를 사용할 때 repository interface를 쉽게 구현할 수 있습니다.
ASP.NET Core는 Application전역에서 개체에 접근할 수 있는 service를 지원하고 있습니다. 이러한 service의 이 점 중 하나는 class가 어떤 구현 class가 사용되는지 알 필요 없이 interface를 사용할 수 있다는 것입니다. 이는 Application 구성요소가 사용 중인 class가 EFStoreRepository 구현 class라는 것을 확인하지 못해도 IStoreRepository interface를 구현하는 개체에 접근할 수 있다는 의미가 됩니다. 이러한 구조는 각각의 구성요소에 변경사항을 일일이 적용하지 않아도 되므로 Application에서 사용하는 구현 class를 쉽게 변경할 수 있습니다.
Program.cs를 아래와 같이 변경하여 EFStoreRepository를 구현 class로 사용하는 IStoreRepository interface에 대한 service를 생성합니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<StoreDbContext>(opts => {
opts.UseSqlServer(
builder.Configuration["ConnectionStrings:CompuMallStoreConnection"]);
});
builder.Services.AddScoped<IStoreRepository, EFStoreRepository>(); //추가
예제에서 AddScoped method는 각 HTTP request마다 자체적인 repository개체를 가져올 수 있도록 하는 service를 생성하는 것으로 Entity Framework Core에서 가장 일반적으로 사용되는 방식입니다.
(6) Database migration 생성
Entity Framework Core에서는 Migration이라는 기능을 통해 Data model classe를 사용하여 Database의 schema를 생성할 수 있습니다. 실제 Migration을 준비할 때 Entity Framework Core는 Database를 준비하기 위해 필요한 SQL 명령을 가진 C# class를 생성합니다. Model class를 변경하는 경우에도 Database를 변경하기 위해 필요한 SQL 명령을 가진 새로운 migration을 만들게 됩니다. 이러한 방식에서는 필요한 SQL을 임의로 작성하거나 제대로 작동하는지 여부를 확인하기 위해 특정 SQL명령을 test 해야 한다는 걱정을 할 필요 없이 오로지 Application의 C# model class에만 집중할 수 있습니다.
Migration은 아래 명령을 통해 생성할 수 있으며 Database를 처음 사용하기 위한 준비를 실행하게 됩니다.
dotnet ef migrations add Initial |
명령 실행이 완료되면 Project에는 Migrations folder가 추가되고 여기에 Entity Framework Core는 migration class를 저장합니다. 저장되는 file명은 [timestamp값]_Initial.cs형태가 되며 database에 대한 초기 schema를 생성하는 데 사용됩니다. 실제 해당 file을 확인해 보면 schema를 생성하기 위해 Product model class가 사용된 방식을 확인해 볼 수 있습니다.
(7) 초기 Data 생성
필요한 경우 생성할 Database에 초기값을 제공할 수 있습니다. 이를 위해 Models folder에 SeedData.cs라는 file을 아래와 같이 추가합니다.
using Microsoft.EntityFrameworkCore;
namespace CompuMallStore.Models;
public static class SeedData
{
public static void AddInitData(IApplicationBuilder app)
{
StoreDbContext context = app.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService<StoreDbContext>();
if (context.Database.GetPendingMigrations().Any()) {
context.Database.Migrate();
}
if (!context.Products.Any()) {
context.Products.AddRange(
new Product {
Name = "CPU",
Description = "Computer CPU",
Category = "Computer",
Price = 1500
},
new Product {
Name = "Memory",
Description = "Computer Memory",
Category = "Computer",
Price = 1000
},
new Product {
Name = "SSD",
Description = "Computer SSD",
Category = "Computer",
Price = 1200
},
new Product {
Name = "Monitor",
Description = "SAMSONG 15inch",
Category = "Peripheral", Price = 2000
},
new Product {
Name = "Keyboard",
Description = "Gamming keyboard",
Category = "Peripheral", Price = 500
},
new Product {
Name = "Mouse",
Description = "Ball mouse",
Category = "Peripheral", Price = 300
}
);
context.SaveChanges();
}
}
}
Program.cs에서는 HTTP요청을 처리하기 위해 middleware component를 등록할 때 IApplicationBuilder interface를 사용하는데 예제의 AddInitData method는 해당 interface를 매개변수로 받고 있습니다. IApplicationBuilder는 또한 Entity Framework Core database context service를 포함해 Application service로의 접근을 제공합니다.
이를 이용해 AddInitData method에서는 IApplicationBuilder interface를 통해 StoreDbContext개체를 가져와 Database를 생성하고 Product개체로 초기 data를 저장해야 할 Migrate이 있다면 Database.Migrate method를 호출하게 됩니다. 다음으로 database에서 Product에 대한 개체가 존재하는지 확인하고 개체가 존재하지 않으면 AddRange method를 통해 Product의 collection으로 data를 채워 넣게 됩니다. 그러 다음 마지막으로 SaveChanges method를 호출함으로써 database에 저장합니다.
Program.cs에서는 Application이 시작할 때 위에서 만든 AddInitData method를 호출함으로써 Database가 생성될 수 있도록 아래와 같이 변경합니다.
app.MapDefaultControllerRoute();
SeedData.AddInitData(app);
app.Run();
Database 초기화
Database를 초기화해야 한다면 아래 명령을 사용합니다.
dotnet ef database drop --force --context StoreDbContext |
그런 다음 Application을 재시작하면 위에서 언급한 과정을 다시 거쳐 새로운 Database를 생성하게 됩니다.
3. Product 목록 표시
지금까지 해왔던 것처럼 ASP.NET Core project를 준비하는 데는 다소 시간이 걸리지만 일단 이렇게만 갖춰지만 필요한 기능을 추가하는 데는 속도를 높일 수 있습니다. 우선 Repository에 있는 Product를 표시하기 위해 필요한 Controller와 Action method를 추가하는 것으로 시작할 것입니다.
Visual Studio scaffolding 사용에 관해
Visual Studio에서는 scaffolding이라는 기능을 통해 Project에 item을 추가할 수 있습니다. scaffolding을 사용하는 것에 관해서는 따로 다루지 않을 것이지만 개인적으로 scaffolding이 생성하는 code와 markup은 여러 상황에 맞지 않는 경우가 대부분이라 scaffolding을 사용해야 하는 이유가 없어 보입니다. 물론 지극히 개인적인 부분이라 scaffolding을 사용하는 것이 스스로에게 적합하다고 판단할 수 있으므로 scaffolding을 충분히 사용할 수 있지만 만약 그렇지 않다면 Visual Studio의 Solution Explorer에서 folder를 mouse로 오른쪽 click 하여 Add > New Item을 선택하고 Add New Item 창에서 item template을 선택해 Project에 item을 추가할 수 있습니다.
(1) Controller 수정하기
Product의 목록을 표시하기 위해 HomeController.cs에 아래와 같이 필요한 구문을 추가합니다.
using CompuMallStore.Models;
using Microsoft.AspNetCore.Mvc;
namespace CompuMallStore.Coantrollers;
public class HomeController : Controller
{
private IStoreRepository repository;
public HomeController(IStoreRepository repo)
{
repository = repo;
}
public IActionResult Index() => View(repository.Products);
}
ASP.NET Core가 HTTP요청을 처리하기 위해 HomeController의 instance를 생성할 때 생성자를 통해 IStoreRepository interface를 구현한 개체가 필요함을 확인하게 됩니다. 이때 어떤 구현 class를 사용할지 결정하기 위해 ASP.NET Core는 Program.cs에서의 설정을 참고하여 사용할 class를 결정합니다. 예제에 따라 ASP.NET Core는 EFStoreRepository의 instance를 사용해 HomeController의 생성자를 호출한 뒤 HTTP요청을 처리할 Controller개체를 만들게 됩니다.
이러한 처리 방식을 의존성 주입이라고 하며 HomeController개체는 설정된 구현 class가 어떤 것인지 알고 있을 필요 없이 IStoreRepository interface를 통해 Application의 Repository에 접근할 수 있도록 합니다. 물론 필요하다면 service를 재설정하여 예컨대 Entity Framework Core를 사용하지 않는 다른 구현 class를 사용하도록 할 수도 있으며 여기서 중요한 점은 의존성 주입으로 인해 Controller자체는 변경하지 않아도 된다는 것입니다.
의존성 주입이 만능은 아닙니다. 일부 개발자들 사이에서는 Project가 더 복잡하게 만들어질 수 있다는 우려 때문에 의존성 주입을 피하는 경우도 있습니다. 개인적으로 의존성 주입을 사용해 볼 것을 권하지만 의존성 주입에 대한 사용여부는 정답이 정해진 것이 없으므로 필요 없다는 확신이 들면 의존성 주입을 도입하지 않을 수도 있습니다.
단위 TEST
아래 단위 test는 Repository에 대한 Mock를 생성하고 이를 HomeController의 생성자자 주입한 뒤 Index method를 호출함으로써 Product 목록을 포함한 응답을 통해 Controller가 Repository에 접근하는 정확성을 test 하는 것입니다. Data는 응답을 통해 들어온 Product개체와 Mock으로 구현한 test data를 비교하는 것으로 판단합니다.
CompuMallStore.Tests Project에 Controllers folder를 생성하고 여기에 HomeControllerTests.cs file을 아래와 같이 추가합니다.
using CompuMallStore.Controllers;
using CompuMallStore.Models;
using Microsoft.AspNetCore.Mvc;
using Moq;
namespace CompuMallStore.Tests.Controllers;
public class HomeControllerTests
{
[Fact]
public void CheckRepository()
{
// Arrange
Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
mock.Setup(m => m.Products).Returns((new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"}
}).AsQueryable<Product>());
HomeController controller = new HomeController(mock.Object);
// Act
IEnumerable<Product>? result = (controller.Index() as ViewResult)?.ViewData.Model as IEnumerable<Product>;
// Assert
Product[] prodArray = result?.ToArray() ?? Array.Empty<Product>();
Assert.True(prodArray.Length == 2);
Assert.Equal("P1", prodArray[0].Name);
Assert.Equal("P2", prodArray[1].Name);
}
}
Action method에서 반환되는 data는 ViewResult 개체이므로 해당 개체의 ViewData.Model 속성의 값을 예상값의 type으로 변환해 줄 필요가 있습니다.
(2) View 수정하기
상기 예제의 Index action method에서는 Repository의 Product 개체 collection을 View로 전달하고 있으므로 View에서 Razor가 HTML content를 생성할 때 해당 개체를 View model로 사용할 수 있습니다. CompuMallStore의 View folder에 있는 Index.cshtml을 아래와 같이 수정하여 Product view model object를 사용해 HTML을 생성할 수 있도록 합니다.
@model IQueryable<Product>
@foreach (var p in Model ?? Enumerable.Empty<Product>()) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
@model 표현식은 view에게 action method로부터 view model로서 Product 개체의 배열이 전달됨을 말해주고 있습니다. 또한 @foreach 표현식을 사용해 해당 sequence를 순회하면서 각 Product개체에 대한 간단한 HTML을 생성하도록 하고 있습니다.
다만 @foreach에서 한 가지 특이한 부분이 있는데 이는 model data가 null 될 수 있고 심지어 @model 표현식에서 지정된 것과는 다른 type이 될 수 있으므로 이때 null 병합 연산자를 통해 Product의 빈 열거로 대체되도록 하였습니다.
View에서는 Product개체가 어디서 들어오는지는, 어디서 만들어지는지 등은 전혀 알 수 없고 다만 HTML을 통해 각 Product개체를 어떻게 표시할지에만 집중합니다.
예제에서는 Price속성을 ToString("c") method를 통해 문자열로 변환함으로써 Server에 적용된 문화권설정에 따라 통화형식으로 값을 표시할 수 있도록 하고 있습니다. 예를 들어 Server의 문화권이 en-US로 설정되어 있다면 (1234.5).ToString("c")은 $1,234.50으로 표시될 것입니다.
(3) Application 실행하기
지금까지 모든 절차는 ASP.NET Core개발에서 사용되는 일반적인 pattern입니다. 초기 설정과정에서 다소 작업시간이 들어가긴 하지만 일단 기반을 만들어 놓으면 그다음 기능을 추가는 빠르게 진행될 수 있습니다. Project를 실행하고 다음과 같이 표시되는지 확인합니다.
4. Paging 기능 추가하기
위 예제에서는 단일 Page안에 모든 Product를 표시하고 있습니다. 따라서 Paging기능을 추가함으로써 Product의 제한된 수만 표시하도록 하고 사용자가 Page사이를 이동하면서 전체 Product를 볼 수 있도록 할 것입니다. 이를 위해 HomeController의 Index method에 아래와 같이 매개변수를 추가합니다.
using CompuMallStore.Models;
using Microsoft.AspNetCore.Mvc;
namespace CompuMallStore.Controllers;
public class HomeController : Controller
{
private IStoreRepository repository;
public int PageSize = 2;
public HomeController(IStoreRepository repo)
{
repository = repo;
}
public IActionResult Index(int page = 1) => View(
repository.Products
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize));
}
PageSize field는 한 번에 보일 Product의 수를 지정합니다. Index method에는 선택적 매개변수를 추가함으로써 method가 매개변수 없이도 호출될 수 있도록 하고 있으며 그런 경우 지정된 값(1)을 사용해 Page를 표시하게 됩니다. Action method안에서는 Product를 primary key인 ID순으로 가져오되 현재 page이전에 Product List를 넘기고 PageSize field에 지정한 수만큼의 Product를 가져옵니다.
단위 TEST
Paging에 관한 단위 test는 Controller에서 특정 page를 요청하고 그 결과를 예상 data와 비교함으로써 구현할 수 있습니다. CompuMallStore.Tests Project의 HomeControllerTests에 다음과 같이 test method를 추가합니다.
[Fact]
public void CheckPaging()
{
//Arrange
Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
mock.Setup(m => m.Products).Returns((new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"}
}).AsQueryable<Product>());
HomeController controller = new HomeController(mock.Object);
controller.PageSize = 2;
// Act
IEnumerable<Product> result = (controller.Index(2) as ViewResult)?.ViewData.Model as IEnumerable<Product> ?? Enumerable.Empty<Product>();
// Assert
Product[] prodArray = result.ToArray();
Assert.True(prodArray.Length == 2);
Assert.Equal("P3", prodArray[0].Name);
Assert.Equal("P4", prodArray[1].Name);
}
test pattern은 기존의 것과 크게 다르지 않습니다.
(1) Page Link 추가
지금까지의 예제를 실행해 보면 다음과 같이 한 페이에 2개의 Product만이 표시되어 있음을 알 수 있습니다.
이 상태에서 다른 Page를 보려면 URL끝에 다음과 같이 query 매개변수를 붙여줘야 합니다.
그러나 사용자에게 위와 같은 방법으로 Page탐색을 요구할 수는 없으므로 목록의 하단에 Page에 관한 Link를 표시하고 사용자가 해당 Link를 통해 Page를 탐색할 수 있도록 해줄 필요가 있습니다. 이를 위해 필요한 Link와 HTML을 생성하는 tag helper를 만들어 보고자 합니다.
● View Model 추가
Tag helper 지원을 위해 표시할 Page 수, 현재 Page 그리고 Repository상에 총 Product 수에 관한 정보를 View에 전달할 것입니다. 이를 실현하기 위한 가장 쉬운 방법은 View model class를 사용하는 것으로 Controller와 View사이에 data를 전달하기 위한 가장 일반적인 방법이라고 할 수 있습니다. Project의 Models folder에 ViewModels folder를 생성하고 그 안에 PagingInfo.cs이름의 class file을 아래와 같이 추가합니다.
namespace CompuMallStore.Models.ViewModels;
public class PagingData
{
public int TotalItems { get; set; }
public int ItemPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages => (int)Math.Ceiling((decimal)TotalItems / ItemPerPage);
}
● Tag Helper class 추가
필요한 View Model을 완성하고 나면 이에 대한 Tag helper class를 만들 것입니다. Project에 Classes folder를 생성하고 그 안에 PageTagHelper.cs file을 아래와 같이 추가합니다. Tag helper는 ASP.NET Core 개발에 있어서 큰 영역을 차지하는 것으로 Tag helper에 대한 자세한 사항은 추후에 자세히 다룰 것입니다.
Classes folder는 Application에 필요한 별도의 Class만을 담아두기 위해 만든 folder로 해당 folder명은 자유롭게 만들 수 있습니다.
using CompuMallStore.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace CompuMallStore.Classes;
[HtmlTargetElement("Div", Attributes ="page-model")]
public class PageTagHelper : TagHelper
{
private IUrlHelperFactory urlHelperFactory;
public PageTagHelper(IUrlHelperFactory helperFactory) {
urlHelperFactory = helperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext? ViewContext { get; set; }
public PagingData? PageModel { get; set; }
public string? PageAction { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (ViewContext != null && PageModel != null) {
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
for (int i = 1; i <= PageModel.TotalPages; i++) {
TagBuilder tag = new TagBuilder("a");
tag.Attributes["href"] = urlHelper.Action(PageAction, new { Page = i });
tag.InnerHtml.Append(i.ToString());
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
}
}
예제의 Tag helper는 Div요소에 Product의 Page에 해당하는 a요소를 추가합니다. 자세한 내용은 추후에 알아볼 테지만 Tag helper는 필요한 C# logic을 View에서 사용하기 위한 가장 유용한 방법 중 하나입니다. Tag helper code는 언뜻 보기에 C# code에서 HTML요소를 사용하는 방식 때문에 복잡해 보일 수 있지만 Tag helper가 단위 test를 하는데 더 유용할 뿐만 아니라 View에 C# code를 직접 포함시키는 것보다 더 나은 방식으로 평가되고 있습니다.
Controller나 View와 같은 대부분의 ASP.NET 구성요소를 자동으로 Project에서 자동으로 감지되지만 Tag helper는 별도의 등록이 필요하므로 View folder의 _ViewImports.cshtml file을 아래와 같이 변경하여 ASP.NET Core가 Tag helper class를 Project에서 인식할 수 있도록 해야 합니다. 또한 @using 표현식을 통해 namespace로 이름을 한정하지 않고도 View에서 view model class를 참조할 수 있도록 하였습니다.
@using CompuMallStore.Models
@using CompuMallStore.Models.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, CompuMallStore
단위 TEST
PageTagHelper를 test 하기 위해서는 test data로 Process method를 호출하여 생성된 HTML을 확인할 TagHelperOutput개체를 제공합니다. CompuMallStore.Tests project에서 Classes folder를 추가하고 여기에 PageTagHelperTests.cs file을 아래와 같이 추가합니다.
using CompuMallStore.Classes;
using CompuMallStore.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Moq;
namespace CompuMallStore.Tests;
public class PageTagHelperTests
{
[Fact]
public void CheckPageLinks()
{
// Arrange
var urlHelper = new Mock<IUrlHelper>();
urlHelper.SetupSequence(x => x.Action(It.IsAny<UrlActionContext>())).Returns("Test/Page1").Returns("Test/Page2");
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>())).Returns(urlHelper.Object);
var viewContext = new Mock<ViewContext>();
PageTagHelper helper = new PageTagHelper(urlHelperFactory.Object) {
PageModel = new PagingData { CurrentPage = 2, TotalItems = 28, ItemPerPage = 10 },
ViewContext = viewContext.Object,
PageAction = "Test"
};
var content = new Mock<TagHelperContent>();
TagHelperOutput output = new TagHelperOutput("div", new TagHelperAttributeList(), (cache, encoder) => Task.FromResult(content.Object));
TagHelperContext ctx = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), string.Empty);
// Act
helper.Process(ctx, output);
// Assert
Assert.Equal(@"<a href=""Test/Page1"">1</a><a href=""Test/Page2"">2</a><a href=""Test/Page3"">3</a>", output.Content.GetContent());
}
}
예제의 test는 tag helper를 만들고 사용하는데 필요한 개체를 만드는 것 때문에 다소 복잡해 보일 수 있습니다. Tag helper는 IUrlHelperFactory를 사용해 Application의 여러 부분을 대상으로 하는 URL을 생성하며 Moq로는 이 interface와 test data를 제공하는 IUrlHelper interface 관련 구현을 생성합니다.
해당 test에서 가장 핵심적인 부분은 큰따옴표를 포함한 실제 문자열 값을 사용해 Tag helper의 결과를 확인하는 것입니다. C#은 문자열에 @로 접미사를 붙이고 하나의 큰따옴표가 있는 곳을 두 개의 큰따옴표로 설정하면 이러한 문자열에 대한 작업을 완벽하게 처리할 수 있습니다. 이때 문자열을 line단위로 나눠서는 안 됩니다. 필요하다면 + 연산자를 사용해 line단위로 문자열을 결합할 수도 있습니다.
● View model data 추가하기
View에서 tag helper를 사용하려면 PagingData view model class의 instance를 View에 제공해야 합니다. 이를 해결하기 위해 Models > ViewModels에 ProductsListViewModel.cs file을 아래와 같이 추가합니다.
namespace CompuMallStore.Models.ViewModels;
public class ProductListViewModel
{
public IEnumerable<Product> Products { get; set; } = Enumerable.Empty<Product>();
public PagingData PagingData { get; set; } = new();
}
다음으로 Index method를 변경하여 ProductsListViewModel class를 사용해 Page로 표시할 Product와 Paging상세 data를 제공합니다.
using CompuMallStore.Models;
using CompuMallStore.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
namespace CompuMallStore.Controllers;
public class HomeController : Controller
{
private IStoreRepository repository;
public int PageSize = 2;
public HomeController(IStoreRepository repo)
{
repository = repo;
}
public IActionResult Index(int page = 1) => View(
new ProductListViewModel {
Products = repository.Products
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingData = new PagingData {
CurrentPage = page,
ItemPerPage = PageSize,
TotalItems = repository.Products.Count()
}
});
}
이로서 ProductsListViewModel개체는 View에 Data model로서 전달됩니다.
단위 TEST
해당 test에서는 Controller가 View에 정확한 Pagging data를 보내는지 확인해 볼 필요가 있습니다. 이에 따라 HomeControllerTests.cs file에 CheckPagingData method를 아래와 같이 추가합니다.
[Fact]
public void CheckPagingData()
{
// Arrange
Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
mock.Setup(m => m.Products).Returns((new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
}).AsQueryable<Product>());
// Arrange
HomeController controller = new HomeController(mock.Object) { PageSize = 3 };
// Act
ProductListViewModel result = (controller.Index(2) as ViewResult)?.ViewData.Model as ProductListViewModel ?? new();
// Assert
PagingData pageInfo = result.PagingData;
Assert.Equal(2, pageInfo.CurrentPage);
Assert.Equal(3, pageInfo.ItemPerPage);
Assert.Equal(5, pageInfo.TotalItems);
Assert.Equal(2, pageInfo.TotalPages);
}
또한 이전에 만든 method도 수정하여 Index method에서 반환하는 result가 반영되도록 할 필요가 있습니다. 아래는 CheckRepository method를 변경한 것이며
//Act
//IEnumerable<Product>? result = (controller.Index() as ViewResult)?.ViewData.Model as IEnumerable<Product>;
ProductListViewModel result = (controller.Index() as ViewResult)?.ViewData.Model as ProductListViewModel ?? new();
//Assert
//Product[] prodArray = result?.ToArray() ?? Array.Empty<Product>();
Product[] prodArray = result.Products.ToArray();
아래는 CheckPaging method를 변경한 것입니다.
//Act
//IEnumerable<Product> result = (controller.Index(2) as ViewResult)?.ViewData.Model as IEnumerable<Product> ?? Enumerable.Empty<Product>();
ProductListViewModel result = (controller.Index(2) as ViewResult)?.ViewData.Model as ProductListViewModel ?? new();
//Assert
//Product[] prodArray = result.ToArray();
Product[] prodArray = result.Products.ToArray();
단위 test는 모든 걸 개별적으로 분리할 필요는 없습니다. 필요하다면 중복정도를 고려하여 좀 더 효율적인 test method를 통합하여 운용할 수 있습니다.
View는 현재 Product 개체의 배열을 다루고 있으므로 이를 아래와 같이 변경하여야 합니다.
@model ProductListViewModel
@foreach (var p in Model.Products ?? Enumerable.Empty<Product>()) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
@model 지시자를 통해 필요한 type으로 변경하여 foreach에서도 data의 source를 Model의 Products속성으로 변경하였습니다.
● Page link 표시
위의 과정을 통해 Index view에 Page link를 추가하기 위한 모든 준비를 마쳤습니다. PagingData를 포함한 View Model을 생성했으며 Controller를 변경하여 해당 View Model을 View로 전달하도록 하였습니다. View에서는 @model 지시자를 통해 View model data type을 지정했습니다. 이제 마지막으로 tag helper가 Page link를 표시할 HTML요소를 아래와 같이 추가합니다.
@foreach (var p in Model.Products ?? Enumerable.Empty<Product>()) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
<div page-model="@Model.PagingData" page-action="Index"></div>
Project를 실행하면 다음과 같이 Page Link가 표시됨을 확인할 수 있습니다.
Razor가 HTML요소에서 page-model 속성을 발견하면 PageTagHelper class를 요청하여 요소를 변환하여 일련의 link를 생성하게 됩니다.
(2) URL 개선하기
목적한 바와 같이 Page link를 붙이긴 했지만 URL을 보면 query string을 사용하여 이동하려는 Page의 정보를 Server로 전달하고 있음을 알 수 있습니다.
localhost:5162/?Page=2 |
이러한 URL은 composable URL pattern을 적용하여 훨씬 단순한 URL로 바꿀 수 있습니다. 이를 위해 ASP.NET Core routing기능을 사용할 것이므로 Program.cs에서 아래와 같이 새로운 route를 추가합니다.
app.UseStaticFiles();
app.MapControllerRoute("paging", "Products/Page{Page}", new { Controller = "Home", action = "Index" });
app.MapDefaultControllerRoute();
위 예제에서 추가한 내용만이 Paging을 위해 URL scheme 바꾸기 위해 필요한 전부입니다. ASP.NET Core와 routing은 서로 밀접하게 통합되어 있어서 Application은 Tag helper에서 만들어낸 URL을 포함하여 Application에서 사용되는 URL의 변경사항을 자동적으로 반영할 수 있습니다.
Project를 다시 시작하고 Page link를 click 해 보면 URL scheme는 다음과 같이 바뀔 것입니다.
(3) Partial View
Partial View는 특정 목적을 위해 미리 구현된 content의 조각으로 template처럼 필요한 모든 View에서 동일한 content의 표시를 위해 사용할 수 있습니다. Partial View는 별도의 장에서 다시 설명하겠지만 content를 반복적으로 사용하는 경우 이에 대한 중복을 최소화할 수 있습니다. 여러 View에서 동일한 Razor markup을 복사/붙여 넣기 하는 것이 아닌 단 하나의 Partial View만을 정의하는 것으로 이러한 목적을 달성할 수 있습니다.
Partial View를 만들기 위해 Views > Shared folder에 ProductList.cshtml이름의 file을 아래와 같이 추가합니다.
@model Product
<div>
<h3>@Model.Name</h3>
@Model.Description
<h4>@Model.Price.ToString("c")</h4>
</div>
위의 Partial View는 Views > Home folder의 Index.cshtml에 다음과 같이 적용할 수 있습니다.
@foreach (var p in Model.Products ?? Enumerable.Empty<Product>()) {
<partial name="ProductSummary" model="p" />
}
예제에서는 기존의 Index.cshtml의 @foreach내부 markup요소를 새롭게 추가한 Partial View로 이동시키고 그것을 다시 partial 요소를 사용해 Partial View를 호출하도록 하였습니다. partial요소에서 name속성은 Parital View의 이름이 지정되고 model속성에는 View의 Model이 지정됩니다. 이러한 방식으로 Parital View를 사용하면 Product의 List를 표시해야 하는 모든 View에 동일한 markup을 삽입할 수 있습니다.
Project를 재실행하면 이전과 동일한 결과를 볼 수 있습니다. Parital View가 자체적으로 Application의 외형을 바꾸는 것은 없으며 단지 Razor가 Browser로 보낼 응답을 생성할 때 content를 찾는 위치만 변경할 뿐입니다.
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] - 8. Shopping mall project 만들기 - 3 (2nd) (0) | 2024.04.19 |
---|---|
[ASP.NET Core] - 7. Shopping mall project 만들기 - 2 (2nd) (0) | 2024.04.15 |
[ASP.NET Core] - 5. 단위 Test (2nd) (0) | 2024.03.29 |
[ASP.NET Core] - 4. 개발도구 사용하기 (2nd) (0) | 2024.03.22 |
[ASP.NET Core] - 3. ASP.NET Core Application 예제 (2nd) (0) | 2024.03.20 |