재미있는 자바스크립트 03, HTCs의 작성과 ASP.NET 사용자 지정 컨트롤 연동

등록일시: 2006-05-25 16:49,  수정일시: 2018-04-07 23:44
조회수: 16,203
본문은 최초 작성 이후, 약 18년 이상 지난 문서입니다. 일부 내용은 최근의 현실과 맞지 않거나 동떨어져 있을 수 있으며 문서 내용에 오류가 존재할 수도 있습니다. 또한 본문을 작성하던 당시 필자의 의견과 현재의 의견에 많은 차이가 존재할 수도 있습니다. 이 점, 참고하시기 바랍니다.

본문의 주제인 HTCs는 IE 10에서 지원이 중단되었다. 따라서, IE 10 이상을 사용하고 계신 분들은 본문의 HTCs를 테스트해보려면 F12 개발자 도구에서 문서 모드를 IE9 표준으로 변경해야 한다. 본문은 단지 과거의 자료를 보존하기 위한 목적으로만 관리되고 있으며 더 이상 HTCs의 사용을 권장하지 않는다. HTCs를 처음 접하시는 분들은 그저 한 때 존재했던 스크립팅 기술 흐름의 한 갈래로 참고만하기 바란다. 또한, 이미 HTCs를 사용 중이신 분들은 점진적으로 jQuery 등의 기술로 이전하기를 권해드린다.

지금까지 우리는 두 차례에 걸쳐 간단하나마 HTCs란 어떠한 기술인지, 구체적으로 어떻게 활용할 수 있는지 살펴봤으며, 간단한 HTCs를 직접 작성해보기도 했다. 본문에서는 HTCs를 작성하는 방법을 조금 더 살펴보고 그 결과로 얻어지는 HTCs를 ASP.NET 사용자 지정 컨트롤과 연동하는 구체적인 사례를 살펴보고자 한다. 만약, 여러분이 전문적인 ASP.NET 개발자가 아니거나 ASP.NET 사용자 지정 컨트롤을 제작해본 경험이 없다면, 부족하나마 본문을 통해서 그 강력한 기능을 일부라도 접해볼 수 있는 의미있는 기회가 될 수 있었으면 한다.

먼저 살펴볼 부분은, 과연 지난글에서 우리들이 작성했던 HTCs가 갖고 있는 문제점은 전혀 없는가 하는 점이다. 이 HTCs의 구체적인 목표는 최종 사용자가 실수나 고의로 숫자값 이외의 데이터를 입력하는 것을 방지하는 것으로, 실제로도 예상한 바와 같이 잘 동작한다는 사실을 이미 확인한 바 있다. 그러나, 이 HTCs에는 비록 간단하지만 매우 치명적인 단점이 한 가지 존재한다. 즉, 최종 사용자가 입력하는 데이터를 대상으로는 상대적으로 엄격한 유효성 검사를 수행하지만, 개발자가 저지르는 실수에 대해서는 거의 완벽하게 무방비 상태라는 점이다. 결과적으로 이 단점을 개선하지 못한다면, 이 HTCs는 재사용성에 있어 단적으로 낮은 평가를 받을 수 밖에 없을 것이다. 간단하지만 다음의 INPUT 태그 예제는 바로 이런 문제점을 적나라하게 보여주고 있다.

<input type="text" id="HTCs1" style="behavior: url(/Content/Lecture/200605250001/asp_0017_01.htc);">

<script language="JavaScript" type="text/javascript">
<!--
document.getElementById("HTCs1").value = "ABC";
//-->
</script>

대번에 알 수 있지만, 이 예제에 사용된 자바스크립트 코드는 실무에서 질려버릴 정도로 빈번하게 접하는 평범한 코드로 설명이 무안할 정도다. 그럼에도 불구하고 HTCs의 기능은 너무나도 쉽게 무너져버렸다. 결과적으로 이런 사례는 이 작성한 HTCs가 그리 치밀한 설계를 바탕으로 작성된 것은 아니라는 반증인 셈이다. 그리고, 다음의 HTML 코드 역시도 이 HTCs가 갖고 있는 문제점들 중 하나를 명확하게 드러내준다. 너무나 어이없는 단순한 조작만으로도 그냥 넘어가기엔 곤란한 문제점이 노출된다는 것을 쉽게 인지할 수 있을 것이다.

<input type="text" id="HTCs2" ... basicValue="ABC">

이 두 가지 문제점들을 해결하는 것에서부터 본격적인 논의를 시작해보자. 그런데, 한 가지 안타까운 사실은 첫 번째 문제점의 경우, 문제의 발생 그 자체를 방지할 수 있는 근본적인 해결 방법은 존재하지 않는다는 점이다. 적어도 필자가 지닌 지식으로는 별다른 방법을 찾을 수 없었다. 일단 손쉽게 고려해 볼 수 있는 방법으로는 기존의 HTCs에 onchange 이벤트 헨들러를 추가하는 방법이 있을 것이다. 그러나, 자바스크립트로 데이터를 변경하는 경우에는 이 이벤트 자체가 발생하지 않는다. 따라서, 이 방법은 거의 무용지물로 그렇다고 별다른 대안도 존재하지 않는다. 결국, 필자가 얻어낼 수 있었던 최선의 결론은 데이터 유효성 검사를 위한 후처리 작업의 일환으로 사용될 수 있는, 그리고, 입력된 데이터의 유효성을 검사해주고 경우에 따라서는 보정 작업까지 처리해주는 함수를 일관적인 형태로 제공해주는 소극적인 방법뿐이었다. 가령, 사용자의 데이터 입력과 관련 있는 모든 종류의 HTCs에 미리 규정한 이름으로 작성된 하나의 함수, 즉 isValidate() 같은 함수를 제공하고 이 HTCs들을 사용하는 모든 페이지에서는 FORM이 제출되기 직전에 이 함수를 호출하여 유효성을 검증하도록 프로그래밍 룰을 정하는 것이다. 그러면, HTCs에 이 함수의 구현을 추가해보도록 하자. 지금까지 해왔던 것처럼 PUBLIC:METHOD 엘레먼트 태그 선언을 다음과 같이 추가해준다.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    <PUBLIC:ATTACH event="onfocus"        for="element" onevent="evtFocus();"        />
    <PUBLIC:ATTACH event="onblur"         for="element" onevent="evtBlur();"         />
    <PUBLIC:ATTACH event="onkeydown"      for="element" onevent="evtKeydown();"      />
    
    <PUBLIC:PROPERTY name="basicValue" value="0" />
    
    <PUBLIC:METHOD name="setBasicValue" />
    <PUBLIC:METHOD name="isValidate"    />
    
    ...

이 함수의 실제 구현은 다음과 같은데, 이제는 상당히 익숙해진 작업 패턴과 코드일 것이라고 생각한다. 기술적인 관점에서 봐도 그리 주목할만한 부분은 없으므로 불필요한 설명은 하지 않도록 하겠다. 비록 냉정하게 평가해본다면 이 방법을 문제 자체에 대한 해결책이라고 볼 수는 없겠지만, 나름대로 하나의 유효성 검증 프로세스를 만들어낸다는 점에서는 긍정적으로 받아들일 수 있을 것이다. 참고로 앞서 간단하게 언급했던 onchange 이벤트를 헨들링하는 방법 대신, onpropertychange 이벤트를 통해서 우회적으로나마 문제점을 해결하는 방법을 나름대로 고민해보고 있으므로, 그 성과가 나오는 대로 본문을 갱신하도록 하겠다.

    ...
    
    function isValidate()
    {
        if (!checkTagType()) return;
        
        var objRegEx = /^\d+$/ig;
        
        return objRegEx.test(trim(element.value));
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

다행스럽게도 두 번째 문제점의 경우, 보다 근본적이고 세련된 해결 방법이 존재한다. 더군다나 매우 간단한 방법이기까지 한데 바로 일반적인 객체 지향 언어의 클래스 구조에서 보편적으로 제공되는 프로퍼티 메서드 메커니즘을 HTCs에서도 내부적으로 제공해주는 것이다. 자바의 getter/setter나 비주얼 C#.NET의 get/set 속성 접근자를 머리에 떠올려보면 필자의 얘기를 쉽게 이해할 수 있을 것이다. 따라서, 두 번째 문제점을 해결하기 위해서는 단지 HTCs에서 제공되는 해당 기능을 충실히 구현하기만 하면 된다. 이번 논의는 각의 과정들이 단계별로 다소 복잡하므로 서두르지 말고 차근차근 따라오기 바란다. 먼저, 다음과 같이 기존에 작성했던 basicValue 속성의 PUBLIC:PROPERTY 엘레먼트 태그에 새로운 두 개의 속성, get과 put을 추가해보자.

<PUBLIC:PROPERTY get="getBasicValue" put="putBasicValue" name="basicValue" ...

이것만으로 엘레먼트 태그의 변경이 다 마무리 된 것은 아니고 계속해서 추가적인 작업이 더 이루어져야만 되므로 잠시 이 점을 염두에 두고 있기 바란다. 일단 지금은 계속해서 다음 단계를 진행하도록 하겠다. 어쨌거나 이 속성들이 의미하는 바는 지극히 간단명료하다. 구체적으로 말해서 get 속성에는 basicValue 프로퍼티의 값이 리턴될 때 호출되는 자바스크립트 함수의 이름을 설정하고, 반대로 put 속성에는 basicValue 프로퍼티에 값을 설정할 때 호출되는 자바스크립트 함수의 이름을 설정하면 되는 것이다. 그리고 물론 지극히 당연한 얘기겠지만 이 두 가지 속성에 지정된 자바스크립트 함수들은 별도로 구현하여 HTCs 에 추가해주어야만 한다. 이를테면 다음의 두 자바스크립트 함수는 그 구현 결과의 한 사례다.

    function getBasicValue()
    {
        return basicValue;
    }
    
    function putBasicValue(value)
    {
        if (!checkTagType()) return;
        
        var objRegEx = /^\d+$/ig;
        
        if (objRegEx.test(value))
            basicValue = value;
        else
            basicValue = 0;
    }

이런 패턴의 자바스크립트 코드는 거의 외우다시피 했을 것이므로 무의미한 코드 설명은 피하도록 하겠다. 아무튼 결과적으로 이제 원하지 않는 기본값이 설정되는 사태는 거의 완벽하게 피할 수 있게 된 것처럼 보인다. 그러나, 과연 정말로 그럴까? 현재까지 작업한 결과를 저장하고 실제로 테스트를 해보면 스택 오버플로우 오류가 발생한다. 그렇다면 그 원인은 무엇일까? 코드를 가만히 살펴보면 그 이유를 쉽게 파악할 수 있을 것이다. 단도직입적으로 말하면 오류가 발생하는 원인은 basicValue 프로퍼티의 내부 변수가 그 어디에도 선언되지 않았기 때문이다. 즉, 이 두 개의 자바스크립트 함수 내부에서는 basicValue 프로퍼티의 값을 설정하거나 리턴하는 작업이 수행되는데, 역설적으로 이 두 함수가 호출되는 시점 자체가 바로 basicValue 프로퍼티의 값을 설정하거나 리턴하고자 시도하는 경우이기 때문이다. 결국 자연스럽게 무한 루프가 발생되며, 그 결과로 자바스크립트의 함수 호출 스택애서 오버 플로우가 발생되는 것이다. 이 오류를 제거하는 방법은 간단하다. 다음과 같이 이 프로퍼티의 값을 담을 전역 변수를 하나 선언해주고 기본값을 초기화해주면 된다.

    ...
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    var basicValue = 0;
    
    ...

전역 변수의 이름과 프로퍼티의 이름이 동일하다는 점에 주의하기 바란다. 비록 이런 특징이 프로퍼티 내부 변수를 선언하기 위해 반드시 필요한 강제적인 룰은 아니지만, 기타 부가적인 작업없이 간단하게 전역 변수를 선언하기 위해서는 전역 변수의 이름과 프로퍼티의 이름이 동일해야만 한다. 만약, 프로퍼티 자체의 이름과 전역 변수의 이름이 같아서 코드가 전반적으로 혼란스럽게 느껴진다면 다음과 같은 방법으로 전역 변수의 이름을 직접 지정해주는 것도 가능하다. 프로퍼티의 PUBLIC:PROPERTY 엘레먼트 태그에 다음과 같이 internalname 속성을 추가하고 그 값으로 프로퍼티의 값을 담고자하는 전역 변수의 이름을 입력해준다. 그리고 앞에서 선언했던 것처럼 이 이름으로 전역 변수를 선언해주면 된다.

<PUBLIC:PROPERTY ... name="basicValue" internalname="_basicValue" value="0" />

그리고, 직전에 구현했던 프로퍼티 메서드 자바스크립트 함수에서 사용된 전역 변수의 이름도 이에 맞추어 변경돼야 한다. 또한, 지금까지 프로퍼티의 기본값 설정을 위해 사용되었던 PUBLIC:PROPERTY 엘레먼트 태그의 value 속성은 더 이상 필요하지 않으므로 삭제한다. 대신, 지금부터는 전역 변수의 초기값이 그 기능을 대신하게 된다. 다음 예제는 이렇게 수정된 HTCs가 올바르지 않은 프로퍼티값 설정 시도를 보정한 모습을 보여주고 있다. 또한, 지금 논의한 이 internalname 속성은 PUBLIC:METHOD 엘레먼트 태그를 대상으로도 동일한 영향력을 발휘하므로, 내부 자바스크립트 함수의 이름과 외부로 노출되는 HTCs 메서드의 이름을 다르게 지정할 수 있으므로 참고하기 바란다.

<input type="text" id="HTCs3" ... basicValue="ABC">

이번에는 논의의 촛점을 조금 달리해보도록 하자. 지금까지 살펴본 내용 대부분이 프로퍼티와 메서드에 관한 것이었다는 점을 감안해보면, 그 다음 단계로 자연스럽게 이벤트에 생각이 미치게 될 것이다. 상식적으로 메서드와 프로퍼티에 대한 기반 구조를 제공해주는 HTCs가 이벤트만 홀대할 이유는 없다. 실제로도 단지 기반 구조를 제공해주는 수준을 넘어선, DHTML의 이벤트 모델과 유기적으로 잘 결합된 메커니즘을 제공해주므로 별다른 노력 없이도 고품질의 결과를 얻을 수 있다. 그러면, 지금까지 구현된 HTCs에 사용자가 올바르지 않은 형식의 데이터를 입력하는 경우, 사용자 정의 이벤트가 발생하도록 코드를 추가해보자. 사용자 정의 이벤트의 이름은 onIncongruent라고 부르기로 하고 동일한 이름으로 태그 속성에 노출될 것이다. 만약, 사용자 정의 이벤트의 이름이 HTML 태그에 존재하는 기본 이벤트의 이름과 동일하다면 사용자 정의 이벤트에 의해서 동작 특성이 덮어씌워지므로 참고바란다. 다음과 같이 PUBLIC:EVENT 엘레먼트 태그를 선언을 추가한다.

<PUBLIC:EVENT name="onIncongruent" id="_Incongruent" />

지금까지 살펴봤던 프로퍼티나 메서드의 엘레먼트 태그 선언과는 달리, 이벤트의 엘레먼트 태그 선언에는 반드시 id 속성이 설정되야 하는데 그 이유는 잠시 뒤에 설명할 것이다. 그 다음에는 이 사용자 정의 이벤트를 발생시키기 위한 용도로 자바스크립트 함수를 하나 만들어야 한다. 사용자 정의 이벤트를 발생시키기 위해서 반드시 별도의 함수를 정의해야만 하는 것은 아니지만, event 개체를 생성하고 필요한 속성값들을 적절하게 설정하는 등의 반복적인 작업을 효과적으로 처리하기 위해서는 이처럼 유틸리티 함수를 하나 만들어주는 것도 좋은 방법이다. 다음 함수를 살펴보자.

    ...
    
    function fireIncongruent(Value)
    {
        objEvent = createEventObject();
        if (Value != null)
            objEvent.returnValue = Value;
        _Incongruent.fire(objEvent);
    }
    
    //-->
    </SCRIPT>

이 함수에는 주의 깊게 살펴볼만한 내용들이 몇 가지 존재한다. 가장 먼저 짚어봐야 될 부분은 가장 마지막 라인으로, PUBLIC:EVENT 엘레먼트 태그를 선언하면서 반드시 id 속성을 설정해야만 하는 이유가 잘 드러나 있다. 사용자 정의 이벤트를 발생시키는 실질적인 역활을 담당하는 fire() 메서드를 호출하기 위해서는 해당 이벤트의 식별자 역활을 담당하는 id 속성이 필요한 것이다. 그리고, fire() 메서드에는 인자로 event 개체의 인스턴스를 생성해서 전달해야 하는데, 여기서 말하는 event 개체는 일반적인 DHMTL 프로그래밍에서 사용되는 event 개체와 정확하게 같은 개체를 뜻하며, createEventObject() 메서드를 사용해서 인스턴스를 생성할 수 있다. 보통 event 개체로부터 제공되는 대부분의 정보들, 즉 마우스 정보나 키보드 정보, 좌표 정보 등을 담고 있는 속성들은 읽기 전용이지만, 지금처럼 createEventObject() 메서드로 인스턴스가 생성된 경우에는 모든 속성값을 설정할 수 있다. 따라서, 개발자들이 값을 자유롭게 설정할 수 있는데 이는 대단한 장점으로 평가할 수 있으며, 본문에서는 하나의 간단한 예로 returnValue 속성값을 설정하는 경우를 보여준다. 이 메서드는 HTCs에서만 사용이 가능한 것이 아니고 document 개체에서도 동일한 기능을 제공해주는 메서드를 동일한 이름으로 지원해주므로 이 메서드가 궁금하신 분들은 MSDN을 살펴보기 바란다. 이제 이 함수를 적절한 위치에서 호출해주기만 하면 사용자 정의 이벤드가 이벤트가 발생된다. 다음 코드는 onblur 이벤트의 HTCs 이벤트 헨들러로 구현된 evtBlur() 함수를 수정한 결과다.

    ...
    
    function evtBlur()
    {
        if (!checkTagType()) return;
        
        var objRegEx = /^\d+$/ig;
        
        if (!objRegEx.test(trim(element.value)))
        {
            element.value = basicValue;
            fireIncongruent("Incongruent Data!");
        }
    }
    
    ...

다음은 지금까지 구현된 모든 코드들이 반영된 HTCs가 적용된 최종 결과물이다. 숫자가 아닌 임의의 문자열을 복사하여 붙여넣기로 입력값을 변경해보면 이벤트가 발생하는 것을 확인할 수 있을 것이다. 그리고, 한 가지 주의해야 할 점은 이렇게 HTCs 수준에서 구현된 사용자 정의 이벤트는 기본적으로 버블링이 되지 않는다는 사실이다. 구체적인 실례로 다음 코드에서 SPAN 태그에 정의된 onIncongruent 이벤트 헨들러의 코드는 전혀 동작하지 않는데 잠시만 곰곰히 생각해보면 매우 당연한 일이라는 것을 알 수 있다.

<span onIncongruent="alert('Event from SPAN Tag.');">
    <input type="text" id="HTCs4" onIncongruent="alert(event.returnValue);" style=" ... >
</span>

지금까지 HTCs의 프로퍼티와 메서드, 그리고 이벤트를 구현하는데 필요한 여러 가지 기법들을 살펴봤다. 이번에는 HTCs 자체의 전반적인 동작 특성을 제어하는 방법들을 살펴보도록 하자. 이와 관련된 엘레먼트 태그는 두 가지로, 이미 앞에서 잠시 살펴보았던 HTCs 자체를 정의하는 기능을 가지고 있는 PUBLIC:COMPONENT 엘레먼트 태그와 지금 처음 등장하는 PUBLIC:DEFAULTS 엘레먼트 태그가 바로 그것이다. 그런데, 다소 무안하기는 하지만 냉정하게 볼 때 우리가 이 엘레먼트 태그들을 가지고서 처리할 수 있는 작업은 현재 시점에서는 그다지 많지가 않다. 왜냐하면 지금까지 살펴본 대부분의 내용들이 Attached Behavior에 관한 것임에 반하여 이 두 엘레먼트 태그가 제공하는 대부분의 기능들은 Element Behavior와 관련된 것들이기 때문이다. 그런 이유로 본문에서는 지금까지 살펴본 내용들과 관련된 극히 일부 기능들에 대해서만 설명할 생각이며 PUBLIC:DEFAULTS 엘레먼트 태그는 아예 거론하지 않을 것이다. 다음은 우리들이 작성한 HTCs 코드의 PUBLIC:DEFAULTS 엘레먼트 태그 선언으로 지금까지 별다른 설명없이 일괄적으로 사용해온 부분이다. 무척이나 심플한 구조를 갖고 있으며 붉은색으로 강조된 부분을 살펴보면 lightWeight라는 속성값이 true로 설정되었다는 사실을 알 수 있다. 이 lightWeight 속성이 갖고 있는 의미는 매우 간단하다. 즉, 이 HTCs가 내부에 마크업, 즉 렌더링되야 할 HTML 태그를 갖고 있는지 여부를 나타낸다.

<PUBLIC:COMPONENT lightWeight="true" />

    ...
    
</PUBLIC:COMPONENT>

렌더링 관점에서 볼 때, 지금처럼 HTCs 내부에 그 어떠한 HTML 태그도 존재하지 않으면 HTML 태그가 존재하는 경우에 비해 상대적으로 가볍다(lightWeight)는 뜻으로 이 값을 true로 지정한다. 그러면, 렌더링 프로세스 자체가 필요 없다는 의미로 받아들여져 퍼포먼스 향상을 기대할 수 있다. 그런데, 여기서 'HTCs의 내부에 존재하는 HTML 태그'라는 말의 의미를 오해하지 말아야 한다. 이미 간략하게 설명한 것처럼 HTCs의 유형에는 크게 Attached Behavior와 Element Behavior라는 두 가지 범주가 존재한다. 두 HTCs 간의 결정적인 차이점은 사용자 인터페이스가 존재하는지 여부로, 본문에서는 사용자 인터페이스가 존재하지 않는 Attached Behavior를 중점적으로 논의하고 있는 중이다. 지금 거론되고 있는 'HTCs의 내부에 존재하는 HTML 태그'는 바로 Element Behavior에서 사용자 인터페이스를 구성하는 HTML 태그들을 뜻하는 것이다. 따라서, 그런 유형의 HTML 태그가 전혀 존재하지 않는 본문의 HTCs에서는, 비록 자바스크립트 코드의 내부에 HTML 태그 조각들이 포함되어는 있지만 lightWeight 속성값을 true로 설정해서 퍼포먼스의 향상을 기대할 수 있는 것이다. 참고로 이 속성의 기본값은 false다.

마지막으로 이런 경우를 한 번 가정해보도록 하자. 어떤 HTML 태그가 존재하고 있는데, 이 태그에는 두 가지 HTCs가 동시에 설정되어 있다고 생각해보자. 게다가, 공교롭게도 두 HTCs에서 MyFunction이라는 메서드를 동시에 정의하고 있다면, 그리고 자바스크립트에서 해당 메서드를 호출하면 어떻게 될까? 단순하게 머리 속으로만 생각해봐도 뭔가 문제가 발생할 것이라는 사실을 짐작할 수 있다. 테스트 해 본 바에 따르면, 이런 경우 명시적인 오류는 발생하지는 않지만 두 개의 HTCs 중에서 순서적으로 먼저 설정된 HTCs의 메서드만 실행된다. 따라서, 각각의 HTCs를 구분해서 지정할 수 있는 어떤 방법이 제공되야만 개발자가 원하는 메서드를 정확하게 지시해서 호출할 수 있을 것이다. 이 목적으로 사용되는 속성이 바로 name이다. 다음 코드를 살펴보자.

<PUBLIC:COMPONENT lightWeight="true" name="NumericOnly" />

    ...
    
</PUBLIC:COMPONENT>

이제 자바스크립트 코드는 다음과 같이 작성될 수 있다. 물론, 해당 태그에 단 하나의 HTCs만 설정되어 있다면 name 속성의 지정 여부와 관계없이 기존과 동일한 방법으로 메서드를 호출할 수 있다. 그리고, 이 특성은 프로퍼티에도 동일하게 적용되므로 참고하기 바란다.

<input type="text" id="HTCs5" style="behavior: url(/Content/Lecture/200605250001/asp_0017_05.htc);">

<SCRIPT language="javascript" type="text/javascript">
<!--
document.getElementById("HTCs5").NumericOnly.setBasicValue("0000");
//-->
</SCRIPT>

지금까지 살펴본 내용들이 HTCs를 효과적으로 사용하기 위해서 필요한 가장 기초적인 사항들이었다면, 지금부터 살펴볼 내용들은 HTCs의 잠재력을 극대화시킬 수 있는 응용 방법에 관한 것이다. 이와 관련하여 여러 가지 흥미로운 사례들이 있을 수 있겠지만, 그 중에서도 필자가 개인적으로 많은 관심을 갖고 있는 영역은 본문의 서두에서도 간단하게 언급한 바처럼 ASP.NET 사용자 지정 컨트롤과 연동하여 모든 혹은 거의 대부분의 클라이언트 측 스크립트를 HTCs로 대체하는 기법이다. 그동안 업무의 일환으로, 또는 학습을 목적으로 ASP.NET 사용자 지정 컨트롤을 작성하면서 언제나 불만스러웠던 점이, 바로 서버 측 작업 영역에 대해서는 .NET 프레임워크가 과거 ASP 프로그래밍 모델에 비해 비약적인 수준으로 향상된 모델을 기본적으로 제공해주는 반면, 클라이언트 측 스크립트 작업 영역에 대해서는 일말의 진보도 없었다는 점이었다. 현실적으로 사용자 인터페이스와 관련된 자바스크립트 코드를 작업으로부터 완전히 배제할 수 없는 상황에서 이런 제약은 웬지 반쪽짜리 컴포넌트를 작성하는 듯한 기분만 느끼게 할 뿐이었다. 그러나, 이제 ASP.NET 사용자 지정 컨트롤에 HTCs를 도입하여 문제점의 대부분을 말끔하게 해소할 수 있게 되었다. 비록 HTCs가 인터넷 익스플로러 환경에서만 동작하기는 하지만, 기업체 내부에서만 가동되는 시스템 같이 사용자 환경에 대한 기본적인 정의가 미리 이뤄지는 환경에서는 기대한 수준을 넘어서는 효과를 얻을 수 있을 것이라는게 필자의 견해다.

다만, 본문은 어디까지나 HTCs에 관해서 논의하고 있는 문서로 분량에 대한 고려나 기타 여러가지 이유로 인해 ASP.NET 사용자 지정 컨트롤에 대한 전반적인 영역들을 모두 살펴볼 수는 없다. 게다가, 필자 역시도 아직 ASP.NET 사용자 지정 컨트롤에 대한 지식이 많이 부족하기 때문에, 본격적인 논의를 진행하기에는 부담이 크다. 따라서, 본문에서는 여러분이 이미 간단한 ASP.NET 사용자 지정 컨트롤을 직접 작성해 본 경험이 있거나, 최소한 기본적인 지식은 갖고 있다는 가정하에 모든 내용을 진행하도록 하겠다. 그러나, 코드 자체가 그다지 어렵지 않을 뿐더러 컨트롤 빌더나 컨트롤 디자이너, 또는 형식 변환기 같은 고급 기법은 전혀 사용하지 않으므로 코드만 보더라도 이해에 큰 어려움은 없을 것이다. 작업 목표 자체는 상당히 단순한데, 본문에서 작성한 HTCs와 ASP.NET에서 제공해주는 기본 TextBox 서버 컨트롤을 결합해서 새로운 ASP.NET 사용자 지정 컨트롤을 만드는 것으로, 다음은 그 최종 결과물의 전체 소스 코드이다.

EgoCube.Web.UI.WebControls.1.0.1.1.zip (35k)

물론, 이 소스 코드도 필자의 사이트에서 공개해왔던 다른 모든 프로그램들과 마찮가지로 소스 코드 자체 및 디자인, 주석 등을 포함한 프로그램의 모든 내용을 수정하거나 추가해서 재배포 할 수 있으며, 상업적인 용도를 포함한 모든 유형의 목적에 있어 그 어떠한 종류의 제약도 존재하지 않는다. 그러나, 이 코드를 수정해서 제작한 자신의 2차 제작물에 대한 권리를 남용하여 다른 사용자들이 이 소스 코드를 자유롭게 사용할 수 있는 권리를 제한하지는 못한다. 이와 관련해 더 자세한 내용을 알고 싶으신 분들은 저작권에 대한 입장을 공식적으로 정리한 ECUM 버전 0.0.2b의 저작권 관련 사항의 내용을 참고하기 바란다. 비록 이 내용은 또 다른 공개 프로그램인 ECUM 버전 0.0.2b에 대한 문서에 작성되어 있지만, 상기 문서에서 밝히고 있는 바처럼 별도로 저작권과 관련된 언급을 하지 않는 이상 필자가 공개하는 모든 프로그램은 이 문서의 규정을 따른다.

그러면, 전반적인 작업 과정을 살펴보기에 앞서 먼저 결과물이 어떻게 생성되는지 살펴보기로 하자. 무엇보다 가장 먼저 짚고 넘어갈 점은 컴파일 된 최종 결과물이 단 하나의 DLL 파일로 이뤄진다는 것이다. 즉, 마지막 배포 단계에서 개발자에게 전달될 파일은 DLL 파일 단 하나 뿐이다. 필자가 본문을 준비하면서 ASP.NET 사용자 지정 컨트롤과 HTCs를 통합하는 사례의 하나로 이 샘플 컨트롤을 설계할 때 가장 주의를 기울였던 부분이 바로 이 부분이다. 가령, 특정 시스템을 구축하는 프로젝트가 존재하고 대략 10여명 내외의 개발자들로 구성된 프로젝트 팀이 존재한다고 생각해보자. 실력이 뛰어난 개발자 한 명이 프로젝트 전용 ASP.NET 사용자 지정 컨트롤을 개발해서 나머지 팀원들에게 배포하면 웹 페이지 개발자들이 해당 컨트롤을 사용하는 형태라고 할 때, 신규 컨트롤이 배포되거나 기존 컨트롤이 변경되어 재배포하는 경우, 가장 이상적인 상황은 최종 개발자들이 해당 컨트롤에 관해서 어떠한 신경도 쓰지 않아도 되는 환경일 것이다. 극단적으로 말하면 최종 개발자들은 HTCs가 존재하게 될 경로나 컨트롤의 버전 등, 자질구레한 일체의 사항들을 신경쓰지 않아도 개발에는 전혀 지장이 없어야 한다.

그런 이유로 이 샘플 ASP.NET 사용자 지정 컨트롤은 컴파일된 코드뿐 아니라 HTCs를 포함한 모든 리소스들이 DLL 파일의 내부에 포함되도록 설계되었으며, 최초 일회 런타임 시에 비로소 HTCs가 DLL 파일로부터 추출되어 미리 지정된 폴더에 생성된다. 그리고, 실제 최종 결과물에 해당하는 HTML 태그는 추출된 HTCs를 링크하도록 자동으로 렌더링되며, HTCs가 지정된 경로에 저장될 때 DLL 파일의 현재 버전 정보를 파일명에 반영하도록 설계되어 있어서 HTCs가 변경된 경우에도 DLL 파일의 버전만 조정해주면 HTCs의 새로운 버전이 전체 웹 프로젝트로 즉시 전파되도록 구성되어 있다. 따라서, 웹 페이지만 개발하는 최종 개발자의 입장에서는 부수적인 내용들을 신경쓰지 않아도, 배포된 DLL 파일을 ASP.NET 웹 응용 프로그램 프로젝트에서 참조하고, 도구 상자에 ASP.NET 사용자 지정 컨트롤을 등록한 다음, ASP.NET에서 기본으로 제공되는 서버 컨트롤과 동일한 방법으로 이 컨트롤을 사용하는 것만으로도 HTCs를 도입해서 얻을 수 있는 여러가지 장점들을 고스란히 활용할 수 있다. 게다가, ASP.NET 사용자 지정 컨트롤 개발자 입장에서도 전반적인 HTCs 관리가 혁신적으로 간단해진다.

다만, 지금 설명하고 있는 이런 ASP.NET 사용자 지정 컨트롤의 설계 방법이나 구현상의 몇 가지 특징들은 HTCs를 보다 효율적으로 활용할 수 있는 방법을 다방면으로 모색한 끝에 얻어진 필자의 지극히 조그마한 개인적 성과일 뿐이라는 점을 강조하고 싶다. 결코 이런 내용들이 다른 방법에 비해 기술적 우위에 있지는 않으며 단지 하나의 흥미로운 구현 사례 정도로 받아들여 주었으면 한다. 어쩌면 지금 본문을 읽고 계신 분들 중에서도 필자의 제안 방법보다 더 좋은 아이디어를 머리에 떠올리고 계신 분이 있을지도 모른다. 지금부터는 설명의 편의를 위해 이 샘플 ASP.NET 사용자 지정 컨트롤을 HTCs TextBox 서버 컨트롤이라고 가칭하여 부르기로 하겠다. HTCs TextBox 서버 컨트롤의 실제 이름은 ASP.NET에서 기본으로 제공되는 TextBox 서버 컨트롤과 동일한 TextBox 서버 컨트롤이다. 물론, 이 컨트롤의 네임스페이스는 EgoCube.Web.UI.WebControls라는 전혀 별개의 네임스페이스로 설정되어 있으므로 클래스가 충돌하거나 하지는 않는다. 그리고, 지금부터의 살펴보게 될 모든 내용들은 마이크로소프트 비주얼 스튜디오 .NET 2003 버전을 사용해서 작업한다고 가정한다.

먼저, 테스트용 웹 페이지를 작성하기 위한 ASP.NET 웹 응용 프로그램 프로젝트를 생성한다. 그리고, 본문에서 다운로드 받은 압축 파일에 들어있는 웹 컨트롤 라이브러리 프로젝트를 방금 생성한 ASP.NET 웹 응용 프로그램 프로젝트의 솔루션에 추가해서 다음과 같은 솔루션 구조를 구성한다. 전반적으로 솔루션의 구조가 대단히 단순하고 구성 의도 또한 명확하므로 구조를 이해하는데 별다른 어려움은 없을 것이다. 그런 다음, 웹 컨트롤 라이브러리 프로젝트를 ASP.NET 웹 응용 프로그램 프로젝트에 프로젝트 형태로 참조한다. 컴파일 된 DLL 파일을 직접 참조할 수도 있겠지만 지금 단계에서는 프로젝트 참조의 형태가 더 바람직할 것이다.

마이크로소프트 비주얼 스튜디오 .NET 2003 솔루션 구성

그리고, 최종 개발자들이 편리하게 웹 페이지를 개발할 수 있으려면 아무래도 컨트롤이 도구 상자에 등록되어 있는 편이 좋다. 따라서, 이번에는 HTCs TextBox 컨트롤을 도구 상자에 등록해보자. 마이크로소프트 비주얼 스튜디어 .NET 2003의 도구 메뉴에서 도구 상자 항목 추가/제거(X)...를 실행하거나, 직접 도구 상자에서 마우스 오른쪽 버튼을 클릭하여 팝업 메뉴를 띄우고 항목 추가/제거(I)... 메뉴를 실행하면 다음과 같이 도구 상자 사용자 지정 대화 상자가 나타난다.

'도구 상자 사용자 지정' 대화 상자

도구 상자 사용자 지정 대화 상자에서 찾아보기(B)... 버튼을 클릭하고, 다운로드 받은 웹 컨트롤 라이브러리 프로젝트의 bin 폴더 하위에 위치한 Debug 폴더나 Release 폴더에 있는 EgoCube.Web.UI.WebControls.dll 파일을 선택하면 다음과 같이 컨트롤이 등록된다. 이미 설명했던 것처럼 컨트롤 자체의 이름은 ASP.NET에서 기본 제공되는 TextBox 서버 컨트롤과 동일한 TextBox 서버 컨트롤이지만 네임스페이스와 어셈블리 이름을 살펴보면 본질이 전혀 다른 컨트롤이라는 점을 알 수 있을 것이다.

'도구 상자 사용자 지정' 대화 상자

이제 확인 버튼을 눌러서 도구 상자 사용자 지정 대화 상자를 닫고, 도구 상자를 살펴보면 다음과 같이 여러가지 도구 상자 탭들 중 하나 - 대부분의 경우 일반 탭 - 에 등록된 HTCs TextBox 컨트롤을 쉽게 찾을 수 있을 것이다. 그리고 눈치가 빠른 분들은 대부분 짐작하고 계시겠지만 재미삼아 얘기해보면 필자의 그래픽 프로그램 실력이 부족한 관계로 기본 TextBox 서버 컨트롤에서 복사한 아이콘을 텍스트 색상만 약간 변경해서 새 컨트롤에 사용했다. 언제나 그렇지만 필자에게 아이콘 만들기란 너무나도 힘든 작업이다.

도구 상자에 등록된 HTCs TextBox 컨트롤

컨트롤을 사용하기 위한 작업이 완료되었으므로 이제 남은 일은 HTCs TextBox 컨트롤을 사용하는 것 뿐이다. 일반적인 ASP.NET 서버 컨트롤과 마찮가지로 마우스를 이용한 드래그 & 드랍 기능을 지원하며, 위의 이미지에서 볼 수 있는 두 번째 TextBox 서버 컨트롤이 바로 그런 방법을 통해서 생성된 것이다. 뿐만 아니라 디자인 뷰 상태에서 페이지에 올려진 HTCs TextBox 서버 컨트롤을 마우스로 선택하면 다음과 같이 컨트롤에 대응하는 속성들이 속성 창에 반영되어 출력된다. 다시 말해서 ASP.NET에서 기본으로 제공되는 서버 컨트롤들과 그다지 차이날 바가 없는 것이다. 그러나, 디자인 타임 지원 기능과 관련해서 결정적으로 부족한 기능이 한 가지 존재하는데 바로 HTML 뷰에서는 인텔리센스 기능이 동작하지 않는다는 점이다. HTML 뷰에서의 인텔리센스 기능 지원은 마이크로소프트 비주얼 스튜디오 .NET 2003 버전 개발 환경에서는 다소 복잡하고 상당히 번거로운 배포 작업을 요구한다. 게다가 더 민감한 문제는 그런 작업들이 서버 컨트롤 개발자뿐만 아니라 컨트롤을 사용하는 웹 개발자들에게도 요구된다는 점으로서, 본문에서는 굳이 문맥을 흐트려가면서까지 적용할 필요는 없다고 판단되어 HTML 뷰에서의 인텔리센스 기능 지원은 구현을 생략했다는 점을 감안하기 바란다.

HTCs TextBox 컨트롤 속성

그런데 이미지만 놓고 보면, 속성 창에 나타난 속성들의 종류가 대단히 다양해서 웹 사용자 지정 컨트롤에 대한 기본적인 지식이 없는 분들은 필자가 이 모든 속성을 직접 구현한 것으로 오해하기가 쉽다. 그러나, 부모 클래스로부터 상속받은 속성들 외에 직접 구현해서 추가한 속성은 '동작' 카테고리 하위의 'HTCs' 속성과 'HTCsbasicValue' 속성, 고작 이렇게 두 가지에 불과하다. 그 밖의 속성들은 모두 ASP.NET 에서 기본으로 제공해주는 TextBox 서버 컨트롤로부터 상속받은 것이므로 사용자 지정 컨트롤을 작성할 때마다 이런 많은 분량의 속성들을 모두 직접 구현해야 하는지 미리부터 염려하는 분들은 없기 바란다. 실제로 TextBox 서버 컨트롤을 상속받는 부분인 클래스 선언부의 코드는 다음과 같이 매우 평범한 코드에 불과하며, 이 단 한 줄의 코드로 인해서 부모 웹 서버 컨트롤의 클래스로부터 모든 속성들을 상속받게 되는데, 사실 이는 너무나도 당연한 결과라고 말할 수 있을 것이다.

    ...
                    
    public class TextBox : System.Web.UI.WebControls.TextBox
    {
    
    ...

컨트롤에 새로운 속성을 추가하려면 컨트롤 클래스에 속성을 구현해주면 된다. 물론, 아무렇게나 마음 내키는대로 클래스 속성을 구현하기만 하면 되는 것은 아니고, 실제로는 여러가지 다양한 기법들이 동원되지만 기본적인 원리는 이를 크게 벗어나지 않는다. 특히, 형식 변환기 등과 같은 디자인 타임 지원 기능이나 템플릿 및 데이터 바인딩 같은 컨트롤의 고급 기능들을 직접 구현하지 않는 경우에는 더욱 그렇다. 가령, 다음 코드는 방금 설명한 HTCs TextBox 서버 컨트롤에 추가된 HTCs 속성과 HTCsbasicValue 속성의 실제 구현 코드다. 문맥상 다소 혼란스러울 수도 있겠지만, Category나 basicValue 등 클래스 라이브러리에 미리 정의된 어트리뷰트(Attribute)들을 지정해서 속성 창에 출력되는 컨트롤의 속성(Property)들을 제어하는 부분 등을 유심히 살펴보기 바란다. 참고로 이런 어트리뷰트들에 대한 보다 상세한 정보는 System.ComponentModel 네임스페이스를 통해서 얻을 수 있다. 그리고, 프로퍼티의 데이터를 저장하기 위해 클래스 내부 변수를 선언하는 대신 ViewState 개체를 사용하는 기법이라던지, HTCs 속성의 리턴 데이터 형이 열거형이라는 점과 그로 인해 HTCs 속성의 속성 창 출력 형태가 자동으로 드롭다운 리스트로 나타난다는 점 등도 한 번 눈여겨 볼만한 부분이다.

    ...
                    
    #region 컨트롤 프로퍼티

    /// <summary>
    /// 컨트롤에 적용되는 HTCs 모드를 지정한다. 만약 마무런 값도 지정되지 
    /// 않으면 컨트롤은 HTCsType.NotSet 을 지정한 것과 동일한 모드로 동작하고, 
    /// 이는 ASP.NET 에서 기본으로 제공되는 TextBox 컨트롤의 동작과 
    /// 정확하게 동일한 것이다.
    /// </summary>
    [Description("컨트롤에 적용되는 HTCs 모드를 지정합니다.")]
    [Category("Behavior")]
    [basicValue(HTCsType.NotSet)]
    public HTCsType HTCs
    {
        get 
        {
            if (this.ViewState["HTCs"] == null)
                return HTCsType.NotSet;
            else
                return (HTCsType) this.ViewState["HTCs"];
        }
        set
        {
            this.ViewState["HTCs"] = value;
        }
    }

    /// <summary>
    /// 컨트롤이 HTCs 모드에서 동작될 때 적용되는 basicValue 속성값을 지정한다. 
    /// 이 속성값은 컨트롤이 HTCs 모드로 동작하지 않는 경우에는 무시된다. 
    /// </summary>
    [Description("컨트롤이 HTCs 모드에서 동작될 때 적용되는 basicValue 속성값을...")]
    [Category("Behavior")]
    [basicValue("")]
    public string HTCsbasicValue
    {
        get
        {
            if (this.ViewState["HTCsbasicValue"] == null)
                return "";
            else
                return (string) this.ViewState["HTCsbasicValue"];
        }
        set
        {
            this.ViewState["HTCsbasicValue"] = value;
        }
    }

    #endregion
    
    ...

그러나, 정말로 세심하게 주의를 기울여서 살펴봐야 될 부분은 지금부터 살펴볼 몇 가지 내용들이다. 이미 언급했던 것처럼 HTCs TextBox 컨트롤은 컴파일된 코드뿐만 아니라 HTCs 파일을 비롯한 모든 리소스들이 DLL 파일의 내부에 포함되도록 설계되어 있다. 이런 결과를 얻기 위해서 우리들이 처리해주어야 할 작업은 매우 간단하다. 단지, DLL 파일 내부에 패키징하고자 하는 리소스 파일의 빌드 작업 속성값을 포함 리소스로 설정해주기만 하면 된다. 다음 이미지는 HTCs_Numeric_Only.htc 파일의 실제 설정 상태를 보여주고 있다. 이것이 필요한 작업의 전부다.

'빌드 작업' 속성값이 '포함 리소스' 로 설정된 HTCs_Numeric_Only.htc 파일

다음 단계로 구현해야 할 작업은 최초 일회 런타임 시에 DLL 파일에서 HTCs 파일의 코드 내용을 추출해서 미리 지정해 놓은 폴더에 실제 파일을 생성하고, 이렇게 만들어진 HTCs 파일을 렌더링되는 HTCs TextBox 컨트롤의 HTML 코드가 링크하도록 처리하는 작업이다. 그런데 이런 코드를 구현하기 전에 몇 가지 미리 고려해야 할 사항들이 있다. 첫 번째로 DLL 파일로부터 추출된 HTCs 코드를 구체적으로 어떤 폴더에 파일로 생성할 것인지 결정해야만 한다. 두 번째로 이미 컨트롤이 개발자들에게 배포된 상황에서 다시 HTCs 코드가 변경되는 경우가 발생하면 당연히 재배포된 컨트롤의 HTCs 파일이 반영돼야 하는데, HTCs 파일이 생성될 폴더에는 이미 이전 버전의 HTCs 파일이 존재하고 있을 수 있으므로, 결국 HTCs 파일간에 버전을 식별할 수 있는 어떠한 방안이 필요하다는 점이다. 마지막 세 번째는 이런 작업들을 처리하기 위해서 필요한 실제 코드가 서버 컨트롤의 라이프 싸이클에서 어떤 단계에서 구현되는 것이 가장 적절한지를 결정해야만 한다. 그러면 먼저 HTCs 파일을 생성할 위치로는 어디가 가장 적당할지에 대해서 논의해보자. 개개인마다 선호하는 방법이 있겠지만 필자는 이 문제를 굳이 어렵게 접근하지 않고 가장 간단한 방법으로 해결하기로 했다. 잠시만 생각해보면 ASP.NET 기반 구조에는 현재 목적에 적절히 활용할 수 있는 폴더가 이미 존재하고 있다는 것을 알 수 있을 것이다. 바로 aspnet_client 폴더가 그것이다. 그러나, 이 폴더를 그대로 사용하기에는 조금 무리가 있는데, 왜냐하면 aspnet_regiis.exe 등의 프로그램이 실행되면 해당 폴더가 덮어씌워지거나 삭제될 수 있는 가능성이 충분히 존재하기 때문이다. 따라서, 약간의 응용이 필요한데 지금부터 그 방법을 알아 보도록 하자. 그리 널리 알려지지는 않은 사실이나 machine.config 파일의 355라인 부근을 살펴보면 다음과 같은 노드를 찾을 수 있을 것이다. 그 유명한 processModel 노드 바로 다음에 나타나는 노드이므로 찾는데 별다른 어려움은 없을 것이다.

    ...
    
    <webControls clientScriptsLocation="/aspnet_client/{0}/{1}/"/>
    
    ...

노드를 한 번 살펴보면 아무리 눈치 없는 사람이라도 뭔가 느껴지는 바가 있을 것이다. 이미 예상하고 있겠지만 바로 이 노드가 ASP.NET에서 사용하는 자바스크립트 생성용 폴더 경로를 설정하는 노드다. 더군다나 이 노드에 존재하는 유일한 속성인 clientScriptsLocation의 값을 살펴보면 고맙게도 String 클래스의 Format() 메서드를 사용하는 것을 전제로 하는양 구성되어 있는 것이다. 따라서, 이 속성값을 읽어들인 다음 간단한 문자열 처리를 거치기만 하면 꽤나 그럴듯하면서 나름대로 합리적인 경로를 얻어낼 수 있다. 그래서, 필자는 다음과 같은 유틸리티 메서드를 작성해서 클래스 내부적으로 사용하고 있다.

    ...
    
    /// <summary>
    /// .NET 설정 파일로부터 HTCs 파일이 위치할 URL 경로를 가져온다.
    /// </summary>
    /// <returns>HTCs 파일이 위치할 URL 경로</returns>
    private string ClientScriptsLocation()
    {
        IDictionary objDictionary = (IDictionary) 
            System.Configuration.ConfigurationSettings.GetConfig("system.web/webControls");
        string ClientScriptsLocation = 
            objDictionary["clientScriptsLocation"].ToString().Trim();
        ClientScriptsLocation = String.Format(ClientScriptsLocation, "EgoCube", "HTCs");

        return ClientScriptsLocation;
    }
    
    ...

그리고, 그 결과 생성되는 폴더의 실제 구조는 다음 이미지를 통해서 미리 살펴볼 수 있다. 이 폴더 구조는 필자의 개발 머신에 현재 생성되어 있는 실제 구조다. ASP.NET의 자체 폴더를 전혀 간섭하지 않으면서도 대부분 납득할 수 있을만한 결과라고 생각한다. 여러분도 각자 자신이 소속된 업체의 회사명이나 기관명 등을 활용해서 aspnet_client 폴더 하위에 고유한 폴더를 추가할 수 있을 것이다. 다만 한 가지 주의해야 할 점이 있는데, 그것은 앞에서도 여러차례 언급했던 것처럼 폴더가 생성되는 시점이 실제로 컨트롤이 최초로 로드되어 사용될 때라는 점이다. 즉, 문제의 핵심은 개발자나 서버 관리자에 의해서가 아닌, 일반 사용자의 페이지 요청에 의해서 폴더 생성이 이뤄진다는 점이다. 따라서, ASP.NET의 기본 실행 계정인 ASPNET 계정이 갖고 있는 제한된 권한만으로는 폴더의 생성이 불가능하므로 결과적으로는 권한 관련 오류가 발생한다. 이런 오류를 피하려면 IIS 6.0을 사용하고 있는 경우, 관련된 응용 프로그램 풀의 ID 설정을 로컬 시스템으로 바꿔주고, 그 이하의 IIS 버전을 사용하고 있는 경우라면 machine.config 파일에서 processModel 노드의 userName 속성값을 SYSTEM으로 바꿔주면 된다. 만약, 응용 프로그램 풀이라는 개념 자체가 익숙하지 않다거나, 응용 프로그램 풀 ID 구성 방법을 잘 모른다면 MSDN 등의 문서를 먼저 참고하기 바란다.

aspnet_client 폴더 하위에 추가된 HTCs 폴더 구조

버전 문제 역시도 생각하기에 따라서 매우 간단히 해결할 수 있다. 물론 다른 좋은 방안들이 존재할 수도 있겠지만 필자가 생각해낸 해결 방법의 핵심은 바로 리플렉션이다. 구체적으로 얘기하자면 HTCs 파일의 버전 관리를 위해서 굳이 별도의 체계를 만들어낼 것이 아니라, 먼저 서버 컨트롤 어셈블리의 버전 정보를 리플렉션으로 가져와서 HTCs 파일의 버전으로 반영시키면 되는 것이다. .NET 어셈블리 버전 정보는 AssemblyInfo.cs 파일의 AssemblyVersion 속성을 통해서 제어가 가능하며, 대부분의 경우 다음의 코드에서 볼 수 있는 것과 같은 기본값으로 설정되어 있다.

    ...
    
    [assembly: AssemblyVersion("1.0.*")]
    
    ...

이 기본 설정을 그대로 사용하는 것도 나쁘지 않겠지만, 서버 컨트롤의 어셈블리 버전 정보를 활용해서 논리적으로 HTCs 파일 버전을 관리하려는 현재의 논의를 위해서는 버전 정보를 명시적으로 설정하는 편이 더 바람직할 것이다. 따라서, 다음과 같이 버전 정보를 명시적으로 설정해보도록 하자. 당연한 얘기지만, 이제 해당 서버 컨트롤 프로젝트를 컴파일하면 버전 정보가 1.0.1.0인 어셈블리 DLL이 생성된다.

    ...
    
    [assembly: AssemblyVersion("1.0.1.0")]
    
    ...

다시 본론으로 돌아와 보자. 방금 우리는 서버 컨트롤의 어셈블리 버전 정보를 설정하는 방법을 간단하게 알아봤다. 이 방법과 HTCs 파일의 버전 관리 사이에는 서로 어떤 관계가 존재하는 것일까? 그 답은 매우 간단한데, HTCs 파일이 생성되는 시점에 리플렉션을 사용해서 서버 컨트롤의 버전 정보를 얻고 그 버전 정보를 실제로 생성되는 HTCs 파일명의 일부로 사용하는 것이다. 그리고, HTCs 파일의 내용이 변경되면 서버 컨트롤 어셈블리 자체의 버전 정보도 변경해서 DLL 파일을 재생성한 다음, 웹 페이지 개발자들에게 다시 DLL 파일을 배포하면 된다. 만약, 또다시 HTCs 파일의 내용이 변경된다면 이번에도 서버 컨트롤 어셈블리의 버전 정보를 역시 다시 변경하여 컴파일하고 재배포하면 된다. 물론, 렌더링되는 컨트롤의 HTML 태그에도 역시 다음과 같이 해당 버전 정보가 반영되어야 하는 것은 당연한 일이다.

    ...
    
    <input basicValue="0" name="txtOnlyDigit" 
        type="text" id="txtOnlyDigit" class="cs_textbox" 
        style="behavior:url('/aspnet_client/EgoCube/HTCs/HTCs_Numeric_Only_1.0.1.0.htc');" />
    
    ...

그러면 이제 서버 컨트롤 어셈블리에서 버전 정보를 읽어오는 방법을 알아볼 차례다. 이미 언급했던 것처럼 리플렉션을 사용하면 이 문제는 쉽게 해결할 수 있다. 일단 System.Reflection 네임스페이스에서 제공되는 Assembly 클래스의 정적 메서드인 GetExecutingAssembly() 메서드를 사용해서 현재 코드가 실행되고 있는 어셈블리 자체에 대한 참조를 Assembly 클래스 형태로 얻는다. 당연한 얘기겠지만 이렇게 얻어낸 Assembly 개체는 서버 컨트롤 어셈블리를 가리키고 있는데, 이 개체의 멤버 메서드인 GetName() 메서드를 사용해서 얻어낼 수 있는 AssemblyName 클래스 타입 개체의 Version 프로퍼티 값으로 간단하게 어셈블리 버전 정보를 가져올 수 있다. 이 과정이 구현된 실제 코드는 다음과 같으며 리소스로부터 Stream 개체 형식으로 읽어들인 HTCs 파일의 내용을 본래 파일명과 어셈블리의 버전 정보를 결합한 새로운 파일명으로 저장하는 전체 과정을 보여준다. 코드를 잘 살펴보면 Assembly 개체의 멤버 메서드인 GetManifestResourceStream() 메서드를 사용하여 간단하게 HTCs 파일의 내용을 얻어내고 있다는 점도 쉽게 알 수 있을 것이다. 사실, 이 리소스라는 개념은 대부분의 웹 개발자들에게는 그리 친숙하지 않은 낯선 개념이지만 윈도우즈 응용 프로그램 개발자들에게는 매우 친숙한 개념으로 이런 식으로 리소스를 활용하는 기법이 그다지 새로운 것은 아니다. 가령, EXE 파일이나 DLL 파일의 내부에 존재하는 아이콘 등의 경우를 머리속에 떠올려 보면 쉽게 이해가 갈 것이다.

    ...
    
    // 리소스로부터 HTCs 파일을 읽는다.
    switch (this.HTCs)
    {
        case HTCsType.Numeric:
            HTCsName = "HTCs_Numeric_Only";
            break;
    }
                
    System.Reflection.Assembly objAssembly 
        = System.Reflection.Assembly.GetExecutingAssembly();
    HTCsResourceName = HTCsRootNameSpace + "." + HTCsName + ".htc";
    using (System.IO.Stream objStream 
        = objAssembly.GetManifestResourceStream(HTCsResourceName))
    {
        using (System.IO.StreamReader objSR = new System.IO.StreamReader(objStream))
        {
            HTCsCode = objSR.ReadToEnd();
        }
    }

    // HTCs 파일 존재 점검 및 생성
    HTCsFullPathName = this.Page.Server.MapPath(this.ClientScriptsLocation()) 
                     + "\\" + HTCsName + "_" + objAssembly.GetName().Version + ".htc";
    if (!System.IO.File.Exists(HTCsFullPathName))
    {
        using (System.IO.StreamWriter objSW 
            = new System.IO.StreamWriter(HTCsFullPathName, false))
        {
            objSW.Write(HTCsCode);
        }
    }
    
    ...

지금까지 논의한 내용들을 하나의 메서드로 구현한 뒤, 적절한 서버 컨트롤의 라이프 싸이클 단계에서 메서드를 호출해주기만 하면 모든 준비과정이 마무리 된다. 필자는 WriteHTCsFile()라는 이름으로 메서드를 작성하고, 부모 클래스인 WebControl 클래스의 OnInit() 이벤트 헨들러 메서드를 재정의하여 다음 코드에서 볼 수 있는 것처럼 WriteHTCsFile() 메서드를 호출함으로서 실제 구현을 처리하고 있다. 물론 이렇게 서버 컨트롤이 로드될 때마다 매번 반복적으로 HTCs 파일의 추출 및 저장 작업을 실행하는 것은 필요없는 퍼포먼스 비용을 발생시키게 되므로 HTCs 속성이 NotSet으로 설정된 경우에는 별다른 작업을 실행하지 않는 등, 간단간단한 대응 코드들이 실제 메서드에는 추가되어 있다. 그러나, 사실 보다 엄격하게 얘기한다면 코드의 효율성이라는 측면에서 볼 때 그 정도 대처만으로는 많이 부족한 것이 사실이고, 보다 근본적인 접근 방식의 개선이 이뤄져야 할 것이다. 예를 들어서, 서버 컨트롤이 디자인 뷰에 드래그 & 드랍되는 시점이라던가 서버 컨트롤의 특정 속성값이 변경되는 시점에 HTCs 파일의 추출 및 저장 작업이 미리 이뤄지는 등의 방법으로 말이다. 그러나, 본문은 어디까지나 ASP.NET 사용자 지정 컨트롤과 HTCs 파일간의 연동에 관한 간단한 개념을 설명하는 것이 본래 목적이므로 이 주제에 대해서는 더이상 언급하지 않을 것이다.

    ...
    
    /// <summary>
    /// WebControl.OnInit 메서드 재정의
    /// </summary>
    /// <param name="e">이벤트 데이터가 들어 있는 EventArgs 개체</param>
    protected override void OnInit(EventArgs e)
    {
        this.WriteHTCsFile();
        base.OnInit(e);
    }
    
    ...

마지막으로 살펴보고자 하는 부분은 지금까지 논의한 내용들을 통해서, 미리 지정한 경로에 실제로 생성된 물리적인 HTCs 파일을 서버 컨트롤의 최종 결과물인 HTML 태그의 렌더링 결과에 반영시키는 작업이다. 여러분들도 이제 익히 알고 있는 바와 같이 HTCs 파일은 케스케이딩 스타일시트의 behavior 속성을 통해서 설정된다. 따라서, 우리가 원하는 작업을 처리하기 위해서는 어떤 방식으로든 서버 컨트롤의 케스케이딩 스타일시트 설정 과정 중에 HTCs를 설정하기 위한 단계가 추가되야 한다는 결론이 자연스럽게 이끌어 진다. 게다가, 경우에 따라서는 basicValue와 같이 HTCs 내부에서 추가적으로 정의한 속성들이나 이벤트 등도 서버 컨트롤의 HTML 속성의 일부로 렌더링돼야만 하므로 이러한 부분들도 같이 고려되어야 하는 것이다. 물론 ASP.NET 웹 사용자 지정 서버 컨트롤 개발을 위한 기반 구조가 이런 작업들을 처리하기 위한 탄탄한 방법들을 제공해주므로 이를 충실하게 구현하기만 하면 원하는 결과를 그다지 어렵지 않게 얻을 수 있다. 구체적으로 말해서 AddAttributesToRender() 메서드를 재정의해주기만 하면 되는데, 이 메서드는 WebControl 클래스로부터 상속되며 서버 컨트롤의 HTML 태그에 렌더링 될 HTML 속성이나 케스케이딩 스타일시트 속성을 추가할 수 있는 보장된 기회를 제공해준다. 다음 코드가 바로 그 실제 결과물이다.

    ...
    
    /// <summary>
    /// WebControl.AddAttributesToRender 메서드 재정의
    /// </summary>
    /// <param name="writer">
    /// 클라이언트에서 HTML 컨텐트를 렌더링하기 위한
    /// 출력 스트림을 나타내는 System.Web.UI.HtmlTextWriter 개체
    /// </param>
    protected override void AddAttributesToRender(HtmlTextWriter writer)
    {
        // HTCs 모드로 동작되는 경우에만 속성값을 추가한다.
        if (this.HTCs != HTCsType.NotSet)
        {
            if (this.HTCs == HTCsType.Numeric)
            {
                // 스타일 속성값에 HTC 설정
                System.Reflection.Assembly objAssembly 
                    = System.Reflection.Assembly.GetExecutingAssembly();
                if (this.Site == null || !this.Site.DesignMode)
                    writer.AddStyleAttribute("behavior", 
                        "url('" + this.ClientScriptsLocation() 
                        + "HTCs_Numeric_Only_" + objAssembly.GetName().Version 
                        + ".htc')");

                // basicValue 속성값 설정
                if (HTCsbasicValue != null && HTCsbasicValue.Trim().Length > 0)
                    writer.AddAttribute("basicValue", this.HTCsbasicValue.Trim());
            }
        }

        base.AddAttributesToRender(writer);
    }
    
    ...

리플렉션을 통해서 어셈블리 버전 정보를 읽고, 그 버전 정보를 바탕으로 HTCs의 전체 파일경로를 구성한 다음 HtmlTextWriter 개체에서 제공되는 AddStyleAttribute() 메서드를 사용해서 케스케이딩 스타일시트 속성에 HTCs 관련 설정을 반영시키고 있다. 그리고, 역시 비슷한 방법으로 AddAttribute() 메서드를 사용해서 렌더링될 서버 컨트롤의 HTML 태그에 HTCs에 구현된 속성을 추가하고 있다. 그 결과로 생성되는 HTML 태그는 다음과 같으며, 사실상 이 HTML 태그가 본문의 최종 결과물인 셈이다.

    ...
    
    <input basicValue="0" 
        name="txtOnlyDigit" type="text" id="txtOnlyDigit" class="cs_textbox" 
        style="behavior:url('/aspnet_client/EgoCube/HTCs/HTCs_Numeric_Only_1.0.1.1.htc');" />

    ...

이렇게 해서 장장 10개월 동안을 끌어오던 본문의 작성이 마무리 되었다. 본문의 내용도 여러모로 미흡한 부분들이 많고 너무 오랜 기간 동안 작성된 관계로 최근에 화제가 되고 있는 다양한 기술적 이슈들과 상당히 동떨어진 주제가 돼버린 감도 없지는 않다. 그러나, 본문에서 논의하고 있는 이슈들 또한 나름대로 의미를 갖고 있는 글이라고 생각해서 늦게나마 급하게 글을 마무리하게 되었다. 마지막으로 그 10개월이라는 시간이 필자와 여러분을 비롯한 ASP.NET 개발자들에게 가져온 변화에 대해서 한 번 생각해보고서 본문을 정리하고자 한다. 먼저 필자가 개인적으로 가장 먼저 손꼽고 싶은 변화로는 파이어폭스의 약진을 들 수 있다. 물론, 파이어폭스는 등장 그 자체부터가 매우 화려하기도 했지만, 최근 몇 달간 이뤄진 파이어폭스의 존재감 확산은 웹 개발자들이 결코 무시할 수 없는 수준으로까지 치달았다라는 것이 개인적인 결론이다. 단적으로 최근의 MSDN 매거진 기사에서는 작성한 자바스크립트 코드가 파이어폭스에서 정상적으로 동작함을 보여주기 위한 이미지 캡춰까지 제시될 정도니 말이다. 아직까지는 이런 일련의 변화들이 기업체 솔루션 구축을 주요 업무로 삼고 있는 필자 같은 개발자들에게까지는 별다른 영향력을 미치고 있지 못하고 있다고 생각하고는 있지만, HTCs 개발이나 ASP.NET 사용자 지정 컨트롤 개발과 같이 전체 개발 범위에서 어떤 공통 분모를 담당하고 있는 개발자들이라면, 지금까지보다 상대적으로 고민해야 될 이슈들이 더 늘어났으며, 게다가 조금 더 민감해지기까지 했다는 점을 인정할 수 밖에 없을 것이다. 따라서, 본문의 주제이기도 하고 본문에서 계속해서 거론되고 있는 HTCs라는 단어를 웹 브라우저 호환 자바스크립트라는 단어로 대신해야만 할 경우도 충분히 있을 수 있을 것이다.

그리고, 개인적으로 재미있게 느껴지는 점은, 필자가 두 번째 변화로 지목하고자 하는 마이크로소프트 비주얼 스튜디오 2005의 정식출시와 그에 따라 점진적으로 증가되는 ASP.NET 2.0의 도입추세야말로 ASP.NET 개발자들이 앞에서 거론된 변화에 대처하기 위해서 대응할 수 있는 가장 적극적인 방법의 시작점이 아닌가 하는 점이다. 물론 이런 주장은 필자의 지극히 개인적인 사견일 뿐이지만, 나름대로 이런 생각을 하고 있는 이유에는 다음과 같은 두 가지 기술적인 근거가 존재한다. 첫 번째 근거는 ASP.NET 2.0에서 제공되는 리소스 기반의 스크립트 코드 관리다. 본문의 후반부에서 살펴봤던 내용은 필자가 나름대로 구현한 리소스 기반 스크립트(포괄적인 관점에서 HTCs 역시도 스크립트이므로) 관리 기법으로 볼 수 있는데, ASP.NET 2.0에서는 프레임워크 차원에서 리소스에 기반한 스크립트 코드 관리 기법을 보다 풍부하고 편리한 방법으로 제공해준다. 따라서, 본문에서 살펴봤던 것처럼 리소스로부터 스크립트 코드를 가져오기 위해 굳이 이런저런 방법을 동원해서 난해한 코드를 직접 구현할 필요가 없어졌다. 두 번째 근거는 코드명 ATLAS로 잘 알려진 마이크로소프트의 AJAX 프레임워크가 바로 그것이다. AJAX를 기술적인 관점에서 살펴볼 때 많은 분들이 간과하고 넘어가는 부분이 한 가지 있다. 분명히 이 기술을 뒷받침해주는 핵심 요소중 한 가지가 페이지를 갱신하지 않고서도 서버 측과 동기적, 또는 비동기적으로 데이터를 주고 받게 해주는 매커니즘이라는 사실을 부인할 수 있는 사람은 아무도 없을 것이다. 그러나 이런 서버 측과의 통신이라는 측면이 너무 강조된 나머지 페이지 렌더링을 위해서 필요한 DOM과 스크립트 측 구현에 대한 중요성이 무시되는 경향이 매우 강하다는 점이다. 만약, 이 부분이 만족스럽게 지원되지 않는다면 AJAX는 또 다른 형태의 업무 증가만을 가져올 뿐이라는 것이 필자의 생각이다. 그러나, 다행스럽게도 마이크로소프트에서 구현한 AJAX 라이브러리인 ATLAS에서는 객체지향적인 방법으로 대부분의 웹 브라우저상에서 동일한 방법으로 클라이언트 측 처리를 구현할 수 있도록 지원해주는 클라이언트 자바스크립트 라이브러리를 지원해주므로 웹 개발자가 크로스 브라우징을 위해서 많은 노력을 기울이지 않더라도 고품질의 결과물을 얻을 수 있게 해준다. 아직 ATLAS가 정식으로 배포되지 않은 상황이고, 필자의 ATLAS에 대한 지식도 매우 부족한 편이기는 하지만 행복한 기대를 해도 좋을 것이라고 믿는다.