뷰: 태그 헬퍼 구현하기

등록일시: 2016-09-26 08:00,  수정일시: 2016-10-05 10:54
조회수: 6,217
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 실제로 몇 가지 유형의 태그 헬퍼를 직접 작성해보면서 태그 헬퍼를 구현하는 기본적인 방법을 알아봅니다.

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

태그 헬퍼 시작하기

본 자습서에서는 태그 헬퍼를 직접 구현하는 방법을 알아봅니다. 태그 헬퍼가 제공해주는 장점들에 관해서는 태그 헬퍼 소개 문서에서 살펴볼 수 있습니다.

태그 헬퍼는 ITagHelper 인터페이스를 구현하는 모든 클래스를 말합니다. 그러나 실제로 태그 헬퍼를 구현할 때는 대부분 TagHelper 클래스를 상속받아서 Process 메서드를 재정의 하는 경우가 많습니다. 본문에서는 TagHelper 클래스가 제공해주는 메서드와 속성들을 하나씩 직접 사용해보면서 살펴보도록 하겠습니다.

  1. 먼저, AuthoringTagHelpers라는 이름으로 새로운 ASP.NET Core 프로젝트를 생성합니다. 이번 프로젝트에 인증 기능은 필요 없으므로 옵션에서 선택을 제외합니다.
  2. 태그 헬퍼가 위치할 TagHelpers라는 이름의 폴더를 생성합니다. 반드시 이 이름으로 폴더를 만들어야만 하는 것은 아니지만 대체적으로 납득할 수 있는 규약입니다. 이제 기본적인 준비가 끝났으므로 지금부터 몇 가지 간단한 태그 헬퍼를 구현해보겠습니다.

Email 태그 헬퍼 구현하기

이번 절에서는 email 태그에 적용되는 태그 헬퍼를 구현해보겠습니다. 예를 들어, 다음의 마크업은:

<email>Support</email>

서버에서 Email 태그 헬퍼를 통해서 다음과 같이 변환될 것입니다:

<a href="mailto:Support@contoso.com">Support@contoso.com</a>

즉, 태그 헬퍼의 결과물로 이메일 링크가 제공되는 앵커 태그가 만들어지게 됩니다. 블로그 엔진을 구현할 경우, 마케팅이나 지원, 또는 동일한 도메인의 모든 기타 연락처에 이메일을 전송해야 할 때, 아마도 이런 기능을 제공하는 태그 헬퍼가 필요할 수 있을 것입니다.

  1. TagHelpers 폴더에 다음과 같은 EmailTagHelper 클래스를 추가합니다.
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;

namespace AuthoringTagHelpers.TagHelpers
{
    public class EmailTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "a";    // Replaces <email> with <a> tag
        }
    }
}

노트:

  • 태그 헬퍼는 기본적으로 대상 요소의 태그 이름을 클래스의 루트 이름(클래스 이름에서 TagHelper 부분을 제외한 이름)으로 사용합니다. EmailTagHelper의 루트 이름은 email 이므로, 이 태그 헬퍼의 대상 요소는 <email> 태그입니다. 대부분의 태그 헬퍼에 이 이름 규약이 적용되는데, 본문에서는 이 규약을 재지정하는 방법도 살펴봅니다.
  • EmailTagHelper 클래스는 TagHelper 클래스를 상속받습니다. TagHelper 클래스는 태그 헬퍼의 구현에 필요한 메서드와 속성들을 제공해줍니다.
  • 재정의 된 Process 메서드는 태그 헬퍼가 실행될 때 수행할 작업을 결정합니다. TagHelper 클래스에서는 동일한 매개변수를 갖고 있는 이 메서드의 비동기 버전(ProcessAsync)도 제공됩니다.
  • Process 메서드와 ProcessAsync 메서드에 전달되는 context 매개변수에는 현재 HTML 태그의 처리에 관련된 정보들이 담겨 있습니다.
  • Process 메서드와 ProcessAsync 메서드에 전달되는 output 매개변수에는 HTML 태그와 콘텐츠의 생성에 사용되는, 원본 소스의 HTML 요소에 대한 상태 저장이 가능한 대체물이 담겨 있습니다.
  • 이 클래스는 이름의 접미사로 TagHelper를 사용하고 있습니다. 필수적인 규약은 아니지만 가장 합리적인 규약이기도 합니다. 그러나 다음과 같이 클래스를 선언해도 전혀 무방합니다:
public class Email : TagHelper
  1. EmailTagHelper 클래스를 모든 Razor 뷰에서 사용할 수 있도록, 다음과 같이 Views/_ViewImports.cshtml 파일에 addTagHelper 지시문을 추가합니다:
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*, AuthoringTagHelpers"

이 코드의 세 번째 줄에서는 어셈블리에 존재하는 모든 태그 헬퍼를 사용할 수 있도록 와일드카드 구문을 사용하고 있습니다. @addTagHelper 지시문 이후의 첫 번째 문자열은 로드할 태그 헬퍼를 지정하고(모든 태그 헬퍼를 로드하기 위해서 "*"를 지정했습니다), 두 번째 문자열인 "AuthoringTagHelpers"는 태그 헬퍼가 존재하는 어셈블리를 지정합니다. 그리고 두 번째 줄에서도 역시 와일드카드 구문을 사용해서 ASP.NET Core MVC의 태그 헬퍼를 가져오고 있는 것을 눈여겨보시기 바랍니다 (이에 관해서는 태그 헬퍼 소개 문서에서 살펴봤습니다). 이 코드에 사용된 @addTagHelper 지시문은 Razor 뷰에서 태그 헬퍼를 사용할 수 있도록 만들어줍니다. 다음과 같이 태그 헬퍼의 정규화된 이름(FQN, Fully Qualified Name)을 지정할 수도 있습니다:

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "AuthoringTagHelpers.TagHelpers3.EmailTagHelper, AuthoringTagHelpers"

이렇게 정규화된 이름으로 뷰에 태그 헬퍼를 추가할 경우, 먼저 정규화된 이름(AuthoringTagHelpers.TagHelpers.EmailTagHelper)을 지정한 다음, 어셈블리 이름(AuthoringTagHelpers)을 지정하면 되지만, 개발자들은 대부분 "*" 와일드카드 구문을 선호하는 편입니다. 태그 헬퍼를 추가하고, 제거하고, 상속하는 방법이나 와일드카드 구문을 사용하는 방법에 대한 보다 자세한 정보는 태그 헬퍼 소개 문서를 참고하시기 바랍니다.

  1. 다음과 같이 Views/Home/Contact.cshtml 파일의 마크업을 변경합니다:
@{
    ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
    One Microsoft Way<br />
    Redmond, WA 98052<br />
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong><email>Support</email><br />
    <strong>Marketing:</strong><email>Marketing</email>
</address>
  1. 이제 응용 프로그램을 실행하고 브라우저를 이용해서 HTML 소스를 살펴보면, email 태그가 앵커 마크업으로 치환된 것을 확인할 수 있습니다 (예, <a>Support</a>). 그러나 비록 SupportMarketing 마크업이 링크로 렌더되기는 하지만, 아직까지는 실질적인 기능을 제공해주는 href 어트리뷰트가 존재하지 않습니다. 계속해서 이어지는 절에서 이 문제점을 수정해보겠습니다.

노트

일반적인 HTML 태그와 어트리뷰트들처럼 Razor와 C# 내부에서 사용되는 태그와 CSS 클래스, 그리고 HTML 어트리뷰트들은 대소문자를 구분하지 않습니다.

실제로 동작하는 Email 태그 헬퍼

이번 절에서는 유효한 이메일 앵커 태그를 생성하도록 EmailTagHelper를 수정해보겠습니다. Razor 뷰에서 정보를 전달받고 (mail-to 어트리뷰트의 형태로), 그 정보를 이용해서 앵커를 생성하도록 코드를 변경할 것입니다.

다시 다음과 같이 EmailTagHelper 클래스를 변경합니다:

public class EmailTagHelper : TagHelper
{
    private const string EmailDomain = "contoso.com";

    // Can be passed via <email mail-to="..." />. 
    // Pascal case gets translated into lower-kebab-case.
    public string MailTo { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "a";    // Replaces <email> with <a> tag

        var address = MailTo + "@" + EmailDomain;
        output.Attributes.SetAttribute("href", "mailto:" + address);
        output.Content.SetContent(address);
    }
}

노트:

  • 파스칼 표기법으로 작성된 태그 헬퍼의 클래스 이름과 속성 이름은 소문자 케밥 표기법으로 변환됩니다. 따라서, MailTo 속성을 사용하려면 <email mail-to="value"/>와 같이 사용하면 됩니다.
  • 코드의 마지막 줄은 최소한의 기능만 갖고 있는 예제 태그 헬퍼의 모든 콘텐츠를 설정합니다.
  • 어트리뷰트를 추가해주는 구문은 다음에 강조된 코드 줄입니다:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
    output.TagName = "a";    // Replaces <email> with <a> tag

    var address = MailTo + "@" + EmailDomain;
    output.Attributes.SetAttribute("href", "mailto:" + address);
    output.Content.SetContent(address);
}

이 접근방식은 현재 어트리뷰트 컬렉션에는 존재하지 않는 "href" 어트리뷰트를 대상으로 잘 동작합니다. 또는 output.Attributes.Add 메서드를 사용해서 태그 어트리뷰트 컬렉션의 마지막에 태그 헬퍼 어트리뷰트를 추가할 수도 있습니다.

  1. 다시 Views/Home/Contact.cshtml 파일의 마크업을 다음과 같이 한번 더 변경합니다:
@{
    ViewData["Title"] = "Contact Copy";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
    One Microsoft Way Copy Version <br />
    Redmond, WA 98052-6399<br />
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong><email mail-to="Support"></email><br />
    <strong>Marketing:</strong><email mail-to="Marketing"></email>
</address>
  1. 이제 응용 프로그램을 실행해보면 정상적으로 링크가 생성되는 것을 확인할 수 있습니다.

노트: 만약 자체적으로 닫힌 email 태그를 작성하면 (<email mail-to="Rick" />), 최종 출력 역시 자체적으로 닫혀서 렌더됩니다. 시작 태그만 사용해서 태그를 작성할 수 있는 기능을 활성화시키려면 (<email mail-to="Rick">) 반드시 클래스에 다음과 같은 어트리뷰트를 적용해야 합니다:

[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)] 
public class EmailVoidTagHelper : TagHelper
{
    private const string EmailDomain = "contoso.com";
    // Code removed for brevity

자체적으로 닫힌 Email 태그 헬퍼를 사용할 경우, <a href="mailto:Rick@contoso.com" />와 같은 출력이 렌더됩니다. 자체적으로 닫힌 앵커 태그는 유효한 HTML이 아니므로 이런 HTML 태그를 만들 이유는 없지만, 자체적으로 닫힌 태그 헬퍼를 만들어야 할 경우는 있을 수 있습니다. 태그 헬퍼는 태그를 읽은 다음, TagMode 속성의 형식을 설정합니다.

역주

이 부분은 원문 자체의 문장이 조금 어색한데, 본문의 후반부 예제에서 이 TagMode 속성을 설정해서 태그가 생성되는 구조를 제어하는 방법을 살펴봅니다.

비동기 Email 태그 헬퍼

이번 절에서는 비동기 Email 태그 헬퍼를 구현해보겠습니다.

  1. EmailTagHelper 클래스의 코드를 다음과 같이 변경합니다:

역주

먼저 클래스에 using System.Threading.Tasks; 문을 추가해야 합니다.

public class EmailTagHelper : TagHelper
{
    private const string EmailDomain = "contoso.com";
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "a";                                 // Replaces <email> with <a> tag
        var content = await output.GetChildContentAsync();
        var target = content.GetContent() + "@" + EmailDomain;
        output.Attributes.SetAttribute("href", "mailto:" + target);
        output.Content.SetContent(target);
    }
}

노트:

  • 이 버전은 비동기 ProcessAsync 메서드를 사용하고 있습니다. 메서드 내부에서 사용되는 비동기 GetChildContentAsync 메서드는 TagHelperContent를 담고 있는 Task 형식을 반환합니다.
  • HTML 요소의 콘텐츠를 얻기 위해서 output 매개변수를 사용하고 있습니다.
  1. 다음과 같이 Views/Home/Contact.cshtml 파일을 변경해서 태그 헬퍼가 대상 이메일을 가져올 수 있도록 수정합니다:
@{
    ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
    One Microsoft Way<br />
    Redmond, WA 98052<br />
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong><email>Support</email><br />
    <strong>Marketing:</strong><email>Marketing</email>
</address>
  1. 이제 응용 프로그램을 실행하고 정상적으로 이메일 링크가 생성됐는지 확인해봅니다.

Bold 태그 헬퍼

  1. 다음과 같은 BoldTagHelper 클래스를 TagHelpers 폴더에 추가합니다.
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
    [HtmlTargetElement(Attributes = "bold")]
    public class BoldTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.RemoveAll("bold");
            output.PreContent.SetHtmlContent("<strong>");
            output.PostContent.SetHtmlContent("</strong>");
        }
    }
}

/*
 * public IActionResult About()
{
    ViewData["Message"] = "Your application description page.";
    return View("AboutBoldOnly");
    // return View();
}
*/

노트:

  • 이 코드에서는 [HtmlTargetElement] 어트리뷰트로 어트리뷰트 매개변수를 전달해서 "bold"라는 이름의 HTML 어트리뷰트가 존재하는 모든 HTML 요소를 대상으로 재정의된 클래스의 Process 메서드를 실행하도록 지정합니다. 이 예제의 Process 메서드는 "bold" 어트리뷰트를 제거하고, 태그 내부의 마크업을 <strong></strong> 태그로 감쌉니다.
  • 태그의 기존 콘텐츠는 변경하지 말아야 하므로, PreContent.SetHtmlContent 메서드를 이용해서 여는 <strong> 태그를 추가하고, PostContent.SetHtmlContent 메서드를 이용해서 닫는 </strong> 태그를 추가합니다.
  1. 이번에는 About.cshtml 뷰를 수정해서 bold 어트리뷰트 값을 추가합니다. 작업을 마친 전체 코드는 다음과 같습니다.
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p bold>Use this area to provide additional information.</p>

<bold> Is this bold?</bold>
  1. 응용 프로그램을 실행합니다. 브라우저를 이용해서 소스를 분석해보면 마크업을 확인할 수 있습니다.

예제 코드에 설정된 [HtmlTargetElement] 어트리뷰트는 "bold"라는 이름의 어트리뷰트를 갖고 있는 HTML 마크업만을 대상으로 지정합니다. 따라서 <bold> 요소는 이 태그 헬퍼에 의해서 변경되지 않습니다.

  1. [HtmlTargetElement] 어트리뷰트를 주석으로 처리하면 기본 규약에 따라 처리 대상이 <bold> 태그, 즉 <bold> 형태의 마크업으로 변경됩니다. 기본적인 이름 규약에 따라서 BoldTagHelper라는 클래스 이름은 <bold> 태그와 매치된다는 점을 기억하시기 바랍니다.
  2. 응용 프로그램을 실행하고 <bold> 태그가 태그 헬퍼에 의해서 처리되는 결과를 확인해보시기 바랍니다.

클래스에 다수의 [HtmlTargetElement] 어트리뷰트를 함께 지정하면 처리 대상이 논리합(Logical-OR)의 형태로 추가됩니다. 가령, 다음과 같은 코드를 사용하면 bold 태그 자체와 bold 어트리뷰트를 가진 태그 모두가 처리 대상이 됩니다.

[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.Attributes.RemoveAll("bold");
        output.PreContent.SetHtmlContent("<strong>");
        output.PostContent.SetHtmlContent("</strong>");
    }
}

반면, 단일 구문에 여러 개의 어트리뷰트가 추가되면, 런타임은 이를 논리곱(Logical-AND)의 형태로 처리합니다. 예를 들어, 다음과 같은 코드를 사용하면 태그 이름이 "bold"면서 "bold"라는 이름의 어트리뷰트를 갖고 있는 HTML 요소들만 (<bold bold />) 처리 대상이 됩니다.

[HtmlTargetElement("bold", Attributes = "bold")]

또한 [HtmlTargetElement] 어트리뷰트를 활용해서 태그 헬퍼의 대상 요소 이름을 변경할 수도 있습니다. 가령, 다음처럼 어트리뷰트를 지정하면 BoldTagHelper<MyBold> 태그에 적용할 수 있습니다:

[HtmlTargetElement("MyBold")]

웹 사이트 정보 태그 헬퍼

  1. Models 폴더를 추가합니다.
  2. Models 폴더에 다음과 같은 WebsiteContext 모델 클래스를 추가합니다:
using System;

namespace AuthoringTagHelpers.Models
{
    public class WebsiteContext
    {
        public Version Version { get; set; }
        public int CopyrightYear { get; set; }
        public bool Approved { get; set; }
        public int TagsToShow { get; set; }
    }
}
  1. TagHelpers 폴더에 다음과 같은 WebsiteInformationTagHelper 클래스를 추가합니다.
using System;
using AuthoringTagHelpers.Models;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
    public class WebsiteInformationTagHelper : TagHelper
    {
        public WebsiteContext Info { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "section";
            output.Content.SetHtmlContent(
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>
<li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li>
<li><strong>Approved:</strong> {Info.Approved}</li>
<li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li></ul>");
            output.TagMode = TagMode.StartTagAndEndTag;
        }
    }
}

노트:

  • 이미 언급했던 것처럼 태그 헬퍼는 파스칼 표기법으로 작성된 태그 헬퍼의 클래스 이름과 속성 이름을 소문자 케밥 표기법으로 변환합니다. 따라서, Razor에서 WebsiteInformationTagHelper를 사용하려면, <website-information />와 같이 작성해야 합니다.
  • 이 클래스는 [HtmlTargetElement] 어트리뷰트를 적용해서 명시적으로 대상 요소를 지정하지 않았기 때문에, 기본 규약에 따라 website-information 태그가 적용대상이 됩니다. 만약 다음과 같은 어트리뷰트를 지정하면 (클래스 이름과 일치하지만 케밥 표기법은 아니라는 점에 주목하십시오):
[HtmlTargetElement("WebsiteInformation")]

소문자 케밥 표기법의 <website-information /> 태그와 일치하지 않게 됩니다. 따라서, [HtmlTargetElement] 어트리뷰트를 사용하고자 한다면, 다음과 같이 케밥 표기법을 사용해야 합니다:

[HtmlTargetElement("Website-Information")]
  • 자체적으로 닫히는 요소는 콘텐츠가 존재하지 않습니다. 이번 예제에서 Razor 마크업 자체는 자체적으로 닫히는 태그를 사용하지만, 태그 헬퍼는 section 요소를 생성해야 합니다 (이 요소는 자체적으로 닫히는 태그도 아니며, 게다가 이 section 요소 내부에 콘텐츠도 작성해야 합니다). 따라서 올바른 출력을 작성하기 위해서는 TagMode 속성을 StartTagAndEndTag로 설정해야만 합니다. 또는, TagMode 속성을 설정하는 줄을 주석으로 처리하고 닫는 태그를 이용해서 마크업을 작성하는 방법도 있습니다. (예제 마크업은 잠시 뒤에 제공됩니다.)
  • 다음과 같이 $ (달러 기호)로 시작되는 줄은 보간된 문자열(Interpolated String)을 사용하는 문자열입니다:
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>
  1. About.cshtml 뷰에 다음 마크업을 추가합니다. 강조된 부분의 마크업이 웹 사이트의 정보를 출력해주게 됩니다.
@using AuthoringTagHelpers.Models
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p bold>Use this area to provide additional information.</p>

<bold> Is this bold?</bold>

<h3> web site info </h3>
<website-information info="new WebsiteContext {
                                    Version = new Version(1, 3),
                                    CopyrightYear = 1638,
                                    Approved = true,
                                    TagsToShow = 131 }" />

노트: 이번 예제에 사용된 Razor 마크업은 다음과 같습니다:

<website-information info="new WebsiteContext {
                                    Version = new Version(1, 3),
                                    CopyrightYear = 1638,
                                    Approved = true,
                                    TagsToShow = 131 }" />

Razor는 info 어트리뷰트가 문자열이 아닌 클래스라는 사실을 인식하고 있으며, 여기에는 C# 코드를 작성해야 합니다. 모든 비-문자열 태그 헬퍼 어트리뷰트는 @ 문자 없이 작성해야만 합니다.

  1. 응용 프로그램을 실행하고, About 뷰로 이동해서 웹 사이트의 정보를 확인합니다.

노트:

  • 다음 마크업과 같이 닫는 태그를 사용하고, 대신 태그 헬퍼에서 TagMode.StartTagAndEndTag 코드 줄을 제거할 수도 있습니다:
<website-information info="new WebsiteContext {
                                    Version = new Version(1, 3),
                                    CopyrightYear = 1638,
                                    Approved = true,
                                    TagsToShow = 131 }" >
</website-information>

조건 태그 헬퍼

조건 태그 헬퍼는 true 값이 전달되는 경우에만 출력을 렌더하는 태그 헬퍼입니다.

  1. 다음의 ConditionTagHelper 클래스를 TagHelpers 폴더에 추가합니다.
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
    [HtmlTargetElement(Attributes = nameof(Condition))]
    public class ConditionTagHelper : TagHelper
    {
        public bool Condition { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (!Condition)
            {
                output.SuppressOutput();
            }
        }
    }
}
  1. 다음 마크업으로 Views/Home/Index.cshtml 파일의 내용을 교체합니다:
@using AuthoringTagHelpers.Models
@model WebsiteContext

@{
    ViewData["Title"] = "Home Page";
}

<div>
    <h3>Information about our website (outdated):</h3>
    <Website-InforMation info=Model />
    <div condition="Model.Approved">
        <p>
            This website has <strong surround="em"> @Model.Approved </strong> been approved yet.
            Visit www.contoso.com for more information.
        </p>
    </div>
</div>
  1. Home 컨트롤러의 Index 메서드를 다음 코드로 대체합니다:
public IActionResult Index(bool approved = false)
{
    return View(new WebsiteContext
    {
        Approved = approved,
        CopyrightYear = 2015,
        Version = new Version(1, 3, 3, 7),
        TagsToShow = 20
    });
}
  1. 이제 응용 프로그램을 실행하고 Home 페이지로 이동해봅니다. 그러면 처음에는 조건적 div 태그가 렌더되지 않을 것입니다. 다시 이번에는 URL에 ?approved=true 같은 쿼리 문자열을 추가해봅니다 (예, http://localhost:1235/Home/Index?approved=true). 그러면 approved 속성이 true로 설정되어 조건적 마크업이 출력될 것입니다.

노트: 이번 예제에서는 대상 어트리뷰트를 지정하기 위해서 Bold 태그 헬퍼에서 사용했던 문자열 지정 방식이 아닌 nameof 연산자 방식을 사용하고 있습니다:

[HtmlTargetElement(Attributes = nameof(Condition))]
// [HtmlTargetElement(Attributes = "condition")]
public class ConditionTagHelper : TagHelper
{
    public bool Condition { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (!Condition)
        {
            output.SuppressOutput();
        }
    }
}

이렇게 nameof 연산자를 사용하면 리팩터링 시 발생할 수 있는 실수로부터 코드를 보호할 수 있습니다 (어쩌면 이후에 어트리뷰트의 이름을 RedCondition으로 변경하고 싶을 수도 있습니다).

태그 헬퍼 간의 충돌 제어하기

이번 절에서는 자동으로 링크를 생성해주는 두 가지 태그 헬퍼를 구현해보려고 합니다. 첫 번째 태그 헬퍼는 HTTP로 시작하는 URL을 포함하고 있는 마크업을 해당 URL을 담고 있는 HTML 앵커 태그로 치환해줍니다 (URL에 링크를 걸기가 쉬워집니다). 그리고 두 번째 태그 헬퍼 역시 www로 시작하는 URL을 대상으로 동일한 작업을 처리해줍니다.

이 두 가지 태그 헬퍼는 서로 밀접한 관계를 갖고 있고, 점차 조금씩 개선해나갈 것이기 때문에 동일한 파일에 작성하도록 하겠습니다.

  1. 다음과 같은 AutoLinkerHttpTagHelper 클래스를 TagHelpers 폴더에 추가합니다.
[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var childContent = await output.GetChildContentAsync();
        // Find Urls in the content and replace them with their anchor tag equivalent.
        output.Content.SetHtmlContent(Regex.Replace(
             childContent.GetContent(),
             @"\b(?:https?://)(\S+)\b",
              "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
    }
}

노트: AutoLinkerHttpTagHelper 클래스는 p 요소를 대상으로 하며, Regex를 이용해서 앵커를 생성합니다.

  1. 다음의 마크업을 Views/Home/Contact.cshtml 파일의 끝 부분에 추가합니다:
@{
    ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
    One Microsoft Way<br />
    Redmond, WA 98052<br />
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong><email>Support</email><br />
    <strong>Marketing:</strong><email>Marketing</email>
</address>

<p>Visit us at http://docs.asp.net or at www.microsoft.com</p>
  1. 응용 프로그램을 실행해서 태그 헬퍼가 정상적으로 앵커를 렌더하는지 확인합니다.
  2. 이제 AutoLinker 클래스를 다시 수정해서, www로 시작하는 텍스트를 앵커 태그로 변환하고, 원본 www 텍스트를 콘텐츠로 사용하는 AutoLinkerWwwTagHelper를 추가합니다. 작업이 완료된 코드가 다음에 강조되어 있습니다:
[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var childContent = await output.GetChildContentAsync();
        // Find Urls in the content and replace them with their anchor tag equivalent.
        output.Content.SetHtmlContent(Regex.Replace(
             childContent.GetContent(),
             @"\b(?:https?://)(\S+)\b",
              "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
    }
}

[HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var childContent = await output.GetChildContentAsync();
        // Find Urls in the content and replace them with their anchor tag equivalent.
        output.Content.SetHtmlContent(Regex.Replace(
            childContent.GetContent(),
             @"\b(www\.)(\S+)\b",
             "<a target=\"_blank\" href=\"http://$0\">$0</a>"));  // www version
    }
}
  1. 다시 응용 프로그램을 실행해봅니다. 이때, www로 시작하는 텍스트는 링크로 렌더되지만, HTTP로 시작하는 텍스트는 원본 그대로 남아있다는 점에 주목하시기 바랍니다. 두 클래스 모두에 중단점을 걸어서 확인해보면, HTTP 태그 헬퍼 클래스가 먼저 실행되는 것을 확인할 수 있습니다. 문제의 원인은 태그 헬퍼의 출력이 캐시된다는 사실로부터 비롯됩니다. 즉, WWW 태그 헬퍼가 실행될 때, HTTP 태그 헬퍼의 캐시된 출력을 덮어 써버리는 것입니다. 그러면 지금부터 태그 헬퍼가 실행되는 순서를 제어하는 방법을 살펴보겠습니다. 먼저 코드를 다음과 같이 수정합니다:
    [HtmlTargetElement("p")]
    public class AutoLinkerHttpTagHelper : TagHelper
    {
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            var childContent = output.Content.IsModified ? output.Content.GetContent() :
                (await output.GetChildContentAsync()).GetContent();

            // Find Urls in the content and replace them with their anchor tag equivalent.
            output.Content.SetHtmlContent(Regex.Replace(
                 childContent,
                 @"\b(?:https?://)(\S+)\b",
                  "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
        }
    }

    [HtmlTargetElement("p")]
    public class AutoLinkerWwwTagHelper : TagHelper
    {
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            var childContent = output.Content.IsModified ? output.Content.GetContent() : 
                (await output.GetChildContentAsync()).GetContent();
  
            // Find Urls in the content and replace them with their anchor tag equivalent.
            output.Content.SetHtmlContent(Regex.Replace(
                 childContent,
                 @"\b(www\.)(\S+)\b",
                 "<a target=\"_blank\" href=\"http://$0\">$0</a>"));  // www version
        }
    }
}

노트: 자동 링크 태그 헬퍼들의 첫 번째 버전에서는 다음과 같은 코드를 이용해서 대상 요소의 내용을 가져왔습니다:

var childContent = await output.GetChildContentAsync();

즉, ProcessAsync 메서드에 전달된 TagHelperOutput 클래스의 GetChildContentAsync 메서드를 호출하고 있습니다. 이미 설명했던 것처럼, 출력이 캐시되기 때문에 이런 방식으로는 항상 마지막으로 실행된 태그 헬퍼의 출력만 반영됩니다. 이 문제점은 다음과 같은 코드로 해결이 가능합니다:

var childContent = output.Content.IsModified ? output.Content.GetContent() :
    (await output.GetChildContentAsync()).GetContent();

이 코드는 콘텐츠가 수정되었는지 여부를 먼저 확인한 다음, 만약 그렇다면 출력 버퍼에서 콘텐츠를 가져옵니다.

  1. 다시 응용 프로그램을 실행해서 두 종류의 링크가 모두 정상적으로 생성되는지 확인합니다. 이제 자동 링크 태그 헬퍼들이 모두 정상적으로 완벽하게 동작하는 것처럼 보이지만, 여전히 미묘한 문제점을 내포하고 있습니다. 만약, WWW 태그 헬퍼가 먼저 실행된다면, www 링크가 올바르게 생성되지 않을 것이기 때문입니다. 따라서 이 문제점을 예방하기 위해서는 태그 헬퍼가 실행되는 순서를 제어하기 위한 Order 재정의 관련 코드를 추가해야 합니다. Order 속성은 같은 요소를 대상으로 실행되는 여러 태그 헬퍼들 간에서 해당 태그 헬퍼가 실행될 순서를 결정합니다. 기본 순서 값은 0이고 작은 값을 가진 인스턴스일 수록 먼저 실행됩니다.
public class AutoLinkerHttpTagHelper : TagHelper
{
    // This filter must run before the AutoLinkerWwwTagHelper as it searches and replaces http and 
    // the AutoLinkerWwwTagHelper adds http to the markup.
    public override int Order
    {
        get { return int.MinValue; }
    }

이 코드를 추가하고 나면 HTTP 태그 헬퍼가 WWW 태그 헬퍼보다 먼저 실행되는 것을 보장받을 수 있습니다. 만약, Order 속성을 MaxValue로 수정하고 테스트 해보면 WWW 태그 헬퍼가 정상적으로 마크업을 생성하지 못하는 것을 확인할 수 있습니다.

자식 콘텐츠 조회하고 살펴보기

태그 헬퍼에서는 자식 콘텐츠를 조회할 수 있는 몇 가지 속성을 제공해줍니다.

  • GetChildContentAsync 메서드의 결과를 output.Content에 추가할 수 있습니다.
  • GetChildContentAsync 메서드 및 GetContent 메서드의 결과를 검토할 수 있습니다.
  • output.Content를 수정할 경우, 자동 링크 예제에서 살펴본 방식으로 GetChildContentAsync 메서드를 호출하지 않으면, TagHelper의 본문이 실행되거나 렌더되지 않을 것입니다:
public class AutoLinkerHttpTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var childContent = output.Content.IsModified ? output.Content.GetContent() :
            (await output.GetChildContentAsync()).GetContent();

        // Find Urls in the content and replace them with their anchor tag equivalent.
        output.Content.SetHtmlContent(Regex.Replace(
             childContent,
             @"\b(?:https?://)(\S+)\b",
              "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
    }
}
  • 캐시된 결과를 사용하지 않겠다는 의미로 false 매개변수를 전달하지 않으면, GetChildContentAsync 메서드를 여러 차례 호출해도 동일한 값이 반환되며, TagHelper의 본문도 재실행되지 않습니다.