파트 6: ASP.NET Core MVC - 컨트롤러 메서드와 뷰 살펴보기

등록일시: 2016-06-24 08:00,  수정일시: 2016-09-26 14:04
조회수: 6,527
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 스캐폴딩 엔진이 자동으로 생성한 MoviesController 클래스의 메서드와 뷰를 자세하게 살펴봅니다.

이제 기본적인 기능의 영화 응용 프로그램이 만들어졌지만 아직 일부 화면의 출력은 만족스럽지 못한 상태입니다. 이를테면, 개봉 일자에는 시간이 의미가 없으며 ReleaseDate 컬럼 머리글은 두 단어로 분리하는 편이 더 자연스럽습니다.

이 문제점을 해결하려면 Models/Movie.cs 파일을 열고 다음 코드에 강조된 부분을 추가합니다:

public class Movie
{
    public int ID { get; set; }
    public string Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; }
    public decimal Price { get; set; }
}
  • 그리고 마우스 오른쪽 버튼으로 구불구불한 붉은 색 선을 클릭한 다음, 빠른 작업 및 리팩터링(Quick Actions)을 선택합니다.

  • 그런 다음, 컨텍스트 메뉴에서 using System.ComponentModel.DataAnnotations;를 선택합니다.

그러면 Visual studio가 클래스에 using System.ComponentModel.DataAnnotations; 구문을 자동으로 추가해줍니다.

이번에는 필요 없는 using 구문을 제거해보도록 하겠습니다. 불필요한 using 구문들은 밝은 회색 글씨로 구분되어 나타납니다. 마우스 오른쪽 버튼으로 Movie.cs 파일의 아무 곳이나 클릭하고 Usings 구성(Organize Usings) > 불필요한 Using 제거(Remove Unnecessary Usings)를 선택합니다.

여기까지 작업을 마친 코드는 다음과 같습니다:

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}

이 코드에 추가된 Display 어트리뷰트는 필드의 이름으로 출력될 문자열을 지정합니다 (여기에서는 "ReleaseDate" 대신 "Release Date"를 지정하고 있습니다). 그리고 DataType 어트리뷰트는 필드 데이터의 형식을 지정하는데, 날짜 형식을 지정했으므로 필드에 저장된 시간 정보가 출력되지 않습니다. DataAnnotations 네임스페이스에 관해서는 본 자습서 시리즈의 이후 파트에서 보다 자세하게 살펴봅니다.

이제 응용 프로그램을 실행하고 Movies 컨트롤러로 이동한 다음, 마우스 포인터를 Edit 링크 위에 올려놓고 대상 URL을 살펴보시기 바랍니다.

이 페이지의 Edit 링크, Details 링크, 그리고 Delete 링크는 Views/Movies/Index.cshtml  파일에서 MVC Core Anchor 태그 헬퍼(MVC Core Anchor Tag Helper)를 이용해서 만들어집니다.

<td>
    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>

태그 헬퍼(Tag Helpers)를 이용하면 Razor 파일에서 서버 측 코드로 HTML 요소의 생성 및 렌더링을 처리할 수 있습니다. 이번 예제 코드에서는 AnchorTagHelper가 컨트롤러의 액션 메서드와 라우트 id로부터 HTML href 어트리뷰트의 값을 동적으로 생성하고 있습니다. 브라우저의 소스 보기(View Source) 기능이나 F12 도구를 사용하면 이렇게 생성된 마크업을 살펴볼 수 있습니다. 다음 그림은 F12 도구를 이용해서 마크업을 살펴보고 있는 모습입니다.

이 마크업과 관련하여 Startup.cs 파일에서 설정하는 라우팅의 형식을 다시 한 번 떠올려보시기 바랍니다.

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

ASP.NET Core는 위의 그림에 나타나 있는 마크업 중, 첫 번째 href 어트리뷰트 값인 http://localhost:1234/Movies/Edit/4라는 URL을 매개변수 ID 값이 4로 설정된, Movies 컨트롤러의 Edit 액션 메서드에 대한 요청으로 해석합니다. (컨트롤러의 메서드를 액션 메서드라고 합니다.)

태그 헬퍼는 ASP.NET Core에서 가장 인기있는 신규 기능 중 하나입니다. 보다 자세한 정보는 추가 자료 절을 참고하시기 바랍니다.

그러면 이번에는 Movies 컨트롤러를 열고 두 버전의 Edit 액션 메서드들을 살펴보겠습니다:

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for 
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Genre,Price,ReleaseDate,Title")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction("Index");
    }
    return View(movie);
}

이 메서드의 매개변수에 지정되어 있는 [Bind] 어트리뷰트는 오버포스팅(Over-Posting) 공격을 방지할 수 있는 한 가지 방안입니다. 반드시 [Bind] 어트리뷰트에는 변경을 허용할 속성들만 포함시켜야 합니다. 더 자세한 정보는 이 보안 노트를 참고하시기 바랍니다. 또 다른 대안으로 뷰 모델을 사용해서 오버포스팅 공격을 방지하는 방법도 있습니다.

그리고 두 번째 Edit 액션 메서드의 앞 부분에 [HttpPost] 어트리뷰트가 지정되어 있는 점에도 주의하시기 바랍니다.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Genre,Price,ReleaseDate,Title")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction("Index");
    }
    return View(movie);
}

[HttpPost] 어트리뷰트는 이 Edit 메서드가 오직 POST 요청에 의해서만 호출되도록 지정합니다. 이와는 반대로 첫 번째 Edit 메서드에 [HttpGet] 어트리뷰트를 적용할 수도 있지만, [HttpGet] 어트리뷰트는 기본값이기 때문에 그럴 필요가 없습니다.

[ValidateAntiForgeryToken] 어트리뷰트는 Edit 뷰 파일에서(Views/Movies/Edit.cshtml ) 생성되는 위조 방지 토큰(Anti-Forgery Token)과 한 쌍으로 크로스 사이트 요청 위조(Cross-Site Request Forgery)를 방지하기 위한 목적으로 사용됩니다. 이 위조 방지 토큰은 Edit 뷰 파일에서 Form 태그 헬퍼(Form Tag Helper)에 의해서 생성됩니다.

<form asp-action="Edit">

Form 태그 헬퍼는 숨겨진 위조 방지 토큰을 생성하는데, 그 값과 컨트롤러의 Edit 메서드에 지정된 [ValidateAntiForgeryToken] 어트리뷰트가 생성하는 위조 방지 토큰이 반드시 일치해야만 합니다. 더 자세한 정보는 Anti-Request Forgery 문서를 참고하시기 바랍니다.

계속해서 HttpGet Edit 메서드는 영화의 ID를 매개변수로 전달받고 Entity Framework의 SingleOrDefaultAsync 메서드로 영화 정보를 조회한 다음, 조회한 영화 정보를 Edit 뷰에 전달합니다. 만약 영화 정보를 찾을 수 없다면 NotFound (HTTP 404)가 반환됩니다.

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

Visual Studio의 스캐폴딩 시스템은 Edit 뷰를 생성할 때, Movie 클래스를 분석해서 클래스에 존재하는 각 속성들에 대해 <label> 요소와 <input> 요소를 렌더하는 코드를 생성합니다. 다음 예제는 스캐폴딩 시스템이 생성한 Edit 뷰의 코드를 보여줍니다:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<form asp-action="Edit">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <input type="hidden" asp-for="ID" />
        <div class="form-group">
            <label asp-for="Genre" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Price" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Title" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
</form>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

뷰 템플릿 파일의 가장 상단에 @model MvcMovie.Models.Movie 구문이 추가되어 있는 점에 유의하시기 바랍니다. 이 구문은 이 뷰가 뷰 템플릿의 모델로 Movie 형식을 기대하고 있음을 말해줍니다.

이 스캐폴드 된 코드에서는 몇 가지 태그 헬퍼 메서드를 활용해서 간결한 HTML 마크업을 구성하고 있습니다. 먼저 Label 태그 헬퍼(Label Tag Helper)는 필드의 이름을 출력합니다 ("Title", "ReleaseDate", "Genre", "Price"). Input 태그 헬퍼(Input Tag Helper)는 HTML <input> 요소를 렌더합니다. 마지막으로 유효성 검사 태그 헬퍼(Validation Tag Helper)는 지정된 속성과 관련된 모든 유효성 검사 오류 메시지를 출력합니다.

응용 프로그램을 실행하고 /Movies URL로 이동한 다음, Edit 링크를 클릭합니다. 그리고 브라우저에서 Edit 페이지의 소스를 살펴보시기 바랍니다. 그러면 다음과 같이 생성된 <form> 요소의 HTML을 확인할 수 있습니다.

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
    <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true" />
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true" />
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

이 HTML 마크업을 살펴보면 action 어트리뷰트가 /Movies/Edit/id URL로 전송되도록 구성된 HTML <form> 요소 내부에, 각 속성들에 대한 <input> 요소들이 생성되어 있는 것을 확인할 수 있습니다. 따라서 사용자가 Save 버튼을 클릭하면 폼의 데이터가 서버로 전송됩니다. 그리고 닫는 </form> 요소 직전의 마지막 줄은 Form 태그 헬퍼에 의해서 생성된 숨겨진 XSRF 토큰을 보여줍니다.

POST 요청 처리하기

다음 목록은 [HttpPost] 버전의 Edit 액션 메서드를 보여주고 있습니다.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Genre,Price,ReleaseDate,Title")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction("Index");
    }
    return View(movie);
}

먼저 [ValidateAntiForgeryToken] 어트리뷰트로 Form 태그 헬퍼에서 위조 방지 토큰 생성기로 생성한 숨겨진 XSRF 토큰의 유효성을 검사합니다.

그런 다음, 모델 바인딩(Model Binding) 시스템이 전송된 폼의 값들을 받아서 movie 매개변수에 전달되는 Movie 개체를 생성해줍니다. 그러면 ModelState.IsValid 메서드가 폼으로 제출된 데이터가 Movie 개체를 갱신하는데 적합한지 여부를 확인합니다. 만약 데이터가 유효하다면, 갱신된 영화 데이터가 데이터베이스 컨텍스트의 SaveChangesAsync 메서드 호출을 통해서 데이터베이스에 저장됩니다. 마지막으로 데이터가 저장된 다음에는, 방금 변경한 영화 정보를 비롯한 모든 영화 정보들의 목록이 제공되는 MoviesController 클래스의 Index 액션 메서드로 페이지가 재전송됩니다.

그러나 이렇게 폼이 서버로 전송되기 전에, 먼저 클라이언트 측 유효성 검사가 필드들에 대한 유효성 검사 규칙들을 모두 검토합니다. 만약 유효성 검사에서 오류가 발견되면 오류 메시지가 출력되고 폼은 서버로 전송되지 않습니다. 만약 JavaScript가 비활성화되어 있다면 클라이언트 측 유효성 검사는 동작하지 않겠지만, 그래도 여전히 서버에서는 전송된 값들이 유효하지 않다는 것을 감지할 수 있으며, 그로 인해서 폼 값들이 오류 메시지와 함께 다시 출력될 것입니다. 본 자습서 시리즈의 이후 파트에서 모델 유효성 검사(Model Validation) 방식의 유효성 검사에 관해서 더 자세하게 살펴볼 것입니다. 각각의 오류 메시지들은 Views/Book/Edit.cshtml 뷰 템플릿에 작성된 유효성 검사 태그 헬퍼(Validation Tag Helper)에 의해서 적절하게 출력됩니다.

MoviesController 클래스에 존재하는 모든 HttpGet 메서드는 전체적으로 비슷한 패턴을 갖고 있습니다. 다시 말해서, Movie 개체를 조회한 다음 (Index 메서드는 개체들의 목록을 조회한 다음), 그 개체를 (모델을) 뷰로 전달합니다. 단, Create 메서드에서는 빈 개체를 Create 뷰에 전달합니다. 그리고 생성, 편집, 삭제 등 모든 유형의 데이터 수정은 [HttpPost] 버전의 오버로드 메서드 내에서만 처리됩니다. HTTP GET 메서드에서 데이터를 변경하면 ASP.NET MVC Tip #46 – Don't use Delete Links because they create Security Holes 포스트에서 설명하고 있는 것처럼 보안적으로 위험할 수도 있습니다. 또한 HTTP GET 메서드에서 데이터 변경 작업을 수행하는 일은, GET 요청에서 응용 프로그램의 상태를 변경하면 안된다고 제시하고 있는 HTTP 권장 사례와 구조적 REST 패턴에 위배됩니다. 다시 말해서, GET 작업은 부작용이 발생하지 않는 안전한 작업만 수행해야 하며, 영구적으로 저장되는 데이터를 변경하는 일은 지양해야 합니다.