공부/인프런 - Rookiss

Part 6-5-6. Blazor 입문 : Templated Component

셩잇님 2024. 9. 7. 03:01
반응형

 

 

🦮 Blazor 입문

 

 지난 시간에는 Blazor의 Cascading Parameter의 개념에 대해서 학습하는 시간을 가져보았다. Cascading Parameter을 사용하여 User에 있는 List<string> 형태의 _options을 ShowUser에서도 사용하였으며, 아울러 부모-자식 관계를 넘어 손자, 증손자까지에게도 이를 적용할 수 있는 것에 대해서 알아보았다. 이번 시간에는 Templated Component 라는 개념에 대해서 학습하는 시간을 가져보며, 이 또한 직접 실습하며 페이지를 제작해보도록 한다.

 


 

🏄‍♀️ Templated Component

 

Templated Component. 단어 그대로 직역하자면 '템플릿 컴포넌트'이다. 여기서 말하는 템플릿의 개념은 C++ 개발자라면 대충 눈치를 챘을 것이다. C++에서 템플릿이란 C#의 제네릭과 동일한 의미이다. 예를 들어 우리가 C# 코드로 List를 만든다면 List 안에는 int, string 등 다양한 정보를 넣을 수 있다. 바로 이것이 제네릭 문법의 장점이다. 따라서 제네릭 형태로 리스트를 한 번만 만들어 주면 int 타입, string 타입, User 타입 등 다양한 타입을 지원한다. 이번에 배울 Templated Component가 바로 이와 같은 것이다.

 

 예를 들어보자. 경우에 따라 FetchData 컴포넌트에 있는 테이블을 다른 곳에서 재사용 한다고 가정해보자. 그렇다면 하나하나 복사해서 사용하기 보다는 어떻게든 이 테이블에 대한 템플릿을 하나 만들어 둔 다음, 이를 필요에 따라 데이터만 교체해서 사용하는 것이 훨씬 편할 것이다. 따라서 기존의 테이블을 템플릿 형태로 만들어보자.

 

@page "/fetchdata"

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

 

 위 테이블을 템플릿화 시켜보도록 하자.

 


 

 먼저 새로운 컴포넌트를 만들기 위해 Pages 폴더 산하에 TableTemplate 컴포넌트를 새롭게 만들어 주도록 하자. 기존의 FetchData에 사용하던 <table> ~ </table>의 내용을 복사-붙여넣기 하여 작성해주도록 하자.

 

@using BlazorApp.Data

<h3>TableTemplate</h3>

<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var forecast in forecasts)
        {
            <tr>
                <td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
        }
    </tbody>
</table>

@code {

    [Parameter]
    public RenderFragment Header { get; set; }
}

 

 이 후, 파라미터 속성을 추가하고 'RenderFragment' 타입의 Header를 생성해주도록 하자. RenderFragment는 일반적인 파라미터와 달리, HTML 태그들의 집합 자체를 넘겨준다. 라고만 이해하도록 하자. 생성해준 Heaer를 통해 <tr> ~ </tr>의 부분을 바꿔치기 하자.

 

@using BlazorApp.Data

<h3>TableTemplate</h3>

<table class="table">
    <thead>
        <tr>
            @Header
        </tr>
    </thead>
    <tbody>
        @foreach (var forecast in forecasts)
        {
            <tr>
                <td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
        }
    </tbody>
</table>

@code {

    [Parameter]
    public RenderFragment Header { get; set; }
}

 

 이 후, Header 데이터를 사용하기 위해 FetchData 컴포넌트로 돌아가 <TableTemplate> ~ </TableTemplate> 영역을 생성해주고, 이를 명시해주도록 하자. 

 

@page "/fetchdata"

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <TableTemplate Items="forecasts" TItem="WeatherForecast">
        <Header>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </Header>
    </TableTemplate>
    
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

 

 위와 같이 TableTemplate 영역과, 기존의 코드를 내버려둔 채 실행해보면 놀랍게도 기존의 Date, ~ Summary가 동일하게 뜨는 것을 볼 수 있다.

 

 


 

 이제 이어서 tbody 부분 또한 동일하게 만들어주도록 하자. 그러나 tbody를 살펴보면 foreach문을 통해 데이터를 정의하고 있는 것을 알 수 있다. 이 부분은 어떻게 처리해야 할까? 이 때에는 IReadOnlyList를 통해 처리하도록 한다. IReadOnlyList는 블레이저 .Net Core 와는 무관하게 System.Colletcion.Generic에 있는 내용임을 알 수 있다. 이는 인덱스로 무엇인가를 접근할 수 있는 컬력션을 나타내는 인터페이스이다. 이를 통해 Items를 정의하자. 

 

@code {

    [Parameter]
    public RenderFragment Header { get; set; }

    [Parameter]
    public IReadOnlyList<WeatherForecast> Items { get; set; }
}

 

 그러나 위와같이 설정할 경우 결국 Items는 <WeatherForecast>의 종속적일 수 밖에 없다. 따라서 나중에 User나 다른 데이터를 넣고 싶을 경우 이를 재사용할 수 없게 되어버린다. 따라서 이 또한 Generic에서 사용하는 느낌인 <T>와 같은 형태로 만들어주어야 하는데 이 문법도 당연히 이미 존재한다. 🤣

 

 코드 최상단에 @typeparam TItem을 만들어주도록 하자. TItem은 Generic에서 사용하는 <T>와 같다고 보면 된다. 따라서 Items를 <WeatherForecast>로 사용하는 것이 아닌 TItem으로 바꿔주도록 하자. 

 

@code {

    [Parameter]
    public RenderFragment Header { get; set; }

    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; }
}

 

 위와 같이 변경해주면 나중에 필요에 따라 <TItem>이 int, string, User 등 다양한 데이터로 처리할 수 있다. 이 경우 forecast를 넘겨 주고 있기 때문에 Items에서는 이를 WeatherForecast로 추론하게 된다. 그러나 경우에 따라 이를 찾지 못하는 경우가 생기니 이를 명시해주도록 하자.

 

@page "/fetchdata"

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <!-- TItem을 WeatherForecast로 명시한다 -->
    <TableTemplate Items="forecasts" TItem="WeatherForecast">
        <Header>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </Header>
    </TableTemplate>
}

@code {
    ...
}

 

 위와 같이 고치면 모든 것이 완벽할 것 같지만 이제는 TableTemplate 컴포넌트 내부의 foreach문이 말썽이다. 이 또한 <T>와 같은 형태로 수정하도록 하자. 사실 <TableTemplate Items="forecasts" TItem="WeatherForecast">를 통해 이를 명시해주어서 foreach문에서 문제가 없을 수 있겠지만 사실 그렇지 않다,

 

 예를들어 FetchData의 <TableTemplate Items="forecasts" TItem="WeatherForecast">의 TItem이 User로 변할 경우, TableTemplate의 foreach문은 이 변한 데이터에 대한 대응을 할 수 없기 때문에 아무리 명시를 해주어도 처리할 수 없는 것이다. 따라서 일전의 우리가 <Header> 부분을 작업한 것과 같이 tbody에서도 RenderFragment를 통해 이를 처리해야 한다. 

 

@code {

    [Parameter]
    public RenderFragment Header { get; set; }

    [Parameter]
    public RenderFragment<TItem> Row { get; set; }

    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; }
}

 

 기존에 Header에서는 RenderFragment의 인자값이 없었지만, Row에서는 <TItem>을 받아야 Generic의 <T>와 같이 활용할 수 있기 때문에 인자값으로 <TItem>을 받아오도록 처리한다. 이렇게 될 경우 TableTemplate의 tbody는 다음과 같이 바꿀 수 있다.

 

<table class="table">
    <thead>
        <tr>
            @Header
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr>
                @Row(item)
            </tr>
        }
    </tbody>
</table>

 

 위와같이 TableTemplate를 설정해주었다면 FetchData도 당연히 수정해주어야 한다. 아래와 같이 수정해주도록 하자.

 

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <TableTemplate Items="forecasts" TItem="WeatherForecast">
        <Header>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </Header>
        <Row Context="forecast">
            <td>@forecast.Date.ToShortDateString()</td>
            <td>@forecast.TemperatureC</td>
            <td>@forecast.TemperatureF</td>
            <td>@forecast.Summary</td>
        </Row>
    </TableTemplate>
}

 

 이를 통해 기존의 tbody의 있던 내용은 Row를 사용한다고 명시해주고, Row의 Context는 forecast라고 다시금 명시해주면 이제는 에러가 발생하지 않는 것을 볼 수 있다. 

 


 

 결론은 Templated Component는 붕어빵처럼 찍어낼 수 있는 컴포넌트 형태로 만들어지는 것을 의미한다. 그리고 반드시 RenderFragment가 필요하며, RenderFragment를 사용한다고 하더라도 사용 시에는 <Header>, <Row>에 각각 역할을 지정해 준 것처럼 역할 지정이 필요하다. 그리고 <T> 타입을 선언하는 부분에서는 @typeparam TItem와 같이 C#의 Generic과 동일한 형태를 띄고 있으며 typeparam과 변수 설정에서 이를 적절히 사용하여 템플릿을 만드는 것을 알 수 있다. 

 

 

반응형