뷰: 레이아웃

등록일시: 2016-09-05 08:00,  수정일시: 2016-11-07 11:25
조회수: 8,183
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 ASP.NET Core MVC 응용 프로그램에서 공통 레이아웃 및 공유 지시문을 사용하는 방법과, 뷰가 렌더되기 전에 공통 코드를 실행하는 방법을 살펴봅니다.

뷰들 간에 시각적 요소나 프로그래밍적인 요소들이 공유되는 경우는 매우 흔합니다. 본문에서는 ASP.NET Core MVC 응용 프로그램에서 공통 레이아웃 및 공유 지시문을 사용하는 방법과, 뷰가 렌더되기 전에 공통 코드를 실행하는 방법을 살펴봅니다.

레이아웃

대부분의 웹 응용 프로그램들은 사용자가 한 페이지에서 다른 페이지로 이동할 때 일관된 경험을 제공해주기 위한 공통 레이아웃(Layout)을 갖고 있습니다. 레이아웃에는 일반적으로 응용 프로그램 헤더나 탐색 및 메뉴 요소, 푸터 같은 공통적인 사용자 인터페이스 요소들이 포함됩니다.

또한 스크립트나 스타일시트 같은 공통 HTML 구조들도 응용 프로그램 내부의 많은 페이지들에서 빈번하게 사용됩니다. 이런 공유되는 요소들 중 상당수는 레이아웃(Layout) 파일에 정의되어, 응용 프로그램에서 사용되는 모든 뷰에서 참조가 가능해집니다. 레이아웃은 뷰들 간의 코드 중복을 줄여주고, 중복배제(DRY, Don't Repeat Yourself)의 원칙을 준수할 있도록 도와줍니다.

규약에 따른 ASP.NET 응용 프로그램의 기본 레이아웃 파일 이름은 _Layout.cshtml 입니다. 가령, Visual Studio ASP.NET Core MVC 프로젝트 템플릿에도 Views/Shared 폴더에 이 레이아웃 파일이 포함되어 있습니다:

이 레이아웃은 응용 프로그램 내부의 뷰들에 대한 최상위 수준의 템플릿을 정의합니다. 응용 프로그램에는 레이아웃이 아예 없을 수도 있고, 하나 이상의 레이아웃을 정의해서 각각의 뷰마다 각기 다른 레이아웃을 지정할 수도 있습니다.

다음은 _Layout.cshtml의 예제입니다:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebApplication1</title>

    <environment names="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
        <link rel="stylesheet" href="~/css/site.css" />
    </environment>
    <environment names="Staging,Production">
        <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    </environment>
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">WebApplication1</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
                    <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
                    <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
                </ul>
                @await Html.PartialAsync("_LoginPartial")
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; 2016 - WebApplication1</p>
        </footer>
    </div>

    <environment names="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
    </environment>
    <environment names="Staging,Production">
        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
                asp-fallback-test="window.jQuery">
        </script>
        <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
        </script>
        <script src="~/js/site.min.js" asp-append-version="true"></script>
    </environment>

    @RenderSection("scripts", required: false)
</body>
</html>

레이아웃 지정하기

Razor 뷰에서는 Layout이라는 속성이 제공됩니다. 뷰마다 각각 이 속성을 설정해서 레이아웃을 지정할 수 있습니다:

@{
    Layout = "_Layout";
}

레이아웃을 지정할 때는 전체 경로를 설정하거나(/Views/Shared/_Layout.cshtml), 이름 부분만 설정할 수 있습니다(_Layout). 이름 부분만 설정할 경우, Razor 뷰 엔진이 기본 검색 과정에 따라서 레이아웃 파일을 검색하게 됩니다. 먼저 컨트롤러 관련 뷰들을 검색한 다음, Shared 폴더를 검색합니다. 이 기본 검색 과정과 부분 뷰(Partial Views) 검색에 사용되는 과정은 동일합니다.

기본적으로 모든 레이아웃은 RenderBody 메서드를 호출해야만 하며, RenderBody 메서드가 호출되는 바로 그 위치에 뷰의 콘텐츠가 렌더됩니다.

섹션

필요한 경우, 레이아웃 내에서 RenderSection 메서드를 호출하여 하나 이상의 섹션(Sections)을 참조할 수 있습니다. 섹션을 활용하면 특정 페이지 요소가 렌더될 위치를 자유롭게 구성할 수 있습니다. RenderSection 메서드를 호출할 때, 해당 섹션이 필수적인지 선택적인지 여부를 지정할 수 있으며, 만약 필수로 지정된 섹션이 발견되지 않으면 예외가 발생하게 됩니다. 각 뷰에서는 @section Razor 구문을 이용해서 섹션에 렌더될 콘텐츠를 지정해야 합니다. 만약 뷰에 섹션이 정의되어 있다면 반드시 렌더되야 합니다 (아니면 오류가 발생합니다).

다음은 뷰에 정의된 @section의 예제입니다:

@section Scripts {
    <script type="text/javascript" src="/scripts/main.js"></script>
}

이 코드는 폼이 존재하는 뷰의 scripts 섹션에 유효성 검사 스크립트를 추가한다고 가정하고 있습니다. 그러나 응용 프로그램의 다른 뷰들에는 이 스크립트가 불필요할 수도 있으며, 그런 경우에는 당연히 scripts 섹션을 정의하지 않으면 됩니다.

역주

예제에 사용된 JavaScript 파일의 이름이 main.js가 아닌 validation.js 등의 이름이었다면 더 그럴듯 했을 것 같습니다.

뷰에 정의된 섹션들은 해당 뷰와 직접 관련된 레이아웃 페이지에서만 사용할 수 있습니다. 부분 뷰나 뷰 구성 요소, 또는 뷰 시스템의 다른 부분들에서는 참조할 수 없습니다.

섹션 무시하기

기본적으로 콘텐츠 페이지에 존재하는 본문과 모든 섹션들은 레이아웃 페이지를 통해서 모두 렌더되어야 합니다. Razor 뷰 엔진은 본문 및 각 섹션들이 렌더됐는지 여부를 추적해서 이를 강제합니다.

뷰 엔진에게 본문이나 섹션을 무시하도록 지시하려면 IgnoreBody 메서드나 IgnoreSection 메서드를 호출하면 됩니다.

Razor 페이지의 본문과 모든 섹션들은 반드시 렌더되거나 무시돼야만 합니다.

공유 지시문 임포트하기

뷰에서는 Razor 지시문을 이용해서 네임스페이스를 임포트하거나 의존성 주입을 수행하는 등, 다양한 작업을 수행할 수 있습니다. 여러 뷰들서 공유되는 지시문들은 공통 _ViewImports.cshtml 파일에서 일괄 지정할 수 있습니다. _ViewImports 파일에서는 다음과 같은 지시문들이 지원됩니다:

  • @addTagHelper
  • @removeTagHelper
  • @tagHelperPrefix
  • @using
  • @model
  • @inherits
  • @inject

이 파일에서는 함수나 섹션 정의 같은 다른 Razor 기능들은 지원하지 않습니다.

다음은 _ViewImports.cshtml 파일의 예제입니다:

@using WebApplication1
@using WebApplication1.Models
@using WebApplication1.Models.AccountViewModels
@using WebApplication1.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

보통 ASP.NET Core MVC 응용 프로그램의 _ViewImports.cshtml 파일은 Views 폴더에 위치합니다. 그러나 _ViewImports.cshtml 파일은 어떤 폴더에도 존재할 수 있으며, 그 경우 해당 폴더와 그 하위 폴더에 존재하는 뷰들에만 반영됩니다. _ViewImports.cshtml 파일들은 최상위 폴더에서부터 처리가 시작되어 뷰가 위치한 폴더에 도달할 때까지 각 폴더들에 위치한 순서대로 누적되어 처리되므로, 최상위 수준에서 지정된 설정이 폴더 수준 설정에 의해서 재정의 될 수도 있습니다.

가령, 최상위 수준의 _ViewImports.cshtml 파일에 @model 지시문과 @addTagHelper 지시문이 지정된 상태에서, 뷰의 컨트롤러와 관련된 폴더에 위치한 두 번째 _ViewImports.cshtml 파일에서 다시 다른 @model 지시문과 추가적인 @addTagHelper 지시문이 지정됐다면, 결과적으로 뷰에서는 두 태그 헬퍼 모두에 접근이 가능하고 나중에 지정된 @model이 사용됩니다.

뷰에 다수의 _ViewImports.cshtml 파일들이 적용될 경우, 다음과 같이 _ViewImports.cshtml 파일들에 포함된 지시문들의 동작이 결합되어 수행됩니다:

  • @addTagHelper, @removeTagHelper: 순서대로 모두 실행됩니다.
  • @tagHelperPrefix: 뷰와 가장 가까운 설정이 다른 설정들을 덮어씁니다.
  • @model: 뷰와 가장 가까운 설정이 다른 설정들을 덮어씁니다.
  • @inherits: 뷰와 가장 가까운 설정이 다른 설정들을 덮어씁니다.
  • @using: 모두 포함되고, 중복은 무시됩니다.
  • @inject: 각 속성별로, 뷰와 가장 가까운 속성이 동일한 이름의 다른 속성들을 덮어씁니다.

뷰가 실행되기 전에 공통 코드 실행하기

모든 뷰가 실행되기 전에 먼저 실행돼야만 하는 코드가 존재한다면, _ViewStart.cshtml 파일에 해당 코드를 작성하면 됩니다. 규약에 따라서 _ViewStart.cshtml 파일은 Views 폴더에 위치하며, 이 파일에 작성된 구문들은 모든 뷰가 실행되기 전에 먼저 실행됩니다 (레이아웃과 부분 뷰는 이에 해당되지 않습니다). _ViewImports.cshtml 파일처럼 _ViewStart.cshtml 파일도 계층적입니다. 컨트롤러 관련 뷰 폴더에 _ViewStart.cshtml 파일이 정의되었다면, 이 파일에 정의된 코드는 Views 폴더의 최상위에 정의된 _ViewStart.cshtml 파일에 정의된 코드가 (존재할 경우) 실행된 다음에 실행됩니다.

다음은 _ViewStart.cshtml 파일의 예제입니다:

@{
    Layout = "_Layout";
}

이 예제 파일은 모든 뷰에서 _Layout.cshtml 레이아웃을 사용하도록 지정하고 있습니다.

노트

일반적으로 _ViewStart.cshtml 파일이나 _ViewImports.cshtml 파일은 /Views/Shared 폴더에 위치하지 않습니다. 이 파일들의 응용 프로그램 수준 버전은 /Views 폴더에 바로 위치해야 합니다.