(by 웹지니, aspnetmvp@gmail.com)

불과 수 년 전까지만 해도 웹 애플리케이션을 개발하면서 웹 애플리케이션의 Globalization을 고려하는 개발자나 그에 대한 수요는 그다지 많지 않았다. 그도 그럴 것이 당시 대한민국 인터넷 사용자를 대상으로 한 시장이 지금처럼 포화상태도 아니었을 뿐더러 대한민국 IT 업계가 아직은 대한민국 인터넷 사용자만으로도 먹고 살만 했기 때문이다. 물론 서비스 자체가 여전히 성장의 여지가 남아있었음도 한 몫 했을테고.

그러나 시간이 점차 흐르면서 이제 국내 시장을 타겟으로 개발되는 서비스들도 글로벌 시장을 고려하지 않을 수 없게 되었다. 기껏해야 5천만, 그 중 인터넷 인구 2천만이 조금 넘는 대한민국 인터넷 시장에서 서비스와 서비스를 제공하는 회사의 성장이 한계에 부딪히기까지는 그다지 많은 시간이 필요하지 않을 것이라는 것쯤은 입아프게 말하지 않아도 조금만 생각해 보면 누구나 알 수 있는 자명한 사실.

그러나 최근 들어 게임 및 포털들의 해외 진출이 시작되고 중소 기업들도 좁은 국내 시장에서 탈피하여 해외로 진출하지 않는 이상 더 이상의 성장이 어렵기 때문에 이제 웹 개발자들에게 Globalization이란 애플리케이션의 설계 단계에서부터 반드시 고려되어야 하는 필수적인 요소가 되어가고 있다. 태생이 꼬부랑 글씨 쓰는 애들은 자기네 나라 말로만 만들어도 지구 상에 왠만한 나라 사람들은 무리 없이 사용할 수 있지만 우리네 한글은 그 우수성에도 불구하고 대한민국에서만 사용되다보니 한글로 아무리 좋은 서비스를 만들어봤자 우리만의 잔치가 될 뿐이며 전 세계 사용자를 대상으로 하는 글로벌 서비스가 될 수 없다.

필자는 현재까지 약 6년여에 걸쳐 여러 글로벌 서비스를 구축, 운영해 온 경험을 가지고 있으며 그런 필자의 경험이 독자 여러분에게 조금이나마 도움이 되기를 바라는 마음으로 글로벌 웹 서비스 구축에 필요한 기본적인 몇 가지를 여러분과 공유하고자 하는 의미에서 이번 포스트 시리즈를 기획하게 되었다.  

글로벌 웹 애플리케이션 구축에서 첫 번째로 소개하고자 하는 내용은 MaxMind사의 IP 데이터베이스를 활용하는 방법이다. 웹 애플리케이션 경험이 조금이라도 있는 개발자라면 MaxMind 사의 홈페이지를 통해 필요한 정보를 어렵지 않게 수집할 수 있겠지만 이번 포스트는 간략한 예제를 통해 웹 애플리케이션에서 IP 데이터베이스를 활용하는 방법을 빠르게 소개하는 것이 목적이다.

글로벌 서비스에서의 IP Database와 위치 기반 콘텐츠의 중요성

IP 데이터베이스는 말 그대로 전 세계에 널려있는 IP 주소에 대한 데이터베이스이다. 특정 IP가 어느 나라, 어느 지역에 할당되어 있으며 어떤 ISP에서 사용하고 있는지, 혹은 어떤 사용자에게 할당되어 있는지 등 유용한 정보가 담겨있다. 애플의 아이폰 덕분에 위치 정보가 어디에 어떻게 활용되는지는 이제 누구나 익숙하겠지만 IP 주소에 기반한 위치 정보는 글로벌 시장을 타겟으로 하는 웹 서비스 입장에서는 매우 중요하다. 왜냐하면 내가 만든 서비스를 어느 나라에서 많이 사용하는지, 또는 어떤 지역에서 사용하는지, 어느 지역 사용자가 어떤 서비스의 활용도가 높은지 등 서비스를 분석하고 향상시키기 위해 필요한 모든 분석 지표에 지역 및 위치 정보가 활용되기 때문이다.

뿐만 아니라 서비스가 제공하는 콘텐츠도 이 위치 정보에 의해 좌우되기도 한다. 예를 들어 얼마 전 구글이 야심차게 오픈했다는 구글 뮤직 페이지를 방문해 보면 아래와 같은 화면을 볼 수 있을 것이다.

그림 1: 구글 뮤직 페이지 - 한국에는 언제 열어주려나...

현재 사용자의 접속 지역을 토대로 페이지의 콘텐츠를 제어한 좋은 예이다. 만일 내가 만드는 서비스가 디지털 아이템, 특히 CP(Content Provider, 콘텐츠 공급자)에 의해 제공되는 아이템을 온라인에서 판매하는 서비스라면 사용자의 접속 지역을 파악하는 것은 더욱 중요해진다. 왜냐하면 이들 콘텐츠의 판매에 대한 라이선스 계약에 따라 특정 지역에서는 해당 아이템을 사용자가 구매하지 못하도록 제어할 필요가 있기 때문이다. 특히 각 국가나 지역별로 판매하는 디지털 아이템의 가격이 달라질 수 있다면 현재 접속 중인 사용자의 위치는 더욱 중요해진다. 사용자의 접속 지역을 잘못 판단하면 향후 매출 집계 및 CP에 대한 정산에 있어 오차가 발생할 수 있으며 이는 곧 회사의 손실로 이어지기 때문이다.

따라서 IP 데이터베이스의 구축 및 사용자의 위치 정보 수집 및 활용 등은 웹 애플리케이션을 위한 프레임워크 레벨에서 구현되어 전체 조직 내의 개발자들이 공통의 코드를 사용하도록 해야 한다. 특히 IP 데이터베이스가 문제인데 구글이나 마이크로소프트 등 대형 인터넷 서비스/소프트웨어 업체 수준의 정밀한 IP 데이터베이스가 우리 회사에도 있다면 얼마나 좋겠냐마는 현실적으로 어려운 이상 대안을 찾을 수 밖에 없다. 구글 신에게 도움을 청해보면 알겠지만 그래도 현재 가장 쓸만한 IP 데이터베이스를 제공하는 곳이 바로 MaxMind.com이다.

MaxMind GeoIP 솔루션 소개

MaxMind사는 전 세계의 IP 데이터베이스를 서비스로 제공하는 회사로 IP를 이용하여 해당 IP가 사용되는 국가 및 지역에 대한 정보 및 해당 IP의 위치(위도 및 경도) 정보를 제공하는 데이터베이스와 API를 제공한다. 크게 무료 버전과 유료 버전으로 구분되며 무료 버전(GeoLiteCity라는 이름으로 제공된다)의 경우 IP에 대한 국가 수준의 정보가 제공되는 반면 유료 버전(GeoIPCity라는 이름으로 제공된다)을 구매하면 도시 수준의 보다 상세한 위치 정보를 얻을 수 있게 된다. 아래 그림은 MaxMind GeoLiteCity 데이터베이스에 대한 소개 및 다운로드 페이지의 모습이다.

그림 2: MaxMind사의 GeoLiteCity 데이터베이스 소개 페이지

그림 2에서 붉게 표시한 링크를 클릭하면 GeoLiteCity.dat.gz 파일을 다운로드할 수 있다. 이 파일은 GeoLiteCity.dat 파일을 압축한 파일이며 이 파일이 바로 전 세계 IP 주소의 위치 정보를 담고 있는 데이터베이스 파일이다. 물론 페이지 아래 쪽에서 CSV 형식의 파일을 다운로드 후 여러분이 사용하는 RDBMS로 가져올 수도 있다. 이 포스트에서는 GeoLiteCity.dat 파일을 사용하기로 한다. GeoLiteCity.dat 파일은 단순한 바이너리 파일이며 따라서 다운로드한 파일을 그대로 웹 서버에 배포하여 사용할 수 있다. 이 파일로부터 특정 IP 주소의 위치 정보를 가져오기 위해서는 별도의 API가 필요하며 그림 2의 웹 페이지 좌측 메뉴엣 [GeoIP APIs] 링크를 클릭하여 독자 여러분의 개발 언어에 따라 적당한 API를 다운로드하면 된다. 이 포스트에서는 C#으로 작성된 .NET용 API를 사용한다. 다운로드 한 C# API 압축 파일의 압축을 해제해보면 아래 그림과 같은 파일들이 존재하는 것을 확인할 수 있다.

그림 3: MaxMind C# API를 구성하는 소스 파일들

전체 소스가 모두 필요한 것은 아니며 ~Example.cs 파일을 제외한 나머지 파일들만 프로젝트에서 사용하면 된다.

Working with MaxMind API

그러면 간단한 예제를 통해 MaxMind API의 사용법을 확인해 보자. 먼저 Visual Studio를 실행하고 아래 그림과 같이 필요한 파일들을 프로젝트에 추가하자.

그림 4: ASP.NET MVC2 프로젝트를 구성한 모습

그림 4에서 보듯이 GeoLiteCity.dat 파일은 웹 애플리케이션 프로젝트의 루트에 추가했으며 MaxMind  API를 구성하는 소스 파일들은 GeoIP라는 폴더에 추가하였다. MaxMind API의 핵심은 LookupService 클래스로 이 클래스에 IP 주소를 전달하면 Location 클래스의 인스턴스를 돌려준다. Location 클래스는 지정된 IP 주소를 사용하는 지역의 국가 코드와 지역 코드, 위도 및 경도, 우편 번호 등의 데이터를 가지고 있다. 그러면 ASP.NET MVC 프로젝트에 기본으로 생성된 HomeController.cs 파일을 열고 Index 액션 메서드를 아래와 같이 수정해보자.

public ActionResult Index()
{
	var lookup = new LookupService(Server.MapPath("GeoLiteCity.dat"));
	var location = lookup.getLocation("168.126.63.1");

	ViewData["country"] = String.Format("Country: {0} ({1})",
		location.countryName, location.countryCode);
	ViewData["location"] = String.Format("Location: Latitude: {0}, Longitude: {1}",
		location.latitude, location.longitude);

	return View();
}

이제 /Views/Home/Index.aspx 파일을 열고 코드를 아래와 같이 수정하자.


    

<%= Html.Encode(ViewData["country"]) %>

<%= Html.Encode(ViewData["location"]) %>


이 페이지를 실행한 결과는 아래 그림과 같다.

그림 5: 예제 코드의 실행 결과

그림에서 보듯이 지정한 IP 주소의 소유 국가와 위치 정보가 잘 나타나는 것을 볼 수 있다. 이처럼 API가 제공하는 정보들을 활용하면 서비스의 콘텐츠를 지역별로 얼마든지 제어할 수 있게 된다.

MaxMind API 사용 시 주의할 점

지금까지 MaxMind API를 이용하여 IP 주소를 기준으로 사용자의 위치 정보를 얻어내는 방법을 소개하였다. 포스트를 마무리하면서 한 가지 주의할 점은 IP 주소를 이용한 위치 정보는 (당연하겠지만) 공인 IP를 대상으로 하기 때문에 사설 IP를 사용하는 사무실 이나 가정에서 개발자 PC에 할당된 주소를 API에 전달하면 null이 리턴된다. 또한 MaxMind IP 데이터베이스의 커버리지는 100%가 아닌 99.5%이므로 0.5%는 잘못된 정보가 리턴되거나 혹은 null이 리턴될 수 있다. 따라서 Location 객체 등을 사용하기 전에는 반드시 null 여부를 검사하는 것이 좋다.

또한 MaxMind가 제공하는 무료 버전의 IP 데이터베이스를 사용할 계획이라면 국가 및 위/경도 값 외에 지역이나 우편 번호 등 나머지 정보들은 커버리지가 낮은 편이므로 이 정보에 너무 의존하지 않는 것이 좋다. 만일 이 정보들이 반드시 필요하다면 유료 버전을 구매할 것을 권한다.




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

안녕하세요 웹지니입니다! 흐헤헤 -ㅅ-;;;
어제 있었던 북한의 도발 때문에 많이들 불안하셨을 것으로 생각합니다.
저로서는 조금 더 부지런히, 열심히 살아야겠다고 다시 한 번 추스르게 된 계기가 되었어요.
뭐... 이번 포스트를 꼭 그래서 올리는 건 아니구요...-ㅅ-;;

예전에 블로그를 통해 Custom HtmlHelper 메서드를 구현하는 것과 관련된 글을 올린 적이 있었어요. 벌써 1년도 넘게 지난 시점이군요. 당시 HtmlHelper 클래스의 확장 메서드를 구현하면서 최종적으로 태그를 생성하는 과정에서 ASP.NET MVC가 제공하는 TagBuilder 클래스를 사용했었는데요.

오늘 포스트는 요즘 유행(?)하는 Fluent Interface 패턴 형태로 TagBuilder 클래스를 손쉽게 활용할 수 있도록 확장하는 내용을 담아봅니다. Fluent Interface 패턴을 활용하면 매우 읽기 쉬운 코드를 작성할 수 있다는 장점이 있지요. 해서 한 번 만들어 봤습니다. 이름하야 FluentTagBuilder!!



Implementing FluentTagBuilder

아시는 분은 아시겠지만 Fluent Interface의 핵심은 메서드가 자기 자신 혹은 관련된 다른 객체의 인스턴스를 리턴하는 것입니다. 즉, 메서드 호출 시 스스로를 계속 리턴하여 연속적으로 메서드를 호출할 수 있도록 구현하는 것이지요. 실질적으로 HTML 태그를 생성하는 기능은 TagBuilder가 이미 훌륭하게 구현하고 있으니 그냥 맡겨버리고 우리는 이 TagBuilder 클래스의 인스턴스를 계속해서 리턴하는 Wrapper 메서드만을 구현해주면 됩니다. 일단 코드를 좀 살펴볼까요?

public class FluentTagBuilder : TagBuilder
{
	public FluentTagBuilder(string tagName)
		: base(tagName)
	{ }

	public FluentTagBuilder Append(FluentTagBuilder innerTag)
	{
		base.InnerHtml += innerTag.ToString();
		return this;
	}

	public FluentTagBuilder AppendHtml(string html)
	{
		base.InnerHtml += html;
		return this;
	}

	public FluentTagBuilder AppendText(string html) {
		  base.InnerHtml += HttpUtility.HtmlEncode(html);
		  return this;
	}

	public FluentTagBuilder With(FluentTagBuilder tagBuilder)
	{
		base.InnerHtml = tagBuilder.ToString();
		return this;
	}

	public FluentTagBuilder With(string attribute, string attributeValue)
	{
		base.Attributes.Add(attribute, attributeValue);
		return this;
	}

	public FluentTagBuilder With(IDictionary attributes)
	{
		base.MergeAttributes(attributes);
		return this;
	}

	public FluentTagBuilder And(string attribute, string attributeValue)
	{
		base.Attributes.Add(attribute, attributeValue);
		return this;
	}

	public FluentTagBuilder AndIf(bool condition, string attribute, string attributeValue)
	{
		if (condition)
		{
			base.Attributes.Add(attribute, attributeValue);
		}
		return this;
	}
}
예제 코드에서 크게 어렵거나 이해가 안 가는 부분은 없으실거라 생각해요. 그러면 실제로 어떻게 사용하는지를 알아볼까요? 예전 포스트에서 구현했던 이미지를 포함하는 링크를 생성하는 ImageLink 메서드를 FluentTagBuilder 클래스를 이용하면 아래와 같이 구현할 수 있습니다.

public static MvcHtmlString ImageLink(
	this HtmlHelper helper, 
	string imageUrl, 
	string actionName, 
	string controllerName, 
	RouteValueDictionary routeValues, 
	IDictionary imgAttributes, 
	IDictionary htmlAttributes)
{
	UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
	string resolvedImageUrl = urlHelper.Content(imageUrl);

	string url = UrlHelper.GenerateUrl(
		null, actionName, controllerName, routeValues, helper.RouteCollection,
		helper.ViewContext.RequestContext, true
	);
	
	FluentTagBuilder imageTagBuilder = new FluentTagBuilder("img")
		.With(imgAttributes)
		.With("src", resolvedImageUrl);

	return MvcHtmlString.Create(new FluentTagBuilder("a")
		.Append(imageTagBuilder)
		.With("href", url)
		.With(htmlAttributes)
		.ToString()
	);
}
18번 라인부터 27번 라인까지의 코드가 바로 FluentTagBuilder 클래스를 이용하여 태그를 생성하는 부분입니다. 예전 포스트와 비교할 때 코드가 확연히 줄어들었을 뿐 아니라 코드를 이해하기도 더 쉬워진 것 같죠?

아직 어딘가에 살아있음을 보여주기 위한 짧은 포스트는 여기서 마치고 이만 물러갑니다.
멋진 하루 보내시기 바랍니다!
Posted by 웹지니 트랙백 0 : 댓글 6

Using NHibernate: Introduction

2010.07.23 14:44 from ASP.NET
안녕하세요? 웹지니입니다.

최근 저는 회사에서 진행 중인 프로젝트에 유명한 ORM도구인 NHIbernate를 적용하고 있습니다. 사실 시작한지 얼마 되지 않아 많은 것을 알지는 못하지만 그래도 제가 경험했던 내용들을 토대로 간단하게 NHibernate를 소개하기 위해 짧은 연재를 진행하려고 합니다. 물론 초급 수준의 포스트가 되겠지만 그래도 많은 분들께 조그마한 도움이 될 수 있기를 바라면서 시작해 보겠습니다.

NHibernate란?

NHibernate는 이미 자바 진영에서는 오래전부터 활용되고 있는 Hibernate 프레임워크를 .NET 환경으로 포팅한 프레임워크입니다. NHibernate는 Linq To SQL이나 Entity Framework와 같은 ORM(Object-Relational Mapping) 프레임워크로 쉽게 말하자면 DB 스키마를 C# 객체로 매핑하여 개발자가 직접 SQL 쿼리를 작성하지 않고 객체 중심의 프로그래밍이 가능하도록 해주는 프레임워크입니다.

최근의 프로그래밍 개발 패턴을 살펴보면 종전처럼 DB 설계부터 시작하여 쿼리를 중심으로 애플리케이션을 구현해 나가는 Bottom-up 방식의 Data-First 패턴에서 모델 중심 혹은 데이터 중심으로 애플리케이션을 설계하고 이에 따라 애플리케이션과 DB를 구현해 나가는 Top-Down 방식의 Model-First 패턴이 주를 이루고 있습니다.

특히 DDD(Domain Driven Development) 방법론이 확산되면서 도메인 모델 패턴이 널리 알려지고 이와 같은 패턴을 통해 구현되는 애플리케이션의 수가 점차 늘어나면서 ORM 도구의 사용 역시 매우 활발하게 이루어지고 있습니다. 그렇다면 .NET 환경에서 선택할 수 있는 ORM 도구는 NHibernate가 유일했을까요? 물론 그렇지 않습니다. .NET 환경에서 ORM은 .NET 3.0의 Linq 기술과 함께 릴리즈된 Linq to SQL에 의해 알려지게 되었습니다.

또한 비교적 소규모의 단조로운 데이터베이스를 위한 Linq to SQL을 보완하기 위해 마이크로소프트는 Entity Framework(이하 EF)라는 또 다른 형태의 ORM 도구를 준비하여 릴리즈 하기도 했습니다. 그리고 EF는 .NET 4에 이르러 PI(Persistence Ignorance) 개념 등 진정한 의미의 도메인 모델 패턴을 지원하기 위해 큰 폭으로 발전하고 있습니다. 그럼에도 불구하고 제가 NHibernate를 선택한 까닭을 요약해 보면 다음과 같습니다.

1. Linq to SQL의 경우 1:1 관계만을 지원하여 복잡한 비즈니스 애플리케이션의 구현에 적합하지 않다.
2. Linq to SQL은 마이크로소프트 SQL Server만을 지원한다.
3. EF의 경우 1:N 관계를 지원하지만 마이크로소프트 .NET 프레임워크에서만 사용이 가능하다. (현재 Mono 프레임워크는 EF를 완벽하게 구현하고 있지 못합니다).
4. EF 역시 마이크로소프트 SQL Server만을 지원한다.  이 부분은 제가 잘못 알고있었기에 정정합니다. EF는 SQL Server 외의 다른 RDBMS벤더도 지원되고 있습니다. 제보(?)해주신 권효중님께 감사드립니다. ㅎㅎ
5. NHibernate는 이미 수 년 전부터 자바 진영에서 활용되어 온 이미 검증된 프레임워크이다.
6. NHibernate는 Mono 프레임워크를 지원한다.
7. NHibernate는 현존하는 거의 모든 RDBMS를 지원한다.
8. 아직 개발 중이긴 하지만 NHibernate의 3.0 버전에서는 HQL(Hibernate Query Language)를 기반으로 한 Linq 구문의 지원이 가능하다 (현재 릴리즈된 최신 버전은 2.1.2의 경우 Linq to NHibernate를 통해 부분적인 Linq 구문의 지원이 가능합니다. 그러나 Linq to NHibernate의 경우 HQL 기반이 아닌 NHibernate의 Criteria API를 기반으로 하고 있어 JOIN 등 여러 가지 구문을 지원하지 못합니다. 3.0 버전은 현재 알파 버전이지만 NHibernate의 소스 저장소에서 다운로드하여 빌드 후 사용이 가능합니다).

그렇습니다. 아시는 분은 아시겠지만 저는 Windows 서버 환경 뿐 아니라 리눅스 서버 환경도 고려한 제품을 개발 중이며 이에 따라 Mono 프레임워크의 지원 여부가 선택에 가장 큰 영향을 미쳤습니다. 그러나 실상 NHibernate 프레임워크는 Mono 프레임워크를 지원한다는 것 외에도 많은 장점을 가진 프레임워크입니다.

왜 ORM 도구를 사용해야 하는가?

.NET 개발자들에게 있어 ORM이라는 도구는 아직은 다소 생소한 도구라고 할 수 있습니다. 해외 개발자들은 그렇지 않겠지만 국내 개발자 중 대부분은 Linq to SQL을 통해 ORM이라는 개념을 처음 접하게 된 경우가 생각보다 많았습니다. 더군다나 Linq to SQL이 처음 .NET 개발자들에게 공개되었을 때 ORM 도구를 반대하던 - 보다 정확히 말하자면 Linq to SQL을 반대하던 - 개발자들은 크게 다음과 같은 점들을 ORM 도구의 단점으로 지적했습니다.

1. ORM 도구가 생성하는 SQL 구문의 변경이나 최적화가 불가능하다.
2. ORM 도구를 사용하게 됨으로써 DataSet과 같은 기존의 범용 클래스를 사용하는 대신 테이블의 각 컬럼과 매핑되는 속성을 가진 별도의 클래스를 구현해야 한다. 애플리케이션의 볼륨이 커질수록 이러한 클래스들의 수 역시 늘어날 것이며 이는 결과적으로 애플리케이션의 실행 성능에 영향을 미칠 수 있다.

여기에서 모델 객체를 추가로 구현해야 하는 부담에 대한 지적이 없었던 이유는 대부분의 ORM 도구들은 데이터베이스 테이블을 기반으로 모델 객체를 자동으로 생성하는 코드 자동 생성 기능을 제공하고 있기 때문입니다. 특히 Linq to SQL과 Entity Framework는 자동 객체 생성 기능을 훌륭하게 지원하고 있습니다. 그에 반해 NHibernate 프레임워크는 개발자가 직접 모델을 구현하고 모델과 테이블 사이의 매핑을 별도의 XML 파일로 구성해야 한다는 단점이 존재합니다. 사실 이런 부분은 생산성의 저하를 가져오게 마련입니다. 그러나 다행히 이런 문제는 Visual NHibernateFluent NHibernate와 같은 도구를 함께 사용하면 얼마든지 극복이 가능합니다.

다시 본론으로 돌아가 앞서 언급했던 단점들이 존재함에도 불구하고 왜 ORM 도구를 사용해야 하는지에 대해 다시 생각해 보겠습니다. 확실히 많은 수의 모델 객체와 이에 대한 매핑 정보를 유지해야 하는 것은 애플리케이션의 실행 성능에 부담을 줄 수 있습니다. 물론 쿼리 자체를 개발자가 임의로 최적화할 수 없다는 점도 단점이 될 수 있습니다. 그럼에도 불구하고 ORM 도구를 사용해야 하는 이유를 저는 다음과 같이 생각합니다.

1. 데이터베이스에 대한 추상화: 대부분의 ORM 도구는 여러 가지 RDBMS를 지원하며 각각의 RDBMS 벤더에 적합한 쿼리를 자동으로 생성합니다. 따라서 개발자는 여러 DBMS를 지원하는 소프트웨어를 개발하기 위해 DBMS에 특화된 쿼리를 매번 작성할 필요없이 객체 기반의 코드를 통해 어떤 DBMS든 사용이 가능하게 됩니다. 또한 데이터베이스에 대한 액세스가 추상화됨으로 인해 TDD(Test Driven Development) 방법론에 따른 테스트 용이성(Testability)을 확보하기가 쉽습니다.

2. 강력하게 형식화된 데이터 액세스: 예전에 DataSet 객체를 사용하는 경우 새로운 데이터를 추가하거나 기존 데이터를 수정하기 위해서는 반드시 해당 컬럼의 데이터 형식에 대해 알고 있어야 했습니다. 그러나 ORM 도구를 사용하게 될 경우 해당 객체의 속성을 통해 강력하게 형식화된 데이터 액세스 API를 구현할 수 있게 됩니다. 또한 이를 통해 비즈니스 규칙에 대한 유효성 검사를 보다 효율적으로 실행할 수 있습니다.

3. 개발 생산성의 향상: 상기 두 가지 항목만 보더라도 이미 개발 생산성이 몰라보게 향상될 수 있음을 깨달을 수 있습니다. 뿐만 아니라 개발자가 잘못된 타입의 데이터를 사용하거나 Null 값이나 객체에 액세스하는 등의 자잘한 실수를 보완할 수 있어 예기치 않은 버그의 발생 및 디버깅에 대한 노력을 최소화할 수 있습니다.

4. 유지보수성의 향상: ORM 도구를 사용하게 된다면 비즈니스 로직이 명확해 집니다. 기존의 방식대로라면 비즈니스 로직 중간에 SQL 쿼리나 저장 프로시저 호출이 포함되고 따라서 이러한 부분에 대한 추가 분석이 따라줘야 전체 로직을 분석할 수 있게 되는 반면 ORM 도구를 사용할 경우에는 DB 관련 로직이 비즈니스 로직에 개발 언어 형태로 포함되기 때문에 코드 분석이 보다 용이해 집니다. 특히 Linq 구문을 활용하게 된다면 더욱 명확해 지겠지요. 이렇게 비즈니스 로직이 명확해지면 코드를 유지보수하기도 매우 쉽고 편리합니다.

물론 저는 이 글을 통해 모든 애플리케이션에 ORM을 반드시 탑재해야 한다고 주장하는 것은 아닙니다. 어떤 도구든지 마찬가지겠지만 필요한 곳에 적절한 활용이 무엇보다 중요합니다. 특히 ORM을 통해 해결할 수 없는 방대한 양의 쿼리나 프로세스가 많은 국내 개발 현실을 비추어볼 때 ORM 도구를 활용하는 것이 적합한지에 대해서는 의구심을 품을 수도 있습니다. Linq to SQL이나 EF가 저장 프로시저를 객체의 메서드 형태로 사용할 수 있도록 하는 것이나 NHibernate가 HQL(Hibernate Query Language)이나 Native SQL을 직접 호출할 수 있도록 구현되어 있는 것도 바로 이런 문제를 보완하기 위해서가 아닐까요. 결론적으로는 이런 도구를 활용하는 여러분의 선택이 무엇보다 중요하다고 할 수 있겠습니다.

NHibernate 다운로드하기

그러면 본격적으로 NHibernate 프레임워크를 사용하기 위한 준비를 시작해 보겠습니다. 이 글을 작성하는 현재 NHibernate는 2.1.2 버전이 가장 최신 버전이며 NHibernate 프레임워크의 공식 웹사이트에서 다운로드가 가능합니다.

그림 1: NHibernate 프레임워크의 공식 웹사이트 (http://nhforge.org)

그림에서 표시된 [Download Now NH 2.1.2] 링크를 클릭하면 NHibernate 2.1.2 버전을 다운로드할 수 있습니다. ZIP 형식으로 압축된 파일이 다운로드되면 별도의 설치 과정은 필요치 않으므로 프로젝트에 어셈블리만 참조하면 곧바로 사용할 수 있습니다. 다운로드된 압축 파일을 풀어보면 아래와 같은 폴더가 존재하는 것을 볼 수 있습니다.

그림 2: NHibernate 프레임워크를 다운로드한 후 압축을 해제한 폴더의 모습

  • Configuration_Templates: NHibernate 프레임워크가 지원하는 DBMS별로 데이터베이스 연결을 위한 설정 예제 파일들이 제공되는 폴더입니다. 이 폴더의 파일들로 알 수 있듯이 NHibernate 프레임워크는 FireBird, Microsoft SQL Server, MySql, Oracle, PostgreSQL, SQLite 등의 RDBMS를 지원합니다.

  • Required_Bins: NHibernate 프레임워크를 사용하기 위한 필수 어셈블리 파일들이 제공되는 폴더입니다. 특히 이 폴더에는 Visual Studio에서 NHibernate의 Configuration 파일들을 작성할 때 인텔리센스를 지원하기 위한 XSD 파일들도 포함되어 있습니다.

  • Required_For_LazyLoading: Lazy-loading을 구현하기 위해 필요한 추가 어셈블리들이 제공되는 파일이 제공되는 폴더입니다. NHibernate의 Lazy-loading은 Castle의 DynamicProxy, LinFu, Spring.NET 등 세 가지 프레임워크를 지원합니다. 이러한 어셈블리들은 세 가지 중 원하시는 것을 선택적으로 사용하실 수 있습니다.
NHibernate 프레임워크와 Linq

앞서 다운로드한 NHibernate 2.1.2에서는 Linq 구문이 지원되지 않습니다. 따라서 2.1.2를 사용한다면 NHibernate의 Criteria API를 이용한 형태의 코드를 작성하여 모델 객체를 조작하게 됩니다. 그러나 Linq to NHibernate를 사용하면 제한된 범위 내에서 Linq를 사용할 수 있습니다. 또한 현재 개발 중인 NHibernate 3.0의 경우에는 HQL 기반의 Linq 기능을 구현하고 있어 Linq to NHibernate에 비해 더 폭넓은 범위의 Linq 구문을 지원받을 수 있습니다.

언제나 그렇듯 개발과 관련된 포스트는 예제 위주의 포스트로 구성하는게 쵝오-ㅅ-b지요. 다음 포스트부터는 간단한 예제 애플리케이션의 구현을 통해 NHibernate 프레임워크의 사용법을 차근히 익혀보겠습니다.







Posted by 웹지니 트랙백 0 : 댓글 12
안녕하세요? 웹지니입니다.

주말 동안에 구슬이 횽아가 멋진 소식을 전했네요. 뭐 거두 절미하고 번역 들어갑니다. ㅋ 원문은 여기를 클릭하세요.
(제가 태우는 담배가 레종 - Raison - 인데 발음이 비슷해서 그런가 왠지 친근한 느낌이... 켈룩 -ㅅ-;;)

달랑 점심 시간 1시간을 투자해서 번역한 글이라 다소 투박하거나 원문과 일치하지 않을 수 있음을 양해해 주시기 바랍니다.

UPDATED: ASP.NET Web Pages라는 이름으로 Razor Syntax를 지원하는 VS 2010 애드온의 베타 1버전이 발표 되었어요. 아쉽게도 현재로서는 .NET 4만 지원하는 것 같습니다. 정식 릴리즈때는 3.5도 지원해주면 좋겠네요. 다운로드는 여기로!!

ASP.NET의 새로운 뷰 엔진 - Razor

우리 팀에서 진행하는 작업 중 하나는 ASP.NET을 위한 새로운 뷰 엔진 옵션을 구현하는 것이었습니다.

ASP.NET MVC는 "뷰 엔진"이라는 개념을 지원하는데 이 뷰 엔진이란 서로 다른 템플릿 문법을 구현하는 교체 가능한 모듈입니다. ASP.NET MVC가 사용하는 기본 뷰 엔진은 ASP.NET 웹 폼의 템플릿과 마찬가지로.aspx/.ascx/.master 파일들을 사용합니다. 그 외에도 ASP.NET MVC 뷰 엔진으로 SparkNHaml이 많이 사용되고 있습니다.

우리가 구현하는 새로운 뷰 엔진은 코드 중심의 템플릿 접근법을 사용한 HTML의 생성에 최적화되어 있습니다. 이 새로운 뷰 엔진의 코드명은 "Razor"로 조만간 첫 번째 공식 베타 버전을 릴리즈할 예정입니다.

디자인 목표

우리는 "Razor"를 구현하면서 다음과 같은 디자인 목표를 수립했습니다.
  • 간결하고 유기적이며 풍부한 표현: Razor는 파일을 작성하는데 필요한 문자와 입력 수를 최소화하여 빠르고 유기적으로 워크플로우를 코딩할 수 있습니다. 다른 템플릿 문법들과 달리 Razor는 HTML 코드 내에 서버 측 코드를 명시적으로 표시할 필요가 없습니다. Razor 파서는 여러분의 코드 내에서 서버 측 코드를 알아서 유추해 냅니다. 따라서 정말 간결하며 풍부한 표현이 가능한 문법을 제공할 수 있기 때문에 빠르고 재미있게 코드를 작성할 수 있습니다.
  • 쉬운 학습: Razor는 배우기 쉬우며 최소한의 개념으로 매우 생산적으로 코드를 작성할 수 있습니다. 여러분은 이미 알고 있는 언어와 HTML 기술을 그대로 활용할 수 있습니다.
  • 새로운 언어는 아닙니다: 우리는 의도적으로 Razor를 위한 새로운 언어를 구현하지 않았습니다. 대신 개발자들이 이미 친숙한 C#과 VB (혹은 다른 언어)를 Razor와 함께 사용할 수 있기를 원했으며 여러분이 사용하는 언어를 토대로 놀라운 HTML 생성 기능을 제공하는 템플릿 마크업 문법을 제공하고자 했습니다.
  • 모든 텍스트 편집기에서 동작: Razor는 특별한 도구를 필요로 하지 않으며 평범한 텍스트 편집기에서도 코드를 작성할 수 있습니다(심지에 메모장으로도 훌륭한 결과를 만들어 낼 수 있습니다).
  • 훌륭한 인텔리센스 지원: Razor가 별도의 도구나 코드 편집기를 필요로 하지는 않지만 Visual Studio에서는 완벽한 인텔리센스를 지원합니다. Visual Studio 2010과 Visual Web Developer 2010은 Razor를 위한 완벽한 기능의 코드 편집기를 제공합니다.
  • 단위 테스트: 새로운 뷰 엔진은 뷰에 대한 단위 테스트를 제공합니다(컨트롤러나 웹 서버 및 특별한 AppDomain 없이도 모든 종류의 단위 테스트 프로젝트를 호스팅할 수 있습니다).
우리는 지난 몇 개월 간 애플리케이션의 구현을 위해 Razor를 사용했으며 (.NET이 아닌 다른 분야의 웹 개발자들의 그룹을 포함한) 다양한 자원봉사자들과 여러 가지 사용성 연구를 하고 있습니다. 많이 사용해 보시고 다양한 피드백을 주시면 감사하겠습니다.

선택과 확장성

ASP.NET의 훌륭한 기능 중 하나는 대부분의 기능들이 교체 가능하다는 점입니다. 만일 여러분이 원하는대로 동작하지 않는 기능을 발견한다면 다른 모듈로 대체할 수 있습니다.

ASP.NET MVC의 다음 버전에서는 "추가 > 뷰" 대화 상자를 새로 구현하여 뷰 템플릿 파일을 생성할 때 여러분이 템플릿 문법을 선택할 수 있도록 개선될 예정입니다. 또한 시스템에 설치되어 사용 가능한 뷰 엔진 중 하나를 손쉽게 선택할 수 있는 기능을 제공하여 보다 자연스럽게 뷰를 선택할 수 있게 될 것입니다.


Razor는 ASP.NET MVC에 내장된 뷰 엔진 중 하나가 될 것입니다. 모든 도우미 메서드들과 프로그래밍 모델 기능들은 Razor와 .ASPX 뷰 엔진에서 동일하게 적용됩니다.

또한 하나의 애플리케이션이나 사이트에서 여러 개의 뷰 엔진을 사용하는 뷰 템플릿을 혼합해서 사용할 수 있습니다. 예를 들어 일부 뷰는 .aspx 파일을 사용하고 다른 일부는 .cshtml이나 vbhtml 파일(각각 C#과 VB.NET을 위한 Razor의 파일 확장자입니다)을 사용할 수 있으며 또 다른 일부는 Spark나 NHaml을 사용하여 구성할 수 있습니다. 또한 어떤 뷰 엔진을 사용하는 뷰 템플릿 내에서 다른 뷰 엔진을 위한 부분 뷰(Partial View)를 중첩할 수도 있습니다. 여러분은 완벽한 선택과 유연성의 혜택을 보게 될 것입니다.

Razor를 이용한 Hello World 예제

Razor를 이용하면 정적 HTML (혹은 다른 텍스트 콘텐츠)에 서버 코드를 추가하여 동적인 페이지를 구성할 수 있습니다. Razor의 핵심 디자인 목표 중 하나는 이러한 코딩 과정을 보다 유기적으로 만들어 최소한의 코딩으로 서버 코드를 HTML 마크업에 추가할 수 있도록 하는 것입니다.

그러면 아래와 같은 결과를 만들어 내는 간단한 "Hello, World" 예제를 만들어 보겠습니다.


.ASPX 코드로 구현된 뷰

이 "Hello World" 예제를 ASP.NET의 ASPX 마크업 문법으로 구현한다면 우리는 HTML 마크업 내의 코드를 표현하기 위해 아래와 같이 <%= %> 태그를 사용해야 합니다.


이 "Hello World" 예제를 잘 살펴보면 코드의 시작과 끝을 알리기 위해 5개의 문자(<%= %>)가 필요합니다. 이들 중 일부(특히 키보드의 가운데 상단에 있는 % 문자)는 입력하기가 쉽지 않습니다.

Razor를 이용한 뷰

Razor에서는 @ 문자를 이용하여 코드의 시작과 끝을 표시할 수 있습니다. <% %> 태그와 달리 Razor는 코드 블록을 명시적으로 닫을 필요가 없습니다.


Razor 파서는 코드 블록 내에서 사용된 C#/VB 코드를 의미적으로 파악하기 때문에 위에서와 같이 코드 블록을 명시적으로 닫을 필요가 없습니다. Razor는 이런 구문들을 자체 코드 블록으로 파악하고 우리를 대신해 묵시적으로 코드 블록을 닫아줍니다.

이렇게 간단한 "Hello World" 예제에서 우리는 벌써 12개의 키 입력을 아낄 수 있었습니다. @ 문자는 % 문자보다 더 쉽게 입력할 수 있기 때문에 전체적으로 빠르고 유기적으로 코드를 입력할 수 있습니다.

루프와 중첩된 HTML 예제

일련의 제품 목록(과 제품의 가격)을 나열하는 또 다른 예제를 살펴보도록 합시다.


.ASPX 코드로 구현된 뷰

ASP.NET의 .ASPX 마크업 문법을 사용한다면 각각의 제품 정보를 나열할 <ul> 목록과 <li> 항목을 동적으로 생성하기 위해 아래와 같이 코드를 작성해야 합니다.


Razor를 이용한 뷰

아래의 예제 코드는 위의 코드를 Razor로 구현한 코드입니다.


위의 예제에서 알 수 있듯이 foreach 구문은 @ 기호를 이용하며 코드 블록 내에서 HTML 콘텐츠를 생성합니다. Razor 파서는 코드 블록 내의 C# 코드의 의미를 알고 있으므로 <li> 콘텐츠는 foreach 구문 내에 존재해야 하며 반복되는 콘텐츠로 처리됩니다. 또한 마지막의 } 문자는 foreach 구문을 종료하기 위한 것이라는 점도 인식합니다.

Razor는 <li> 태그 내의 @p.Name과 @p.Price 구문이 서버 측 코드이며 루프 내에서 매번 실행해야 한다는 것을 인식할 만큼 충분히 똑똑합니다. 또한 HTML과 코드가 혼합된 상황에서 유추를 통해 @p.Name과 @p.Price 코드를 자동적으로 닫아야 한다는 점도 스스로 알아냅니다.

이처럼 코드 블록을 열거나 닫기 위한 기호 없이도 코드를 작성할 수 있다는 것은 전체적인 코딩 프로세스를 매우 유기적이며 빠르게 만들어 줍니다.

If 블록과 다중 구문

아래와 같은 몇 가지 예제 코드를 더 살펴보겠습니다.

if 구문

foreach 예제와 마찬가지로 if 구문(혹은 C#이나 VB외의 다른 언어 구조)에서도 명시적인 코드 블록의 시작과 끝을 지정하지 않고도 콘텐츠를 포함할 수 있습니다. 예제를 살펴보겠습니다.


다중 구문

또한 @{ code } 블록내에 여러 줄의 구문을 포함할 수도 있습니다.


위의 예제에서 알 수 있듯이 변수들은 여러 개의 서버 코드 블록으로 확장될 수 있습니다. 예를 들어 message 변수는  여러 구문을 포함하는 @{ } 블록에 선언되었지만 @message 코드 블록에서도 사용됩니다. 이는 개념적으로 .aspx 마크업 파일의 <% %>와 <%= %> 구문과 동일합니다.

다중 토큰 구문

@( ) 구문은 여러 개의 토큰을 가질 수 있습니다. 예를 들어 문자열을 조합하는 앞서 예제의 코드는 다음과 같이 @( ) 코드 블록으로 재작성할 수 있습니다.


콘텐츠와 코드의 통합

Razor 파서는 여러분이 명시적으로 해왔던 작업들을 간소화할 수 있는 여러 가지 내장 기능들을 제공합니다.

HTML에서 메일 주소에 사용된 @와 충돌은 없나요?

Razor의 언어 파서는 템플릿에 사용된 @ 문자가 코드인지 정적 콘텐츠인지를 구분할 수 있습니다. 예를 들어 아래의 @ 문자는 메일 주소의 일부로 사용되고 있습니다.


이 파일을 파싱할 때 Razor 파서는 @ 문자 오른편의 콘텐츠가 C# 코드인지(CSHTML 파일의 경우) 아니면 VB 코드인지(VBHTML 파일의 경우) 아니면 그냥 정적인 콘텐츠인지를 판단합니다. 위의 코드는 아래와 같은 HTML 코드를 만들어 냅니다(메일 주소는 정적인 콘텐츠로 표현되며 @DateTime.Now는 서버 코드로 표현되고 있습니다).


콘텐츠가 코드로도 사용될 수 있는 경우(그리고 여러분은 이 콘텐츠를 콘텐츠로 처리하고 싶은 경우)에는 @@을 입력하여 명시적으로 @문자로 취급하도록 할 수도 있습니다.

중첩된 코드의 식별

if/else 구문이나 foreach 등 코드 블록 구문을 이용하여 HTML 콘텐츠를 중첩할 때 콘텐츠 블록의 시작을 명시적으로 판별할 수 있도록 HTML이나 XML 요소로 내부의 콘텐츠를 감싸야 합니다.

예를 들어 아래의 코드는 (서버 코드를 포함한) 다중 콘텐츠 블록을 <SPAN> 요소로 감싸고 있습니다.


이 코드는 다음과 같이 <SPAN> 태그를 그대로 사용하는 HTML 콘텐츠를 만들어 냅니다.


중첩된 콘텐츠를 <text> 블록으로 감싸면 콘텐츠를 감싸는 태그를 생략하고 나머지 콘텐츠만 렌더링할 수 있습니다.


위의 코드는 아래와 같이 콘텐츠를 감싸는 태그를 생략한 결과를 만들어 냅니다.


HTML 인코딩

기본적으로 @ 블록에 의해 출력되는 콘텐츠는 XSS 공격을 차단하기 위해 자동적으로 HTML 인코딩된 값으로 출력됩니다.

레이아웃/마스터 페이지 시나리오 - 기초

웹 사이트나 웹 애플리케이션에서 일관된 UI를 제공하는 것은 매우 중요한 일입니다. ASP.NET 2.0은 "마스터 페이지" 개념을 도입하여 .aspx 기반 페이지와 템플릿에 일관된 UI를 구현할 수 있는 기능을 제공합니다. Razor 또한 "레이아웃 페이지"를 제공하기 때문에 일반적인 사이트의 템플릿을 정의하고 이를 상속하여 사이트 내의 뷰와 페이지가 일관된 UI를 구현할 수 있습니다.

간단한 레이아웃 예제

아래의 예제는 SyteLayout.cshtml이라는 파일에 작성한 간단한 레이아웃 페이지입니다. 이 페이지에는 정적인 HTML 콘텐츠는 물론 동적인 서버 코드도 작성할 수 있습니다. 또한 "RenderBody()" 도우미 메서드를 호출해서 요청된 URL에 대한 콘텐츠를 채워넣을 수 있습니다.


그런 후 요청된 페이지를 위한 페이지 본문을 구성하는 콘텐츠/코드를 가지고 있는 "Home.cshtml" 뷰 템플릿을 생성하고 아래와 같이 콘텐츠를 작성하면 됩니다.


위의 코드에서 보듯이 LayoutPage 속성에 Home.cshtml 파일을 지정하였습니다. 이렇게 하면 이 뷰에 대한 레이아웃 페이지로 SiteLayout.cshtml 파일이 사용됩니다. 혹은 ASP.NET MVC 컨트롤러가 Home.cshtml 파일을 뷰 템플릿으로 호출할 때 레이아웃 파일을 지정하거나 아니면 사이트를 위한 기본 레이아웃을 설정할 수도 있습니다(이 경우 프로젝트 내의 모든 뷰 템플릿이 자동적으로 지정된 하나의 레이아웃 페이지를 사용하게 됩니다).

Home.cshtml 파일을 뷰 템플릿으로 렌더링하면 레이아웃과 자식 페이지의 콘텐츠가 하나로 합쳐져 아래와 같은 결과를 만들어 냅니다.


간결하고 명확하며 풍부한 표현이 가능한 코드

위의 예제에서 알 수 있듯이 레이아웃을 정의하고 사용하면 뷰나 페이지를 간결한 코드로 명확하게 구현할 수 있습니다. SiteLayout.cshtml 파일과 Home.cshtml 파일의 코드는 모든 콘텐츠를 두 개의 .cshtml 파일로 구성합니다. 즉, 추가적인 설정이나 태그도 필요치 않으며 <%@Page %> 디렉티브도 필요하지 않으며 다른 어떤 태그나 값을 설정할 속성도 필요치 않습니다.

우리는 여러분이 간력하고 쉬우며 유기적인 코드를 작성할 수 있도록 노력하고 있습니다. 또한 텍스트 편집기 만으로 모든 작업이 손쉽게 가능하도록 구현하고자 합니다. 여기에는 어떠한 코드 생성이나 인텔리센스도 필요치 않습니다.

레이아웃/마스터 페이지 시나리오 - 섹션 덮어쓰기

레이아웃 페이지에는 또 다른 섹션을 정의하여 해당 레이아웃을 사용하는 뷰 템플릿이 사용자 정의 콘텐츠를 채워넣을 수 있는 기능을 제공합니다. 이 기능을 활용하면 단편화된 콘텐츠 영역을 레이아웃 페이지에 채워넣어 사이트의 레이아웃에 대한 유연성을 확보할 수 있습니다.

예를 들어 SiteLayout.cshtml 파일에 두 개의 섹션을 추가하여 사이트가 제공하는 뷰 템플릿을 어디에 채워넣을지를 결정할 수 있습니다. 새로 추가된 섹션들은 "menu"와 "footer"라고 명명하고 RenderSection() 메서드를 호출할 때 optional=true 매개 변수를 지정하여 해당 섹션이 선택적인(즉, 반드시 필요하지는 않은) 부분이라고 표시할 수 있습니다(이 기능은 제가 예전에 포스팅했던 C#의 새로운 선택적 매개 변수 문법을 통해 구현됩니다).


새로 추가된 두 섹션은 선택적인 섹션으로 표시하였기 때문에 이 두 섹션을 Home.cshtml 파일에 정의할 필요는 없습니다. 따라서 이 사이트는 두 개의 섹션에 채워질 뷰 템플릿이 존재하지 않더라도 잘 동작합니다.

그러면 Home.cshtml 파일로 돌아가 Menu와 Footer 섹션을 정의해 보겠습니다. 아래의 코드는 Home.cshtml 파일의 모든 콘텐츠를 구현한 코드입니다. 별도로 다른 파일에 구현할 필요는 없습니다. 한 가지 변경된 것은 LayoutPage 속성 설정을 전체 사이트를 위한 설정으로 변경했다는 점입니다.


Menu와 Footer 섹션은 파일 내의 @section {}  블록을 통해 재정의되었습니다. 주 콘텐츠와 본문 콘텐츠를 구별할 필요 없이 단순히 인라인 코드로 정의하기만 하면 됩니다 (이렇게 함으로써 키 입력을 줄일 수 있음은 물론 이미 존재하는 페이지의 코드를 변경하지 않고도 레이아웃 페이지에 새로운 섹션을 손쉽게 추가할 수 있습니다).

Home.cshtml 파일을 다시 렌더링해보면 레이아웃과 서브 페이지들의 콘텐츠가 결합되어 새로 정의한 두 개의 섹션이 나타나게 될 것입니다. 클라이언트에 전송되는 HTML 코드는 다음과 같습니다.


캡슐화와 HTML 도우미 클래스의 재사용

지금까지 레이아웃 페이지를 이용해 사이트 전반의 UI를 구성하는 방법에 대해 알아보았습니다. 이제는 HTML 도우미 클래스를 재사용하여 HTML 생성 기능을 전체 사이트 - 는 물론 서로 다른 여러 사이트에서도 - 에서 재사용하는 방법에 대해 알아보겠습니다.

코드 기반 HTML 도우미

ASP.NET MVC에서는 코드 블록 내에서 호출이 가능하며 HTML 코드를 생성하는 HTML 도우미 메서드라는 개념을 제공합니다. 이들은 순수하게 코드로 (특히 확장 메서드 형태로) 구현되어 있습니다. ASP.NET MVC에 내장된 모든 HTML 확장 메서드는(여러분이 직접 구현하든 다른 사람이 구현했든) Razor 뷰 엔진에서도 그대로 활용할 수 있습니다.


선언적 HTML 도우미

코드만으로 구성된 클래스를 이용하여 HTML 출력물을 생성하는 방법은 잘 동작하기는 하지만 이상적인 형태는 아닙니다.

우리가 Razor를 통해 제공하고자 했던 기능 중 하나는 재사용 가능한 HTML 도우미를 보다 선언적인 방법으로 사용할 수 있도록 하는 것이었습니다. 아래와 같이 @helper {} 문법을 이용하면 재사용 가능한 도우미를 정의할 수 있습니다.


여러분은 Views\Helpers\ 디렉토리에 세 개의 .cshtml 파일을 구현하고 이를 사이트 내의 다른 어떤 뷰나 페이지에서도 재사용할 수 있습니다(특별히 다른 단계를 밟을 필요가 없습니다).


위의 예제에서 보듯이 ProductListing() 도우미 메서드는 매개 변수와 인자를 정의할 수 있습니다. 따라서 여러분은 원하는 어떤 것이든 매개 변수로 전달할 수 있습니다(또한 선택적 매개 변수와 Nullable 타입, 제네릭 등 기존의 언어적 특성들을 그대로 활용할 수 있습니다). 또한 Visual Studio를 사용한다면 이 메서드들을 디버깅할 수도 있습니다.

참고: @helper 구문은 Razor의 첫 번째 베타에는 포함되지 않을 예정이지만 이후의 버전에는 포함될 것입니다. 코드 기반 도우미는 첫 번째 베타에서 정상적으로 동작할 것입니다.

인라인 템플릿을 매개 변수로 전달하기

Razor의 또 다른 유용한 (그리고 정말 강력한) 기능은 "인라인 템플릿"을 도우미 메서드의 매개 변수로 전달할 수 있다는 점입니다. 이러한 "인라인 템플릿"은 HTML과 코드를 모두 포함할 수 있으며 필요에 따라 도우미 메서드에서 호출할 수도 있습니다.

아래의 예제는 DataGrid를 렌더링하는 "Grid" HTML 도우미 메서드를 사용하는 방법을 보여줍니다.


위의 예제에서 호출하는 Grid.Render() 메서드는 C# 구문입니다. 우리는 C#의 새로운 명명된 매개 변수 문법을 이용하여 강력하게 형식화된 인수를 Grid.Render 메서드에 전달합니다. 따라서 위의 구문은 컴파일 시점에 완벽한 코드 완성 및 인텔리센스의 지원을 받을 수 있습니다.

우리가 컬럼을 정의할 때 전달한 "format" 매개 변수가 바로 "인라인 템플릿"이며 보시다시피 HTML과 코드를 모두 포함하여 출력될 데이터의 형식을 재정의할 수 있습니다. 이 기능의 강력한 점은 Grid 도우미가 인라인 템플릿을 마치 대리자 메서드처럼 호출할 수 있으며 필요에 따라 몇 번이고 호출할 수 있다는 점입니다. 예제에서는 Grid의 각 행을 렌더링할 때 매번 인라인 템플릿이 호출되며 데이터를 표시하는 역할을 담당하는 템플릿에서 참조할 수 있는 "item" 변수를 전달해 줍니다.

이 방법을 이용하면 보다 풍부한 HTML 도우미 메서드를 개발할 수 있습니다. 이러한 도우미 메서드는 코드 접근 방식(현재 확장 메서드를 이용하는 방식) 뿐만 아니라 @helper {} 구문을 이용하는 선언적 방식으로도 구현이 가능합니다.

Visual Studio 지원

앞서 설명했듯이 Razor의 구현 목표 중 하나는 최소한의 입력이며 기본적인 텍스트 편집기에서도 손쉽게 편집이 가능하도록 하는 것입니다. 이를 위해 우리는 문법 자체를 간결하며 명확하게 유지했습니다.

또한 우리는 Razor를 디자인함에 있어 Visual Studio의 풍부한 코드 편집 기능을 활용할 수 있도록 구현했습니다. Razor 기반 파일에서도 HTML과 자바스크립트, C#/VB 코드에 대한 인텔리센스 기능을 완벽하게 지원합니다.


위의 그림에서 보듯이 foreach 구문 내에 포함된 코드에서 @p를 통해 참조하는 Product 객체에 대해 인텔리센스 기능이 동작하는 것을 볼 수 있습니다. 또한 솔루션 탐색기를 보면 Views 폴더가 .aspx 뷰 템플릿과 .cshtml 뷰 템플릿을 모두 가지고 있음도 알 수 있습니다. 여러분은 하나의 애플리케이션에서 여러 가지 뷰 엔진을 사용할 수 있으며 이로 인해 여러분에게 적합한 뷰 엔진을 손쉽게 선택할 수 있습니다.

요약

우리는 "Razor"가 코드 중심의 템플릿을 위한 새로운 뷰 엔진이라고 생각합니다. Razor를 이용하면 빠르고 풍부하며 재미있는 코딩을 즐길 수 있습니다. 문법은 매우 간결하며 입력을 최소로 줄여주는 동시에 마크업과 코드의 가독성을 전체적으로 향상시켜줍니다. Razor는 ASP.NET MVC의 다음 버전에서는 내장 뷰 엔진 중 하나로 제공될 것입니다. 또한 개별적인 .cshtml/.vbhtml 파일을 애플리케이션에 추가하여 개별적인 페이지로 실행시킬 수도 있습니다. 이를 통해 ASP.NET 웹 애플리케이션에서도 Razor를 활용할 수 있게 됩니다.

우리가 지난 몇 달간 고생해서 만든 Razor를 많은 개발자들이 사용해보고 다양한 피드백을 제공해 주기를 기대합니다. 조만간 첫 번째 공개 베타를 공개할 예정이므로 많은 피드백을 제공해 주시기를 바랍니다.

이 글이 도움이 되었기를 바라며

Scott

P.S: 블로그 외에도 저는 트위터를 통해 업데이트 소식과 관련 링크를 전달하고 있습니다. twitter.com/scottgu를 통해 저를 팔로우 해주세요.


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

안녕하세요? 웹지니입니다.
지난 포스트에서 말씀드렸듯이 10월 2일을 기점으로 ASP.NET MVC 2.0의 Preview 2 버전이 릴리즈 되었습니다. 이번 Preview 2 버전에서는 약간의 새로운 기능들이 추가되었는데요. 이번 포스트에서는 클라이언트 측 유효성 검사 기능에 대해 알아보도록 하겠습니다.

예제 다운로드:  MvcApplication1.zip (302.64 kb)

Valdiation with DataAnnotations API

ASP.NET MVC 2 Preview 1에서는 .NET 3.5 SP1에서 새롭게 선보인 DataAnnotations API를 이용한 유효성 검사 기능이 추가되어 있습니다. 예전에 올렸던 ASP.NET MVC 2 Preview 1 릴리즈에 대한 번역 포스트에서 이미 한 번 소개해 드렸었지요. 이 방식의 문제점은 데이터 유효성 검사를 위해 매번 폼을 POST 해야 한다는 점이었습니다. 즉 유효성 검사가 무조건 서버 측 코드에 의해 실행되기 때문에 폼이 Submit 되기 전에는 입력 요소에 대한 유효성 검사를 할 수 없다는 뜻입니다. 사실 말이야 바른 말이지 요새 누가 이런 식으로 구현하겠어요?

Client-Side Valdiation in ASP.NET MVC 2 Preview 2

해서 ASP.NET MVC 2 Preview 2에서는 Client-Side Validation API를 제공하여 이와 같은 불편을 해소하고 있습니다. 거두절미하고 예제를 통해 한 번 살펴볼까요? 우선 아래와 같이 모델 객체와 모델 객체에 대한 메타데이터 클래스가 존재한다고 가정해 보겠습니다.

코드 1: Profile 클래스와 ProfileMetadata 클래스

  1: namespace MvcApplication1.Models
  2: {
  3: 	[MetadataType(typeof(ProfileMetadata))]
  4: 	public class Profile
  5: 	{
  6: 		public string Name { get; set; }
  7: 		public string Email { get; set; }
  8: 	}
  9: 
 10: 	public class ProfileMetadata
 11: 	{
 12: 		[Required(ErrorMessage="이름을 입력해 주세요.")]
 13: 		public object Name;
 14: 
 15: 		[Required(ErrorMessage="국가를 입력해 주세요.")]
 16: 		public object Email;
 17: 	}
 18: }
 19: 

그리고 이 객체를 생성하는 HomeController 클래스의 Create 액션 메서드는 다음과 같이 구현되어 있습니다.

코드 2: HomeController.Create 액션 메서드

  1: [HandleError]
  2: public class HomeController : Controller
  3: {
  4: 	public ActionResult Create()
  5: 	{
  6: 		return View();
  7: 	}
  8: 
  9: 	[HttpPost]
 10: 	public ActionResult Create(FormCollection forms)
 11: 	{
 12: 		Profile profile = new Profile();
 13: 		this.TryUpdateModel<Profile>(profile);
 14: 		return View();
 15: 	}
 16: }

마지막으로 /Home/Create 뷰 페이지는 다음과 같이 구현되어 있습니다.

코드 3: /Home/Create 뷰 페이지의 소스 코드

  1: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
  2: 
  3:     <h2>Create</h2>
  4: 
  5:     <%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>
  6: 
  7:     <% using (Html.BeginForm()) {%>
  8: 
  9:         <fieldset>
 10:             <legend>Fields</legend>
 11:             <p>
 12:                 <label for="Name">Name:</label>
 13:                 <%= Html.EditorFor(p => p.Name) %>
 14:                 <%= Html.ValidationMessage("Name", "*") %>
 15:             </p>
 16:             <p>
 17:                 <label for="Email">Email:</label>
 18:                 <%= Html.EditorFor(p => p.Email)%>
 19:                 <%= Html.ValidationMessage("Email", "*") %>
 20:             </p>
 21:             <p>
 22:                 <input type="submit" value="Create" />
 23:             </p>
 24:         </fieldset>
 25: 
 26:     <% } %>
 27: 
 28:     <div>
 29:         <%=Html.ActionLink("Back to List", "Index") %>
 30:     </div>
 31: 
 32: </asp:Content>

예제 코드에서 보듯이 13번 라인과 19번 라인은 기본적으로 생성되는 Html.TextBox 메서드 대신 Html.EditorFor 메서드를 사용하도록 수정한 상태입니다. 이 상태에서 예제 애플리케이션을 실행하고 /Home/Create 뷰를 실행하면 아래와 같은 그림이 나타나게 됩니다.

그림 1: /Home/Create 뷰에서 Submit 버튼을 클릭하여 서버 측 유효성 검사가 실행된 모습
img1

위의 그림 1의 모습은 /Home/Create 뷰에서 Create 버튼을 클릭하여 서버 측 유효성 검사가 실행된 모습입니다. Preview 1에 추가된 DataAnnotations API를 이용한 유효성 검사가 잘 동작하고 있지만 앞서 설명드렸듯이 이 페이지는 이미 서버 측으로 페이지가 Submit 된 이후의 모습입니다. 그러면 이 /Home/Create 뷰를 클라이언트 측 유효성 검사가 동작하도록 수정해 보겠습니다.

코드 3: 수정된 /Home/Create 뷰 페이지의 소스 코드

  1: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
  2: 
  3:     <script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
  4:     <script src="../../Scripts/jquery.validate.js" type="text/javascript"></script>    
  5:     <script src="../../Scripts/MicrosoftMvcJQueryValidation.js" type="text/javascript"></script>
  6: 
  7:     <h2>Create</h2>
  8:     <% Html.EnableClientValidation(); %>
  9:     <%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>
 10: 
 11:     <% using (Html.BeginForm()) {%>
 12: 
 13:         <fieldset>
 14:             <legend>Fields</legend>
 15:             <p>
 16:                 <label for="Name">Name:</label>
 17:                 <%= Html.EditorFor(p => p.Name) %>
 18:                 <%= Html.ValidationMessage("Name", "*") %>
 19:             </p>
 20:             <p>
 21:                 <label for="Email">Email:</label>
 22:                 <%= Html.EditorFor(p => p.Email)%>
 23:                 <%= Html.ValidationMessage("Email", "*") %>
 24:             </p>
 25:             <p>
 26:                 <input type="submit" value="Create" />
 27:             </p>
 28:         </fieldset>
 29: 
 30:     <% } %>
 31: 
 32:     <div>
 33:         <%=Html.ActionLink("Back to List", "Index") %>
 34:     </div>
 35: 
 36: </asp:Content>

예제 코드에서 눈 여겨 볼 부분은 3번 라인부터 5번 라인까지 스크립트를 페이지에 추가하는 코드와 8번 라인의 Html.EnableClientValidation() 메서드 호출입니다. 이 메서드는 페이지에 클라이언트 측 유효성 검사를 활성화하는 역할을 담당하며 이는 앞서 설명한 세 개의 스크립트 파일이 페이지에 추가되어 있어야 가능하게 됩니다. 그 이외의 코드는 전혀 변한 것이 없지만 페이지를 실행해 보면 아래 그림과 같이 그림 1과는 다소 다른 모습을 볼 수 있습니다.

그림 2: 클라이언트 측 유효성 검사가 동작한 모습
img2

그림 2에서 보듯이 유효성 검사 메시지가 Html.ValidationMessage 메서드를 호출한 위치에 나타나지만 ValidationSummary 메서드를 호출한 영역에는 아무것도 나타나지 않습니다. 또한 텍스트 상자에 값을 입력하면 유효성 오류 메시지는 곧바로 사라지도록 구현이 되어 있습니다. 제법 잘 만들어 진 것 같지요?

이렇게 함으로써 우리가 얻을 수 있는 이점은 모델 객체에 대한 메타데이터 클래스만 구현함으로써 모델 객체에 대한 유효성 검사 코드를 전혀 작성할 필요가 없다는 점. 또한 클라이언트 측 유효성 검사 기능을 이용함으로써  메타데이터 클래스 만으로 서버 측과 클라이언트 측 유효성 검사를 동시에 처리할 수 있다는 점 등이 장점으로 작용할 수 있겠습니다. 시간이 지날수록 점점 더 편리하면서도 강력한 기능을 제공하는 ASP.NET MVC 2의 정식 버전이 벌써부터 기다려지네요. =)

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

안녕하세요? 웹지니입니다.
모두 추석 연휴는 즐겁게 보내셨나요? 저도 오랫만에 식구들과 편안한 시간을 가졌습니다. 열심히 재충전 했으니 이제 또 열심히 달려봐야죠?
추석 연휴 전날 즉 10월 1일부터 재충전 한답시고 인터넷을 뚝 끊었다가 오늘 출근해서 다시 보니 ASP.NET MVC 2.0 Preview 2 버전이 그새 릴리즈 되었더라구요 -ㅅ-;;
이번 Preview 2 버전에서 달라진 점들을 간략히 요약해 볼까요?

1. Client-Side Validation
ASP.NET MVC 2.0 Preview 2에서는 jQuery 기반의 클라이언트 측 유효성 검사 라이브러리가 추가되었습니다.

2. Areas
ASP.NET MVC 2.0 Preview 2에서는 Area 지원이 보다 원활해 졌습니다. 이미 Preview 1버전에서도 지원은 가능했지만 자동화되어 있지 않았기에 개발자가 직접 프로젝트 파일을 수정해야 하는 불편이 있었지요.

3. Model Validation Providers
모델 객체를 바인딩할 때 개발자가 원하는 유효성 검사를 수행할 수 있게 되었습니다. 기본적으로는 DataAnnotations API를 이용하고 있네요. 사실 아시는 분은 아시겠지만 DataAnnotations API는 아직 문제가 좀 있지요?

4. Metadata Providers
앞서 모델 객체의 유효성 검사와 연관되는 사항으로 모델 객체에 대한 사용자 정의 메타데이터 객체를 지정할 수 있습니다. 기본적으로 사용되는 메타데이터 제공자 역시 DataAnnotations API를 활용하고 있습니다.

Preview 1에 비해 많이 달라진 부분은 없는 것 같네요. 아마도 안정성의 향상과 Preview 1에서의 불편했던 점을 해소하는 방향으로 개발이 진행된 느낌입니다. 저도 아직 설치를 못해봤으니 빨리 설치해 봐야겠어요. 참, 다운로드는 여기를 클릭하세요!

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

안녕하세요? 웹지니입니다.
이번 포스트에서는 ASP.NET MVC 1.0에서 뷰의 렌더링 방식을 관리하는 ViewEngine에 대해 알아보고 사용자 정의 ViewEngine을 구현하여 MVC 프레임워크의 뷰 렌더링 방식을 커스터마이징하는 방법에 대해 알아보겠습니다.

예제 코드 다운로드: MvcApplication1.zip (535.71 kb) 

Understanding ViewEngines

ViewEngine은 ASP.NET MVC 프레임워크에서 지정된 뷰 파일을 탐색하고 해당 뷰를 렌더링하기 위해 필요한 일련의 작업을 담당하는 컴포넌트입니다. 우선 아래 그림을 살펴볼까요?

그림 1: MVC 요청의 처리 순서
img1

위의 그림은 MSDN 매거진 2009년 7월호에 게재된 Dino Esposito의 아티클 Comparing Web Forms and ASP.NET MVC에서 발췌한 MVC 요청의 실행 다이어그램입니다. 간략히 설명하자면 먼저 웹브라우저에 의해 MVC 요청이 발생하면 URL 라우팅 엔진이 해당 요청을 분석하고 이 요청을 처리할 컨트롤러 객체의 인스턴스를 생성하게 됩니다. 그런 후 지정된 액션이 호출되고 이 액션을 처리할 컨트롤러의 액션 메서드가 실행됩니다. 이 액션 메서드가 View() 메서드나 PartialView() 메서드를 호출하여 뷰를 렌더링하고자 하면 해당 뷰를 탐색(Lookup View)하고 렌더링하는 (Render View) 과정을 거치게 되는데 이 역할을 담당하는 것이 바로 ViewEngine입니다.

IViewEngine 인터페이스

ASP.NET MVC의 ViewEngine은 IViewEngine 인터페이스를 구현하는 클래스입니다. IViewEngine 인터페이스는 다음과 같이 정의되어 있습니다.

코드 1: IViewEngine 인터페이스

public interface IViewEngine
{
    ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
    ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
    void ReleaseView(ControllerContext controllerContext, IView view);
}


위의 코드에서 보듯이 IViewEngine 인터페이스는 지정된 뷰를 찾기 위한 FindView 메서드와 FindPartialView 메서드를 정의하고 있습니다. ReleaseView 메서드는 렌더링이 완료된 뷰를 메모리에서 해제하기 위해 호출되는 메서드입니다. 따라서 IViewEngine 인터페이스를 구현하는 클래스를 정의하고 FindView 메서드나 FindPartialView 메서드만 구현하면 사용자 정의 ViewEngine을 구현할 수 있습니다.

특히 ReleaseView 메서드에는 IView 인터페이스가 매개 변수로 전달되는데 이 IView 인터페이스는 ViewEngine에 의해 관리되는 뷰 자체를 구현하기 위한 인터페이스입니다. ASP.NET MVC에는 기본적으로 WebFormView 클래스가 이 인터페이스를 구현하고 있으며 그런 이유로 웹 폼 페이지(*.aspx)나 사용자 정의 컨트롤(*.ascx)을 뷰로 활용할 수 있게 되는 것입니다. 따라서 IView 인터페이스를 구현하는 클래스를 정의한다면 이 또한 웹 폼 뷰를 대체하여 전혀 다른 형태의 뷰를 구현할 수 있게 되는 셈입니다. IView 인터페이스는 다음과 같이 정의되어 있습니다.

코드 2: IView 인터페이스

  1: public interface IView
  2: {
  3:     void Render(ViewContext viewContext, TextWriter writer);
  4: }


위의 코드에서 보듯이 IView 인터페이스는 Render라는 이름의 메서드만을 정의하고 있습니다. 따라서 그림 1에서 Lookup View 과정과 Render View 과정이 각각 ViewEngine과 View 객체에서 처리된다는 것을 알 수 있습니다.

Implementing Your Own ViewEngine

그러면 이제 새로운 ViewEngine을 직접 구현해 보도록 하겠습니다. 이번 포스트에서 작성해 볼 ViewEngine은 지정된 컨트롤러의 액션 메서드를 위한 HTML 마크업 코드를 XML 파일로부터 가져와 렌더링하는 ViewEngine입니다. 예제로 구현된 것이기에 중간에 서버 측 코드를 실행하는 등의 기능은 실행할 수 없지만 아주 불가능한 것은 아니므로 그 부분은 독자 여러분의 몫으로 남겨놓고 이번 예제에서는 ViewEngine의 구현 과정에 대해서 이해할 수 있는 수준의 예제로 구현해 보겠습니다.

우선 Visual Stduio 2008을 열고 새로운 ASP.NET MVC Web Application 프로젝트를 생성합니다. 새로 생성된 프로젝트의 솔루션 탐색기의 모습은 아래 그림과 같을 것입니다.

그림 2: ASP.NET MVC Web Application 프로젝트를 새로 생성한 모습
img2

프로젝트를 생성했으면 MvcApplication1 프로젝트의 App_Data 폴더에 Contents.xml이라는 이름의 XML 문서를 생성하고 다음과 같이 예제 콘텐츠를 작성합니다.

코드 3: Contents.xml 파일의 콘텐츠

  1: <?xml version="1.0" encoding="utf-8" ?>
  2: <contents>
  3: 	<content>
  4: 		<key>Home_Index</key>
  5: 		<method />
  6: 		<body>
  7: 			<![CDATA[
  8: 				<html>
  9: 				<body>
 10: 					<h2>Home_Index</h2>
 11: 					<a href="/Home/About">About</a>
 12: 				</body>
 13: 				</html>
 14: 			]]>
 15: 		</body>
 16: 	</content>
 17: 	<content>
 18: 		<key>Home_About</key>
 19: 		<method>POST</method>
 20: 		<body>
 21: 			<![CDATA[
 22: 				<html>
 23: 				<body>
 24: 				<h2>Home_About_Post</h2>
 25: 				</body>
 26: 				</html>
 27: 			]]>
 28: 		</body>
 29: 	</content>
 30: 	<content>
 31: 		<key>Home_About</key>
 32: 		<method>GET</method>
 33: 		<body>
 34: 			<![CDATA[
 35: 				<html>
 36: 				<body>
 37: 				<h2>Home_About_Get</h2>
 38: 				
 39: 				<form action="/Home/About" method="post">
 40: 					<input type="submit" />
 41: 				</form>
 42: 				</body>
 43: 				</html>
 44: 			]]>
 45: 		</body>
 46: 	</content>
 47: </contents>


위의 코드에서 정의된 <content> 요소는 <key>라는 이름의 요소를 가지고 있습니다. 잘 보면 이 요소의 값은 [컨트롤러_액션]의 형태를 가지고 있습니다. 뿐만 아니라 <method> 요소를 이용하여 HTTP 요청에 사용된 메서드가 GET인지 POST인지를 구분할 수 있도록 정의되어 있습니다. 이제 프로젝트에 ViewEngine이라는 이름의 폴더를 생성하고 여기에 XmlViewEngine이라는 이름의 클래스를 추가한 후 다음과 같이 코드를 작성합니다.

코드 4: XmlViewEngine 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: 
  7: namespace MvcApplication1.ViewEngine
  8: {
  9: 	public class XmlViewEngine : IViewEngine
 10: 	{
 11: 		#region IViewEngine Members
 12: 
 13: 		public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
 14: 		{
 15: 			throw new NotImplementedException();
 16: 		}
 17: 
 18: 		public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
 19: 		{
 20: 			string controller = String.Empty;
 21: 			string action = String.Empty;
 22: 
 23: 			if (!String.IsNullOrEmpty(viewName))
 24: 			{
 25: 				string[] contentKey = viewName.Split(new char[] { '_' });
 26: 				if (contentKey.Length != 2)
 27: 				{
 28: 					throw new InvalidOperationException();
 29: 				}
 30: 
 31: 				controller = contentKey[0];
 32: 				action = contentKey[1];
 33: 			}
 34: 
 35: 			if (String.IsNullOrEmpty(controller))
 36: 			{
 37: 				controller = controllerContext.RouteData.Values["controller"].ToString();
 38: 			}
 39: 
 40: 			if (String.IsNullOrEmpty(action))
 41: 			{
 42: 				action = controllerContext.RouteData.Values["action"].ToString();
 43: 			}
 44: 			
 45: 			ViewEngineResult result = new ViewEngineResult(
 46: 				new XmlView(
 47: 					controller,
 48: 					action
 49: 				),
 50: 				this
 51: 			);
 52: 
 53: 			return result;
 54: 		}
 55: 
 56: 		public void ReleaseView(ControllerContext controllerContext, IView view)
 57: 		{
 58: 			IDisposable disposable = view as IDisposable;
 59: 			if (disposable != null)
 60: 			{
 61: 				disposable.Dispose();
 62: 			}
 63: 		}
 64: 
 65: 		#endregion
 66: 	}
 67: }
 68: 


예제로 구현하는 XmlViewEngine 클래스는 FindPartialView 메서드는 지원하지 않으며 FindView 메서드만 지원합니다. FindView 메서드를 구현하는 코드는 그다지 어렵지는 않습니다. 간략히 설명드리면 viewName 인수에 전달된 뷰 이름을 언더스코어 문자(“_”)로 구분하여 컨트롤러와 액션 메서드의 이름을 각각 알아냅니다. 만일 값이 비어있다면 ControllerContext 클래스의 RouteData 속성에서 현재 요청에 사용된 컨트롤러와 액션 메서드의 이름을 그대로 사용합니다. 그런 후에는 새로운 ViewEngineResult 클래스의 인스턴스를 생성하며 이 때 XmlView 클래스의 인스턴스를 전달합니다. ReleaseView 메서드는 view 매개 변수에 전달된 IView 객체가 IDisposable 인터페이스를 구현하고 있다면 Dispose 메서드를 호출해 줍니다. XmlView 클래스는 다음과 같이 구현합니다.

코드 5: XmlView 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: using System.IO;
  7: using System.Web.Routing;
  8: using System.Xml.Linq;
  9: using System.Web.Hosting;
 10: 
 11: namespace MvcApplication1.ViewEngine
 12: {
 13: 	public class XmlView : IView
 14: 	{
 15: 		private string contentKey = String.Empty;
 16: 		private string requestMethod = String.Empty;
 17: 
 18: 		public XmlView(string controller, string action)
 19: 		{
 20: 			this.contentKey = String.Format(
 21: 				"{0}_{1}",
 22: 				controller,
 23: 				action
 24: 			);
 25: 
 26: 			requestMethod = HttpContext.Current.Request.HttpMethod;
 27: 		}
 28: 
 29: 		public XmlView(string controller, string action, string method)
 30: 			: this(controller, action)
 31: 		{
 32: 			requestMethod = method;
 33: 		}
 34: 
 35: 		private string GetContents(string contentKey, string httpMethod)
 36: 		{
 37: 			string contentFilePath = HostingEnvironment.MapPath("~/App_Data/Contents.xml");
 38: 			if (!File.Exists(contentFilePath)) {
 39: 				throw new FileNotFoundException();
 40: 			}
 41: 
 42: 			XDocument contents = XDocument.Load(contentFilePath);
 43: 
 44: 			var selectedContent = from content in contents.Descendants("content")
 45: 								  where content.Element("key").Value == this.contentKey
 46: 								  &&
 47: 								  (
 48: 									(content.Element("method").Value == String.Empty ||
 49: 									content.Element("method").Value == this.requestMethod)
 50: 								  )
 51: 								  orderby content.Element("method").Value descending
 52: 								  select content;
 53: 
 54: 			if (selectedContent == null || selectedContent.Count() == 0)
 55: 			{
 56: 				return String.Empty;
 57: 			}
 58: 			else
 59: 			{
 60: 				return selectedContent.First<XElement>().Element("body").Value;
 61: 			}
 62: 		}
 63: 
 64: 		#region IView Members
 65: 
 66: 		public void Render(ViewContext viewContext, TextWriter writer)
 67: 		{
 68: 			string content = this.GetContents(this.contentKey, this.requestMethod);
 69: 			writer.Write(content);
 70: 		}
 71: 
 72: 		#endregion
 73: 	}
 74: }
 75: 


XmlView 클래스는 GetContents 메서드와 Render 메서드를 가지고 있습니다. 이 중 Render 메서드는 IView 인터페이스에 정의된 메서드이며 GetContents 메서드를 호출하여 자신이 렌더링할 HTML 마크업 코드를 가져옵니다. 따라서 GetContents 메서드가 이 클래스의 핵심이며 코드에서 알 수 있듯이 Contents.xml 파일을 읽은 후 LINQ to XML을 이용하여 지정된 이름의 (컨트롤러_액션 형식의) 콘텐츠를 가져와 가장 첫 번째 요소의 <body> 요소 내의 문자열을 리턴하게 됩니다.

이렇게 해서 XmlViewEngine 클래스와 XmlView 객체를 완성했습니다. 이제 Controller 클래스가 XML 파일 내의 콘텐츠를 렌더링할 수 있도록 XmlView()라는 이름의 메서드를 구현해 보겠습니다. C#의 확장 메서드(Extension Method)를 이용하면 다음과 같이 ControllerExtensions 클래스를 구현해 보겠습니다.

코드 6: ControllerExtensions 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: 
  7: namespace MvcApplication1.ViewEngine
  8: {
  9: 	public static class ControllerExtensions
 10: 	{
 11: 		public static XmlViewResult XmlView(this Controller controller)
 12: 		{
 13: 			return XmlView(controller, null, null);
 14: 		}
 15: 
 16: 		public static XmlViewResult XmlView(this Controller controller, string actionName)
 17: 		{
 18: 			return XmlView(controller, actionName, null);
 19: 		}
 20: 
 21: 		public static XmlViewResult XmlView(this Controller controller, string actionName, string controllerName)
 22: 		{
 23: 			if (String.IsNullOrEmpty(actionName))
 24: 			{
 25: 				actionName = controller.ControllerContext.RouteData.Values["action"].ToString();
 26: 			}
 27: 
 28: 			if (String.IsNullOrEmpty(controllerName))
 29: 			{
 30: 				controllerName = controller.ControllerContext.RouteData.Values["controller"].ToString();
 31: 			}
 32: 
 33: 			XmlViewResult result = new XmlViewResult();
 34: 			result.Controller = controllerName;
 35: 			result.Action = actionName;
 36: 
 37: 			return result;
 38: 		}
 39: 	}
 40: }
 41: 


코드에서 보듯이 ControllerExtensions 클래스는 총 세 개의 XmlView라는 확장 메서드를 정의하고 있습니다. 이 메서드들은 Controller 클래스 및 이를 상속하는 클래스로 확장되며 XmlViewResult 클래스의 인스턴스를 생성하여 리턴합니다. XmlViewResult 클래스는 컨트롤러의 액션 메서드가 리턴하는 ActionResult 클래스를 상속한 클래스로 다음과 같이 구현합니다.

코드 7: XmlViewResult 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: 
  7: namespace MvcApplication1.ViewEngine
  8: {
  9: 	public class XmlViewResult : ActionResult
 10: 	{
 11: 		public XmlViewResult()
 12: 		{
 13: 		}
 14: 
 15: 		public IView View { get; set; }
 16: 		public string Controller { get; set; }
 17: 		public string Action { get; set; }
 18: 
 19: 		protected ViewEngineResult FindView(ControllerContext context)
 20: 		{
 21: 			if (String.IsNullOrEmpty(this.Controller)) {
 22: 				this.Controller = context.RouteData.Values["controller"].ToString();
 23: 			}
 24: 
 25: 			if (String.IsNullOrEmpty(this.Action)) {
 26: 				this.Action = context.RouteData.Values["action"].ToString();
 27: 			}
 28: 
 29: 			string viewName = String.Format("{0}_{1}", this.Controller, this.Action);
 30: 			ViewEngineResult result = ViewEngines.Engines.FindView(context, viewName, null);
 31: 
 32: 			if (result.View != null)
 33: 			{
 34: 				return result;
 35: 			}
 36: 			throw new InvalidOperationException();
 37: 		}
 38: 
 39: 		public override void ExecuteResult(ControllerContext context)
 40: 		{
 41: 			ViewEngineResult result = null;
 42: 			if (this.View == null)
 43: 			{
 44: 				result = this.FindView(context);
 45: 				this.View = result.View;
 46: 			}
 47: 
 48: 			ViewContext viewContext = new ViewContext(context, this.View, new ViewDataDictionary(), new TempDataDictionary());
 49: 			this.View.Render(viewContext, context.HttpContext.Response.Output);
 50: 			if (result != null)
 51: 			{
 52: 				result.ViewEngine.ReleaseView(context, this.View);
 53: 			}
 54: 		}
 55: 	}
 56: }
 57: 


XmlViewResult 클래스는 ActionResult 클래스에 정의된 ExecuteResult 메서드를 구현해야 합니다. 이 메서드는 액션 메서드가 리턴한 ActionResult 클래스가 실행될 때 호출되며 XmlViewResult 클래스는 자신의 View 속성에 IView 인터페이스 객체가 존재하지 않으면 FindView 메서드를 호출하여 뷰를 탐색하고 탐색된 뷰의 Render 메서드를 호출합니다. FindView 메서드는 ViewEngines.Engines 컬렉션의 FindView 메서드를 호출하여 리턴된 ViewEngineResult 클래스의 View 속성이 null이 아니면 이를 다시 리턴하게 됩니다.

이로서 XmlViewEngine과 관련된 클래스의 구현을 모두 마쳤습니다. 이제 ASP.NET MVC 프레임워크가 우리가 만든 XmlViewEngine 클래스를 사용할 수 있도록 XmlViewEngine 클래스를 등록해 주어야 합니다. Global.asax.cs 파일을 열고 아래와 같이 코드를 작성합니다.

코드 8: Global.asax.cs 파일의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: using System.Web.Routing;
  7: using MvcApplication1.ViewEngine;
  8: 
  9: namespace MvcApplication1
 10: {
 11: 	// Note: For instructions on enabling IIS6 or IIS7 classic mode, 
 12: 	// visit http://go.microsoft.com/?LinkId=9394801
 13: 
 14: 	public class MvcApplication : System.Web.HttpApplication
 15: 	{
 16: 		public static void RegisterRoutes(RouteCollection routes)
 17: 		{
 18: 			routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 19: 
 20: 			routes.MapRoute(
 21: 				"Default",                                              // Route name
 22: 				"{controller}/{action}/{id}",                           // URL with parameters
 23: 				new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
 24: 			);
 25: 
 26: 		}
 27: 
 28: 		public static void RegisterViewEngine()
 29: 		{
 30: 			ViewEngines.Engines.Add(new XmlViewEngine());
 31: 		}
 32: 
 33: 		protected void Application_Start()
 34: 		{
 35: 			RegisterRoutes(RouteTable.Routes);
 36: 			RegisterViewEngine();
 37: 		}
 38: 	}
 39: }


이로써 필요한 모든 준비를 마쳤습니다.이제 프로젝트를 빌드하고 실행해보면 Home/Index URL을 호출했을 때 아래 그림과 같은 페이지가 나타나는 것을 볼 수 있습니다.

그림 3: Home/Index 페이지의 모습
 img3

여기서 [About] 링크를 클릭해보면 아래 그림과 같이 Home/About 페이지의 GET 형식 콘텐츠가 나타납니다.

그림 4: Home/About 페이지의 GET 요청에 대한 모습
 img4

여기서 다시 [Submit] 버튼을 클릭해보면 이번에는 Home/About 페이지의 POST 형식 콘텐츠가 나타나는 것을 볼 수 있습니다.

그림 5: Home/About 페이지의 POST 요청에 대한 모습
 img5

이상으로 간단한 사용자 정의 ViewEngiine의 구현에 대해 알아보았습니다.

Conculsion

다소 급하게 작성한 글이라 설명이 여러 모로 부족한 것을 느끼지만 예제 코드 자체가 그다지 어렵지 않으므로 이해하시기에는 무리가 없으리라 판단됩니다. 이번 포스트를 통해 살펴보았듯이 ASP.NET MVC의 가장 큰 장점 중 하나는 ASP.NET MVC 프레임워크 자체가 다양한 형태로 확장이 가능하도록 구현되어져 있으며 이를 이용하면 ASP.NET MVC 프레임워크의 세밀한 동작을 개발자가 마음대로 제어할 수 있다는 점입니다. 이런 장점은 HTTP 프로토콜에 대한 추상화 덕택에 오히려 확장성 면에서는 다소 떨어지는 모습을 보이는 ASP.NET MVC 웹 폼 모델과 비교해보면 더욱 크게 느껴질 수도 있습니다.

다음 포스트에도 ASP.NET MVC의 여러 가지 재미있는 기능들을 소개해 드릴 수 있게 되기를 기대하며 이번 포스트를 마칩니다.

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

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

며칠 전부터 ASP.NET MVC 2의 Preview 1 버전을 살펴보고 있었지만 그노무 NDA때문에 입을 열지 못했는데 드뎌 Scott Guthrie 횽아가 ASP.NET MVC 2 Preview 1 릴리즈에 대한 글을 올렸네요. 바로 번역질 들어갑니다!

ASP.NET MVC V2 Preview 1 Released!

ASP.NET MVC 팀이 얼마 전 ASP.NET MVC 버전 2의 Preview 1 버전을 릴리즈했습니다. 여기에서 다운로드하실 수 있어요.

이번 Preview 버전은 .NET 3.5 SP1과 Visual Studio 2008에서 동작하며 기존의 ASP.NET MVC 1.0이 설치된 시스템에 함께 설치할 수 있습니다 (즉, 두 버전이 서로 충돌하지 않으며 MVC 버전 2를 설치해도 기존의 MVC 버전 1 애플리케이션은 아무런 영향을 받지 않습니다). ASP.NET MVC 1.0과 ASP.NET MVC 2.0을 하나의 시스템에 모두 설치하면 Visual Studio 2008의 "새 프로젝트" 대화 상자에 아래와 같이 두 개의 ASP.NET MVC 프로젝트 템플릿이 나타나게 됩니다.

img1

ASP.NET MVC 2 Preview 1의 릴리즈 노트에는 ASP.NET MVC V2의 새롭고 강력한 기능들을 사용하기 위해 기존의 ASP.NET MVC 1.0 프로젝트를 업그레이드하려는 개발자들을 위해 업그레이드 절차를 상세히 소개하고 있습니다.

새로운 기능들

ASP.NET MVC V2는 여러 가지 새로운 기능들을 제공합니다 (이들 중 일부는 이미 ASP.NET MVC 로드맵 페이지에서 언급되고 있습니다). 이번에 릴리즈 된 Preview 1 버전은 이런 새로운 기능들의 일부를 구현하고 있습니다. 다른 많은 기능들은 이후의 Preview 버전에서 차차 제공될 예정입니다. Preview 1 버전은 아직은 무르익지 않은 버전이며 ASP.NET 팀은 여러분의 피드백을 받고자 이번 Preview 버전을 공개하게 되었습니다.

그러면 이제 Preview 1 버전의 여러 기능들을 간략하게 살펴볼까요?

영역(Area)의 지원

ASP.NET MVC 2는 MVC 애플리케이션을 분리하고 그룹화할 수 있는 영역(Area)라고 불리는 새로운 기능을 제공합니다.

영역들은 컨트롤러나 뷰를 그룹화하여 대용량 애플리케이션들을 구성하는 구성 요소들을 관련된 것들만 모아 그룹화할 수 있는 기능을 제공합니다. 각각의 영역은 별도의 ASP.NET MVC 프로젝트로 구현하여 주 애플리케이션이 참조하도록 구성할 수 있습니다. 이렇게 하면 대용량 애플리케이션을 구현할 때 복잡도를 줄이고 여러 팀이 동일한 애플리케이션을 함께 구현할 수 있습니다.

아래의 스크린샷은 세 개의 프로젝트로 구성된 하나의 솔루션을 보여주고 있습니다. 그 중 "CompanySite"라는 이름의 프로젝트는 사이트의 핵심 콘텐츠와 레이아웃, 컨트롤러와 뷰 등을 가지고 있습니다. 그리고 "Blogs"와 "Forums"라는 두 개의 "영역" 프로젝트가 존재하네요. 이 두 프로젝트는 각각 웹사이트의 /Blogs와 /Forums 부분에 필요한 기능들을 구성하기 위한 라우팅 규칙과 컨트롤러, 뷰 등을 구현하는 프로젝트입니다.

img2

Preview 1 버전은 영역 개념의 초기 구현을 포함하고 있습니다. 이 기능에는 아직 아무런 보조 도구들이 제공되지 않습니다(지금 당장은 영역 프로젝트를 수동으로 추가하고 구성해야 합니다). 이후의 Preview 버전에서는 영역 프로젝트 구성을 위한 도구들이 제공될 예정이며 지속적으로 개선되고 확장될 것입니다.

DataAnnotation 유효성 검사 지원

ASP.NET MVC 2는 .NET 3.5 SP1에서 제공되며 ASP.NET Dynamic Data와 .NET RIA Service에서 활용되되고 있는  DataAnnotation 유효성 검사 기능을 내장하고 있습니다. DataAnnotation을 이용하면 모델과 뷰 모델 클래스에 유효성 검사 규칙을 손쉽게 적용할 수 있으며 UI 핼퍼를 이용하여 유효성 검사를 자동화할 수 있습니다.

이 기능이 동작하는 것을 확인하기위해 아래와 같이 "Customer"라는 이름의 뷰 모델 클래스를 생성해 보겠습니다(아래의 코드는 C#의 자동 속성 기능을 이용하여 구현하였습니다).

img3

그리고 System.ComponentModel.DataAnnotations 네임스페이스에 구현된 DataAnnotation 특성들을 이용하여 각 속성에 적당한 유효성 검사 규칙을 지정합니다. 아래의 코드는 4개의 내장된 유효성 검사 규칙인 [Required], [StringLength], [Range], [RegularExpression] 특성들을 사용한 코드입니다. 이 네임스페이스에는 여러분이 원하는 유효성 검사 규칙을 구현하기 위한 기반 클래스(ValidationAttribute)도 포함되어 있습니다.

img4

이제 CustomersController 클래스를 생성하고 두 개의 Create 메서드를 구현해봅시다. 첫 번째 Create 메서드는 "/Customers/Create" URL에 대한 HTTP GET 방식의 요청을 처리하며 빈 Customer 객체를 이용하여 뷰를 렌더링합니다. 두 번째 Create 액션 메서드는 같은 URL에 대한 HTTP POST 방식의 요청을 처리합니다 (그렇지만 Customer 객체를 매개 변수로 사용합니다).  두 번째 Create 액션 메서드는 사용자의 입력을 서버로 전송할 때 모델 바인딩 오류가 발생했는지를 검사하며 만일 오류가 발생했다면 이미 입력된 데이터와 함께 뷰 템플릿을 다시 렌더링합니다. 만일 오류가 없다면 사용자에게 성공했음을 알리는 "Success" 뷰를 렌더링합니다.

img5

마지막으로 두 개의 Create 액션 메서드를 마우스 오른쪽 버튼으로 클릭하고 "Add View" 메뉴를 선택한 후 "Scaffold" 항목을 "Create"로 선택하면 Customer 객체를 기준으로 뷰 템플릿을 자동으로 생성해 줍니다. 이렇게 생성된 뷰 템플릿은 아래와 같은 HTML 코드로 만들어집니다.

img6

이제 /Customers/Create URL을 요청하면 브라우저는 아래와 같이 빈 폼을 보여줍니다.

img7

우리가 잘못된 값을 입력하고 폼을 서버로 전송하면 ASP.NET MVC 2의 모델 바인더는 Customer 클래스에 DataAnnotations 특성이 지정된 것을 감지하고 자동적으로 입력된 값에 대한 유효성 검사를 수행합니다. 만일 유효성 오류가 발견되면 액션 메서드는 폼을 아래와 같이 적절한 오류 메시지와 함께 폼을 다시 렌더링합니다. 이 때 Html.Validation 핼퍼 메서드에 의해 DataAnnotation 특성에 지정한 오류 메시지가 페이지에 함께 출력되는 것을 볼 수 있습니다. 더욱이 이와 같은 동작을 구현하기 위해서 더 많은 코드를 작성할 필요도 없습니다.

img8

위 그림의 폼은 사용자가 잘못된 값을 입력하고 폼을 서버로 전송하려고 할 때마다 오류 메시지를 보여주게 됩니다.

다음 버전의 ASP.NET MVC 2 Preview 버전에서는 jQuery 유효성 검사 플러그인을 기본 프로젝트 템플릿에 포함하여 제공할 예정이며 DataAnnotation 규칙을 이용한 자동화된 클라이언트 측 자바스크립트 유효성 검사를 지원하게 될 것입니다. 이렇게 하면 개발자는 한 번의 코드를 작성함으로써 모델이나 뷰 모델 객체에 손쉽게 유효성 검사 기능을 추가할 수 있으며 애플리케이션에서 이런 객체들이 사용되는 곳이라면 어디든 클라이언트 및 서버 측 유효성 검사 기능을 갖출 수 있게 됩니다.

만일 모델이나 뷰 모델 객체에 직접 데이터 힌트를 주고싶지 않다면 모델 클래스와 함께 별도의 클래스를 구현하여 DataAnnotation 규칙을 분리하여 캡슐화할 수 있습니다. 이런 기능은 Visual Studio가 코드를 생성하고 클래스의 속성을 직접 수정하는 경우나 자동으로 생성된 코드에 특성을 쉽게 적용할 수 없는 경우(LINQ to SQL이나 LINQ to Entities 디자이너에 의해 만들어진 클래스들)에 유용하게 사용할 수 있습니다.

게다가 DataAnnotation 지원 기능이 내장되어있는 것 외에도 ASP.NET MVC V2의 DefaultModelBinder 클래스에는 새로운 virtual 메서드가 추가되어 우리가 원하는 어떤 종류의 유효성 검사 프레임워크(예를 들면 Castle Validator, EntLib Validation 등)와도 손쉽게 연동될 수 있습니다. ASP.NET MVC의 유효성 검사 UI 핼퍼 메서드들은 모든 종류의 유효성 검사 프레임워크를 지원하도록 디자인 되었습니다(또한 유효성 검사 프레임워크들은 DataAnnotation을 직접 사용하지 않습니다).

형식화된 UI 핼퍼

ASP.NET MVC V2는 형식화된(Strongly-Typed) 새로운 HTML UI 핼퍼 메서드들을 제공하며 람다 식을 이용하여 뷰 템플릿의 뷰 모델을 참조할 수 있게 되었습니다. 이런 기능을 통해 뷰를 컴파일 시점에 검사할 수 있게 되었으며(따라서 런타임 시점에 발생할 수 있는 버그를 줄일 수 있습니다) 뷰 템플릿에서 보다 향상된 인텔리센스를 지원할 수 있게 되었습니다.

향상된 인텔리센스가 동작하는 모습은 아래 그림과 같습니다. 그림을 보면 새롭게 제공되는 Html.EditorFor() 핼퍼 메서드를 호출할 때 Customer 모델 객체의 속성들이 나열되는 것을 볼 수 있습니다.

img9

Preview 1에는 새로운 Html.EditorFor(), Html.LabelFor(), Html.DisplayFor() 등의 핼퍼 메서드가 제공됩니다. 새롭게 버전 업된 MVC Futures의 어셈블리들은 Html.TextBoxFor(), Html.TextAreaFor(0, Html.DropDownListFor(), Html.HiddenFon(), Html.ValidationMessageFor() 등의 메서드들도 함께 제공됩니다 (나중에 이 메서드들은 ASP.NET MVC 2 어셈블리로 통합될 예정입니다).

아래의 그림은 Create 뷰 템플릿을 수정한 코드를 보여줍니다. 아래의 코드에서는 Customer 객체를 참조하는 문자열 표현식을 사용하는 대신 형식화 된 람다 식을 UI 핼퍼 메서드들과 함께 사용합니다. 따라서 컴파일 시점에 완벽한 인텔리센스의 지원을 받을 수 있게 되었습니다.

img10

UI 핼퍼 템플릿 지원

Html.EditorFor() 메서드와 Html.DisplayFor() 메서드는 표준 데이터 타입은 물론 여러 개의 속성을 가진 복합 객체(Complex Objects)들도 렌더링할 수 있습니다. 앞서 설명했듯이 [DisplayName]이나 [ScaffoldColumn]과 같은 특성을 뷰 모델 객체에 적용하여 얼마든지 마음대로 렌더링이 가능합니다.

종종 개발자들은 UI 핼퍼들이 만들어 주는 출력 결과도 재정의하고 싶어 합니다. Html.EditorFor() 메서드와 Html.DisplayFor() 메서드는 이와 같은 개발자들의 욕구를 충족시키기 위해 템플릿 매커니즘을 이용하여 개발자들이 외부의 테플릿을 이용하여 출력되는 결과를 재정의할 수 있도록 구현되었습니다. 이 기법은 데이터 타입 별 혹은 클래스 별로 사용될 때 더욱 유용합니다.

Preview 1을 사용하면 "EditorTemplates"와 "DisplayTemplates" 폴더를 \Views\[ControllerName]\ 디렉터리에 추가하거나(특정 컨트롤러가 사용하는 뷰들을 재정의할 경우) \Views\Shared\ 폴더에 추가(애플리케이션의 모든 뷰와 컨트롤러를 재정의하는 경우)할 수 있습니다.

그리고 부분적인 템플릿 파일들을 이런 폴더에 추가하여 출력 결과가 개별적인 데이터 타입이나 클래스를 토대로 수행될 수 있도록 할 수 있습니다. 예를 들어 아래의 그림에서 저는 EditorTemplates 폴더를 \Views\Shared\ 폴더에 추가했으며 이 폴더에는 세 개의 템플릿 파일이 존재합니다.

img11

이 그림에서 "Customer.ascx" 템플릿은 Html.EditorFor() 메서드에 전달되는 Customer 객체를 렌더링한 결과를 재정의하고 싶을 때 사용합니다(예를 들면 Customer 객체의 속성들이 렌더링 되는 순서나 레이아웃을 재정의할 수 있습니다). "DateTime.ascx" 템플릿은 Html.EditFor() 메서드에 전달된 DateTime 형식의 속성을 렌더링하는 방법을 재정의하고 싶을 경우 사용합니다(예를 들어 일반 텍스트 상자 대신 자바스크립트로 구현된 Date Picker를 사용할 수도 있습니다). 또한 "Object.ascx" 템플릿을 추가하여 모든 객체를 내 마음대로 렌더링할 수도 있습니다.

타입 별로 렌더링을 재정의할 수 있는 것은 물론 "명명된 템플릿"을 폴더에 추가할 수도 있습니다. 일반적으로 "CountryDropDown" 템플릿은 문자열 데이터 타입을 처리할 것입니다. 그러나 표준 텍스트 상자를 사용하는 대신 국가 값들을 가진 <select> 드롭다운 리스트를 사용하여 선택하도록 할 수 있습니다. 아래의 그림은 CountryDropDown.ascx 편집용 템플릿을 구현한 코드입니다.

img12

이 템플릿을 사용하려면 Html.EditFor() 메서드를 호출할 때 인자로 이름을 전달하면 됩니다. 예를 들어 아래의 코드는 Country 속성을 위한 람다 식을 지정하면서 렌더링에 사용할 편집용 템플릿의 이름도 함께 지정합니다.

img13

또는 뷰 모델 객체에 UIHint 특성을 지정할 수도 있습니다. 이렇게 하면 기본 편집기를나 표시기 템플릿을 한 곳에서 지정할 수 있으며 애플리케이션 내의 모든 뷰에서 활용할 수 있습니다 (즉, Html.EditFor() 메서드를 호출할 때 일일이 템플릿의 이름을 지정할 필요가 없습니다).

아래의 코드는 Customer.Country 속성에 UIHint 특성을 지정하여 기본적으로 이 속성 값을 렌더링할 때 CountryDropDown 템플릿을 사용하도록 하는 코드입니다.

img14

이 특성을 일단 뷰 모델 객체에 한 번 지정하면 Html.EditFor() 메서드를 호출할 때 템플릿의 이름을 명시적으로 지정할 필요가 없습니다. 이제 /Customers/Create URL을 새로 고치면 Country 속성이 아래 그림과 같이 표준 텍스트 상자 대신 드롭다운 목록으로 보여질 것입니다.

img15

그 외의 멋진 기능들

ASP.NET MVC 2 Preview 1 버전에는 작지만 아주 멋진 기능들이 다수 포함되어 있습니다. 그 중에 제가 좋아하는 것들을 살펴볼까요?

[HttpPost] 특성

ASP.NET MVC 프로젝트에서는 두 개의 액션 메서드로 하나의 URL을 처리하는 경우가 비일비재합니다. 즉, 하나는 GET 방식의 요청을 처리하고 다른 하나는 POST 방식의 요청을 처리하는 것이지요.

ASP.NET MVC 1에서는 아래와 같이 [AcceptVerbs(HttpVerbs.Post)] 특성을 지정하여 POST 방식의 요청을 처리할 액션 메서드를 지정했었습니다.

img16

ASP.NET MVC 2에서도 이 방법을 사용할 수는 있지만 [HttpPost] 특성을 이용하면 더욱 간편하게 동일한 결과를 얻을 수 있습니다.

img17

매개 변수의 기본 값

선택적인 매개 변수를 처리하는 것은 웹에서는 매우 일반적인 시나리오입니다. ASP.NET MVC 1에서는 선택적인 매개 변수들을 처리하기 위해서 라우팅 규칙을 재정의하여 기본 값을 지정하거나 혹은 액션 메서드의 매개 변수에 nullable 타입을 사용하여 액션 메서드가 null 값을 받아들일 수 있도록 했었습니다.

ASP.NET MVC 2 Previewe 1에서는 이제 액션 메서드의 매개 변수에 System.ComponentModel 네임스페이스에 구현된 DefaultValueAttribute 특성을 지정하여 기본 값을 지정할 수 있게 되었습니다. 이 특성을 이용하면 ASP.NET MVC가 현재 요청에 전달되지 않은 값이 존재하는 경우 매개 변수에 기본 값을 전달할 수 있습니다. 예를 들어 아래의 코드는 /Products/Browse/Beverages와 /Products/Browse/Beverages?page=2 URL에 대한 요청을 처리할 수 있는 액션 메서드를 보여줍니다. page 매개 변수는 쿼리 문자열에 값이 존재하지 않는 경우 1이라는 기본 값을 갖게 됩니다.

img18

현재는 VB도 VB 언어 내에서 매개 변수의 기본 값을 지정할 수 있습니다(앞서처럼 Defaultalue 특성을 명시적으로 지정할 필요가 없습니다). C#도 Visual Studio 2010 부터는 선택적 매개 변수를 지원할 예정이어서 위의 코드가 아래와 같이 간소화될 수 있습니다.

img19

이렇게 하면 기본 값 혹은 선택적 매개 변수가 필요한 경우를 보다 쉽고 깔끔하게 구현할 수 있습니다.

바이너리 데이터의 바인딩

ASP.NET MVC 2 Preview 1에는 Base64 형식으로 인코딩된 문자열 값을 byte[] 타입이나 System.Data.Linq.Binary 타입에 바인딩할 수 있는 기능이 제공됩니다. 그런 이유로 Html.Hidden() 메서드가 이런 데이터 타입을 받아들일 수 있도록 추가로 두 가지 형식이 더 재정의 되었습니다.  이 기법을 이용하면 데이터베이스의 timestamp 값 등을 사용하는 애플리케이션을 더욱 견고하게 제어할 수 있습니다.

요약

여기를 클릭하면 이번 포스트에서 사용한 ASP.NET MVC 2 프로젝트를 담은 .ZIP 파일을 다운로드 할 수 있습니다.

ASP.NET MVC 2는 이제 첫 번째 Preview 버전이 발표되었을 뿐입니다. 이후에는 더 많은 Preview 버전들이 공개될 것이며 ASP.NET 팀은 ASP.NET MVC 2를 더욱 발전시킬 수 있는 다양한 피드백을 전달받기를 원합니다.

이렇게 정기적으로 Preview 버전을 공개하는 목적은 이처럼 피드백을 받는 과정을 공개하고 누구든지 원한다면 함께 작업할 수 있는 환경을 구성하기 위한 것입니다. 피드백, 제안 혹은 문제점들을 www.asp.netASP.NET MVC 포럼에 올려주세요. 또한 Phil Haack의 MVC 2 포스트에서도 많은 것을 배울 수 있으며 Preview 1의 릴리즈에 대한 Phil과 Scott Hanselman의 Channel 9 동영상도 있습니다.

도움이 되셨기를 바랍니다.

Scott

P.S: 저는 요즘 트위터를 사용하는 재미에 푹 빠졌어요. 저와 함께 트위터를 즐기시려면 http://twitter.com/scottgu를 방문해 주세요. (제 트위터 별칭은 @scottgu랍니다)

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

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

무척이나 오랫만에 기술 관련 포스팅을 하는 것 같습니다. 게다가 영어로 된 제목이라니!!! (역시나 간지에 치중하는 모습을 버리지 못하는 지니입니다 -ㅅ-;;) 이번 포스트에서는 ASP.NET MVC의 HtmlHelper 클래스와 요 녀석이 제공하는 메서드들에 대해 간략하게 살펴보고 내게 필요한 Custom HtmlHelper 메서드를 구현하는 방법에 대해 알아보겠습니다.

예제 소스 다운로드: MvcApplication1.zip (289.72 kb)

Prologue

최근 웹지니는 ASP.NET MVC Framework 1.0 기반의 프로젝트를 진행하고 있습니다. 처음에 공부하는 시점에서는 요것이 과연 유용할 것인가에 대해 고민을 많이 했었지만 실제 프로젝트에 도입한 후로는 제법 쓸만한 녀석이라고 느껴집니다. 다만 모든 면에서 100% 만족할 수 있는 수준은 아니었고 특히 HtmlHelper, 요 녀석에게 불만(?)이 조금 있습니다. 오늘 포스팅의 목적도 바로 이 부분을 해소하기 위한 것이라 할 수 있어요.

HtmlHelper Class

다들 아시다시피 HtmlHelper 클래스는 ASP.NET MVC 프로젝트에서 View를 통해 HTML UI 요소들을 렌더링하기 위한 메서드들을 제공하는 클래스입니다. 그런데 요 녀석이 제공하는 메서드는 그렇게 많지 않습니다. 실제로 HTML UI 요소들을 렌더링 하기 위한 대부분의 메서드들은 확장 메서드 (Extension Methods)로 구현되어 있기 때문입니다. HtmlHelper 클래스의 멤버들은 여기서 확인하실 수 있어요. 이 중 HTML UI 요소들과 관련되어 자주 사용되는 메서드를 살펴보면 다음과 같습니다.

string ActionLink(...);    // 지정된 Controller 클래스의 Action 메서드를 호출하는 <A> 요소를 렌더링합니다.
string CheckBox(...);      // <input type="checkbox"> 요소를 렌더링합니다.
string DropDownList(...);  // <select> 요소를 드롭다운 목록을 렌더링합니다.
string Hidden(...);        // <input type="hidden"> 요소를 렌더링합니다.
string ListBox(...);       // <select> 요소를 이용하여 리스트 상자를 렌더링합니다.
string Password(...);      // <input type="password"> 요소를 렌더링합니다.
string RadioButton(...);   // <input type="radio"> 요소를 렌더링합니다.
string RouteLink(...);     // 특정 라우팅 규칙에 해당하는 URL로 이동하는 <A> 요소를 렌더링합니다.
string TextArea(...);      // <textarea> 요소를 렌더링합니다.
string TextBox(...);       // <input type="text"> 요소를 렌더링합니다.

어예~ 제법 많은 메서드들이 제공되고 있습니다!! 요것들만 있으면 어떤 HTML 문서도 만들어낼 수 있겠군요!!!! 된장... 절대 그렇지 않습니다. -ㅅ-;; 그 자주 사용되는 <img> 태그를 렌더링하기 위한 메서드도 없네요 췟...

So, What's the Problem?

우리는 어떤 컨트롤러의 액션 메서드를 호출하기 위한 링크를 생성할 때 ActionLink 메서드를 사용하게 됩니다. 예를 들어 다음의 코드를 살펴보죠.

<div>
    <%= Html.ActionLink("로그인", "Login", "Account"); %>
</div>

위의 코드는 다음과 같은 <A> 태그를 렌더링하게 됩니다.

<div>
    <a href="/Account/Login">로그인</a>
</div>

문제가 뭐냐구요? 음... 뭐 이 부분이 굳이 문제라고 생각되진 않습니다. 그러면 다음의 코드는 어떻게 만들어 낼 수 있을까요?

<div>
    <a href="/Account/Login"><img src="login.jpg" /></a>
</div>

오호 저 따위 코드는 HtmlHelper 클래스의 ActionLink 메서드로 만들 수 있다구! 이렇게 말이야!

<div>
    <%= Html.ActionLink("<img src=\"login.jpg\" />", "Login", "Account") %>
</div>

흐헤헤헤헤! 쉽잖아!

그러나 위의 메서드는 다음과 같은 결과를 렌더링하게 됩니다.

<div>
    <a href="/Account/Login">&gt;img src=&guot;login.jpg&quot; /&lt;</a>
</div>

ASP.NET MVC의 소스 코드를 뒤집어 보면 아시겠지만 ActionLink 메서드는 첫 번째 인자로 전달되는 linkText 변수에 전달된 문자열을 HTML 인코딩 해버립니다. 이런 된장...

Implementing the "Image" Method

다행히 HtmlHelper 클래스가 제공하는 대부분의 메서드는 확장 메서드이며 따라서 우리도 마음만 먹으면 얼마든지 확장 메서드를 구현하여 HtmlHelper 클래스에 추가할 수 있습니다. 그렇다면 위와 같은 <IMG> 태그를 렌더링하기 위한 메서드를 우리가 직접 구현할 수도 있겠습니다. 그러려면 다음의 클래스와 메서드에 대해 우선 알아야 합니다.

// 컨트롤러의 액션 메서드를 호출하거나
// 지정된 라우팅 규칙에 해당하는 URL을 생성해 주는 클래스입니다.
public class UrlHelper {
    // 지정된 컨트롤러의 액션 메서드를 호출하는 URL을 생성합니다.
    public string Action(...);
    // 지정된 라우팅 규칙에 해당하는 URL을 생성합니다.
    public string RouteUrl(...);
}

그렇습니다. 오늘 포스트의 핵심은 이 UrlHelper 클래스와 Action, RouteUrl 메서드 형제입니다. 요 녀석들을 이용하면 우리도 얼마든지 액션 메서드를 호출하는 요소들을 메서드 형태로 구현할 수 있다 이겁니다! 그러면 앞서 액션 메서드를 호출하는 Image 메서드를 한 번 구현해 볼까요? 우선 Visual Studio 2008에서 새로운 ASP.NET MVC Application 프로젝트를 생성하고 MyLinkExtension이라는 이름의 클래스를 추가한 후 다음과 같이 코드를 작성합니다.

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: 
  7: namespace MvcApplication1
  8: {
  9: 	public static class MyLinkExtension
 10: 	{
 11: 		public static string Image(
 12: 			this HtmlHelper helper, 
 13: 			string imageSource, 
 14: 			string actionName)
 15: 		{
 16: 			// 아래 구현된 Image 메서드를 호출합니다.
 17: 			return Image(helper, imageSource, actionName, null);
 18: 		}
 19: 
 20: 		public static string Image(
 21: 			this HtmlHelper helper, 
 22: 			string imageSource, 
 23: 			string actionName, 
 24: 			string controllerName)
 25: 		{
 26: 			// UrlHelper 클래스의 인스턴스를 생성합니다.
 27: 			UrlHelper urlHelper = new UrlHelper(
 28: 				helper.ViewContext.RequestContext
 29: 			);
 30: 
 31: 			// 컨트롤러와 액션 메서드의 이름을 지정하여 URL을
 32: 			// 생성합니다.
 33: 			string url = urlHelper.Action(actionName, controllerName);
 34: 
 35: 			// <A> 요소를 렌더링할 TagBuilder 클래스의
 36: 			// 인스턴스를 생성합니다.
 37: 			TagBuilder anchorBuilder = new TagBuilder("a");
 38: 
 39: 			// 이동할 URL을 지정할 href 특성을 추가합니다.
 40: 			anchorBuilder.MergeAttribute("href", url);
 41: 
 42: 			// <IMG> 요소를 렌더링할 TagBuilder 클래스의
 43: 			// 인스턴스를 생성합니다.
 44: 			TagBuilder imgBuilder = new TagBuilder("img");
 45: 			
 46: 			// 이미지 경로를 지정할 src 특성을 추가합니다.
 47: 			imgBuilder.MergeAttribute("src", imageSource);
 48: 
 49: 			// <A> 요소에 <IMG> 요소를 렌더링합니다.
 50: 			anchorBuilder.InnerHtml = imgBuilder.ToString();
 51: 			
 52: 			// <A> 요소를 렌더링한 결과를 리턴합니다.
 53: 			return anchorBuilder.ToString();
 54: 		}
 55: 	}
 56: }
 57: 

주석을 표시해 두었으니 아마 코드를 이해하는데 큰 무리는 없을 것입니다. 코드를 작성했으면 아래와 같이 Home/Index.aspx 페이지를 열고 메서드를 시험해 봅시다.

  1: <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
  2: 
  3: <%@ Import Namespace="MvcApplication1" %> <!-- 요거 중요!! -->
  4: 
  5: <!-- 나머지 코드 생략 -->
  6: <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
  7:     <%= Html.Image("/content/webgenie.png", "LogOn", "Account") %>
  8: </asp:Content>
  9: 

앞서 우리가 정의한 Image 메서드를 사용하려면 이 메서드가 구현된 클래스의 네임스페이스를 Import 해야 합니다. web.config 파일에 네임스페이스 참조를 추가하면 모든 페이지에서 손쉽게 사용할 수 있겠지만 예제에서는 3번 라인에서와 같이 페이지 수준에서 네임스페이스를 Import했습니다. 그리고 실제로 메서드를 사용하는 부분은 7번 라인의 코드입니다. 이제 이 페이지가 동작하는 모습을 살펴볼까요?

img1

그림에서 보듯이 이미지가 올바르게 표시되는 것을 볼 수 있으며 이미지 위에 마우스 포인터를 가져가면 우리가 지정한 대로 Account 컨트롤러의 LogOn 메서드로의 링크가 설정되어 있음을 볼 수 있습니다. 유후~ 간단하군요!

Extending "Image" Method

오랫만의 포스팅이라 이대로 끝을 내기는 살짝 아쉬워서 한 가지 팁을 더 소개할까 합니다. HtmlHelper 클래스의 대부분의 메서드들은 object 타입 혹은 RouteValueDictionary 타입을 이용하여 렌더링되는 HTML UI 요소에 적용될 HTML 특성들을 지정할 수 있습니다. 바로 다음과 같이 말이지요.

  1: <div>
  2: <%= Html.ActionLink("실행!", "Execute", new { style = "color:#EA0000;" }) %>
  3: </div>

위의 코드는 ActionLink 메서드가 렌더링하는 <A> 태그에 style 특성을 지정합니다. 즉, 다음과 같은 결과가 렌더링됩니다.

  1: <div>
  2:     <a href="/Controller/Execute" style="color:#EA0000;">실행!</a>
  3: </div>

그렇다면 우리가 만든 Image 메서드도 저런 것을 지원하면 좋겠습니다! 그리고 이제와 하는 말이지만 앞서 렌더링 된 이미지는 테두리가 쳐져 있어서 살짝 보기가 안좋군요! HTML 특성에 border="0"을 지정할 수 있다면 매우 깔쌈하게 이미지를 출력할 수 있을텐데 말이지요. 해서 Image 메서드를 다음과 같이 확장해 보았습니다.

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: using System.Web.Routing;
  7: 
  8: namespace MvcApplication1
  9: {
 10: 	public static class MyLinkExtension
 11: 	{
 12: 		public static string Image(
 13: 			this HtmlHelper helper, 
 14: 			string imageSource, 
 15: 			string actionName)
 16: 		{
 17: 			// 바로 아래 구현된 Image 메서드를 호출합니다.
 18: 			return Image(helper, imageSource, actionName, null);
 19: 		}
 20: 
 21: 		public static string Image(
 22: 			this HtmlHelper helper,
 23: 			string imageSource,
 24: 			string actionName,
 25: 			string controllerName)
 26: 		{
 27: 			// 요것도 바로 아래의 Image 메서드를 호출합니다.
 28: 			return Image(helper, imageSource, actionName, controllerName, null);
 29: 		}
 30: 
 31: 		public static string Image(
 32: 			this HtmlHelper helper, 
 33: 			string imageSource,
 34: 			string actionName, 
 35: 			string controllerName,
 36: 			object htmlAttributes)
 37: 		{
 38: 			// UrlHelper 클래스의 인스턴스를 생성합니다.
 39: 			UrlHelper urlHelper = new UrlHelper(
 40: 				helper.ViewContext.RequestContext
 41: 			);
 42: 
 43: 			// 컨트롤러와 액션 메서드의 이름을 지정하여 URL을
 44: 			// 생성합니다.
 45: 			string url = urlHelper.Action(actionName, controllerName);
 46: 
 47: 			// <A> 요소를 렌더링할 TagBuilder 클래스의
 48: 			// 인스턴스를 생성합니다.
 49: 			TagBuilder anchorBuilder = new TagBuilder("a");
 50: 
 51: 			// 이동할 URL을 지정할 href 특성을 추가합니다.
 52: 			anchorBuilder.MergeAttribute("href", url);
 53: 
 54: 			// <IMG> 요소를 렌더링할 TagBuilder 클래스의
 55: 			// 인스턴스를 생성합니다.
 56: 			TagBuilder imgBuilder = new TagBuilder("img");
 57: 			
 58: 			// htmlAttribute 매개 변수에 전달된 특성들을 병합합니다.
 59: 			if (htmlAttributes != null)
 60: 			{
 61: 				imgBuilder.MergeAttributes<String, Object>(
 62: 					new RouteValueDictionary(htmlAttributes)
 63: 				);
 64: 			}
 65: 
 66: 			// 이미지 경로를 지정할 src 특성을 추가합니다.
 67: 			imgBuilder.MergeAttribute("src", imageSource);
 68: 
 69: 			// <A> 요소에 <IMG> 요소를 렌더링합니다.
 70: 			anchorBuilder.InnerHtml = imgBuilder.ToString();
 71: 			
 72: 			// <A> 요소를 렌더링한 결과를 리턴합니다.
 73: 			return anchorBuilder.ToString();
 74: 		}
 75: 	}
 76: }
 77: 

36번 라인과 같이 object 타입의 htmlAttribute 매개 변수를 추가하고 58번 라인부터 64번 라인까지의 코드와 같이 이 매개 변수에 null 이 아닌 값이 전달되면 이를 RouteValueDictionary 객체로 변환하여 TagBuilder 클래스의 MergeAttributes 메서드를 통해 <IMG> 태그에 병합하기만 하면 끝입니다! 이제 Index.aspx 페이지의 코드를 다음과 같이 수정해 볼까요?

  1: <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
  2:    <%= Html.Image(
  3:        "/content/webgenie.png", 
  4:        "LogOn", 
  5:        "Account", 
  6:        new { border = 0 })%>
  7: </asp:Content>

6번 라인과 같이 new { border = 0 } 구문을 이용하여 <IMG> 태그에 border 특성을 지정했으며 그 결과는 아래 그림과 같습니다.

img2

어예~ <IMG> 태그에 border="0" 특성이 적용되어 이미지 테두리가 사라졌군요! 이제서야 뭔가 좀 만들어 낸 것 같은 느낌이 듭니다.

Epilogue

이상으로 간단하게 HtmlHelper 클래스를 확장하는 방법에 대한 소개를 마치고자 합니다. 오늘 포스트에서 소개해 드린 내용은 사실 간단하기 그지 없는 내용이지만 이 내용을 토대로 여러분이 원하는 다양한 확장 메서드들을 구현하여 보다 편리하게 HTML 요소들을 활용하실 수 있게 되기를 바랍니다. (그런데 과연 이 내용이 필요한 분들이 있을까요 -ㅅ-;;)

다음엔 조금 더 유용한 (쿨럭..) 포스트로 찾아뵙겠습니다. 즐거운 수요일 되세요!!

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

예제 코드 다운로드: 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