Razor 뷰 기초
서버에서 화면을 그립니다.
이번에는 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 참고).
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>
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 아래에서 인코딩 규칙만 따로 실행해 볼 수 있습니다.
컨트롤러는 어느 파일을 그릴지 적지 않았습니다.
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 과 태그 헬퍼 등록이 모든 화면에 적용됩니다.
이름 앞의 밑줄은 이것들이 직접 그려지는 화면이 아니라는 표시입니다.
값은 자동으로 인코딩됩니다
위 결과에서 두 번째 항목이 <script> 로 나갔습니다. Razor 가 알아서 바꾼 것입니다. 사용자가 적은 글에 태그가 들어 있어도 그것이 코드로 실행되지 않습니다.
그런데 한글도 우 로 바뀌었습니다. 기본 인코더가 ASCII 가 아닌 글자를 모두 바꾸기 때문입니다. 아래를 실행해 확인해보십시오.
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));
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
범위를 넓혀도 태그는 그대로 막힙니다. 둘째 묶음에서 한글은 풀렸지만 <script> 는 그대로입니다. 푸는 것은 글자이지 <·>·& 가 아니므로 방어는 유지됩니다.
화면은 어느 쪽이든 같아 보이지만, 바꾸어 두면 한글 한 자가 여덟 바이트로 부풀지 않고 소스를 글자 그대로 읽는 도구에도 문장이 보입니다. 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>
사용할 자리는 우리가 만든 글뿐입니다. 편집기로 받은 글을 내보내야 한다면, 내보낼 때 푸는 것이 아니라 담을 때 허용할 태그만 남기는 쪽이 맞습니다.
태그 헬퍼
주소를 글자로 적는 대신 HTML 태그에 속성을 붙여 만들 수 있습니다.
asp-controller·asp-action
을 적으면 라우팅 규칙을 보고 주소를 만들어 넣습니다. 주소 모양이 바뀌어도
화면을 고치지 않아도 됩니다.
사용하려면 _ViewImports.cshtml 에
@addTagHelper 를 적어야 합니다.
등록이 어긋나면 오류 없이 조용히 동작하지 않습니다 —
asp-action 이 그냥 무시되어 주소가 비어 있는
링크가 됩니다.
직접 해보기
할일이 하나도 없으면 목록 대신 할일이 없습니다. 를 보이도록 고쳐보세요.
@if 의 중괄호 안에는 HTML 을 그대로 적을 수 있습니다.아래 넷 가운데 괄호를 넣어야 하는 것을 선택하고 까닭을 적어보세요.
① <p>@todo.Title 입니다</p> ② <p>@todo.Id번</p> ③ <p>@todo.Title.Length자</p> ④ <p>@Model.Count()개</p>
AddWebEncoders 로 모든 범위를 허용하면 한글이 그대로
나갑니다. 그런데도 XSS 방어가 유지되는 까닭을 적어보세요.
Html.Raw 와 무엇이 다릅니까.
AddControllersWithViews()를 등록하고Controller를 물려받아View()를 돌려줍니다.- 뷰는 Views/{컨트롤러}/{메서드}.cshtml 에서 찾고, 없으면 Views/Shared/ 를 봅니다.
@뒤는 C# 으로 읽을 수 있는 데까지입니다. 한글이 이어 붙으면@(…)로 경계를 적습니다.- 값은 자동으로 인코딩됩니다.
Html.Raw는 그것을 건너뛰므로 사용자가 적은 글에 사용하지 않습니다. - 기본 인코더는 한글까지 바꿉니다.
AddWebEncoders로 풀어도 태그는 그대로 막힙니다.