'부분 렌더링'에 해당되는 글 1건

  1. 2008.11.15 웹 서비스를 이용한 페이지 부분 렌더링 (4)

예제 코드 다운로드: WebApplication1.zip (34.81 kb)

안녕하세요? 웹지니입니다.

여러분, AJAX 많이 사용하시지요? XMLHttpRequest 객체를 이용한 비동기 호출과 자바스크립트를 이용한 HTML 문서의 DOM 객체 조작을 이용하여 페이지를 동적으로 구성하는 AJAX 기법은 이제는 거의 모든 페이지에서 한 두 번은 반드시 사용하게 되는 필수적인 기능이 된 것 같아요.

그런데 사실 AJAX를 이용해서 어떤 기능을 구현할 경우 생각보다 많은 자바스크립트 코드로 인해 곤욕을 치를 때가 있을거에요. 예를 들어 게시판을 AJAX로 구현하는 경우를 생각해 보자면 페이지를 이동할 때마다 각 게시글 목록을 구성하는 DOM 객체들을 제거하고 다시 새로운 DOM 객체들을 만들어 페이지의 어딘가에 쑤셔(?) 넣어야 하죠. 이 과정을 구현하기 위한 자바스크립트 코드가 그닥 짧지 않을 것이라는 점은 직접 구현해 보지 않았더라도 충분히 예상할 수 있을거에요. 게다가 페이징을 위한 UI도 매번 새로 만들어줘야 하고요.

여기에 게시글 목록을 출력하는 과정에서 자바스크립트로는 해결이 안되어 C# 코드의 도움을 받아야 하는 경우 - 예를 들면 글 작성자의 상세 정보를 가져와 UI를 구성해야 한다면 - 라면 훨씬 더 고통스러운 자바스크립트 코딩 작업이 될거에요. 이런 경우라면 차라리 속 편하게 UpdatePanel 컨트롤을 사용하는 게 백번 나은 판단일 수도 있고요. 하지만 UpdatePanel 컨트롤을 사용하게 되면 서버 폼이 반드시 필요하고 또한 ViewState 정보가 AJAX 요청에 포함되어 사실 상 꼭 필요한 데이터만을 요청하고 전달받는다는 AJAX의 기본 철학과는 거리가 먼 이야기가 되고 맙니다.

물론 지금부터 제가 설명할 내용들도 이러한 AJAX의 기본 철학과는 다소 거리감이 느껴집니다. 하지만 UpdatePanel 컨트롤을 사용할 수 없는 환경 또는 사용하기 싫은 경우 어쩌면 좋은 대체 방법이 될 수 있을지도 모른다는 생각이 들어 용감하게 포스팅을 해 봅니다. 그러면 지금부터 웹서비스와 사용자 컨트롤 (*.ascx 파일)을 이용한 페이지 부분 렌더링 예제를 구현해 볼까요?

HttpServerUtility.Execute 메서드

우선 오늘 포스트에서 소개할 내용을 간단하게 한 줄로 요약하자면 

"웹서비스를 이용해서 사용자 컨트롤 (*.ascx) 파일을 렌더링한 결과로 생성된 HTML 코드를 클라이언트로 전달"

하는 방법입니다. 이 때 사용자 컨트롤을 렌더링하는 방법이 문제가 되는데요. 이 문제를 해결해 줄 수 있는 녀석이 바로 우리가 익히 알고 있는 HttpServerUtility.Execute 메서드입니다. HttpServerUtilityServer.Execute 메서드는 과거 ASP에서 다른 페이지를 실행한 결과를 현재 페이지 중간에 끼워넣기 위해 자주 사용되었던 방법이죠. 그렇다면 이 녀석을 어떻게 활용하면 뭔가 이뤄지지 않을까요? MSDN에서 HttpServerUtility.Execute 메서드에 대한 도움말을 찾아보면 이 메서드가 다음과 같이 재정의 되어 있다는 것을 알 수 있습니다.

HttpServerUtility.Execute(string path);
HttpServerUtiltiy.Execute(string path, bool preserveForm);
HttpServerUtility.Execute(string path, TextWriter writer);
HttpServerUtility.Execute(string path, TextWriter writer, bool preserveForm);
HttpServerUtility.Execute(IHttpHandler handler, TextWriter writer, bool preserveForm);


이 Execute 메서드는 void 타입을 리턴하지만 매개 변수에 TextWriter 타입을 사용하는 재정의된 버전들은 이 TextWriter 객체에 지정된 페이지의 실행 결과로 만들어진 HTML 코드를 전달할 수 있습니다. 그런데 가장 마지막 재정의를 보니 첫 번째 매개 변수가 IHttpHandler 인터페이스 타입이네요. 그리고 System.Web.UI.Page 타입은 IHttpHandler 인터페이스를 구현하고 있지요. 그렇다면 여기에 Page 클래스의 인스턴스를 넘겨줄 수 있다는 뜻???

연장 챙겼으면 삽질 고고씽!

앞서 살펴본 HttpServerUtility.Execute 메서드가 바로 이번 포스트의 핵심이라는 것을 눈치채셨을테니 곧바로 예제 애플리케이션의 구현으로 넘어가 보겠습니다. 구현된 예제 코드를 설명해 드리기 앞서 노파심에 덧붙이자면 이 예제는 단순히 활용방법을 소개하기 위한 것일 뿐 실전에서 사용하기에는 다소 무리가 있습니다. 해서 이번 포스트의 내용이 현재 진행 중인 프로젝트에 필요하다고 판단하신다면 예제 코드를 적절히 수정해서 사용하셔야 합니다. 그러면 실질적인 예제의 구현을 시작해 보죠.

우선 Visual Studio 2008에서 새로운 ASP.NET 웹 애플리케이션 프로젝트를 하나 생성하고 아래 그림과 같이 폴더들을 구성합니다.

그림 1: 예제 애플리케이션의 폴더 구조
img1  

그림에서 보듯이 Classes 폴더에는 몇 개의 인터페이스와 클래스가 정의되어 있습니다. 우선 이번 예제에서 Entity 객체로서의 역할을 수행할 Book 클래스의 소스 코드를 살펴볼까요?

코드 1: Book 클래스

  1: using System;
  2: 
  3: namespace WebApplication1.Classes
  4: {
  5: 	public class Book
  6: 	{
  7: 		public string Title { get; set; }
  8: 		public string Author { get; set; }
  9: 	}
 10: }


겁나게 간단한 코드로 구현된 이 클래스는 Title과 Author라는 두 가지 속성을 제공하며 이번 예제에서는 Book 클래스 객체의 컬렉션을 데이터 바인딩을 위한 데이터 원본으로 사용합니다. 자 그럼 다음으로 부분 렌더링에 사용될 사용자 컨트롤을 위한 인터페이스인 IView 인터페이스를 정의해 보겠습니다.

코드 2: IView 인터페이스

  1: using System;
  2: using System.Collections.Generic;
  3: 
  4: namespace WebApplication1.Classes
  5: {
  6: 	public interface IView
  7: 	{
  8: 		Dictionary<String, Object> ViewData { get; }
  9: 	}
 10: }
 11: 

IView 인터페이스는 ViewData 라는 이름의 속성을 제공하며 이 속성은 웹서비스로부터 사용자 컨트롤로 전달될 여러 가지 데이터를 보관하기 위한 용도로 사용됩니다. IView 인터페이스는 사용자 컨트롤의 기반 클래스로 사용될 ViewControl 클래스가 다음과 같이 구현하고 있습니다.

코드 3: ViewControl 클래스

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Web;
  4: using System.Web.UI;
  5: 
  6: namespace WebApplication1.Classes
  7: {
  8: 	public class ViewControl : UserControl, IView
  9: 	{
 10: 		#region IView Members
 11: 
 12: 		public Dictionary<string, object> ViewData
 13: 		{
 14: 			get { return HttpContext.Current.Items["__ViewData"] as Dictionary<String, Object>; }
 15: 		}
 16: 
 17: 		#endregion
 18: 	}
 19: }

ViewControl 클래스는 UserControl 클래스를 상속하며 IView 인터페이스를 구현합니다. IView 인터페이스가 정의한 ViewData 속성은 HttpContext 객체에 담겨져 전달될 예정이므로 14번 라인과 같이 HttpContext 객체로부터 뷰 데이터를 가져와 리턴하는 간단한 코드가 구현되어져 있습니다. 앞으로 부분 렌더링에 사용될 사용자  컨트롤들은 모두 ViewControl 클래스를 상속하여 구현하면 되겠죠?

이제 웹서비스 쪽으로 준비를 해보겠습니다. 우선 IPresenter 라는 이름의 인터페이스를 다음과 같이 정의합니다.

코드 4: IPresenter 인터페이스

  1: using System;
  2: using System.Collections.Generic;
  3: 
  4: namespace WebApplication1.Classes
  5: {
  6: 	public interface IPresenter
  7: 	{
  8: 		Dictionary<String, Object> ViewData { get; }
  9: 
 10: 		string Render(string viewName);
 11: 	}
 12: }
 13: 


이 인터페이스 역시 ViewData라는 속성을 제공하며 앞서 IView 인터페이스와 동일한 Dictionary<String, Object> 타입을 사용합니다. 그리고 Render 라는 메서드를 정의하고 있네요. 이 Render 메서드는 지정된 이름의 사용자 컨트롤을 찾아 컨트롤을 렌더링하고 결과 HTML을 리턴하는 역할을 담당합니다. 이 인터페이스는 AjaxPresenterService 웹서비스가 다음과 같이 구현합니다.

코드 5: AjaxPresenterService 클래스

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Web.Services;
  4: using System.Web;
  5: using System.Web.UI;
  6: using System.Web.Compilation;
  7: using System.Text;
  8: using System.IO;
  9: 
 10: namespace WebApplication1.Classes
 11: {
 12: 	public class AjaxPresenterService : WebService, IPresenter
 13: 	{
 14: 		private Dictionary<String, Object> _viewData = null;
 15: 
 16: 		public AjaxPresenterService() : base()
 17: 		{
 18: 			this._viewData = new Dictionary<string, object>();
 19: 			if (HttpContext.Current != null)
 20: 				HttpContext.Current.Items.Add("__ViewData", this._viewData);
 21: 		}
 22: 
 23: 		#region IPresenter Members
 24: 
 25: 		public Dictionary<string, object> ViewData
 26: 		{
 27: 			get { return this._viewData; }
 28: 		}
 29: 
 30: 		public string Render(string viewName)
 31: 		{
 32: 			string viewPath = String.Format("~/Views/{0}.ascx", viewName);
 33: 			Type type = BuildManager.GetCompiledType(viewPath);
 34: 			ViewControl control = Activator.CreateInstance(type) as ViewControl;
 35: 
 36: 			Page dummyPage = new Page();
 37: 			dummyPage.Controls.Add(control);
 38: 			
 39: 			StringBuilder response = new StringBuilder();
 40: 			StringWriter responseWriter = new StringWriter(response);
 41: 			Server.Execute(dummyPage, responseWriter, true);
 42: 
 43: 			return response.ToString();
 44: 		}
 45: 
 46: 		#endregion
 47: 	}
 48: }


코드가 조금 길어보이지만 그다지 어려운 코드는 없지요? 우선 이 서비스 클래스는 생성자 내에서 ViewData 속성을 구현하기 위한 _viewData 멤버를 초기화하며 HttpContext 객체가 존재하는 경우 생성된 _viewData 멤버를 HttpContext 클래스의 Items 컬렉션에 추가합니다. 중요한 것은 바로 32번 라인부터 시작하는 Render 메서드를 구현하는 코드이겠지요.

32번 라인에서 알 수 있듯이 Render 메서드는 viewName 매개 변수에 전달된 사용자 컨트롤의 이름을 이용하여 ~/Views/viewName.ascx 파일을 찾습니다. 예를 들어 Render("MyView")와 같이 호출하면 ~/Views/MyView.ascx 파일을 찾게 되는 것이지요.

이렇게 사용자 컨트롤의 가상 경로를 알아내게 되면 33번 라인과 같이 BuildManager 클래스의 GetCompiledType 메서드를 호출하여 해당 사용자 컨트롤이 컴파일된 클래스의 타입 정보를 얻어올 수 있으며 이 타입 정보를 Activator 클래스의 CreateInstance 메서드에 전달하면 해당 클래스의 인스턴스를 얻어올 수 있습니다. 이렇게 얻어온 인스턴스를 ViewControl 타입으로 형 변환하면 해당 사용자 컨트롤이 부분 렌더링을 지원하기 위한 사용자 컨트롤인지를 알 수 있게 되는 것이지요.

사실 사용자 컨트롤을 직접 렌더링할 수 있는 방법은 존재하지 않습니다. 즉 사용자 컨트롤을 렌더링하려면 반드시 페이지의 도움을 받아야 한다는 것이지요. 36번, 37번 라인의 코드가 Page 클래스의 인스턴스를 생성하고 Controls 컬렉션에 앞서 얻어온 사용자 컨트롤의 인스턴스를 추가하는 것은 바로 이런 이유입니다. 이제 임시로 생성한 페이지를 렌더링할 준비가  끝났으니 렌더링된 결과를 받아올 TextWriter 타입의 객체를 생성해야하겠지요? 39번 라인과 40번 라인에서는 StringBuilder 클래스와 StringWriter 클래스의 인스턴스를 준비하고 Server.Execute 메서드에 StringWriter 클래스의 인스턴스를 전달하여 최종적으로 dummyPage 객체가 렌더링된 결과를 가져옵니다. 이렇게 렌더링 된 결과를 String 타입으로 리턴하면 이 HTML 코드를 클라이언트 스크립트에서 그대로 활용할 수 있겠지요? 그러면 이와 같이 웹 서비스를 호출할 페이지를 아래와 같이 구현해 보겠습니다.

코드 6: Default.aspx 페이지의 소스 코드

  1: <head runat="server">
  2:     <title>Untitled Page</title>
  3:     <script language="javascript" type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
  4:     <script language="javascript" type="text/javascript">
  5: 		function renderView() {
  6: 			$.ajax(
  7: 			{
  8: 				type: "POST",
  9: 				url: "/Services/MyService.asmx/Hello",
 10: 				contentType: "application/json; charset=utf-8",
 11: 				data: "{viewName: 'MyView'}",
 12: 				dataType: "json",
 13: 				success: function(data) {
 14: 					$("#result").html(data.d);
 15: 				},
 16: 				error: function(data) {
 17: 					alert(data.statusText);
 18: 				}
 19: 			}
 20: 			);
 21: 		}
 22:     </script>
 23: </head>
 24: <body>
 25:     <input type="button" value="GetView" onclick="renderView()" />
 26:     <div id="result">
 27:     </div>
 28: </body>


이 페이지는 jQuery를 이용하여 AJAX 호출을 수행하는 클라이언트 스크립트와 이 스크립트 함수를 호출하는 하나의 버튼을 가지고 있습니다. 6번 라인에서 20번 라인까지의 코드를 보면 알 수 있듯이 jQuery.ajax 함수를 이용하여 MyService.asmx 라는 이름의 서비스가 제공하는 Hello라는 이름의 메서드를 호출합니다. 이 메서드는 앞서 AjaxPresenterService 서비스의 Render 메서드를 호출하여 특정 사용자 컨트롤을 렌더링한 결과를 전달받아 26번 라인의 <DIV> 태그에 삽입하게 됩니다. MyService.asmx 서비스는 다음과 같은 코드로 구현되어 있습니다.

코드 7: MyService.asmx.cs 파일의 소스 코드

  1: [System.Web.Script.Services.ScriptService]
  2: public class MyService : AjaxPresenterService
  3: {
  4: 	[WebMethod]
  5: 	public string Hello(string viewName)
  6: 	{
  7: 		this.ViewData.Add("Books", CreateDataSource());
  8: 		return this.Render(viewName);
  9: 	}
 10: 
 11: 	private List<Book> CreateDataSource()
 12: 	{
 13: 		List<Book> books = new List<Book>(10);
 14: 
 15: 		for (int i = 1; i <= 10; i++)
 16: 		{
 17: 			Book book = new Book()
 18: 			{
 19: 				Title = String.Format("Title {0}", i),
 20: 				Author = String.Format("Author {0}", i)
 21: 			};
 22: 
 23: 			books.Add(book);
 24: 		}
 25: 
 26: 		return books;
 27: 	}
 28: }


MyService 서비스는 앞서 구현한 AjaxPresenterService 서비스 클래스를 상속하며 Hello 메서드는 전달받은 viewName 매개 변수를 Render 메서드에 전달하여 지정된 사용자 컨트롤을 렌더링한 HTML 코드를 리턴합니다. 물론 그 전에 ViewData 속성에 CreateDataSource 메서드가 리턴하는 데이터 원본을 대입하해 주어야겠지요. CreateDataSource 메서드는 Book 클래스의 인스턴스 10개를 구성하는 매우 간단한 코드이지만 실제로는 여러분의 데이터 저장소에서 데이터를 가져오는 기능을 구현해야 합니다. 한 편 앞서 Default.aspx 파일의 스크립트에서 MyService 서비스의 Hello 메서드에 전달할 매개 변수로 MyView라는 값이 전달되었습니다. 이 MyView가 바로 사용자 컨트롤이며 Views 폴더 아래에 다음과 같이 구현됩니다.

코드 8: MyView.ascx 파일의 마크 업

  1: <asp:Repeater ID="repeater1" runat="server">
  2: 	<HeaderTemplate>
  3: 		<ul>
  4: 	</HeaderTemplate>
  5: 	<ItemTemplate>
  6: 		<li><%# Eval("Title") %> by <%# Eval("Author") %></li>
  7: 	</ItemTemplate>
  8: 	<FooterTemplate>
  9: 		</ul>
 10: 	</FooterTemplate>
 11: </asp:Repeater>


그리고 이 사용자 컨트롤의 비하인드 코드는 다음과 같습니다.

코드 9: MyView.ascx.cs 파일의 소스 코드

  1: public partial class MyView : ViewControl
  2: {
  3: 	protected override void OnLoad(EventArgs e)
  4: 	{
  5: 		this.repeater1.DataSource = this.ViewData["Books"];
  6: 		this.repeater1.DataBind();
  7: 	}
  8: }


MyView 사용자 컨트롤은 Repeater 컨트롤에 ViewControl 클래스의 ViewData 속성에 보관된 데이터 원본을 가져와 바인딩하는 것을 볼 수 있습니다. 이 ViewData 속성에 보관된 데이터 원본은 앞서 MyService.Hello 메서드에서 CreateDataSource 메서드를 호출해 얻은 10개의 Book 클래스 객체의 컬렉션인 것이지요. 이것으로 예제의 구현을 모두 마쳤습니다. 예제를 실행하고 버튼을 클릭하면 AJAX 호출로 포스트 백 과정 없이 데이터가 바인딩된 결과가 페이지에 출력되는 것을 볼 수 있습니다.

그림 2: 예제를 실행한 모습

img2

마치며...

사실 이번 포스트에서 소개한 방법은 그다지 완벽하지 않습니다. 우선 서버 폼을 필요로 하는 <asp:Button>과 같은 컨트롤은 사용할 경우 문제가 있습니다. 이 경우 MyView.ascx 파일에 서버 폼을 선언하면 <FORM> 태그가 렌더링 될텐데 만일 사용자 컨트롤을 렌더링하는 페이지가 이미 서버 폼이나 클라이언트 폼을 가지고 있다면 문제가 되겠죠?

그렇지 않다면 AjaxPresenterService.Render 메서드에서 Page 클래스의 인스턴스를 생성할 때 사용자 컨트롤을 Page 객체에 추가하는 것이 아니라 HtmlForm 클래스의 인스턴스를 하나 더 생성하여 사용자 컨트롤을 HtmlForm.Controls 컬렉션에 추가하고 HtmlForm 객체를 다시 Page 객체에 추가하는 방법을 사용할 수 있습니다. 하지만 이 경우에도 페이지가 이미 <FORM>  태그를 가지고 있을 수 있으므로 정규표현식을 이용하거나 기타 다른 방법으로 StringBuilder에 기록된 결과 HTML에서 <FORM> 태그나 ViewState 필드를 제거해 주는 추가적인 절차가 필요합니다.

따라서 반드시 필요한 경우가 아니라면 이번 예제 애플리케이션처럼 서버 폼이 필요없는 UI 요소들을 이용하여 인터페이스를 구현하는 것이 좋겠습니다.

자, 그럼 전 이만 물러갑니다. 다음 포스트에서 다시 만나요!

Posted by 웹지니 트랙백 0 : 댓글 4