ASP.NET Core 2.4 · 2부. MVC / Minimal API

Razor 뷰 기초

서버에서 화면을 그립니다.

예상 학습 시간 18분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 기초
개념 설명

이번에는 HTML 을 돌려줍니다

2.1부터 2.3까지는 데이터만 돌려주었습니다. 화면은 받는 쪽이 알아서 그린다는 전제입니다. 이번에는 서버가 HTML 을 만들어 보냅니다.

Razor 는 그것을 적는 방법입니다. 확장자가 .cshtml 인 파일에 HTML 을 적고, C# 이 필요한 자리에 @ 를 붙입니다. 그 파일은 컴파일됩니다. css·js 처럼 고쳐서 새로 고치면 되는 것이 아니라 빌드를 거칩니다.

지금 보고 계신 이 화면도 Razor 로 그린 것입니다. 단원 하나가 .cshtml 파일 하나이고, 머리와 앞뒤 이동은 그것을 감싸는 다른 파일이 그립니다.

최소 예제

컨트롤러가 넘기고 뷰가 그립니다

먼저 Program.cs 에서 뷰를 함께 등록합니다. 2.2의 AddControllers() 대신 뒤가 붙은 것을 사용합니다.

builder.Services.AddControllersWithViews();

컨트롤러는 ControllerBase 가 아니라 Controller 를 물려받습니다. 그래야 View() 를 사용할 수 있습니다(2.2 참고).

TodosController.cs · Views/Todos/Index.cshtml
public class TodosController : Controller {
    private static readonly List<Todo> _todos = [
        new(1, "우유 사기", false),
        new(2, "<script>alert('안녕')</script>", true)
    ];

    public IActionResult Index() => View(_todos);
}

// ───────────── Views/Todos/Index.cshtml ─────────────

@model IEnumerable<Todo>
@{
    var 남은것 = Model.Count(t => !t.Done);
}

<h1>할일 @Model.Count()개</h1>
<p>아직 @(남은것)개 남았습니다.</p>

<ul>
@foreach (var todo in Model)
{
    <li class="@(todo.Done ? "done" : "")">@todo.Title</li>
}
</ul>
받은 HTML
<h1>할일 2개</h1> <p>아직 1개 남았습니다.</p> <ul> <li class="">&#xC6B0;&#xC720; &#xC0AC;&#xAE30;</li> <li class="done">&lt;script&gt;alert(&#x27;…&#x27;)&lt;/script&gt;</li> </ul>

이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 아래에서 인코딩 규칙만 따로 실행해 볼 수 있습니다.

컨트롤러는 어느 파일을 그릴지 적지 않았습니다. View() 는 컨트롤러와 메서드 이름을 보고 Views/Todos/Index.cshtml 을 찾습니다. 거기에 없으면 Views/Shared/ 를 다시 찾습니다.

상세 사용법

어디까지가 C# 인지

@ 뒤부터 C# 으로 읽을 수 있는 데까지가 식입니다. 그래서 이름 뒤에 글자가 붙으면 그것까지 이름으로 봅니다.

막히는 것
<p>아직 @남은것개 남았습니다.</p>

// error CS0103: '남은것개' 이름이
// 현재 컨텍스트에 없습니다.
경계를 적은 것
<p>아직 @(남은것)개 남았습니다.</p>

// 아직 1개 남았습니다.

@Model.Count()개 는 괄호로 끝나 경계가 분명하므로 그대로 두어도 됩니다. 한글이 이어 붙는 자리에서 자주 걸립니다.

적는 법하는 일
@이름값을 내보냅니다.
@(식)경계를 손으로 정합니다.
@{ … }여러 줄을 실행합니다. 내보내지 않습니다.
@if · @foreach제어문입니다. 중괄호 안에 HTML 을 그대로 적습니다.
@* … *@주석입니다. 응답에 나가지 않습니다.
@@골뱅이 한 글자입니다. 메일 주소를 적을 때 필요합니다.

감싸는 파일이 따로 있습니다

머리와 바닥은 화면마다 되풀이됩니다. 그것을 _Layout.cshtml 에 한 번 적고, 각 화면은 가운데만 적습니다.

// Views/Shared/_Layout.cshtml
<body>
    <header>머리</header>
    @RenderBody()          // 각 화면이 여기에 들어갑니다
    <footer>바닥</footer>
</body>

// Views/_ViewStart.cshtml — 모든 화면이 그리기 전에 이것을 먼저 봅니다
@{
    Layout = "_Layout";
}

_ViewStart.cshtml화면마다 적지 않아도 적용됩니다. 어느 한 화면만 다른 것을 사용하려면 그 파일에서 Layout 을 다시 정하고, null 로 두면 감싸지 않습니다.

_ViewImports.cshtml 도 같은 방식입니다. 여기에 적은 @using 과 태그 헬퍼 등록이 모든 화면에 적용됩니다. 이름 앞의 밑줄은 이것들이 직접 그려지는 화면이 아니라는 표시입니다.

상세 사용법

값은 자동으로 인코딩됩니다

위 결과에서 두 번째 항목이 &lt;script&gt; 로 나갔습니다. Razor 가 알아서 바꾼 것입니다. 사용자가 적은 글에 태그가 들어 있어도 그것이 코드로 실행되지 않습니다.

그런데 한글도 &#xC6B0; 로 바뀌었습니다. 기본 인코더가 ASCII 가 아닌 글자를 모두 바꾸기 때문입니다. 아래를 실행해 확인해보십시오.

Program.cs
using System.Text.Encodings.Web;
using System.Text.Unicode;

string[] 값 = ["우유 사기", "<script>alert('안녕')</script>"];

Console.WriteLine("-- 기본 인코더 --");
foreach (var v in 값) Console.WriteLine(HtmlEncoder.Default.Encode(v));

Console.WriteLine();
Console.WriteLine("-- 모든 범위를 허용했을 때 --");
var 넓은것 = HtmlEncoder.Create(UnicodeRanges.All);
foreach (var v in 값) Console.WriteLine(넓은것.Encode(v));
출력
-- 기본 인코더 -- &#xC6B0;&#xC720; &#xC0AC;&#xAE30; &lt;script&gt;alert(&#x27;&#xC548;&#xB155;&#x27;)&lt;/script&gt; -- 모든 범위를 허용했을 때 -- 우유 사기 &lt;script&gt;alert(&#x27;안녕&#x27;)&lt;/script&gt;

코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.

범위를 넓혀도 태그는 그대로 막힙니다. 둘째 묶음에서 한글은 풀렸지만 &lt;script&gt; 는 그대로입니다. 푸는 것은 글자이지 <·>·& 가 아니므로 방어는 유지됩니다.

화면은 어느 쪽이든 같아 보이지만, 바꾸어 두면 한글 한 자가 여덟 바이트로 부풀지 않고 소스를 글자 그대로 읽는 도구에도 문장이 보입니다. Program.cs 에 한 줄이면 됩니다.

builder.Services.AddWebEncoders(options =>
    options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All));

Html.Raw 는 마지막 수단입니다

인코딩하지 않고 그대로 내보내는 방법이 있습니다. 사용자가 적은 글에는 사용하지 마십시오. 위 예제의 두 번째 항목을 이것으로 내보내면 그 script 가 실제로 실행됩니다.

<p>@Html.Raw(Model.Last().Title)</p>

// 받은 HTML — 인코딩되지 않은 채 그대로 나갔습니다
// <p><script>alert('안녕')</script></p>

사용할 자리는 우리가 만든 글뿐입니다. 편집기로 받은 글을 내보내야 한다면, 내보낼 때 푸는 것이 아니라 담을 때 허용할 태그만 남기는 쪽이 맞습니다.

버전 배지

태그 헬퍼

ASP.NET Core 1.0 부터

주소를 글자로 적는 대신 HTML 태그에 속성을 붙여 만들 수 있습니다. asp-controller·asp-action 을 적으면 라우팅 규칙을 보고 주소를 만들어 넣습니다. 주소 모양이 바뀌어도 화면을 고치지 않아도 됩니다.

사용하려면 _ViewImports.cshtml@addTagHelper 를 적어야 합니다. 등록이 어긋나면 오류 없이 조용히 동작하지 않습니다asp-action 이 그냥 무시되어 주소가 비어 있는 링크가 됩니다.

실습 문제

직접 해보기

1. 비어 있을 때 다른 문구 난이도 하

할일이 하나도 없으면 목록 대신 할일이 없습니다. 를 보이도록 고쳐보세요.

@if 의 중괄호 안에는 HTML 을 그대로 적을 수 있습니다.
@if (!Model.Any()) { <p>할일이 없습니다.</p> } else { <ul> @foreach (var todo in Model) { <li>@todo.Title</li> } </ul> }
2. 경계가 필요한 자리 찾기 난이도 중

아래 넷 가운데 괄호를 넣어야 하는 것을 선택하고 까닭을 적어보세요.

① <p>@todo.Title 입니다</p>
② <p>@todo.Id번</p>
③ <p>@todo.Title.Length자</p>
④ <p>@Model.Count()개</p>
이름 뒤에 곧바로 이어 붙은 글자가 있는지를 보십시오. 빈칸이나 괄호가 있으면 거기서 끊깁니다.
② 와 ③ 입니다. ① 빈칸이 있어 Title 에서 끊깁니다. ② Id번 을 하나의 이름으로 봅니다. → @(todo.Id)번 ③ Length자 를 하나의 이름으로 봅니다. → @(todo.Title.Length)자 ④ 괄호로 끝나 경계가 분명합니다.
3. 인코딩을 풀어도 안전한 까닭 난이도 상

AddWebEncoders 로 모든 범위를 허용하면 한글이 그대로 나갑니다. 그런데도 XSS 방어가 유지되는 까닭을 적어보세요. Html.Raw 와 무엇이 다릅니까.

허용한 것은 "어떤 글자를 바꾸지 않을지" 입니다. 태그를 이루는 글자가 그 안에 들어 있습니까.
// AddWebEncoders 는 "바꾸지 않을 글자의 범위"를 넓힌 것입니다. // < > & " ' 는 그 범위와 무관하게 언제나 바뀝니다. 태그가 될 수 // 있는 글자이기 때문입니다. 그래서 한글만 풀리고 방어는 남습니다. // Html.Raw 는 인코딩 자체를 건너뜁니다. 태그를 이루는 글자까지 // 그대로 나가므로 사용자가 적은 글에는 사용할 수 없습니다. // 실제 출력으로 확인할 수 있습니다. // 범위를 넓힘 : &lt;script&gt;alert(&#x27;안녕&#x27;)&lt;/script&gt; // Html.Raw : <script>alert('안녕')</script>
요약
  • AddControllersWithViews() 를 등록하고 Controller 를 물려받아 View() 를 돌려줍니다.
  • 뷰는 Views/{컨트롤러}/{메서드}.cshtml 에서 찾고, 없으면 Views/Shared/ 를 봅니다.
  • @ 뒤는 C# 으로 읽을 수 있는 데까지입니다. 한글이 이어 붙으면 @(…) 로 경계를 적습니다.
  • 값은 자동으로 인코딩됩니다. Html.Raw 는 그것을 건너뛰므로 사용자가 적은 글에 사용하지 않습니다.
  • 기본 인코더는 한글까지 바꿉니다. AddWebEncoders 로 풀어도 태그는 그대로 막힙니다.