콘텐츠 순환의 묘미

Duncan Mackenzie

Microsoft Developer Network

2004년 8월 25일

적용 대상:

   Microsoft Visual Basic .NET

   Microsoft Visual C# .NET

요약 : Duncan Mackenzie가 ASP.NET의 간단한 "콘텐츠 순환" 작성에 대해 설명합니다(8페이지/인쇄 페이지 기준).

이 기사의 소스 코드를 다운로드하십시오.

소개

ASP.NET 코딩 작업을 하는 경우 이전 버전의 .NET ASP에서도 사용할 수 있었던 Ad Rotator 컨트롤에 대해 알고 있을 것입니다. 그러므로 그 배경에 대해서는 간단하게만 설명하겠습니다. Ad Rotator는 사이트에서 광고(이미지) 집합을 순환하여 페이지를 요청할 때마다 다른 광고가 표시될 수 있도록 하는 서버 컨트롤입니다. 각 광고에는 하이퍼링크가 있으며 이미지의 대체 텍스트가 연결되어 있습니다. 모든 광고에 대한 정보는 컨트롤이 참조하는 XML 파일에 저장됩니다. 또한 각 광고 항목에는 상대 값이 연결되어 있습니다. 이 상대 값은 다른 광고에 대해 특정 광고가 표시되어야 하는 시간을 제어합니다.

이 컨트롤은 간단하지만 매우 유용합니다. 웹 사이트 디자이너는 수동으로 HTML을 편집할 필요 없이 광고 목록을 순환할 수 있는 기능을 매우 반기고 있습니다. 그러나 개인적으로는 이 컨트롤을 볼 때마다 이미지만을 포함할 수 있다는 점에 항상 불만을 가지고 있습니다. 이미지와 텍스트뿐 아니라 서식, 이미지 등이 들어 있는 복잡한 html을 비롯하여 다양한 콘텐츠를 순환하고 싶을 경우에는 어떻게 해야 합니까 보다 일반적인 해결 방법은 이미지 대신 HTML을 순환하는 것입니다. 이 경우 콘텐츠에 <img> 태그를 넣어 주면 이미지도 계속 사용할 수 있습니다.

이런 생각을 자주 해 보기는 하지만, 솔루션을 구현한 적은 없습니다. 현재 다양한 상용 "콘텐츠 순환"을 시중에서 구할 수 있으므로, 저 혼자만 이런 생각을 했던 건 아니었던 거죠. 최근, 필자가 관리하는 MSDN의 영역(Microsoft Visual Basic 및 Microsoft Visual C# Developer Centers)에서 이 컨트롤에 대한 기사가 필요하다는 결정을 내렸습니다. 이 사이트에서 기능적 커뮤니티 사이트, 서적, 블로그 항목 등을 여러 개 다루고 싶지만 한 번에 한 항목만 표시할 수 있는 공간이 있는 경우 이 컨트롤 형식을 사용하여 이러한 여러 항목을 순환할 수 있습니다.

그래서 새 컨트롤을 만들기 시작했으며, 이전 ASP.NET 중심 기사에서 사용된 .dll 방식 대신에 .ascx형 컨트롤을 만들기로 했습니다. 또한, 단순하게 만들기로 했기 때문에 인라인 코드가 모두 포함된 컨트롤을 쉽게 만들 수 있도록 Web Matrix IDE를 다운로드하여 만들기로 했습니다.

디자인

기본 디자인은 정밀한 개발 프로세스가 아니었기 때문에 몇 줄의 코드만으로도 설명할 수 있습니다.

컨트롤은 HTML 콘텐츠 및 상대 정보가 포함된 목록을 참조합니다. 컨트롤은 목록에서 항목을 임의로 선택한 다음(이때 개별 항목은 콘텐츠 목록에서 해당 "상대" 값을 기준으로 선택될 수 있음) 출력하여 표시합니다. 새로 고칠 때마다 표시 정보를 순환할 필요가 없으며, 전체 표시 빈도가 "상대" 정보에 해당하는 경우에는 한 행에서 같은 항목을 두 번 이상 선택해도 문제가 없습니다.

콘텐츠 목록

첫 번째 작업은 콘텐츠 정보를 저장할 위치와 방법을 결정하는 것이지만, 가장 간단한 작업 과정은 Ad Rotator의 지시에 따라 XML 파일을 사용하여 진행하는 것입니다.

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Content>
    <Item>
        <html><![CDATA[<b>Value1</b>]]></html>
        <Impressions>20</Impressions>
    </Item>
    <Item>
        <html><![CDATA[<b>Value2</b>]]></html>
        <Impressions>20</Impressions>
    </Item>
    <Item>
        <html><![CDATA[<b>Value3</b>]]></html>
        <Impressions>50</Impressions>
    </Item>
</Content>

위 코드는 Ad Rotator 파일과 정확하게 일치하는 것은 아니지만, 매우 유사합니다. 일부 요소 이름을 변경하고 keyword 요소를 제거했으며, HTML을 보관하는 데 사용할 <html> 요소를 CDATA 블록에 포함시켰습니다.

컨트롤

앞에서 이 컨트롤을 .ascx 컨트롤로 만들고 인라인 코드를 사용할 계획이라고 언급했지만, 컨트롤의 공용 속성에 대해서는 여전히 의문점이 있습니다. 즉, 이 컨트롤은 어떤 속성을 제공할 것인가 하는 의문점이죠. 컨트롤을 개발하면서 더 많은 속성을 추가할 수 있을 것이라고 생각했지만, 간신히 ContentFile이라는 단 한 개의 속성만을 포함할 수 있었습니다. ContentFile 속성은 콘텐츠의 파일 이름을 지정하는 데 사용되며, 상대 경로나 절대 경로를 통해 컨트롤에 제공할 수 있습니다.

왜 .dll 대신에 .ascx를 만들기로 했는지, 그리고 왜 .ascx 파일에 직접 인라인 코드를 작성하기로 했는지에 관해 궁금하실 것입니다. 여기에는 몇 가지 이유가 있습니다.

  • Microsoft FrontPage 또는 메모장 등을 통해 파일을 직접 편집하는 방식이 훨씬 간편합니다.

  • MSDN에서 사용하기 위해 작성한 몇 가지 다른 컨트롤은 .ascx 파일로 작성되므로, 나머지 팀원들과 일관성을 유지하고 싶었습니다.

  • 그리고 마지막으로, 한 번쯤은 시도해 보고 싶었기 때문에 이러한 방식으로 컨트롤을 만들기로 했습니다.

컨트롤의 C# 버전과 Visual Basic 버전을 만들었습니다. 이 두 가지 버전은 다운로드에 포함되어 있습니다.

알고리즘

상대 값을 기준으로 각 콘텐츠 항목에 가중치를 두기 위해 0과 모든 상대 값의 합계 사이에서 정수를 임의로 선택합니다. 그런 다음 상대 값의 누계를 유지하면서 첫 번째 항목에서 마지막 항목까지 검색합니다. 누계가 임의의 수보다 크거나 같을 때마다 중지하고 현재 항목의 콘텐츠를 표시합니다.

code4fun09072004_fig1.gif

그림 1. 임의의   항목   선택

이 알고리즘을 사용해도 각 항목이 항상 정확히 올바른 비율로 표시되는 것은 아니지만, 순환 횟수가 매우 클 경우 거의 근접하게 표시됩니다.

구현

앞에서 언급했듯이, 이 기사의 컨트롤에는 공용 속성이 ContentFile 하나뿐입니다. 이 속성은 개발자가 콘텐츠 XML 파일의 위치를 구성하는 방법입니다. 이 속성은 간단하게 구현되지만, 또 다른 루틴(LoadContentFile)을 호출하여 디스크에서 XML Document 개체로 XML 파일을 로드합니다.

Private m_ContentFile As String
    Public Property ContentFile As String
        Get
            Return m_ContentFile
        End Get
        Set(ByVal Value As String)
            If m_ContentFile <> Value Then
                m_ContentFile = Value
                LoadContentFile
            End If
        End Set
    End Property

그런 다음 LoadContentFile은 두 개의 루틴, 즉 캐시를 위해 실제 파일을 로드하는 GetXMLDoc(), 그리고 XmlDocument를 사용하여 모든 콘텐츠 항목의 상대 값 합계를 구하는 **GetSumOfImpressions()**를 호출합니다.

Private Sub LoadContentFile()
        m_Content = GetXMLDoc
        m_TotalImpressions = GetSumOfImpressions(m_Content)
    End Sub

GetXMLDoc는 콘텐츠 파일 이름을 Microsoft ASP.NET Cache 개체의 키로 사용하여 XmlDocument 인스턴스가 캐시에 있는지를 먼저 확인합니다. XmlDocument가 캐시에 없으면 새 인스턴스가 만들어지고 파일이 로드된 다음 파일 이름으로 설정된 파일 종속성을 사용하여 새 XmlDocument 개체가 캐시됩니다. 콘텐츠 XML 파일을 수정하지 않는 한 캐시된 XmlDocument는 유효하기 때문에, 이 경우 파일 종속성을 사용하는 것이 특히 유용합니다. 바로 이것이 파일 종속성이 수행하는 작업입니다. 즉, 모니터되는 파일이 변경될 때마다 캐시된 데이터는 무효화됩니다.

Private Function GetXMLDoc() as System.Xml.XmlDocument
        Dim fileName As String _
= Me.Page.MapPath(m_ContentFile)
        Dim doc As System.Xml.XmlDocument
        Dim obj As Object
 
        obj = Me.Cache.Get(fileName)
        If obj Is Nothing Then
            Dim fd As New System.Web.Caching.CacheDependency(fileName)
            doc = New System.Xml.XmlDocument()
            doc.Load(fileName)
            Me.Cache.Insert(fileName, doc, fd)
        Else
            doc = DirectCast(obj,System.Xml.XmlDocument)
        end if
 
        Return doc
    End Function

상대 값의 합계를 계산하는 작업은 루프로 처리할 수 있지만, 이 루프는 XML 문서에 저장되기 때문에 XPath와 같은 더 효율적인 기능을 사용하는 것이 좋겠습니다. XPath sum() 함수를 사용하면 코드 몇 줄만으로 상대 값 합계를 구할 수 있습니다. 이 함수를 통해 코드를 단순화할 수 있을 뿐 아니라 모든 항목을 처리하는 시간을 절약할 수 있습니다.

Private Function GetSumOfImpressions( _
          xmlDoc as System.Xml.XmlDocument) As Integer
          Dim nav as System.Xml.XPath.XPathNavigator
          Dim total as Integer
          Dim expr as System.Xml.XPath.XPathExpression
 
          nav = xmlDoc.CreateNavigator()
          expr = nav.Compile("sum(//Item/Impressions)")
          total = CType(nav.Evaluate(expr), Integer)
          Return total
    End Function

위의 모든 코드는 실제 출력을 생성하지만, 이 컨트롤에서 실제로 내용을 작성하는 유일한 코드는 Render() 메서드입니다.

Protected Overrides Sub Render(writer As HtmlTextWriter)
        Dim selectedItem as Integer
        Dim itemCount as Integer = m_TotalImpressions
        Dim rnd as new System.Random()
        Dim found As System.Xml.XmlNode
        If Not m_Content Is Nothing Then
            Dim elemList As System.Xml.XmlNodeList _
                = m_Content.GetElementsByTagName("Item")
            Dim runningTotal As Integer = 0
            selectedItem = rnd.Next(0,itemCount)
 
            For Each nd as System.Xml.XmlNode in elemList
               runningTotal += Cint(nd("Impressions").InnerText)
               If runningTotal >= selectedItem Then
                  found = nd
                  Exit For
               End If
            Next nd
        End If
 
        If Not found Is Nothing Then
            writer.WriteLine()
            writer.WriteLine("<!-- Begin Rotated Content VB-->")
            writer.WriteLine(found("html").InnerText)
            writer.WriteLine( _
             String.Format("<!-- Rendered at {0} -->",Now))
            writer.WriteLine("<!-- End Rotated Content -->")
        End If
    End Sub

Microsoft Internet Explorer에서 생성된 페이지의 html 소스를 확인하여 컨트롤의 렌더링 구성 요소에 대한 세부 정보를 볼 수 있도록 중심 출력 근처에 주석 줄을 추가했습니다.

<!-- Begin Rotated Content C# -->
<b>Value3</b>
<!-- Rendered at 7/13/2004 5:06:56 AM -->
<!-- End Rotated Content -->

출력을 렌더링한 날짜/시간을 알면 캐싱이 사용되며 제대로 기능하는지를 확인할 때 유용합니다.

캐싱 전략

XmlDocument의 인스턴스로 XML 파일을 캐시했지만 이 개체에 대한 모든 요청에서 표시할 항목을 결정해야 합니다. 이 작업은 불필요할 수도 있습니다. 콘텐츠가 순환되는 횟수를 줄이기 위해 페이지에서 출력 캐싱을 사용하기로 했습니다.

<%@ outputcache duration="60"
 shared="true" varybyparam="ContentFile" %> 

이 설정은 ASP.NET 엔진이 이 컨트롤에서 출력을 캐시하고 60초마다 다시 그리도록 지시합니다. 이 설정을 사용하면 콘텐츠가 분당 최대 한 번 변경되고 코드도 이에 따라 실행됩니다. 그러므로 추가 캐싱 코드를 루틴에 추가할 필요가 없습니다. varybyparam="ContentFile"과 shared="true"의 조합은 동일한 ContentFile을 지정하는 경우 이 컨트롤은 사용되는 모든 페이지에 대해 한 번 캐시됨을 의미합니다. 이는 순환하는 내용에 따라 적절하지 않을 수도 있지만, 이 컨트롤이 여러 페이지에서 사용되는 경우에는 서버 리소스를 크게 절약할 수 있습니다.

ASP.NET의 출력 캐싱에 대한 자세한 내용은 다음 링크를 참조하십시오.

결론

사용자 지정 컨트롤을 개발하면 코드를 캡슐화하여 쉽게 다시 사용할 수 있는 형태로 만들 수 있습니다. Microsoft Windows Forms나 ASP.NET에서도 그 개념은 동일합니다. 이전에 이러한 방식으로 개발해 본 적이 없다면 직접 시도해 보고 기존 코드를 하나 이상의 컨트롤로 나눌 수 있는지 확인해 보십시오. 다음은 ASP.NET에서 컨트롤을 개발할 때 참조할 수 있는 리소스입니다.

 

사이트

뉴스 그룹/포럼

애호가 콘텐츠에 대한 아이디어가 있으십니까 duncanma@microsoft.com으로 알려 주시고 즐거운 코딩을 경험하시기 바랍니다.

Coding4Fun

Duncan Mackenzie는 주로 MSDN의 Microsoft Visual Basic .NET 콘텐츠 전략가로 일하며 전업 코더를 겸하고 있습니다. 얼 그레이 홍차를 마셔야 일이 잘 풀린다는 그의 훌륭한 성과를 기대해 봅니다. Duncan에 대한 자세한 내용은 Duncan의 사이트tous.gif를 참고하십시오.