Tech for good

[Blazor, C#] Blazor WebAssembly Blog Series (1~3) 본문

IT/Computer Science

[Blazor, C#] Blazor WebAssembly Blog Series (1~3)

Diana Kang 2022. 2. 23. 15:37

* 해당 영상을 공부하며 정리한 자료입니다.

https://www.youtube.com/playlist?list=PLF1jhYUTnHo5XFX9lgS0YsNSDJHpYnRxK 

 

Blazor WebAssembly Blog Series

 

www.youtube.com

 

 

1. First Steps with Blazor WebAssembly & Razor Components

Blazor WebAssembly 기본 세팅 후 앱 빌드하기
- Blazor WebAssembly, .NET6으로 진행

 

* 폴더 및 파일 구조는 크게 아래와 같이 구성된다.

 

  • BlazorBlog.Client
    • Pages
      • Index.razor
      • Post.razor
    • Services
      • BlogService.cs
      • IBlogService.cs
    • Shared
      • BlogPost.razor
      • MainLayout.razor
      • NavMenu.razor
    • _Imports.razor
    • App.razor
    • Program.cs
  • BlazorBlog.Server
    • Controllers
      • BlogController.cs
    • Pages
      • Error.cshtml
    • appsetting.json
    • Program.cs
  • BlazorBlog.Shared
    • BlogPost.cs

 

※ Tips!
- Class 이름은 보통 단수로 쓴다. BlogServices.cs 이렇게 복수로 쓰지 않는다.
- 네임스페이스에 정의된 형식을 사용할 수 있게 해주는 @using 지시문(e.g. @using BlazorBlog.Shared)은 Client/_Imports.razor 파일에 작성해준다. 

 

2. Services, Dependency Injection & Page Parameters with Blazor

2.1. Services

인터페이스클래스의 개념을 배울 수 있다.
인터페이스
: 메서드, 속성, 이벤트 등을 갖긴 하지만 이를 직접 구현하지 않고 단지 정의(prototype definition)만을 갖는다.

클래스
: 메서드, 속성, 이벤트 등을 직접 구현한다!

 

  • IBlazorService.cs (인터페이스)
// BlazorBlog.Client > Services > IBlogService.cs

namespace BlazorBlog.Client.Services
{
    interface IBlogService
    {
        List<BlazorBlog.Shared.BlogPost> GetBlogPosts();  // 블로그의 모든 포스트를 받는다.
        BlazorBlog.Shared.BlogPost GetBlogPostByUrl(string url);  // URL을 받는다.
    }
}

 

// BlazorBlog.Shared > BlogPost.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BlazorBlog.Shared
{
    public class BlogPost
    {
        public int Id { get; set; }
        public string Url { get; set; }  // 새로운 프로퍼티 추가
        public string Title { get; set; }
        public string Content { get; set; }
        public string Description { get; set; }
        public string Author { get; set; }
        public DateTime DateCreated { get; set; } = DateTime.Now;
        public bool IsPublished { get; set; } = true;
        public bool IsDeleted { get; set; } = false;

    }
}

 

  • BlazorService.cs (클래스)
// BlazorBlog.Client > Shared > BlogPosts.razor 
@code
    public List<BlogPosts> Posts {get; set; } = new List</BlogPosts>()
    {
        new BlogPost { Url = "new-tutorial", Title = "A New Tutorial about Blazor with Web API", Description="This is a new tutorial, showing you how to build a blog with Blazor"},
        new BlogPost { Url = "first-post", Title = "My First Blog Post with Web API", Description = "Hi! This is my new shiny blog."}
    };

 

위의 부분을 복사하여 BlogService.cs 파일에 아래와 같이 추가한다.

 

// BlazorBlog.Client > Services > BlogService.cs
namespace BlazorBlog.Client.Services
{
    public class BlogService : IBlogService
    {

        public List<BlogPost> Posts { get; set; } = new List<BlogPost>
        {
            new BlogPost { Url = "new-tutorial", Title = "A New Tutorial about Blazor with Web API", Description="This is a new tutorial, showing you how to build a blog with Blazor"},
            new BlogPost { Url = "first-post", Title = "My First Blog Post with Web API", Description = "Hi! This is my new shiny blog."}
        };

        public BlogPost GetBlogPostByUrl(string url)
        {
            throw new NotImplementedException();
        }

        public List<BlogPost> GetBlogPosts()
        {
            return Posts;
        }
    }
}

 

 

2.2. Dependency Injection

@inject 지시문을 사용하여 .razor 파일에서 종속성을 주입받는 방법을 배울 수 있다.
// BlazorBlog.Client/Program.cs

using BlazorBlog.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<BlazorBlog.Client.Services.IBlogService, BlazorBlog.Client.Services.BlogService>(); 

await builder.Build().RunAsync();

 

그리고 나서 BlogPosts.razor 파일로 돌아가, 상단에 @inject BlazorBlog.Client.Services.IBlogService BlogService를 추가한다.

 

// BlazorBlog.Client > Shared > BlogPosts.razor

@inject BlazorBlog.Client.Services.IBlogService BlogService

~~~~

@code {
    private List<BlogPost> Posts = new List<BlogPost>();

    protected override void OnInitialized()
    {
        Posts = BlogService.GetBlogPosts();
    }
}

 

@inject 지시문을 사용하는 이유는 .razor 파일에서 종속성을 주입받기 위함이다.

즉, 해당 인터페이스(IBlogService)를 변수(Property; BlogService)로 선언해서 사용할 수 있게 해주는 것이다.

이렇게 razor 페이지에서 사용할 변수를 선언하기 위해 사용하는 것이 @inject 지시문이다.

 

 

2.3. Page Parameters with Blazor

Read more... 클릭 시 웹 주소가 변경되며 다른 화면이 나오게 만드는 방법을 배울 수 있다.
// BlazorBlog.Client > Shared > BlogPosts.razor

@inject BlazorBlog.Client.Services.IBlogService BlogService

@foreach (var post in Posts)
{
    <div style="margin: 20px;">
        <div>
            <h3>@post.Title</h3>
        </div>
        <div>
            @post.Description
        </div>
        <div>
            <a href="/posts/@post.Url">Read more...</a>
        </div>
    </div>
}

~~~
// BlazorBlog.Client > Pages > Post.razor

@page "/posts/{url}"   // 파라미터 값 전달
@inject BlazorBlog.Client.Services.IBlogService BlogService

<h3>@CurrentPost.Title</h3>

<div>
    @CurrentPost.Content
</div>

@code {
    private BlazorBlog.Shared.BlogPost CurrentPost;

    [Parameter]
    public string Url { get; set; }

    protected override void OnInitialized()
    {
        CurrentPost = BlogService.GetBlogPostByUrl(Url);
    }

}
// BlazorBlog.Client > Services > BlogService.cs

namespace BlazorBlog.Client.Services
{
    public class BlogService : IBlogService
    {
				public List<BlazorBlog.Shared.BlogPost> Posts { get; set; } = new List<BlazorBlog.Shared.BlogPost>
        {
		        new BlazorBlog.Shared.BlogPost { Url = "new-tutorial", Title = "A New Tutorial about Blazor with Web API", Description="This is a new tutorial, showing you how to build a blog with Blazor", Content = "I'm going to introduce what Blazor is from now on. Enjoy!"},
		        new BlazorBlog.Shared.BlogPost { Url = "first-post", Title = "My First Blog Post with Web API", Description = "Hi! This is my new shiny blog.", Content = "This is my 'Tech for good Blog'. I'm going to write it down more often in 2022 :)"}
	       };

        public BlazorBlog.Shared.BlogPost GetBlogPostByUrl(string url)
        {
            return Posts.FirstOrDefault(p => p.Url.ToLower().Equals(url.ToLower()));
        }

        public List<BlazorBlog.Shared.BlogPost> GetBlogPosts()
        {
            return Posts;
        }
    }
}

 

FirstOrDefault() 메서드는 리턴되는 ResultSet이 Null일 수 있을 때 사용하며, 여러 데이터 중 1개의 데이터가 무조건 조회된다. 

(cf. First()메서드는 리턴값이 반드시 존재할 때 그 해당 조건의 첫번째 row를 리턴하기 위해 사용된다. 만약 First() 호출에서 리턴 결과가 없으면 Exception을 발생시킨다.)

 

 

3. Build a Web API & Make HTTP Calls with Blazor WebAssembly

API Controller - Empty (No methods)Web API를 생성하고, 비동기로 서버-클라이언트가 통신하는 방법을 배운다.
// BlazorBlog.Server > Controllers > BlogController.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace BlazorBlog.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BlogController : ControllerBase
    {
        public List<BlazorBlog.Shared.BlogPost> Posts { get; set; } = new List<BlazorBlog.Shared.BlogPost>
        {
        new BlazorBlog.Shared.BlogPost { Url = "new-tutorial", Title = "A New Tutorial about Blazor with Web API", Description="This is a new tutorial, showing you how to build a blog with Blazor", Content = "I'm going to introduce what Blazor is from now on. Enjoy!"},
        new BlazorBlog.Shared.BlogPost { Url = "first-post", Title = "My First Blog Post with Web API", Description = "Hi! This is my new shiny blog.", Content = "This is my 'Tech for good Blog'. I'm going to write it down more often in 2022 :)"}
        };

        [HttpGet]
        public ActionResult<List<BlazorBlog.Shared.BlogPost>> GiveMeAllTheBlogPosts()
        {
            return Ok(Posts);
        }

        [HttpGet("{url}")]
        public ActionResult<BlazorBlog.Shared.BlogPost> GiveMeThatSingleBlogPost(string url)
        {
            var post = Posts.FirstOrDefault(p => p.Url.ToLower().Equals(url.ToLower()));
            if (post is null)
            {
                return NotFound("This post does not exist.");
            }

            return Ok(post);
        }
    }
}

 

위의 파일을 보면 BlogController가 ControllerBase를 상속받고 있음을 알 수 있다. 기존에 MVC에서 사용되던 Controller는 Controller 클래스를 상속받는 데 반해, API 전용의 Controller에서는 API만을 위해 좀 더 경량화된 버전의 ControllerBase를 기본으로 상속받는다.

 

Routing 경로는 api/[controller]이며, [controller]는 생성된 Controller 클래스 이름에 해당한다. 따라서 실제 Routing 경로는 api/Blog가 된다.

 

Request URL

 

 

만약 다른 경로로 Routing 되기를 희망한다면 아래와 같이 [Route("api/abc")] 고정적인 경로를 줄 수도 있고, [Route("api/abc/def")] 처럼 서브경로를 포함해 지정해 줄 수도 있다. 

 

// 고정 라우팅 설정 예시

namespace BlazorBlog.Server.Controllers
{
    [Route("api/abc")]
    [ApiController]

    public class BlogController : ControllerBase
    {
        public List<BlazorBlog.Shared.BlogPost> Posts { get; set; } = new List<BlazorBlog.Shared.BlogPost>
        {
        new BlazorBlog.Shared.BlogPost { Url = "new-tutorial", Title = "A New Tutorial about Blazor with Web API", Description="This is a new tutorial, showing you how to build a blog with Blazor", Content = "I'm going to introduce what Blazor is from now on. Enjoy!"},
        new BlazorBlog.Shared.BlogPost { Url = "first-post", Title = "My First Blog Post with Web API", Description = "Hi! This is my new shiny blog.", Content = "This is my 'Tech for good Blog'. I'm going to write it down more often in 2022 :)"}
        };

 

// BlazorBlog.Client > Services > BlogService.cs


namespace BlazorBlog.Client.Services
{
    public class BlogService : IBlogService
    {
        private readonly HttpClient _http;
				
	// HttpClient 의존성 주입받을 생성자 BlogService
        public BlogService(HttpClient http)
        {
            _http = http;
        }
				
	// 비동기 처리
        public async Task<BlazorBlog.Shared.BlogPost> GetBlogPostByUrl(string url)
        {
            var post = await _http.GetFromJsonAsync<BlazorBlog.Shared.BlogPost>($"api/Blog/{url}");
            return post;
        }

        public async Task<List<BlazorBlog.Shared.BlogPost>> GetBlogPosts()
        {
            return await _http.GetFromJsonAsync<List<BlazorBlog.Shared.BlogPost>>("api/Blog");
        }

    }
}

 

// BlazorBlog.Client > Services > IBlogService.cs

namespace BlazorBlog.Client.Services
{
    interface IBlogService
    {
        Task<List<BlazorBlog.Shared.BlogPost>> GetBlogPosts();
        Task<BlazorBlog.Shared.BlogPost> GetBlogPostByUrl(string url);
    }
}

 

인터페이스도 마찬가지로 앞에 Task<> 리턴 타입을 붙여 비동기 처리한다.

 

 

// BlazorBlog.Client > Shared > BlogPosts.razor

@inject BlazorBlog.Client.Services.IBlogService BlogService

@foreach (var post in Posts)
{
    <div style="margin: 20px;">
        <div>
            <h3>@post.Title</h3>
        </div>
        <div>
            @post.Description
        </div>
        <div>
            <a href="/posts/@post.Url">Read more...</a>
        </div>
    </div>
}

@code {
    private List<BlogPost> Posts = new List<BlogPost>();

    protected override async Task OnInitializedAsync()
    {
        Posts = await BlogService.GetBlogPosts();
    }
}
// BlazorBlog.Client > Pages > Post.razor

@page "/posts/{url}"
@inject BlazorBlog.Client.Services.IBlogService BlogService

<h3>@CurrentPost.Title</h3>

<div>
    @CurrentPost.Content
</div>

@code {
    private BlazorBlog.Shared.BlogPost CurrentPost;

    [Parameter]
    public string Url { get; set; }

    protected override async Task OnInitializedAsync()
    {
        CurrentPost = await BlogService.GetBlogPostByUrl(Url);
    }

}

 

.razor 파일들도 마찬가지로 비동기로 구현하기 위해 위와 같이 코드를 수정한다. 

 

 

 

 

여기까지 하면 웹 통신은 정상적으로 잘 되지만, CurrentPost가 null값일 때는 위와 같은 오류가 발생한다는 것을 알 수 있다. 따라서 아래와 같이 @if(CurrentPost == null)문으로 조건을 추가해준다.

 

 

// BlazorBlog.Client > Pages > Post.razor

@page "/posts/{url}"
@inject BlazorBlog.Client.Services.IBlogService BlogService

@if (CurrentPost == null)
{
    <span>Getting that blog post from the service...</span>
}
else
{
    <h3>@CurrentPost.Title</h3>

    <div>
        @CurrentPost.Content
    </div>

}

@code {
    private BlazorBlog.Shared.BlogPost CurrentPost;

    [Parameter]
    public string Url { get; set; }

    protected override async Task OnInitializedAsync()
    {
        CurrentPost = await BlogService.GetBlogPostByUrl(Url);
    }

}

 

그러면 위와 같이 오류 없는 화면이 잘 나타난다!

 

 

4. Outro

이번 시간에는 크게 아래의 세 가지 항목들을 학습하였다.

 

  • 1) First Steps with Blazor WebAssembly & Razor Components
    • Blazor WebAssembly 기본 세팅 후 앱 빌드하는 방법을 살펴보았다.
  • 2) Services, Dependency Injection & Page Parameters with Blazor
    • 인터페이스와 클래스의 차이를 학습하였다.
    • @inject 지시문을 사용하여 .razor 파일에서 종속성을 주입받는 방법을 살펴보았다.
    • 경로 매개변수를 활용하여 라우팅하는 방법을 살펴보았다.
  • 3) Build a Web API & Make HTTP Calls with Blazor WebAssembly
    • API Controller로 Web API를 생성하고 비동기로 서버-클라이언트 통신하는 방법을 배웠다.