천객만래 [千客萬來] (It has an interminable succession of visitors)

'템플릿'에 해당되는 글 1건

  1. 2015.06.01 [개발/asp.net] UserControl 템플릿을 이용한 공통 UI 렌더링

출처: http://www.taeyo.pe.kr/lecture/NET/UcTemplate.asp

강좌 최초 작성일 : 2006년 11월 14일
   강좌 최종 수정일 : 2006년 11월 15일

   강좌 읽음 수 : 4359 회

   작성자 : Taeyo(김 태영)
   편집자 : Taeyo(김 태영)

   강좌 제목 : UserControl 템플릿을 이용한 공통 UI 렌더링 

강좌 전 태오의 잡담>

이제 2006년의 크리스마스도 한달 정도밖에 남지 않았네요.
여러분들은 크리스마스에 무엇을 하실 생각이신가요?
무엇을 계획하던 한 해를 잘 정리하면서~ 즐겁게 보내시기 바랍니다.


공통적으로 자주 사용하는 UI 영역을 필요로 할 경우 여러분은 주로 어떻게 작업을 해 오셨나요? 대부분의 경우는 그 영역을 User Control이나 Custom Control로 제작해서 사용해 오셨을 것입니다. "나는 둘 다 아니었다!!"라고 자신하시는 분은 좋았어!! 패스!! -_-+

대개의 경우는 User Control을 이용했을텐데요. 이는 User Control의 개발이 상당히 쉽기 때문일 것입니다. Custom Control의 경우는 개발에 적지 않은 내공과 시간이 요구되기에 쉽사리 덤벼들 수 없지만, User Control은 기존에 존재하는 컨트롤들을 복합적으로 묶어서 쉽게 출력 영역을 꾸밀 수 있으니까요.

이번 강좌에서 하고자 하는 이야기는 바로 이러한 User Control을 사용함에 있어서, UI 개발자들이 좀 더 손쉽게 이를 다룰 수 있게, 그리고 이를 일종의 템플릿처럼 이용할 수 있게 하는 작은 기반을 만드는 방법에 대한 것입니다. 반드시 필요한 작업은 아닐 수 있지만, 일단 작성해두면 전반적인 개발이 편리해진다고나 할까요? 개발 생산성 및 유지 보수성이 좋아지는 방안이라고 보시면 될 듯 합니다.

제가 업무 프로젝트에서 하는 역할이 주로 아키텍처를 잡는다거나, 공통 모듈을 설계 및 가이드 한다거나, 개발자들의 개발 생산성을 높이기 위한 프레임워크 작업을 한다거나 하는 작업이다보니 이러한 쪽에 대한 관심이 본의 아니게 생길 수 밖에 없더군요(실은, 업체의 개발표준팀에서 그런 것들을 요구해 오거든요. 이런 게 있으면 편할 것 같은데 만들어 주세요. 이것 좀 자동화할 수 있는 도구가 있었으면 좋겠어요. 등등 -_-+).

아! 그러니깐, 정확히 이번 강좌가 어떤 내용을 다루는지 감이 잘 오지 않으신다구요? 그렇다면, 좀 더 구체적으로 말씀을 드려야 할 듯 하네요. 이번 강좌의 주 내용은 화면에 공통적으로 사용되는 출력 영력을 User Control로 만들고 이를 동적으로 손쉽게 페이지에 끼워넣을 수 있게 하려 합니다.

"설마, Page.LoadControl() 메서드를 이용해서 페이지에 동적으로 User Control을 넣자는 이야기를 하려는 것은 아니겠죠?" 라고 하시는 분들이 계시는데요. 다행히도 그 이야기는 아닙니다(엄밀히 말하면, 일부는 맞기도 합니다만 -_-). Page.LoadControl() 메서드를 이용해서 페이지에 동적으로 User Control을 추가하는 것은 User Control의 출력 데이터가 정적인 경우에 한해 부합할 것입니다. 만일, User Control이 Repeater나 DataList 등의 컨트롤로 구성되어 있고(예를 들면, 공지사항 목록 같은), 데이터 바인딩이 된 상태로 Page에 추가되어야 한다면 어떻게 할까요?

"뭡니까? 장난하십니까? 그럼 User Control안에서(예를 들면, Control의 Page_Load 이벤트) 데이터 바인딩 작업을 하면 되잖아요"라고 이야기하시는 분들 계십니다. 핫!!! 맞는 말씀입니다. 그런 방법이 있었군요. 음… 그렇네요.. 흠… 그렇구나…

그렇습니다. 일반적으로는 그렇게들 작업을 하셨을 것입니다. 물론, 공지사항 목록의 출력 같은 경우는 공통적으로 여기 저기에서 자주 사용되기에 분명 User Control(혹은 별도의 컨트롤)로 만들어서 사용하는 것이 이로울 것입니다. 그리고, 그 경우 대부분 데이터 액세스 하는 코드가 .ascx 파일 안에 담기게 되곤 하죠. N-Tier 작업을 많이 해보신 분들이라면 Business Logic 레이어와 Presentation 레이어를 명확하게 구분하여 작업을 하셨을 것입니다. 그렇기에 물론, 이와 같은 경우라 해도, ascx 파일 안에 데이터를 직접적으로 액세스 하는 코드는 작성하지 않을 것입니다. 다만, 비즈니스 로직 컴포넌트(BLC)나 데이터 액세스 컴포넌트(DAC)를 호출하여 얻어온 결과를 바인딩 하는 코드 정도만이 놓여져 있겠죠? 요는 어쨌든 그러한 코드가 있기는 있어야만 한다는 것입니다. 그렇기에, 그 User Control은 오로지 공지사항을 출력하기 위한 "공지사항 용 User Control"이란 이름을 갖게 될 것입니다. 왜냐하면, 그러한 목적으로 User Control을 꾸민 것이니까요.

하지만, User Control을 작성하다 보면, 출력하는 데이터만 다를 뿐이지, 그 출력 유형이 매우 유사한 경우를 많이 접하게 됩니다. 바로 여기서 한 가지 아이디어가 뾰료롱 생겨납니다. User Control을 템플릿처럼 사용할 수는 없을까? 즉, 공지사항 데이터던 주소 목록 데이터던 데이터를 던져주면 목록을 출력하는 User Control을 만들어두면 개발 생산성이 더 나아지지 않을까? 하는 생각이 들 수 있다는 것이죠. 바로 그것이 이번 강좌에서 이야기하고자 하는 내용입니다.

전용 User Control(공지사항용 컨트롤, 공통 데이터 출력용 컨트롤 등 특정 데이터를 출력하기 위해 작성한 컨트롤)을 작성하는 것이 나쁘다는 이야기는 아닙니다. 그는 매우 일반적인 방법이니까요. 다만, 상황에 따라서는 이를 좀 더 생산성이 높은 방향으로 바꿔보는 것도 나쁘지 않다는 생각입니다(생산성은 좋아지는 반면, 성능적으로는 약간의 희생이 따를 수 있습니다). 즉, 별도의 컨트롤러 클래스가 있어서 그가 동적으로 User Control에 데이터를 채우고 바인딩을 하도록 하자는 것이죠. 이런 식으로 구성하게 되면, ascx는 단지 출력을 위한 코드만을 담게되어 매우 간결해지며, ascx를 일종의 화면 템플릿 격으로 사용할 수 있게 됩니다.

예를 들면, 다음은 제가 작성해 본 공지사항 User Control(View.ascx)의 소스 코드 전체입니다.

View.ascx

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="View.ascx.cs" Inherits="Template_View" %>
<asp:DataList ID="DataList1" runat="server">
    <ItemTemplate>
        <%# Eval("title") %>
    </ItemTemplate>
</asp:DataList>

View.ascx.cs

public partial class Template_View : System.Web.UI.UserControl
{
    private object _data;
    public object Data
    {
        get { return _data; }
        set { _data = value; }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        DataList1.DataSource = Data;
        DataList1.DataBind();
    }
}

보시다시피, User Control에는 데이터 바인딩 코드 외에는 아무것도 존재하지 않습니다. 바인딩을 위한 데이터를 가져오는 코드도 존재하지 않습니다. 이는 외부 컨트롤러 클래스가 동적으로 데이터를 밀어넣도록 할 것이기 때문입니다.

이 방식의 장점은 화면 개발자가 User Control의 HTML 구역에만 집중해서 작업을 할 수 있다는 것입니다. 내부적으로 데이터가 어떻게 바인딩되는지 어쩌는지 전혀 관심을 갖지 않아도 됩니다. 화면의 디자인만을 생각하면 되기에 이 User Control을 일종의 템플릿처럼 이를 사용할 수 있다는 것이죠. 주어지는 데이터가 어떤 것이냐에 따라 공지사항을 출력할 수도 있고, 다른 목록 데이터들을 출력할 수도 있습니다.

결과적으로, 이는 동일한 결과를 얻기 위한 또 다른 방식(개발 생산성을 높이는 방식)입니다. 하지만, 그렇다고 이 방식이 꼭 옳다고 말할 수는 없습니다. 제가 진행하는 프로젝트에서는 이를 이용하여 개발성 향상과 유지 보수성의 향상이 있었습니다만, 여러분의 상황에서는 오히려 번잡스럽고 어렵게 느껴질 수도 있습니다. 그러니, 끝까지 잘 살펴보시고 판단하시기 바랍니다. 그러나, 이를 여러분의 업무에 적용하지는 않는다 하더라도, 컨셉 정도는 느껴보시기를 권해 봅니다.

사실, 이 컨셉은 ASP.NET의 대부인 Scott Guthrie의 블로그에 올라온 "Cool UI Templating Technique to use with ASP.NET AJAX for non-UpdatePanel scenarios"란 글에서 힌트를 얻어 작성한 것입니다. 해서, 사실 이 컨셉은 Ajax 애플리케이션을 제작할 경우에 더 적절하다고 보여집니다.

 http://weblogs.asp.net/scottgu/archive/2006/10/22/Tip_2F00_Trick_3A00_-Cool-UI-Templating-Technique-to-use-with-ASP.NET-AJAX-for-non_2D00_UpdatePanel-scenarios.aspx

자. 그렇다면, 전체적인 구현 설계를 살펴보고, 실제 구현을 한번 해보도록 하겠습니다.



TitlesManager란 클래스는 pubs 데이터베이스의 titles 테이블에서 책 데이터(DataTable)를 가져오는 역할을 수행하며, ViewManager를 이용하여 얻어온 데이터를 User Control에 바인딩한 뒤, 이를 반환하는 메서드를 제공하는 클래스입니다. 설명만으로는 이해가 어려울 수 있지만, 잠시 뒤에 소스 코드를 보시면 이해가 되실 것입니다. 사실, TitlesManager 클래스를 별도로 제작할 필요는 없습니다만(그냥 aspx 페이지에 코드를 넣어도 동작하는 데에는 문제가 없으니), 논리적으로 역할 구분을 하는 것이 설계적으로 좋아보이기에 별도의 클래스로 작성해 보았습니다.

ViewManager의 역할은 이 컨셉에서 매우 중요하지만, 그 내용은 의외로 간단합니다. 특정 ascx 컨트롤의 속성에 동적으로 데이터(TitlesManager가 넘겨주는 책 목록 데이터-DataTable)를 추가한 뒤, 그를 바인딩한 User Control을 반환하는 역할을 제공하는 것입니다. 기본 예제에서는 동적으로 User Control에 데이터를 건네주기 위해서 리플렉션 기술을 이용하고 있지만, 이는 차후 인터페이스를 이용하는 방식으로 바꾸어 성능적인 향상을 꾀할 수도 있을 것입니다.

어렵다구요? 말로만 설명하면 복잡한 감이 없지 않으니 직접 소스를 보면서 이해해 보도록 하시죠. 우선 TitlesManager.cs 클래스부터 살펴보도록 하겠습니다.

TitlesManager.cs

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

/// <summary>
/// TitlesManager의 요약 설명입니다.
/// </summary>
public class TitlesManager
{
    private DataTable GetTitles()
    {
        SqlConnection con = new SqlConnection();
        con.ConnectionString = "server=(local);database=pubs;uid=**;pwd=**";
        string sql = "Select title from Titles";
        SqlCommand cmd = new SqlCommand(sql, con);
        SqlDataAdapter adapter = new SqlDataAdapter(cmd);
        DataSet ds = new DataSet();

        adapter.Fill(ds);
        return ds.Tables[0];
    }

    public UserControl GetTitlesRenderControl()
    {
        DataTable titles = this.GetTitles();

        if (titles.Rows.Count > 0)
            return ViewManager.GetViewControl("~/Template/View.ascx", titles);
        else
            return ViewManager.GetViewControl("~/Template/NoView.ascx", null);
    }
}

TitlesManager 클래스는 우선 GetTitles()라는 메서드를 가지고 있습니다. 이는 데이터베이스로부터 책 목록 데이터를 가져와서 그를 DataTable 형식으로 반환하는 메서드입니다. n-Tier 구조로 설계되었다면, 사실상 이 메서드는 TitlesManager 클래스가 아닌 Data Access Component 쪽에 놓여져야 하겠습니다만, 여기서는 데모의 목적으로 코드를 여기에 직접 작성해 보았습니다. 만일, 여러분의 프로젝트에 개발 프레임워크가 도입되어 있고, 이미 Business Logic 계층과 Data Access 계층이 존재한다면 이 메서드는 그 안쪽에 작성되어야 할 것입니다.

TitlesManager 클래스의 핵심 메서드는 사실 GetTitlesRenderControl() 입니다. 이 메서드가 바로 ViewManager 클래스를 사용해서! 동적으로 필요한 User Control을 얻어내는 메서드이니까요. 해서, 소스를 보시면 이는 우선 GetTitles() 메서드를 이용해서 필요한 DataTable을 얻어내고 있으며, 그 가져온 데이터가 존재한다면(한 개 이상의 Row를 가진다면) View.ascx란 템플릿을, 데이터가 존재하지 않는다면 NoView.ascx란 템플릿을 동적으로 얻어와 반환하는 것을 볼 수 있습니다(View.ascx란 템플릿을 사용하는 경우에는 바인딩 데이터를 필요로 하므로, DataTable 개체를 인자로 넘겨주어야 합니다).

대략적인 TitlesManager 클래스의 역할이 이해가 되시나요? 그렇습니다. 별 것 없습니다. 쿼리 결과 데이터가 존재하면 View.ascx를, 데이터가 없으면 NoView.ascx를 얻어오는 ViewManager 클래스의 GetViewControl 메서드를 호출하는 것이 전부이니까요.

그렇기에, 여기서 중요한 것은 무엇보다도 ViewManager.GetViewControl() 라는 것을 여러분은 번쩍! 눈치 채셨을 것입니다. 실제로 ascx 템플릿과 주어진 데이터를 이용해 동적으로 User Control을 생성하는 핵심 역할을 하는 친구가 바로 ViewManager.GetViewControl()이니까요. 그렇다면, 이제 ViewManager 클래스의 코드를 한번 살펴보도록 하겠습니다.

ViewManager.cs

using System;
using System.Web;
using System.Web.UI;
using System.IO;
using System.Reflection;

public class ViewManager
{
    public static UserControl GetViewControl(string path, object data)
    {
        Page page = new Page();
        UserControl viewControl = (UserControl) page.LoadControl(path);

        if (data != null)
        {
            Type viewControlType = viewControl.GetType();
            PropertyInfo prop = viewControlType.GetProperty("Data");

            if (prop != null)
            {
                prop.SetValue(viewControl, data, null);
            }
            else
            {
                throw new Exception(path + "는 Data 속성을 가지고 있지 않습니다");
            }
        }

        return viewControl;
    }
}

ViewManager 클래스의 핵심은 GetViewControl라는 메서드입니다. 이는 주어진 ascx 파일명과 바인딩 데이터를 인자로 받아서 그를 이용해 동적으로 User Control을 생성하는 역할을 합니다. 해서, 처음 두 줄의 코드는 일단 지정된 경로의 ascx 파일을 User Control로 얻어내는 작업을 수행하고 있어요.

Page page = new Page();
UserControl viewControl = (UserControl) page.LoadControl(path);

만일, 이렇게 얻어온 User Control이 정적인 데이터만을 가진다면, 현재의 viewControl 개체를 그대로 반환해도 무관하겠지만, 고작 그런 작업을 하려했다면 굳이 이렇게 복잡하게 ViewManager를 제작할 필요조차 없었을 겁니다. ViewManager가 필요한 이유는 바로 동적으로 User Control의 속성을 설정할 필요가 있기 때문이니까요.

코드는 page.LoadControl() 메서드를 이용해서 UserControl 개체를 얻어오긴 했으나, 문제는 이 상태로는 해당 UserControl이 Data 라는 속성을 갖는지 어떤지 알 수가 없다는 것입니다. UserControl 형식의 .NET 클래스는 Data라는 필드를 기본적으로 가지고 있지 않으니까요.

하지만, 우리가 동적으로 로드하고자 하는 View.ascx 는 분명 Data라는 공개 속성을 가지고 있습니다. 그렇다면, 어떻게 프로그래밍적으로 이 개체의 Data 필드를 접근할 수 있을까요?

우리가 위에서 작성해놓은(위에서 소스를 보여드렸었죠?) View.ascx의 소스를 다시금 살펴보면 그는 Data라는 object 형식의 속성을 가지고 있음을 알 수 있습니다. 그리고, 컨트롤의 Page_Load 이벤트 시에는 그 Data를 가지고 데이터 바인딩을 시도함을 알 수 있죠. 이는 우리끼리 사전 정의해 놓은 규칙입니다. 즉,

"모든 ascx 템플릿들은 Data라는 공개 속성을 가지며, 데이터 바인딩은 그 데이터를 가지고 이루어진다"

가 우리끼리 약속해놓은 규칙이라는 것이죠. 다시 말해서, 우리가 템플릿으로 이용할 모든 ascx들은 Data라는 공개 속성을 가져야만 합니다. 현실적으로 이야기하자면, 이 규칙은 프로젝트의 클래스 설계 표준문서에 기록되어 있어야 할 것이고, 개발자들에게 사전에 숙지시켜야만 하는 부분이어야 할 것입니다.

그렇습니다. 방법은 크게 두 가지가 있습니다. 하나는 Data라는 속성을 갖는 인터페이스를 작성해서 View.ascx 클래스가 그 인터페이스(예를 들면, IcommonUC란 이름으로)를 구현하도록 하고, 다음과 같이 코드를 작성하는 방법이 있겠죠?

Page page = new Page();
ICommonUC viewControl = (ICommonUC)page.LoadControl(path);

사실, 이 방법이 제가 강좌에서 설명하려는 리플렉션 방법보다 훨씬 더 효과적인 방법임에는 틀림이 없습니다. 인터페이스를 사용한다면 위의 GetViewControl() 메서드도 다음과 같이 매우!! 간단해질 수 있을테니까요.

public static UserControl GetViewControlwInterface(string path, object data)
{
    Page page = new Page();
    UserControl ctl = (UserControl)page.LoadControl(path);
    ICommonUC viewControl = ctl as ICommonUC;

    if(viewControl != null)
        viewControl.Data = data;

    return ctl;
}
// 요 부분 코드는 유 경상 수석이 간섭했다는 후문이 돌고 있습니다.

사실, 결과론적으로는 상기와 같이 인터페이스를 사용하는 것을 강력히 추천합니다. 하지만, 이 강좌는 리플렉션을 이용해서 속성을 설정하는 내용을 보여드리고 있습니다. 다시 보여드리자면,

Type viewControlType = viewControl.GetType();
PropertyInfo prop = viewControlType.GetProperty("Data");

if (prop != null)
{
    prop.SetValue(viewControl, data, null);
}

가 바로 그러한 코드이죠. 리플렉션을 이용해서 현재 개체 형식이 Data라는 속성을 갖는지 여부를 검사하고, 만일 해당 속성이 존재한다면 그 속성에 데이터(우리의 경우 책 목록 DataTable)을 밀어넣는 것입니다.

리플렉션을 이용하는 방식은 성능적으로 그다지 좋지 않기에 크게 권장하지는 않습니다만, 제가 참고한 원래의 포스트 글이 이 방식을 사용했기에 저도 이 방식으로 글을 작성했습니다. 개인적으로는 리플렉션보다는 인터페이스를 사용하는 쪽을 권장하고 싶습니다. ^^;

자. 이제 TitlesManager와 ViewManager가 모두 완성되었네요. ViewManager는 다른 곳에서도 두고두고 재사용이 가능한 독립적인 모듈입니다. 만일, 여러분이 책 목록이 아닌 고객 데이터 목록을 다루고 싶다면, CustomerManager와 같은 클래스를 만들고 (코드는 TitlesManager와 유사하겠죠?) 내부적으로 ViewManager를 사용하시면 될 것입니다. ^^

자. 그럼 이제 이를 이용하는 ASPX 페이지를 만들어봐야 겠죠? 저는 default.aspx 페이지에 Label 컨트롤을 하나 올려놓고(굳이 Label일 이유는 없지만), 코드 비하인드에는 다음과 같이 코드를 작성해 보았습니다.

default.aspx.cs

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        UserControl ctl = (new TitlesManager()).GetTitlesRenderControl();
        lblData.Controls.Add(ctl);
    }
}

간단하죠? 다음은 전체 소스가 동작하여 나온 결과화면입니다.(저는 약간의 디자인을 입혀봤습니다. 그래봐야 큰 차이는 없지만 -_-)



나름대로 멋지지 않나요? ㅎㅎ

이것이 이번 강좌에서 전하고 싶었던 내용입니다. 사실, 제대로 이해하려면 전체 소스를 한번 돌려보고 디버깅 잡고, 소스를 따라가면서 확인하는게 더 좋긴 하죠. 해서, 강좌 하단에서는 전체 소스를 다운로드 할 수 있도록 준비해 두었습니다. 전체적으로 돌아가는 방식을 확인하시고 나면 이 방식이 맘에 드시는 분들도 조금 있지 않을까 생각해 봅니다.

그리고, 추가적으로 ViewManager 클래스를 보면 User Control을 직접적으로 반환하지 않고, 그 User Control의 내용을 HTML 문자열로 렌더하여 반환하는 메서드도 들어있는 것을 보실 수 있을 것입니다. ASP.NET 애플리케이션에서는 이 메서드가 그다지 유용하지 않을 수 있지만, AJAX 애플리케이션을 개발한다면 나름대로 쓸만하다고 할 수 있습니다. 그 메서드를 웹 서비스의 메서드로 빼 놓으면, 클라이언트 측에서 손쉽게 목록 데이트 출력을 얻어와 화면에 반영할 수 있을테니까요 ^^;

그리 어려운 내용은 아닌데, 제가 설명을 간결하게 하지 못해서 오히려 여러분을 어렵게 만든 것은 아닌지 걱정됩니다. 하지만, 맘에는 드셨죠?



UcTemplate.zip





Posted by SB패밀리