기본적인 ASP.NET Core Project를 시작하면 대부분 필요한 기본적인 모든 설정이 포함된 형태의 Project가 생성됩니다. 다만 그렇게 하면 모르고 지나칠 수 있는 부분이 있기에 Computer 부품 판매를 위한 Mall을 만든다는 가정하에 가능한 한 아주 작은 형태의 Project를 먼저 생성하고 하나씩 살을 붙이는 방법으로 진행하면서 ASP.NET Core의 전체적인 구조를 파악해 보고자 합니다.
1. Project 생성
먼저 아래 명령을 통해 ASP.NET Core Web Project를 생성합니다.
dotnet new globaljson --sdk-version 6.0.400 --output MyWebApp/MyWebApp
dotnet new web --output MyWebApp/MyWebApp --framework net6.0
dotnet new sln -o MyWebApp
dotnet sln MyWebApp add MyWebApp/MyWebApp
|
예제에서 .NET Version은 6.0.400로 지정되었으나 이는 각 Computer에 설치된 .NET Version에 따라 값이 달라질 수 있습니다. 또한 Project와 전체 Solution이름을 MyWebApp으로 하였는데 이 이름 또한 원하는 다른 이름으로 얼마든지 바꿀 수 있습니다.
생성된 Solution과 Project를 Visual Studio에서 불러들이면 다음과 같이 될 것입니다.
(1) 필수 Folder 만들기
Project가 생성되면 아래와 같은 Folder를 Project에 추가합니다.
- Models : Data Model과 Application의 Database로 부터 실제 Data로의 접근 기능 제공할 Class들을 포함합니다.
- Controllers : HTTP 요청을 처리할 Controller Class들을 포함합니다.
- Views : Razor File들을 포함합니다. 또한 필요한 경우 Home과 같이 기타 다른 하위Foler를 포함할 수 있습니다.
- Views/Shared : 모든 Controller에서 공통적으로 사용하게될 Razor File들을 포함합니다.
(2) 필요한 Service와 요청 Pipeline 설정
Project의 Program.cs File은 ASP.NET Core Application의 각종 설정을 위해 사용됩니다. Program.cs File을 아래와 같이 수정하여 Application의 기본적인 기능들을 설정하도록 합니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseStaticFiles();
app.MapDefaultControllerRoute();
app.Run();
builder.Service는 Application전역과 의존성 주입(Dependency Injection)이라는 기능을 통해 Access 할 수 있는 Service객체의 설정을 위해 사용되는데 여기서 AddControllersWithViews() Method는 MVC Framework와 Razor View Engine을 사용하는 Application에서 필요로 하는 공유 객체를 설정합니다.
ASP.NET Core는 HTTP요청을 수신받으면 이를 App 속성을 사용해 등록된 Middleware Component로 채워진 요청 Pipeline을 따라 전달하게 됩니다. 각각의 Middleware Component는 들어오는 요청을 분석하거나, 수정하고, 응답을 생성하거나 다른 Component에 의해 처리된 응답을 변경합니다. 이와 같이 요청 Pipeline은 ASP.NET Core에 있어서 가장 핵심적인 부분이라고 할 수 있습니다.
UseStaticFiles() Method는 wwwroot라는 folder에 존재하는 여러 정적인 요소(image, css, javascript, html 등)들을 Service 할 수 있도록 하는 Method입니다.
특히 Middleware Component에서 가장 중요한 것중 하나는 HTTP 요청을 흔히 Endpoint라고 말하는 Application의 기능과 일치시켜 응답을 생성하는 Endpoint Routing기능을 제공하는 것인데 이 기능은 요청 Pipeline에 의해 자동으로 추가되며 이때 MapDefaultControllerRoute() 메서드는 요청을 Class나 Method에 일치시키는 규칙을 사용하여 MVC Framework를 endpoint의 소스로서 등록합니다.
(3) Razor View Engine 구성하기
Razor VIew Engine은 .cshtml확장자를 가진 View file을 처리하여 HTML을 생성하고 그 결과를 응답하기 위한 것인데 Application에서는 View를 생성하고 Razor를 구성하도록 하는 몇몇 초기 준비가 필요합니다.
우선 Views폴더에 _ViewImports.cshtml이름의 View file을 생성하고 다음과 같은 내용으로 저장합니다.
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using구문은 View에서 MyWebApp.Models라는 Namespace를 사용하기 위한 것이며 @addTagHelper구문에서는 ASP.NET Core 자체적인 Tag Helper를 사용하도록 하는 것입니다. 이와 같이 모든 View에서 공통적으로 적용될 선언문을 Import file인 _ViewImports.cshtml file에서 지정하면 중복으로 선언이 필요한 부분을 생략할 수 있게 됩니다.
file을 위와 같이 작성하게 되면 구문상 오류가 발생할 수 있으나 곧 해결할 것이므로 무시하고 넘어가도록 합니다.
다음으로 _ViewStart.cshtml이라는 이름의 Start file을 Views folder에 생성합니다. file의 내용은 다음과 같습니다.
@{
Layout = "_Layout";
}
이 file은 Razor가 전체적으로 Web에 적용될 공통적인 Layout을 위해 사용할 file을 명시하도록 합니다. 이 설정 또한 모든 View에 공통적으로 적용될 것입니다. 예제에서는 공통 Layout을 위해 _Layout file을 사용한다고 지정했으므로 _Layout.cshtml 이름의 file을 Views->Shared에 추가하고 아래 내용으로 수정합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>My Web Application</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
@RenderBody는 다른 View에서 생성될 HTML을 의미합니다. 따라서 모든 View가 _Layout.cshtml안에서 생성될 것이므로 모든 View file에서 위의 HTML이 삽입된 일정한 Layout을 형식을 따를 수 있게 됩니다.
(4) Controller와 View의 생성
Controllers folder에서 HomeController.cs라는 이름의 file을 추가합니다. 아마도 file을 추가하게 되면 아래와 같은 내용이 자동으로 채워져 있을 것입니다.
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
}
위에서 언급했던 MapDefaultControllerRoute() method는 어떻게 ASP.NET Core가 URL과 Controller class를 일치시키는지를 알려주게 되는데 이렇게 적용된 설정은 Home Controller에 정의된 Index Action method가 요청을 처리하는 데 사용되도록 합니다.
Index Action method는 특별한 동작없이 Controller Base class로부터 상속된 View() method의 호출을 반환하는데 이는 ASP.NET Core가 Action method에 할당된 기본적인 View를 Render 하도록 합니다. 따라서 Index.cshtml이름의 Razor View file을 Views->Home folder에 생성하여 그에 맞는 응답이 이루어질 수 있도록 합니다.
<h1>환영합니다.</h1>
(5) Data Model 만들기
대부분의 ASP.NET Core project에서는 자신만의 Data Model을 만들게 되는데 여기서는 Computer부품을 판매하기 위한 Application을 제작중이므로 이와 관련된 Data Model을 필요로 할 것입니다. 이를 위해 Models폴더에 Product.cs file을 생성하고 아래와 같이 판매할 제품을 위한 Model을 정의하도록 합니다.
using System.ComponentModel.DataAnnotations.Schema;
namespace MyWebApp.Models
{
public class Product
{
public int Id { get; set; }
public string Category { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
[Column(TypeName = "decimal(8, 2)")]
public decimal Price { get; set; }
}
}
Model에서 Price property는 해당 속성값에 의해 값이 저장될 때 사용될 SQL Server의 Data Type을 명시하는 Column이라는 Attribute를 적용하였습니다. 모든 C# Type이 SQL Type과 일치하는 것이 아니므로 이런 경우 위에서 처럼 Attribute를 사용하여 Database가 해당 Application Type에 대응해 적절한 Type이 사용될 수 있도록 해줄 필요가 있습니다.
(6) Application 동작
project를 실행하여 다음과 같은 결과가 나오는지를 확인합니다.
2. Application의 Data다루기
위의 과정을 통해 최소한의 Application설정과 함께 비교적 단순한 응답을 발생시키는 project를 완성하게 되었습니다. 이제 Application의 완성도를 높이기 위해 필요한 Data를 추가해 보고자 합니다. 해당 project는 data의 저장을 위해 SQL Server LocalDB Database를 사용할 것이며 Entity Framework Core를 통해 data에 access 할 것입니다. Entity Framework Core는 Micorsoft의 ORM(object-to-relational mapping) framework로 ASP.NET Core에서 가장 흔하게 사용되는 것입니다.
project에서 NuGet Package Manager를 열고 다음과 같이 Microsoft.EntityFrameworkCore.Design과 Microsoft.EntityFrameworkCore.SqlServer package를 설치해 줍니다.
이들 package는 project에 SQL Server를 위한 Entity Framework Core를 설치하게 될 것입니다. 그리고 ASP.NET Core Application에서는 필요한 Database를 생성하고 준비하는데 필요한 command-line 도구가 있는데 이를 포함한 Tool Package를 설치해야 하므로 Terminal에서 아래 명령을 수행합니다
dotnet tool uninstall --global dotnet-ef dotnet tool install --global dotnet-ef |
위 명령은 현재 설치된 Tool Package를 모두 제거하고 가장 최신의 dotnet-ef를 설치하도록 합니다. 또한 option에서 볼 수 있는 것처럼 Tool Package는 global 하게 설치되므로 해당 명령은 어느 foler에서도 실행될 수 있습니다.
(1) 연결 문자열 설정하기(Connection String)
Database의 연결 문자열과 같은 설정값은 대부분 project의 JSON 구성 file에 저장됩니다. 따라서 연결 문자열 설정을 적용하기 위해 project의 appsettings.json file을 열고 아래의 ConnectionStrings설정을 추가해 줍니다.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MyDBConnection": "Server=(localdb)\\MSSQLLocalDB;Database=MyDB;MultipleActiveResultSets=true"
}
}
연결 문자열은 LocalDB로 MyDB를 지정하고 MARS(multiple active result set)를 사용하도록 명시하고 있는데 특히 MARS는 Entity Framework Core를 사용하는 Application에 의해서 만들어지는 다수의 Database query를 처리하기 위해 필요한 설정입니다.
DB는 기본적으로 'C:\Users\[사용자명]\'의 위치에 생성됩니다. 만약 해당 경로를 바꾸고자 한다면, 예컨데 현재 Project폴더의 AppData에 DB file이 생성되어야 한다면 프로젝트에 AppData folder를 만들고 경로설정을 아래와 같이 추가합니다.
"MyDBConnection": "Server=(localdb)\\MSSQLLocalDB;Database=MyDB;AttachDbFileName=[ProjectPath]\\AppData\\MyDB.mdf;Trusted_Connection=True;MultipleActiveResultSets=true"
(2) Database Context Class 만들기
Entity Framework Core는 database로의 접근을 Context Class를 통해 제공합니다. MyDbContext.cs라는 이름의 file을 Models folder에 추가하고 아래와 같은 내용으로 file을 저장합니다.
using Microsoft.EntityFrameworkCore;
namespace MyWebApp.Models
{
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> context) : base(context)
{
}
public DbSet<Product> Products { get; set; }
}
}
DbContext base class는 Entity Framework Core의 기본적인 기능에 대한 접근방법을 제공하며 Products Property는 database의 Product 객체에 대한 접근을 제공할 것입니다. MyDbContext class는 DbContext로부터 상속받은 뒤 Application에서 필요한 data를 읽고 쓰는 데 사용될 Property를 추가하게 되는데 예제에서는 Product객체로의 접근을 제공하게 될 Products라는 하나의 속성을 정의하였습니다.
(3) Entity Framework Core 구성하기
Entity Framework Core에는 연결할 database의 유형과 연결에 필요한 정보가 담긴 연결 문자열 그리고 database에서 data를 표현할 Context Class를 알 수 있는 정보를 제공해야 합니다. 따라서 project의 Program.cs file을 아래와 같이 수정합니다.
using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<MyDbContext>(opts =>
{
opts.UseSqlServer(builder.Configuration["ConnectionStrings:MyDBConnection"]);
});
var app = builder.Build();
app.UseStaticFiles();
app.MapDefaultControllerRoute();
app.Run();
IConfiguration Interface는 appsettings.json file의 설정을 포함한 ASP.NET Core의 설정 system으로의 접근을 제공하는데 이때 설정 data의 접근은 builder.Configuration속성을 통해 이루어지며 이것으로 database로의 연결 문자열을 취득하게 되고 Entity Framework Core는 Database Context Class를 등록하며 Database와의 관계를 구성하는 AddDbContext() method를 통해 구성합니다. 그리고 UseSQLServer() method는 Database로 SQL Server를 사용할 것임을 선언합니다.
이전에 연결문자열을 설정할때 'AttachDbFileName'로 DB file의 경로설정을 할 수 있다고 했었는데 그 설정값을 자세히 보면 '[ProjectPath]'라는 문자열이 존재함을 알 수 있습니다. 실제 경로를 지정하기 위해 위와 같이 설정한 경우라면 '[ProjectPath]'를 실제 Project의 경로로 치환될 수 있도록 UseSQLServer() method를 다음과 같이 호출해야 합니다.
string path = Directory.GetCurrentDirectory();
builder.Services.AddDbContext<MyDbContext>(opts =>
{
opts.UseSqlServer(builder.Configuration["ConnectionStrings:MyDBConnection"].Replace("[ProjectPath]", path));
});
(4) Repository 생성
이제 Repository Interface와 구현 class를 작성해야 할 차례입니다. Repository는 ASP.NET Core Application에서 가장 광범위하게 사용되는 개발 pattern중의 하나로 Database Context Class에 의해 제공되는 기능에 접근할 수 있는 일관된 방법을 제공합니다. IMyDBRepository.cs라는 file을 Models folder에 추가하고 아래와 같은 Interface를 정의합니다.
namespace MyWebApp.Models
{
public interface IMyDBRepository
{
IQueryable<Product> Products { get; }
}
}
예제의 Interface는 IQueryable<T>를 사용하여 Product 객체의 배열을 가져오도록 하고 있습니다. IQueryable<T> Interface는 IEnumerable<T>로부터 상속된 것이며 database로 질의될 수 있는 객체의 집합을 표현합니다.
IMyDBRepository interface에 의존성을 갖는 class는 어떻게 data가 저장되고 이 data를 어떻게 구현 class가 가져올 수 있는지에 대한 상세한 정보를 알 필요 없이 Product객체를 가져올 수 있게 됩니다.
IQueryable<T> interface는 database로 부터 Product객체의 일부만을 가져오는 경우처럼 객체의 Collection을 효휼적으로 질의하는데 유용하게 사용될 수 있습니다. 이때 database에 data가 어떤 식으로 저장되어 있는지 또는 query가 어떤 식으로 처리되는지를 알 필요없이 LINQ구문을 사용하여 database로 필요한 객체를 요청하여 가져올 수 있습니다. 필요한 Product 객체를 가져오는 다른 방법으로 일단 모든 Product 객체를 가져온 뒤 필요하지 않는 것은 버릴 수도 있는데 이때는 IQueryable<T> interface 없이 IEnumerable<T>가 사용될 수 있으나 다만 모든 data를 가져온다는 전제가 생기므로 처리 비용이 다소 발생하는 문제가 있을 수 있습니다. 이러한 이유 때문에 database의 repository interface와 class에서는 IEnumerable<T>보다는 IQueryable<T>가 더 유용하게 사용될 수 있습니다.
다만 IQueryable<T>는 객체의 Collection을 매번 열거해야 한다는 단점이 있습니다. 그때 마다 query는 재평가 될 수 있는데 이는 새로운 query가 매번 database로 보내질 수 있음을 의미하며 곧 IQueryable<T>에서 얻을 수 있는 이점을 상쇄하게 됩니다. 이러한 경우 IQueryable<T> interface는 ToList 또는 ToArray라는 확장 method를 사용해 객체를 변환할 수 있습니다.
위에서 처럼 interface를 생성하면 그다음으로 구현 class를 생성해야 합니다. MyDBRepository.cs라는 파일을 Models folder에 만들고 class를 아래와 같이 정의합니다.
namespace MyWebApp.Models
{
public class MyDBRepository : IMyDBRepository
{
private MyDbContext _dbContext;
public MyDBRepository(MyDbContext dbContext)
{
_dbContext = dbContext;
}
public IQueryable<Product> Products => _dbContext.Products;
}
}
예제의 Repository구현체는 그저 IMyDBRepository에서 구현된 Products Property를 MyDbContext class에서 구현된 Products Property로 Mapping 할 뿐이며 context class의 Products Property는 IQueryable<T> interface를 구현하고 Entity Framework Core를 사용할 때 Repository interface를 쉽게 구현할 수 있는 DbSet<Product> 객체를 반환합니다.
ASP.NET Core는 객체가 Application전역에서 접근을 허용하도록 하는 service를 지원하는데 이를 통해 class는 어떤 구현 class가 사용되는지를 알 필요 없이 interface를 사용하도록 할 수 있습니다. 이것은 예제에서처럼 Application 구성요소가 IMyDBRepository Interface에서 MyDBRepository 구현 class를 사용하는지를 알 필요 없이 IMyDBRepository Interface를 구현하는 객체에 접근할 수 있음을 의미하며 Application에서 각각의 구성요소에 대해 일일이 구현 class의 변경을 반영할 필요 없이 구현 class의 수정을 비교적 쉽게 적용할 수 있도록 합니다. Program.cs를 아래와 같이 수정하여 MyDBRepository를 사용하는 IMyDBRepository interface의 service를 생성합니다.
builder.Services.AddDbContext<MyDbContext>(opts =>
{
opts.UseSqlServer(builder.Configuration["ConnectionStrings:MyDBConnection"]);
});
builder.Services.AddScoped<IMyDBRepository, MyDBRepository>();
AddScoped() method는 각 HTTP 요청이 자체 repository객체를 가져오는 service를 생성하는데 이는 Entity Framework Core가 전형적으로 사용되는 방법에 해당합니다.
(5) Database Migration 생성
Entity Framework Core는 Migration이라는 기능을 통해 Data Model Class를 사용하여 Database의 Schema를 생성할 수 있습니다. Migration에서 Entity Framework Core는 Database를 준비하는데 필요한 SQL 명령을 포함한 C# Class를 만들게 되는데 만약 Model Classe를 수정하는 경우라면 변경한 내용을 반영한 SQL 명령으로 새로운 Migration을 생성하게 됩니다. 이러한 방식은 동작에 필요한 SQL명령을 직접 작성하거나 test 할 필요 없이 오로지 Application의 C# Model Class에만 집중할 수 있도록 합니다.
Entity Framework Core명령은 Command Line에서 수행되므로 새로운 Terminal을 열고 Project folder에서 아래 명령을 입력해 필요한 Database를 준비할 migration class를 생성합니다.
dotnet ef migrations add initial
|
명령을 수행하고 나면 project folder에는 Migrations라는 새로운 folder가 생성되는데 이 folder에는 Entity Framework Core가 생성한 migration class를 포함하고 있습니다. 내부에 저장된 file 중 하나는 timestamp를 포함한 _Initial.cs형식의 file을 볼 수 있는데 이 file은 Database에서 초기 schema를 생성하는 데 사용되며 file의 내용을 통해서는 보면 어떻게 Product model class가 Schema를 생성하는데 사용되는지를 확인해 볼 수 있습니다.
(6) Data 생성
초기 Database에 미리 필요한 Data를 채워 넣으려면 해당 동작을 수행하는 별도의 class file을 만들어야 합니다. Models폴더에 MyData.cs라는 이름으로 파일을 생성하고 다음과 같은 내용으로 저장합니다.
using Microsoft.EntityFrameworkCore;
namespace MyWebApp.Models
{
public static class MyData
{
public static void InitData(IApplicationBuilder app)
{
MyDbContext context = app.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService<MyDbContext>();
if (context.Database.GetPendingMigrations().Any())
context.Database.Migrate();
if (!context.Products.Any())
{
context.Products.AddRange(
new Product
{
Name = "Intel CPU",
Description = "최신 20세대 프로세서",
Category = "Computer",
Price = 1000
},
new Product
{
Name = "Memory",
Description = "5000Mz 짜리",
Category = "Computer",
Price = 1500
},
new Product
{
Name = "Mainboard",
Description = "최신 Serial Port 있음",
Category = "Computer",
Price = 2000
},
new Product
{
Name = "Printer",
Description = "8bit 흑백",
Category = "Peripheral",
Price = 5000
},
new Product
{
Name = "Mouse",
Description = "부드러운 볼 탑재",
Category = "Peripheral",
Price = 1500
});
}
context.SaveChanges();
}
}
}
예제에서 InitData() 정적 Method는 IApplicationBuilder 매개변수를 전달받고 있는데 이 interface는 Program.cs에서 HTTP 요청을 처리하기 위한 Middleware component를 등록하는 데 사용됩니다. IApplicationBuilder는 또한 Entity Framework Core database context service를 포함하여 Application services로의 접근을 제공합니다.
InitData() method는 MyDbContext의 객체를 IApplicationBuilder interface를 통해 얻게 되고 GetPendingMigrations().Any()를 통해 Product 객체를 저장하기 위한 database의 생성과 준비가 필요한지를 판단하여 Database.Migrate()라는 method를 호출하게 됩니다. 그다음 Products.Any()로 Products의 Data의 존재를 확인한 뒤 아무런 객체도 존재하지 않는다면 AddRange() method를 통해 지정된 Product Collection을 Database에 적용하고 마지막 SaveChanges() method로 Product 객체들을 Database에 저장합니다.
마지막으로 Program.cs에서는 아래와 같이 InitData() Method를 호출하게 함으로써 application이 시작할 때 해당 method가 동작할 수 있도록 지정해 줍니다.
app.UseStaticFiles();
app.MapDefaultControllerRoute();
MyData.InitData(app);
app.Run();
만약 생성된 database를 제거하고 새롭게 생성하고자 한다면 project folder에서 아래 명령을 수행한 뒤 Application을 다시 시작하면 새롭게 database가 생성되고 지정한 초기 data를 다시 채우게 될 것입니다.
dotnet ef database drop --force --context MyDbContext |
3. Product 목록 표시하기
이제까지의 과정을 통해 ASP.NET Core project의 초기 생성과정은 다소 시간과 노력이 필요하다는 것을 알 수 있습니다. 하지만 이렇게까지만 완성해 둔다면 기능의 추가와 같은 과정은 더 빠르고 신속하게 진행될 수 있습니다. 이제 필요한 Controller와 Action Method를 만들어 Repository의 Product 객체를 볼 수 있도록 완성할 것입니다.
(1) Controller 변경하기
project의 Controllers folder에서 HomeController.cs file을 아래와 같이 수정합니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
namespace MyWebApp.Controllers
{
public class HomeController : Controller
{
private readonly IMyDBRepository _repository;
public HomeController(IMyDBRepository repository)
{
_repository = repository;
}
public IActionResult Index()
{
return View(_repository.Products);
}
}
}
ASP.NET Core가 HTTP 요청을 처리하기 위해 HomeController의 Instance를 생성할 때 생성자를 확인하여 IMyDBRepository interface를 구현한 객체가 필요함을 알게 됩니다. 이때 ASP.NET Core는 어떤 구현 class를 사용해야 할지를 결정하기 위해 Program.cs에 만들어진 설정을 파악하여 MyDBRepository class를 사용하고 모든 요청에서 새로운 instance가 생성되어야 한다는 것을 확인합니다. 따라서 새로운 MyDBRepository의 객체를 만들고 해당 객체를 HomeController의 생성자로 전달함으로써 Controller의 객체를 생성해 HTTP 요청을 처리할 것입니다.
이러한 방식은 의존성 주입(dependency injection)으로 알려져 있으며 구현 class를 직접 다룰 필요 없이 HomeController의 객체가 Application의 Repository에 IMyDBRepository interface를 사용하여 접근하게 하는 방법인데 이러한 방식은 곧 code의 변경 없이 필요한 구현 class를 바꿀 수 있는 유연성을 제공해 줄 수 있습니다.
(2) View 변경하기
위의 과정을 통해 Index action method는 Product 객체의 Collection을 Repository로부터 View Method로 전달하게 됩니다. 따라서 Product 객체는 View Model로 사용되어 Razor가 View로부터 HTML content를 생성하는 데 사용될 수 있습니다. 따라서 Product view model 객체를 사용해 content를 생성할 수 있도록 View를 아래와 같이 수정해 줍니다.
<h1>환영합니다.</h1>
@model IQueryable<Product>
<table border="1">
<thead>
<tr>
<th>제품명</th>
<th>설명</th>
<th>단가</th>
</tr>
</thead>
<tbody>
@foreach (var p in Model ?? Enumerable.Empty<Product>())
{
<tr>
<td>@p.Name</td>
<td>@p.Description</td>
<td>@p.Price.ToString("#,###")</td>
</tr>
}
</tbody>
</table>
@model 표현식은 Action Method로부터 View가 View Model로서 Product의 배열을 받게 됨을 알려주는 것입니다. 그런 후 @foreach 표현식을 통해 각각의 Product 객체를 표현하는 HTML Element를 생성하고 있습니다. 특히 Model의 요소를 가져오는 부분에서는 ??이라는 null 병합 연산자를 사용하여 Model이 Null인 경우에 빈 Product형식을 가져오도록 지정하였습니다. 이와 같이 Model을 다룰 때에는 Model이 null이 될 수 있는 경우도 고려하는 것이 좋습니다.
View는 Product 객체가 어떻게 생성되고 어디서 오게 되는지는에 관해서는 관심을 두지 않습니다. 그저 각 Product 객체를 HTML Element를 통해 어떤 식으로 표현할지에 대해서만 담당할 뿐입니다.
(3) Application 실행하기
Project를 실해하여 아래와 같은 결과가 나오는지 확인합니다.
이제까지의 과정은 ASP.NET Core를 통해 Application을 개발하기 위한 일반적인 방법이라 할 수 있으며 필요한 초기 설정을 거치면서 각 부분에 대한 세부적인 내용을 같이 알아보았습니다.
4. Paging 처리하기
위 결과는 전체적인 Product의 목록을 하나의 Page에 모두 표시하는 형식을 취하고 있습니다. 현재는 Product의 수가 5개에 불과하지만 보여줄 Product수가 늘어나면 그만큼 모든 Product를 보여주기 위한 처리가 진행되어야 할 것입니다. 한꺼번에 표시해야 할 목록의 수가 늘어나면 사용자가 보기에도 힘들고 Loading시간이 오래 걸릴 수 있으므로 이 상태에서 Paging기능을 추가해 Application의 편의성을 높여보고자 합니다.
Paging기능을 추가하기 위해 HomeController.cs를 아래와 같이 수정합니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
namespace MyWebApp.Controllers
{
public class HomeController : Controller
{
private readonly IMyDBRepository _repository;
public int PageSize = 2;
public HomeController(IMyDBRepository repository)
{
_repository = repository;
}
public IActionResult Index(int currentPage = 1)
{
var result = _repository.Products.OrderBy(p => p.Id)
.Skip((currentPage - 1) * PageSize)
.Take(PageSize);
return View(result);
}
}
}
PageSize변수는 한꺼번에 보일 Product의 수를 나타내는 데 사용됩니다. 현재 Product의 수는 5개에 불과하므로 이를 Paging 하기 위해 2의 값을 설정하여 한번에 2개씩의 Product를 표시하도록 합니다. Index() Method에서는 currentPage 매개변수를 정의하였고 기본값을 1로 설정하였습니다. 이는 Index() Method가 매개변수 없이 호출(처음 Page가 표시되는 경우처럼)되는 경우 기본값을 적용해 항상 첫 번째 Page가 표시되도록 하기 위함입니다. Index() Method안에서는 Product의 Primary Key인 Id를 통해 정렬을 수행한 후 지정된 currentPage만큼 Skip을 수행하고 다시 PageSize를 통해 필요한 Product의 목록을 가져오도록 하고 있습니다.
(1) Paging 표시하기
Project를 실행해 보면 아래와 같이 의도한 대로 2개의 Product만을 표시하는 Page를 볼 수 있습니다.
여기서 나머지 Product를 보기 위해서는 currentPage이름으로 query string을 붙여줘야 합니다.
하지만 사용자에게 이러한 방식의 동작을 유도할 수 없으므로 Paging HTML markup을 생성하는 tag helper를 만들어 Page에 사용자가 선택할 수 있는 Paging Link를 표시하고자 합니다.
● Paging을 위한 View Model 추가하기
우선 Controller에서 View에 가능한 Page수와 현재 Page 그리고 Repository에 있는 Product의 합계 건수에 대한 정보를 전달할 Model Class를 만들어야 합니다. Project의 Models folder아래에 ViewModels folder를 생성하고 아래의 내용으로 PageInfo.cs class file을 추가합니다.
namespace MyWebApp.Models.ViewModels
{
public class PageInfo
{
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages => (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage);
}
}
● Tag Helper 생성하기
위와 같이 필요한 View Model을 추가하고 나면 이어서 Project에 Classes라는 folder를 생성한 뒤 Tag Helper class인 PagingTagHelper.cs file을 아래 내용으로 추가합니다. 참고로 Tag helper는 이것만으로 ASP.NET Core의 개발 부분에서 큰 영역을 차지하는데 이에 대한 상세한 설명은 다른 Posting을 통해 다시 논의할 것이므로 지금은 Paging기능에만 집중할 것입니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using MyWebApp.Models.ViewModels;
namespace MyWebApp.Classes
{
[HtmlTargetElement("div", Attributes = "page-model")]
public class PagingTagHelper : TagHelper
{
private IUrlHelperFactory _urlHelperFactory;
public PagingTagHelper(IUrlHelperFactory urlHelperFactory)
{
_urlHelperFactory = urlHelperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext? ViewContext { get; set; }
public PageInfo? 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 { currentPage = i });
tag.InnerHtml.Append(i.ToString());
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
}
}
}
위의 tag helper class는 Product의 page에 해당하는 a element를 가진 div element를 표시하게 됩니다. Tag helper는 C# code에서 HTML element를 어설프게 섞은 것처럼 보일 수 있지만 역으로 View에 C# code를 포함시키기에는 좋은 선택이 될 수 있습니다.
Controller와 View와 같은 대부분의 ASP.NET Core component들은 일정한 규칙에 의해 자동적으로 탐색될 수 있지만 tag helper의 사용을 위해서는 별도의 등록과정을 거쳐야 합니다. 이를 위해 Views folder에 있는 _ViewImports.cshtml file를 아래와 같이 수정합니다.
@using MyWebApp.Models
@using MyWebApp.Models.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebApp
이 과정을 통해 ASP.NET Core는 MyWebApp project에서도 tag helper class를 찾아보게 될 것입니다. 참고로 @using 표현식을 통해 view model class를 View에서 별도로 namespace의 이름을 지정하지 않고도 참조할 수 있도록 지정하였습니다.
● View Model Data 추가하기
이제 PageInfo view model class의 Instance를 View로 전달해야 하는데 이를 위해 Models -> ViewModels folde에 ProductsListViewModel.cs file을 아래 내용으로 추가합니다.
namespace MyWebApp.Models.ViewModels
{
public class ProductsListViewModel
{
public IEnumerable<Product> Products { get; set; } = Enumerable.Empty<Product>();
public PageInfo PageInfo { get; set; } = new();
}
}
그리고 HomeController.cs의 Index() 메서드를 수정하여 기존의 Product 배열 객체를 직접 보내는 대신 위에서 만든 ProductsListViewModel clsss를 대신 반환하도록 합니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
using MyWebApp.Models.ViewModels;
namespace MyWebApp.Controllers
{
public class HomeController : Controller
{
private readonly IMyDBRepository _repository;
public int PageSize = 2;
public HomeController(IMyDBRepository repository)
{
_repository = repository;
}
public IActionResult Index(int currentPage = 1)
{
var result = _repository.Products.OrderBy(p => p.Id)
.Skip((currentPage - 1) * PageSize)
.Take(PageSize);
return View(
new ProductsListViewModel {
Products = result,
PageInfo = new PageInfo {
CurrentPage = currentPage,
ItemsPerPage = PageSize,
TotalItems = _repository.Products.Count()
}
});
}
}
}
실제 Index()의 View에는 현재 Model로 IQueryable<Product>가 명시되어 있으므로 이를 ProductsListViewModel로 바꿔주고 @foreach에서도 Model.Products 속성을 붙여 Product의 객체를 가져올 수 있도록 해야 합니다.
● Page Link 표시하기
앞서 Paging정보를 포함하는 View Model을 만들고 controller를 수정하여 View로 해당 Paging정보를 전달하도록 하였으며 @model 지시자를 통해 새로운 View Model인 ProductListViewModel을 지정하였습니다. 이제 남은 건 위에서 만든 tag helper를 통해 page link에 관한 HTML element를 생성하는 것입니다. 이 작업은 그저 View Page에 아래와 같이 Tag Helper를 추가해 주기만 하면 됩니다.
<table border="1">
<thead>
<tr>
<th>제품명</th>
<th>설명</th>
<th>단가</th>
</tr>
</thead>
<tbody>
@foreach (var p in Model.Products ?? Enumerable.Empty<Product>())
{
<tr>
<td>@p.Name</td>
<td>@p.Description</td>
<td>@p.Price.ToString("#,###")</td>
</tr>
}
</tbody>
</table>
<div page-model="@Model?.PageInfo" page-action="Index"></div>
project를 실행하면 아래와 같이 Paging을 위한 Link가 표시됨을 알 수 있고 각 Link를 click 하면 해당하는 product목록을 볼 수 있게 됩니다.
View에서 Razor는 div element의 page-model이라는 attribute를 발견하게 되고 곧 PagingTagHelper class에 일련의 Link를 표현하는 element의 변환을 요청하게 됩니다. 그리고 그 결과로 아래와 같은 HTML element를 생성할 것입니다.
<div><a href="/?currentPage=1">1</a><a href="/?currentPage=2">2</a><a href="/?currentPage=3">3</a></div>
(2) URL 개선하기
Link를 통해 지정된 URL은 Page정보를 Server로 전달하기 위해 여전히 '?currentPage=2'와 같은 QueryString을 사용하고 있습니다. 이를 composable URLs의 pattern을 따르는 scheme를 생성함으로써 아래와 같은 좀 더 직관적인 URL을 생성할 수 있습니다.
http://localhost/Page2 |
ASP.NET Core routing은 Application에서 URL scheme를 변경할 수 있는 기능을 제공하고 있는데 이것은 Program.cs를 수정하여 새로운 route를 추가하는 것만으로 해당 기능을 손쉽게 구현할 수 있습니다.
app.UseStaticFiles();
app.MapControllerRoute("paging", "Page{currentPage}", new { Controller = "Home", action = "Index" });
app.MapDefaultControllerRoute();
ASP.NET Core과 Routing기능은 서로 밀접하게 연결되어 있어서 Application은 사용할 URL의 변경사항을 자동적으로 반영하게 되는데, 이로 인해 예제의 tag helper에 의해 생성되는 element까지도 변경사항을 적용받게 됩니다.
5. Partial View
부분 뷰(Partial View)는 다른 여러 View에서 공통으로 사용할 수 있는 것으로 여러 View에서 동일하게 표현하는 특정 영역을 Partial View로 분리하여 사용합니다. 이렇게 되면 공통영역을 각 VIew마다 그대로 복사&붙여 넣기 하듯 일일이 작업할 필요 없이 분리된 Parital View를 특정 VIew의 필요한 영역에 첨부시키기만 하면 동일한 화면을 그대로 공유할 수 있게 됩니다.
예를 들어 현재 Index.cshtml은 아래와 같은 내용으로 구성되어 제품의 목록을 표시하고 있는데
<h1>환영합니다.</h1>
@model ProductsListViewModel
<table border="1">
<thead>
<tr>
<th>제품명</th>
<th>설명</th>
<th>단가</th>
</tr>
</thead>
<tbody>
@foreach (var p in Model.Products ?? Enumerable.Empty<Product>())
{
<tr>
<td>@p.Name</td>
<td>@p.Description</td>
<td>@p.Price.ToString("#,###")</td>
</tr>
}
</tbody>
</table>
<div page-model="@Model?.PageInfo" page-action="Index"></div>
다른 View에서도 Index.cshtml과 동일하게 제품 목록을 표시해야 한다는 과제가 생겼을때 위 page에서 @model부터 Table Tag까지의 내용을 Paritla View로 분리할 수 있는 것입니다. 이를 위해 Views->Shared folder에 ProductList.cshtml이라는 이름의 View file을 만들고 Index.cshtml에 있는 제품목록 부분을 모두 ProductList.cshtml에 작성합니다.
@model ProductsListViewModel
<table border="1">
<thead>
<tr>
<th>제품명</th>
<th>설명</th>
<th>단가</th>
</tr>
</thead>
<tbody>
@foreach (var p in Model.Products ?? Enumerable.Empty<Product>())
{
<tr>
<td>@p.Name</td>
<td>@p.Description</td>
<td>@p.Price.ToString("#,###")</td>
</tr>
}
</tbody>
</table>
그리고 Index.cshtml에서는 기존의 내용을 제거하고 아래와 같이 위에서 만든 Partial View를 지정하면 Parital View의 Content가 그대로 Index.cshtml에 반영될 것입니다.
<h1>환영합니다.</h1>
@model ProductsListViewModel
<partial name="ProductList" model="Model" />
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] Shopping mall project - 주문완료하기 (0) | 2022.08.26 |
---|---|
[ASP.NET Core] Shopping mall project - Category탐색과 장바구니 구현 (0) | 2022.08.22 |
[ASP.NET Core] 초간단 Application 만들어 보기 (0) | 2022.08.03 |
[ASP.NET Core] 시작하기 (0) | 2022.07.29 |
[ASP.NET Core] ASP.NET Core 개요 (0) | 2022.07.27 |