뷰: 뷰 구성 요소

등록일시: 2016-10-17 08:00,  수정일시: 2016-10-31 08:52
조회수: 7,473
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 ASP.NET Core MVC의 뷰 구성 요소 기능에 관해서 알아보고 간단한 예제를 살펴봅니다.

주의

이 페이지의 문서는 버전 1.0.0-rc1을 기준으로 작성되었으며, 아직 버전 1.0.0으로 갱신되지 않았습니다.

GitHub에서 샘플 코드 확인 및 다운로드 받기

뷰 구성 요소 소개

ASP.NET Core MVC에서 새로 도입된 뷰 구성 요소(View Components)는 부분 뷰와 매우 비슷하지만, 훨씬 더 강력한 기능입니다. 뷰 구성 요소는 모델 바인딩을 사용하지 않으며, 뷰 구성 요소를 호출하면서 제공되는 데이터만 사용합니다. 뷰 구성 요소는:

  • 전체 응답이 아닌 청크(Chunk)를 렌더합니다.
  • 컨트롤러와 뷰에서 제공되는 것과 같은 관심사의 분리 및 테스트 용이성의 장점을 제공합니다.
  • 매개변수를 전달 받을 수 있고, 업무 로직을 포함할 수 있습니다.
  • 대부분 레이아웃 페이지에서 호출됩니다.

뷰 구성 요소는 다음과 같이 부분 뷰로 구현하기에는 너무 복잡하지만, 재사용 가능한 렌더링 로직이 필요한 상황 어디에서나 사용할 수 있습니다:

  • 동적 탐색 메뉴
  • 태그 클라우드 (데이터베이스 질의가 필요합니다.)
  • 로그인 패널
  • 쇼핑카트
  • 최근 배포된 글
  • 일반적인 블로그의 사이드바 콘텐츠
  • 모든 페이지에 렌더되어 사용자의 로그인 상태에 따라 로그인 링크나 로그아웃 링크를 제공해주는 로그인 패널

뷰 구성 요소는 클래스(일반적으로 ViewComponent를 상속받습니다)와 그 클래스가 반환하는 결과 (일반적으로 뷰), 이렇게 두 가지 부분으로 구성됩니다. 뷰 구성 요소도 컨트롤러처럼 POCO로 구현할 수는 있지만, 대부분 개발자들은 ViewComponent 클래스를 상속받아서 제공되는 메서드와 속성들을 활용하는 방식을 선호합니다.

뷰 구성 요소 구현하기

이번 절에서는 뷰 구성 요소를 구현하기 위한 전반적인 요구 사항들을 살펴봅니다. 본문의 이후 절에서는 각 단계들을 보다 자세히 살펴보면서 뷰 구성 요소를 직접 구현해 볼 것입니다.

뷰 구성 요소 클래스

뷰 구성 요소 클래스는 다음 중 한 가지 방법으로 생성할 수 있습니다:

  • ViewComponent 클래스를 상속받습니다.
  • 클래스에 [ViewComponent] 어트리뷰트를 적용하거나, [ViewComponent] 어트리뷰트가 적용된 클래스를 상속받습니다.
  • 클래스 이름의 접미사로 ViewComponent 가 지정된 클래스를 생성합니다.

뷰 구성 요소는 컨트롤러와 마찬가지로 public이고, 중첩되지 않은, 비 추상 클래스로 만들어져야 합니다. 뷰 구성 요소의 이름은 클래스 이름에서 "ViewComponent" 접미사를 제외한 부분입니다. 또는 ViewComponentAttribute.Name 속성을 설정해서 뷰 구성 요소의 이름을 명시적으로 지정할 수도 있습니다.

뷰 구성 요소 클래스는:

  • 생성자 의존성 주입을 완벽하게 지원합니다.
  • 컨트롤러의 수명 주기에 포함되지 않는데, 이 말은 뷰 구성 요소에서는 필터를 사용할 수 없다는 뜻입니다.

뷰 구성 요소 메서드

뷰 구성 요소는 IViewComponentResult 형식을 반환하는 InvokeAsync 메서드에 로직을 정의합니다. 또한 모델 바인딩을 사용하지 않고 뷰 구성 요소를 호출할 때 직접 매개변수를 전달받습니다. 뷰 구성 요소는 요청을 직접 처리하지 않습니다. 뷰 구성 요소는 일반적으로 모델을 초기화한 다음, View 메서드를 호출해서 모델을 뷰로 전달합니다. 뷰 구성 요소의 메서드는:

  • IViewComponentResult 형식을 반환하는 InvokeAsync 메서드를 정의합니다.
  • 일반적으로 모델을 초기화한 다음, ViewComponent 클래스의 View 메서드를 호출해서 이를 뷰로 전달합니다.
  • HTTP가 아닌 메서드 호출을 통해서 매개변수를 전달받으며, 모델 바인딩을 사용하지 않습니다.
  • HTTP 끝점의 형태로 직접 호출할 수 없으며, 코드를 통해서 실행됩니다 (일반적으로 뷰 내에서). 뷰 구성 요소는 요청을 처리하지 않습니다.
  • 현재 HTTP 요청의 세부 정보가 아닌, 시그니처에 따라 재정의됩니다.

뷰 검색 경로

런타임은 다음과 같은 경로에서 뷰를 검색합니다:

  • Views/<controller_name>/Components/<view_component_name>/<view_name>
  • Views/Shared/Components/<view_component_name>/<view_name>

뷰 구성 요소의 기본 뷰 이름은 Default 로, 뷰 파일의 이름은 Default.cshtml 이 되는 경우가 많습니다. 물론, 뷰 구성 요소 결과를 생성하거나 View 메서드를 호출할 때, 다른 뷰 이름을 지정할 수도 있습니다.

뷰 파일의 이름으로 Default.cshtml 을 사용하고, 경로로는 Views/Shared/Components/<view_component_name>/<view_name> 을 사용하는 것을 권장합니다. 가령, 본문의 PriorityList 예제에서는 뷰 구성 요소로 Views/Shared/Components/PriorityList/Default.cshtml 을 사용합니다.

뷰 구성 요소 호출하기

뷰 구성 요소를 사용하려면 뷰에서 @Component.InvokeAsync("Name of view component", <anonymous type containing parameters>)를 호출하면 됩니다. 이때 익명 개체에 지정된 매개변수들은 InvokeAsync 메서드의 매개변수로 전달됩니다. 예를 들어, 본문에서 살펴볼 예제에서는 Views/Todo/Index.cshtml 뷰 파일에서 PriorityList 뷰 구성 요소가 호출됩니다. 가령 다음 코드의 경우, 두 개의 매개변수와 함께 InvokeAsync 메서드가 호출됩니다:

@await Component.InvokeAsync("PriorityList", new { maxPriority = 2, isDone = false })

컨트롤러에서 직접 뷰 구성 요소 호출하기

뷰 구성 요소는 뷰에서 호출되는 경우가 거의 대부분이지만, 컨트롤러 메서드에서 직접 호출할 수도 있습니다. 컨트롤러와 달리 뷰 구성 요소는 끝점을 정의할 수 없지만, ViewComponentResult의 콘텐츠를 반환하는 컨트롤러 액션은 간단하게 구현할 수 있습니다.

본문의 예제 뷰 구성 요소는 다음과 같이 컨트롤러에서 직접 호출될 수 있습니다:

public IActionResult IndexVC()
{
    return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
}

따라하기: 간단한 뷰 구성 요소 만들기

먼저, 기본 코드를 다운로드 받아서 빌드하고 테스트해봅니다. 이 프로젝트는 Todo 항목들의 목록을 출력해주는 Todo 컨트롤러를 포함하고 있는 간단한 프로젝트입니다.

ViewComponent 클래스 추가하기

먼저 ViewComponents 폴더를 생성한 다음, 아래와 같은 PriorityListViewComponent 클래스를 추가합니다.

using Microsoft.AspNet.Mvc;
using Microsoft.Data.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
    public class PriorityListViewComponent : ViewComponent
    {
        private readonly ToDoContext db;

        public PriorityListViewComponent(ToDoContext context)
        {
            db = context;
        }

        public async Task<IViewComponentResult> InvokeAsync(int maxPriority, bool isDone)
        {
            var items = await GetItemsAsync(maxPriority, isDone);
            return View(items);
        }

        private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
        {
            return db.ToDo.Where(x => x.IsDone == isDone &&
                                 x.Priority <= maxPriority).ToListAsync();
        }       
    }
}

코드의 주요 부분들을 살펴보면:

  • 뷰 구성 요소 클래스는 프로젝트 내의 어떤 폴더에도 위치할 수 있습니다.

  • PriorityListViewComponent라는 클래스 이름은 ViewComponent라는 접미사로 끝나므로, 뷰에서 클래스 구성 요소를 참조할 때 런타임에서 "PriorityList"라는 문자열을 사용하게 됩니다. 잠시 후에 이에 관해서 더 자세히 살펴보도록 하겠습니다.

  • [ViewComponent] 어트리뷰트를 적용하면 뷰 구성 요소를 참조할 때 사용되는 이름을 변경할 수 있습니다. 예를 들어서, 다음과 같이 XYZ라는 이름으로 클래스를 만들고 ViewComponent 어트리뷰트를 적용할 수 있습니다:

    [ViewComponent(Name = "PriorityList")]
    public class XYZ : ViewComponent
  • 이 코드에 적용된 [ViewComponent] 어트리뷰트는 PriorityList라는 이름을 사용해서 구성 요소와 관련된 뷰를 검색하도록 뷰 구성 요소 선택기에게 지시하고, 뷰에서 클래스 구성 요소를 참조할 때 "PriorityList"라는 문자열을 사용하도록 만듭니다. 잠시 후에 이에 관해서 더 자세히 살펴보도록 하겠습니다.

  • 이 구성 요소는 의존성 주입을 통해서 데이터 컨텍스트를 사용하고 있습니다.

  • 뷰에서 호출할 수 있는 InvokeAsync 메서드를 노출하고 있으며, 이 메서드는 임의의 숫자 값과 완료 여부를 인자로 받을 수 있습니다.

  • 본문의 예제에서는 기본적으로 InvokeAsync 메서드를 호출해서, 완료되지 않았으며 우선순위가 maxPriority 값과 같거나 작은 ToDo 항목들의 집합을 반환합니다.

뷰 구성 요소 Razor 뷰 생성하기

  1. Views/Shared/Components 폴더를 생성합니다. 이 폴더의 이름은 반드시 Components 여야만 합니다.
  2. Views/Shared/Components/PriorityList  폴더를 생성합니다. 이 폴더의 이름은 반드시 뷰 구성 요소 클래스의 이름이나 클래스 이름에서 접미사를 제외한 이름과 (규약에 따라서 클래스 이름에 ViewComponent 접미사를 지정한 경우) 일치해야만 합니다. 만약 ViewComponent 어트리뷰틀 적용했다면, 폴더의 이름과 어트리뷰트에 지정한 이름이 일치해야만 합니다.
  3. Views/Shared/Components/PriorityList/Default.cshtml  Razor 뷰를 생성합니다.
@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h3>Priority Items</h3>
<ul>
    @foreach (var todo in Model)
    {
        <li>@todo.Name</li>
    }
</ul>

이 Razor 뷰는 TodoItem의 목록을 전달받고 이를 출력합니다. 뷰 구성 요소의 InvokeAsync 메서드가 뷰의 이름을 전달하지 않으면 (본문의 예제처럼), 규약에 따라 뷰 이름으로 Default 가 사용됩니다. 본문에서는 잠시 후에 뷰의 이름을 명시적으로 전달하는 방법도 살펴볼 것입니다. 특정 컨트롤러에 대한 기본적인 뷰 구성 요소의 마크업 형태를 재정의하려면 컨트롤러 전용 뷰 폴더에 뷰 파일을 추가하면 됩니다 (Views/Todo/Components/PriorityList/Default.cshtml).

뷰 구성 요소가 컨트롤러 전용인 경우에도 컨트롤러 전용 폴더에 뷰를 추가하면 됩니다 (Views/Todo/Components/PriorityList/Default.cshtml).

  1. Views/Todo/index.cshtml  파일의 하단에 우선순위 목록 구성 요소에 대한 호출을 담을 div 요소를 추가합니다:
    }
</table>
<div>
    @await Component.InvokeAsync("PriorityList", new { maxPriority = 2, isDone = false })
</div>

이 코드에서 @Component.InvokeAsync 마크업은 뷰 구성 요소를 호출하는 구문의 사례를 보여줍니다. 첫 번째 인자는 실행하거나 호출하고자 하는 구성 요소의 이름입니다. 이어지는 매개변수들은 구성 요소로 전달됩니다. 이 코드의 InvokeAsync 메서드는 인자로 임의의 숫자와 완료 여부를 전달받을 수 있습니다.

다음 그림은 우선순위의 항목들을 보여줍니다:

다음과 같이 컨트롤러에서 직접 뷰 구성 요소를 호출할 수도 있습니다:

public IActionResult IndexVC()
{
    return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
}

뷰 이름 지정하기

복잡한 뷰 구성 요소에서는 일부 조건에 따라서 비-기본 뷰를 지정해야 할 수도 있습니다. 다음 코드는 InvokeAsync 메서드에서 "PVC" 뷰를 지정하는 방법을 보여줍니다. PriorityListViewComponent 클래스의 InvokeAsync 메서드를 변경합니다.

public async Task<IViewComponentResult> InvokeAsync(int maxPriority, bool isDone)
{
    string MyView = "Default";
    // If asking for all completed tasks, render with the "PVC" view.
    if (maxPriority > 3 && isDone == true)
    {
        MyView = "PVC";
    }
    var items = await GetItemsAsync(maxPriority, isDone);
    return View(MyView, items);
}

그런 다음, Views/Shared/Components/PriorityList/Default.cshtml  파일을 Views/Shared/Components/PriorityList/PVC.cshtml  이라는 뷰 이름으로 복사합니다. 그리고 PVC 뷰가 사용되고 있음을 확인할 수 있도록 머리말을 추가합니다.

@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h2>PVC Named Priority Component View</h2>
<h4>@ViewBag.PriorityMessage</h4>
<ul>
    @foreach (var todo in Model)
    {
        <li>@todo.Name</li>
    }
</ul>

다음과 같이 Views/TodoList/Index.cshtml  파일을 수정합니다.

</table>

<div>
    @await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })
</div>

응용 프로그램을 실행하고 PVC 뷰가 사용되는 것을 확인합니다.

만약 PVC 뷰가 렌더되지 않는다면, 뷰 구성 요소를 호출할 때 [ViewComponent] 인자 값으로 4 이상을 지정했는지 확인하시기 바랍니다.

뷰 경로 살펴보기

  1. maxPriority 매개변수를 3 이하로 지정해서 PVC 뷰가 반환되지 않도록 조정합니다.

  2. 그리고 임시로 Views/Todo/Components/PriorityList/Default.cshtml  파일의 이름을 Temp.cshtml  로 변경합니다.

  3. 이제 응용 프로그램을 테스트 해보면, 다음과 같은 오류가 발생할 것입니다:

    An unhandled exception occurred while processing the request.
    
    InvalidOperationException: The view 'Components/PriorityList/Default'
       was not found. The following locations were searched:
       /Views/ToDo/Components/PriorityList/Default.cshtml
       /Views/Shared/Components/PriorityList/Default.cshtml.
    Microsoft.AspNetCore.Mvc.ViewEngines.ViewEngineResult.EnsureSuccessful()
  1. Views/Shared/Components/PriorityList/Default.cshtml 파일을 Views/Todo/Components/PriorityList/Default.cshtml 로 복사합니다.
  2. Todo 뷰 구성 요소의 뷰에 간단한 마크업을 추가해서 해당 뷰가 Todo 폴더에 위치한 뷰임을 확인할 수 있도록 만듭니다.
  3. 다시 비-공유 구성 요소 뷰를 테스트 해봅니다.

마법 문자열 사용하지 않기

컴파일 시점 안정성을 확보하기 위해서, 하드 코딩된 뷰 구성 요소 이름을 클래스 이름으로 대체할 수 있습니다. 먼저 "ViewComponent" 접미사 없이 뷰 구성 요소를 생성합니다:

using Microsoft.AspNet.Mvc;
using Microsoft.Data.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
    public class PriorityList : ViewComponent
    {
        private readonly ToDoContext db;

        public PriorityList(ToDoContext context)
        {
            db = context;
        }

        public async Task<IViewComponentResult> InvokeAsync(int maxPriority, bool isDone)
        {
            var items = await GetItemsAsync(maxPriority, isDone);
            return View(items);
        }

        private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
        {
            return db.ToDo.Where(x => x.IsDone == isDone &&
                                 x.Priority <= maxPriority).ToListAsync();
        }
    }
}

그런 다음, Razor 뷰 파일에 using 구문을 추가하고 nameof 연산자를 사용합니다:

@using ViewComponentSample.Models
@using ViewComponentSample.ViewComponents
@model IEnumerable<TodoItem>

<h2>ToDo nameof</h2>
<!-- Markup removed for brevity.  -->
    }
</table>

<div>
    @await Component.InvokeAsync(nameof(PriorityList), new { maxPriority = 4, isDone = true })
</div>