12 min read

블레이저 웹 어셈블리로 헤드리스 CMS 만들어보기

Justin Yoo

정적 웹사이트 구현 시나리오 중 하나는 바로 자신의 블로그 사이트를 운영하는 것이다. 이 때 만약 그동안 서비스형 워드프레스를 이용해 블로그 사이트를 운영해 왔다면, 이를 정적 웹사이트로 이전하는 작업 역시 만만치 않다. 그런데, 만약 기존의 워드프레스 사이트를 그대로 두고 껍데기만 정적 웹사이트로 구현 가능하다면 어떨까? 게다가 그 정적 웹사이트를 C#을 이용한 블레이저 웹어셈블리 형식으로 구현할 수 있다면? 기존의 워드프레스 사이트는 데이터 저장소 용도로 사용하고 나머지는 내 맘대로 작성할 수 있다면 무척이나 편리할 것이다.

이 포스트에서는 워드프레스를 데이터 저장소이자 헤드리스 CMS로 두고, 블레이저 웹어셈블리를 이용해 정적 웹사이트를 만든 후 이를 애저 정적 웹 앱 인스턴스에 배포하는 것 까지 한 번 해 보기로 한다.

이 포스트에서 사용한 샘플 애플리케이션 코드는 깃헙 리포지토리를 통해 다운로드 받을 수 있다.

서비스형 워드프레스 사이트

예전에 운영하다가 더이상 운영하지 않는 서비스형 워드프레스 사이트가 하나 있다.

Old Wordpress Blog Site

이 사이트를 정적 웹사이트로 이전하기 위해서는 우선 이 사이트의 컨텐츠에 접근할 수 있어야 한다. 워드프레스 사이트는 HTTP API를 제공하기 때문에 이를 이용해서 데이터를 가져올 수 있다. 아래 명령어를 터미널에서 입력해 보자.

curl -X GET https://public-api.wordpress.com/rest/v1.1/sites/<site-name>.wordpress.com/posts/

혹은 포스트맨을 통해 실행시켜 보면 아래와 같이 컨텐츠를 받아올 수 있다.

Result from Wordpress API

이제 이 데이터를 블레이저 웹어셈블리 앱에 출력시키기만 하면 된다. 이 때 두 가지 방법이 있다.

  1. 블레이저 웹 어셈블리 앱에서 직접 API를 호출하는 방법
  2. 블레이저 웹 어셈블리 앱에서 프록시 API를 거쳐 API를 호출하는 방법

여기서는 두 번째 방법으로 시도해 보기로 하자.

프록시 API 앱

애저 정적 웹 앱을 호스팅하기 위해 프록시 API 앱은 굳이 필요하지는 않다. 다만, CORS 이슈가 있다거나, 다양한 종류의 API를 호출한다거나, 보안 이슈가 있을 경우에는 프록시 API를 사용하는 것이 좋다. 애저 정적 웹 앱은 자체적으로 이 프록시 API 기능을 제공하고 있으므로 여기서도 그 기능을 이용하기로 한다. 우선 아래와 같이 애저 펑션앱의 HTTP 트리거를 생성한다.

워드프레스 API를 호출하기 위한 엔드포인트 URL을 GetPosts로 두고 HttpClient 인스턴스를 생성한다.

public static class PostHttpTrigger
{
    private const string GetPosts = "https://public-api.wordpress.com/rest/v1.1/sites/{0}/posts";
    private static HttpClient http = new HttpClient();

아래와 같이 애저 펑션의 OpenAPI 확장 기능을 추가한다. 이를 이용하면 블레이저 웹어셈블리 앱에서 좀 더 쉽게 이 프록시 API에 접근할 수 있다. 이 부분은 다시 아래에서 언급할 예정이다.

    [FunctionName("PostHttpTrigger")]
    [OpenApiOperation(operationId: "posts.get", tags: new[] { "posts" })]
    [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(PostCollection), Description = "The OK response")]
    public static async Task<IActionResult> GetPostsAsync(
        [HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "posts")] HttpRequest req,
        ILogger log)
    {

환경 변수 값을 통해 실제 워드프레스 사이트 이름을 가져온다.

        var requestUri = new Uri(string.Format(GetPosts, Environment.GetEnvironmentVariable("SITE__NAME")));

마지막으로 이 워드프레스 API 엔드포인트를 호출한 후 값을 받아와서 PostCollection 이라는 개체로 비직렬화 시킨다.

        var json = await http.GetStringAsync(requestUri).ConfigureAwait(false);
        var posts = JsonConvert.DeserializeObject<PostCollection>(json);

        return new OkObjectResult(posts);
    }
}

PostCollection 개체와 그 부속 개체는 대략 아래와 같이 생겼다. 앞서 스크린샷과 같이 굉장히 방대한 데이터가 있지만, 여기서는 딱 필요한 부분만 정의하기로 한다.

public class PostCollection
{
    public virtual int Found { get; set; }

    public virtual List<PostItem> Posts { get; set; }

    [JsonProperty("meta")]
    public virtual Metadata Metadata { get; set; }
}

public class PostItem
{
    [JsonProperty("ID")]
    public virtual int PostId { get; set; }

    public virtual Author Author { get; set; }

    [JsonProperty("date")]
    public virtual DateTimeOffset DatePublished { get; set; }

    public virtual string Title { get; set; }

    [JsonProperty("URL")]
    public virtual string Url { get; set; }

    public virtual string Excerpt { get; set; }

    public virtual string Content { get; set; }
}

public class Author
{
    [JsonProperty("ID")]
    public virtual int AuthorId { get; set; }

    [JsonProperty("first_name")]
    public virtual string FirstName { get; set; }

    [JsonProperty("last_name")]
    public virtual string Surname { get; set; }

    public virtual string Name { get; set; }
}

public class Metadata
{
    public virtual Dictionary<string, string> Links { get; set; }

    [JsonProperty("next_page")]
    public virtual string NextPage { get; set; }

    [JsonProperty("wpcom")]
    public virtual bool IsWordpressCom { get; set; }
}

마지막으로 local.settings.json 파일을 아래와 같이 설정한다. 위 코드에서 SITE__NAME 값을 참조하기 때문에 환경 변수를 설정했고, 앞으로 작성할 블레이저 웹어셈블리 앱과 연결시키기 위해 CORS 설정을 한다.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",

    "SITE__NAME": "<site-name>.wordpress.com"
  },
  "Host": {
    "CORS": "*"
  }
}

위와 같이 정리한 후 프록시 API 앱을 실행시켜 보면 대략 아래와 같이 데이터를 확인할 수 있다.

Result from Proxy API

이제 프록시 API는 준비가 끝났다. 프록시 API에 접근하기 위한 OpenAPI 문서 URL은 아래의 셋 중 하나를 선택하면 된다.

  • http://localhost:7071/api/swagger.json – OpenAPI V2
  • http://localhost:7071/api/openapi/v2.json – OpenAPI V2
  • http://localhost:7071/api/openapi/v3.json – OpenAPI V3

블레이저 웹어셈블리 앱

우선 비주얼 스튜디오를 이용해서 블레이저 웹 어셈블리 앱 프로젝트를 생성한다.

Blazor WASM Project

프로젝트 이름을 WebApp으로 하고 나머지는 모두 기본값으로 둔 후 앱을 생성한다. 앱이 생성된 후에 곧바로 F5 키를 눌러 실행시켜 보면 아래와 같이 보일 것이다.

Blazor WASM App

이제 여기에 앞서 생성한 프록시 API를 연결할 차례이다. 우선 백그라운드로 애저 펑션 앱이 현재 작동중인 것을 확인한 후 아래와 같이 "Connected Services" 메뉴를 클릭한다.

Connected Service Menu

그리고 나오는 화면에서 "➕" 버튼을 클릭한다.

Add Reference

서비스 레퍼런스를 선택할 수 있는 화면이 나오는데, 여기서 "OpenAPI"를 선택한다.

Choose OpenAPI

그러면 나오는 화면에서 OpenAPI URL을 입력하고, 네임스페이스를 WebApp.Proxies, 클라스 이름을 ProxyClient로 하고 마무리한다.

Enter OpenAPI Details

그러면 아래와 같이 프록시 API가 추가된 것을 볼 수 있다.

OpenAPI Reference Added

이 때, 오른쪽의 점 세 개 버튼을 클릭해서 보이는 "View generated code" 메뉴를 클릭하면 OpenAPI 문서를 읽어서 자동으로 코드를 생성해 준 것을 확인할 수 있다.

Auto-generated Code from OpenAPI

우리는 이제 이 코드를 사용하기만 하면 되므로, 아래와 같이 Program.cs 파일을 통해 ProxyClient에 대한 의존성을 정의한다.

로컬 개발일 경우에는 기본 설정된 Base URL 값이 http://localhost:7071/api이기 때문에 상관 없지만, 애저로 배포할 경우에는 실제 호스트 주소를 사용하라는 내용도 포함되어 있다 (line #9-13).

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

// Add these lines to inject the dependency of ProxyClient
builder.Services.AddScoped(sp =>
{
    var client = sp.GetService<HttpClient>();
    var api = new ProxyClient(client);
    if (!builder.HostEnvironment.IsDevelopment())
    {
        var baseUrl = $"{builder.HostEnvironment.BaseAddress.TrimEnd('/')}/api";
        api.BaseUrl = baseUrl;
    }

    return api;
});

이제 블레이저 웹어셈블리 앱은 프록시 API를 자유롭게 호출할 수 있게 되었으니, 실제로 페이지상에서 이를 호출해 보기로 하자. 여기서는 index.razor 페이지를 이용해서 블로그 포스트 리스트만 보여주는 과정을 구현한다.

우선 아래와 같이 앞서 Program.cs에 추가했던 ProxyClient 의존성 개체를 주입한다.

@page "/"

@* Inject ProxyClient dependency *@
@using WebApp.Proxies
@inject ProxyClient Api

아래과 같이 <SurveyPrompt> 컴포넌트 밑에 테이블로 포스트 리스트를 받아올 수 있게 처리한다.

...
<SurveyPrompt Title="How is Blazor working for you?" />

@if (posts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Author</th>
                <th>Title</th>
                <th>Excerpt</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var post in posts)
            {
                <tr>
                    <td>@post.Date.ToString()</td>
                    <td>@post.Author.Name</td>
                    <td><a href="@post.URL">@post.Title</a></td>
                    <td>@((MarkupString)post.Excerpt)</td>
                </tr>
            }
        </tbody>
    </table>
}

아래와 같이 프록시 API를 호출해서 포스트 리스트를 가져온다.

@code {
    private List<PostItem> posts;

    protected override async Task OnInitializedAsync()
    {
        var collection = await Api.Posts_getAsync().ConfigureAwait(false);
        posts = collection?.Posts.ToList();
    }
}

이제 다시 블레이저 앱을 실행시켜보자. 그러면 아래와 같이 첫 페이지에서 블로그 포스트 리스트를 확인할 수 있다.

List of Blog Posts

여기까지 한 후 프록시 API 앱과 블레이저 웹어셈블리 앱을 깃헙 리포지토리에 푸시한다.

애저 정적 웹 앱에 호스팅하기

위와 같이 모든 앱 코딩이 끝났다면, 이제 애저 정적 웹 앱에 배포할 차례이다. 이 부분은 예전에 이미 다뤄봤기 때문에 여기서는 별도의 언급 없이 이전 포스트 링크로 대체하기로 하자.

이렇게 애저 정적 웹 앱 호스팅이 끝났다면, 실제로 웹사이트에 접속하면 아래와 같이 보인다.

List of Blog Posts on Azure


지금까지 서비스형 워드프레스를 헤드리스 CMS로 이용해서 애저 정적 웹 앱블레이저 웹어셈블리애저 펑션 프록시 API를 통해 웹사이트를 구현하는 방법에 대해 알아보았다. 이 포스트에서는 간단한 컨셉 정도만을 구현했지만, 기본적인 큰 틀은 다 구현을 해봤기 때문에 적절한 API 호출과 UI 레이아웃만 구성한다면, 기존의 서비스형 워드프레스를 그대로 사용한 채로 나만의 멋진 블로그 사이트를 운영할 수도 있을 것이다.