파트 7: ASP.NET Core MVC - 검색 기능 추가하기

등록일시: 2016-07-01 08:00,  수정일시: 2016-09-26 14:05
조회수: 10,116
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 장르나 제목으로 영화를 검색할 수 있도록 Index 액션 메서드에 검색 기능을 추가해봅니다.

이번 파트에서는 장르제목으로 영화를 검색할 수 있도록 Index 액션 메서드에 검색 기능을 추가해봅니다.

먼저 검색이 가능하도록 Index 액션 메서드를 수정합니다:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

Index 액션 메서드의 첫 번째 줄은 영화 정보를 조회하기 위한 LINQ 쿼리를 생성합니다:

var movies = from m in _context.Movie
             select m;

그러나 이 시점에는 단지 쿼리가 정의 되었을 뿐, 실제로 데이터베이스를 대상으로 실행되는 것은 아닙니다.

그런 다음, searchString 매개변수에 검색할 문자열이 담겨 있으면, 다음 코드를 이용해서 검색 문자열 값으로 필터를 설정해서 movies 쿼리를 변경합니다:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title.Contains(searchString));
}

이 코드에 사용된 s => s.Title.Contains() 구문은 람다 식(Lambda Expression)입니다. 메서드 기반의 LINQ 쿼리에서는 람다가 이 코드에 사용된 Where 메서드나 Contains 메서드 같은 표준 쿼리 연산자 메서드의 인자로 사용됩니다. 그러나 LINQ 쿼리는 해당 쿼리가 정의되는 시점이나, WhereContains, 또는 OrderBy 같은 메서드 호출에 의해 쿼리가 변경되는 시점에는 실행되지 않습니다. 대신 쿼리의 실제 값이 루프문 등에서 구체적으로 접근되거나, ToListAsync 같은 특정 메서드가 호출될 때까지 표현식의 평가가 미뤄지고 쿼리 실행이 지연됩니다. 지연된 쿼리 실행(Deferred Query Execution)에 관한 보다 자세한 정보는 쿼리 실행(Query Execution) 문서를 참고하시기 바랍니다.

노트

이번 예제에 사용된 Contains 메서드는 C# 코드가 아닌 데이터베이스에서 실행됩니다. 그리고 Contains 메서드는 데이터베이스에서 대소문자를 구분하지 않는 SQL LIKE 문으로 매핑됩니다.

응용 프로그램을 실행하고 /Movies/Index 로 이동합니다. 그리고 주소 표시줄의 URL에 ?searchString=ghost 같은 쿼리 문자열을 추가해봅니다. 그러면 필터링 된 영화 목록이 나타나는 것을 확인할 수 있습니다.

여기서 한 단계 더 나가서 Index 메서드의 시그니처를 변경해서 매개변수의 이름을 id로 바꾸면, 다음과 같이 {id} 파일에 정의된 기본 라우트의 {id} 자리 표시자와 id 매개변수가 일치하게 됩니다.

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

이때, 이름 바꾸기 명령을 이용하면 searchString 매개변수의 이름을 id로 간단하게 변경할 수 있습니다. searchString을 마우스 오른쪽 버튼으로 클릭한 다음, 이름 바꾸기(Rename)을 선택합니다.

그러면 이름을 변경할 대상들이 강조되어 표시됩니다.

이 상태에서 매개변수의 이름을 id로 변경하면, 모든 searchString 변수들이 id로 변경됩니다.

기존 Index 메서드의 코드는 다음과 같았습니다:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

이제 변경된 Index 메서드의 코드는 다음과 같습니다:

public async Task<IActionResult> Index(string id)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title.Contains(id));
    }

    return View(await movies.ToListAsync());
}

이제 검색할 제목을 쿼리 문자열 값 대신 라우트 데이터(URL 세그먼트)를 이용해서 전달할 수 있습니다.

그렇지만 사용자들이 영화를 검색할 때마다 매번 이렇게 URL을 수정하리라고 기대할 수는 없습니다. 따라서 이번에는 영화를 검색할 때 편리하게 사용할 수 있는 검색 UI를 추가해보겠습니다. 만약, 라우트 기반 ID 매개변수 전달 방식을 테스트 해보기 위해서 Index 메서드의 시그니처를 변경했다면, 다시 searchString 매개변수를 전달받게 변경합니다:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

그리고 Views\Movies\Index.cshtml  파일에 다음에 강조된 코드를 추가합니다:

@model IEnumerable<MvcMovie.Models.Movie>

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        Title: <input type="text" name="SearchString">
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>

이 코드의 HTML <form> 태그는 Form 태그 헬퍼를 사용하고 있습니다. 따라서 폼을 제출하면 Movies 컨트롤러의 Index 액션 메서드로 필터 문자열이 전송됩니다. 변경 사항을 저장하고 필터를 테스트 해봅니다.

기대하고 있었을지도 모르지만 Index 메서드의 [HttpPost] 오버로드 버전은 존재하지 않습니다. 이 메서드는 데이터를 필터링만 할 뿐, 응용 프로그램의 어떠한 상태도 변경하지 않기 때문에 [HttpPost] 오버로드 버전은 필요가 없습니다.

물론 다음과 같이 [HttpPost] Index 메서드를 추가할 수도 있습니다.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

이 오버로드 버전의 Index 메서드를 생성하면서 notUsed 매개변수를 사용하고 있는데, 이 매개변수에 관해서는 나중에 설명합니다.

이 메서드를 추가하면, 액션 호출자(Action Invoker)[HttpPost] Index 메서드를 인식해서 실행하게 되고, 그 결과 아래 그림과 같은 결과가 나타나게 됩니다.

그러나 지금 상태로는 설령 Index 메서드의 [HttpPost] 버전을 추가하고 코드까지 구현하더라도 한계가 존재할 수 밖에 없습니다. 가령, 특정 검색 결과를 북마크 해두거나 친구에게 링크를 보내서 친구가 그 링크를 클릭했을 때, 필터링 된 동일한 영화 목록이 나타나기를 원한다고 상상해보십시오. 여기에서 주목해야 할 점은 HTTP POST 요청의 URL이 기본적인 GET 요청의 URL(localhost:xxxxx/Movies/Index)과 동일하기 때문에, URL에 검색에 관한 정보가 전혀 포함되어 있지 않다는 사실입니다. 그 이유는 검색 문자열 정보가 폼 필드 값의 형태로 서버에 전달되고 있기 때문입니다. 이 사실은 F12 개발자 도구Fiddler 도구로 확인해볼 수 있습니다. F12 도구를 시작합니다:

그리고 F12 도구의 네트워크(Network) 탭에서 http://localhost:xxx/Movies HTTP POST 200 줄을 클릭한 다음, 본문(Body) > 요청 본문(Request Body)을 살펴봅니다.

역주: 본문의 내용을 확인하려면, 먼저 F12 도구를 실행하고 프로파일링 세션을 시작한 상태에서 검색을 실행해야 합니다.

그러면 요청 본문에 담겨 있는 검색 매개변수와 XSRF 토큰을 확인할 수 있습니다. 이전 자습서 파트에서 설명한 것처럼, Form 태그 헬퍼XSRF 위조-방지 토큰을 생성합니다. 그러나 검색 기능은 데이터를 변경하지 않으므로 컨트롤러 메서드에서 토큰의 유효성을 검사할 필요는 없습니다.

이처럼 검색 매개변수가 URL 대신 요청 본문에 담겨서 전달되기 때문에, 이 상태로는 검색 정보를 북마크로 저장해두거나 다른 사람들과 공유할 수 없습니다. 따라서, HTTP GET 방식으로 요청하도록 지정하면 이 문제를 해결할 수 있습니다. 마크업을 실제로 수정할 때 지원되는 인텔리센스를 주목해서 살펴보시기 바랍니다.

그리고 <form> 태그에 사용된 독특한 폰트도 주의해서 살펴보십시오. 이 독특한 폰트는 해당 태그가 태그 헬퍼(Tag Helpers)에 의해서 지원되고 있음을 나타내줍니다.

이제 다시 검색을 수행해보면 검색 쿼리 문자열이 URL에 포함되는 것을 확인할 수 있습니다. 이제는 HttpPost Index 메서드가 존재하더라도 HttpGet Index 액션 메서드가 실행됩니다.

장르 검색 추가하기

먼저, Models 폴더에 다음과 같이 MovieGenreViewModel 클래스를 추가합니다:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie> movies;
        public SelectList genres;
        public string movieGenre { get; set; }
    }
}

이 영화-장르 뷰 모델에는 다음과 같은 정보들이 담기게 됩니다:

  • 영화 정보들의 목록
  • 장르들의 목록이 담긴 SelectList 형식의 필드. 이 정보를 이용해서 사용자가 목록에서 장르를 선택할 수 있도록 구현합니다.
  • 사용자가 목록에서 선택한 장르를 담는 movieGenre 필드

계속해서 이번에는 Index 메서드의 코드를 다음 코드로 대체합니다:

public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    if (!String.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel();
    movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync());
    movieGenreVM.movies = await movies.ToListAsync();

    return View(movieGenreVM);
}

다음 코드는 데이터베이스에서 모든 장르들을 조회하는 LINQ 쿼리입니다.

IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

그리고 장르들의 SelectListDistinct 메서드를 이용해서 중복되지 않는 장르들만 취합해서 만들어집니다 (선택 목록에 같은 장르가 여러 번 나타나는 것을 피하기 위한 작업입니다).

movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync());

Index 뷰에 장르 검색 추가하기

다시 Views\Movies\Index.cshtml  파일을 다음 코드와 같이 변경합니다:

@model MovieGenreViewModel

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <select asp-for="movieGenre" asp-items="Model.genres">
            <option value="">All</option>
        </select>

        Title: <input type="text" name="SearchString">
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.movies[0].Genre)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.movies[0].Price)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.movies[0].Title)
        </th>
        <th></th>
    </tr>
    <tbody>
        @foreach (var item in Model.movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <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>
            </tr>
        }
    </tbody>
</table>

응용 프로그램을 실행하고 장르 검색이나 영화 제목 검색, 그리고 두 가지 모두를 이용한 검색을 테스트 해보시기 바랍니다.