재미있는 자바스크립트 02, HTCs의 작성

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

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

본격적으로 HTCs의 작성 방법을 살펴보기 전에 간단한 질문 한 가지를 던져보고자 한다. 지난 글에서 잠시 살펴봤던 것처럼 HTCs는 확장자가 .htc인 텍스트 파일이다. 그리고, 파일의 내부는 우리가 익숙하게 접해왔던 자바스크립트 코드와 몇 가지 낯선 태그들로 구현되어 있고, 케스케이딩 스타일시트를 통해서 적용된다. 그렇다면, .htc 파일의 본질은 무엇일까? 자바스크립트 .js 파일이나 케스케이딩 스타일시트의 .css 파일 등과 비슷한 파일로 보는 것이 올바른 시각일까? 예상과 전혀 다른 결과일 수도 있겠지만 .htc는 HTML 파일의 일종이라는 것이 올바른 답이다. 더불어, 그 귀결로 HTCs는 개발자들에게 완벽한 하나의 컴포넌트 모델을 제공해주게 된다. 지금까지 살펴본 HTCs 샘플들은 이미 존재하고 있는 HTML 태그에 특정 기능(Functionality)을 부여하는 형태의 것이었다. 그러나, 단지 그 정도가 HTCs가 제공해주는 기능의 전부라면 HTCs를 완벽한 컴포넌트 구현 메커니즘이라고 말하기는 어려울 것이다. 컴포넌트라고 하는 큰 주제에는 사용자 인터페이스와 관련된 이슈가 상당한 비중을 차지하고 있기 때문이다. 잘 알려진 사실은 아니지만 HTCs는 사용자 인터페이스 관련 컴포넌트 메커니즘까지 모두 지원하고 있으며, HTML 파일의 일종이라는 본질 그대로 사용자 인터페이스와 관련된 부분들은 HTML 문서 구조를 바탕으로 한다. 극단적으로 필자의 생각을 말해보자면, 사용자 인터페이스가 구현된 HTCs는 커스텀 태그라는 다른 이름을 갖고 있는 특수한 유형의 IFRAME 태그라고 생각하는 것이 HTCs를 이해하는 가장 부담없는 관점 중 하나라고 생각한다.

결과적으로 정리해보면 HTCs는 크게 두 가지 종류로 분류될 수 있으며 세부적인 구현 방법도 어느 정도의 차이가 존재하는데, MSDN에서는 이를 Attached Behavior와 Element Behavior라는 용어로 구분하고 있다. 본문에서는 두 용어를 특별히 번역하지 않고 원문 그대로 사용하려고 하므로 참고하기 바란다. 아무튼 Attached Behavior는 별도의 사용자 인터페이스 없이 기존에 존재하는 HTML 태그에 특정 기능만 추가하는 형태의 HTCs를 말하는 것으로, 지난글에서 살펴본 HTCs들이 모두 여기에 포함된다. 그에 비해서 Element Behavior는 일종의 커스텀 태그를 구현한 것이라고 생각하면 무리가 없으며, 참고로 우리는 지금까지 이런 유형의 HTCs를 한 번도 살펴본 적이 없다. 쉽게 말해서 ASP .NET 프로그래밍 경험이 있으신 분들은 사용자 정의 컨트롤을 머리 속에 떠올려보면 필자가 무얼 얘기하고 있는지 간단히 알 수 있을 것이다. 그래도 쉽게 이해가 되지 않는 분들은 여기를 클릭해보기 바란다. 마이크로소프트에서 제공되는 샘플 중 하나로 HTML 소스를 자세히 살펴보면 많은 도움이 될 것이다. 본문에서는 Attached Behavior 유형의 HTCs를 하나 구현해보려고 하는데, 단순히 샘플 수준에서 그치는 것이 아니라 실제로 프로젝트에서 사용할 수 있는 수준까지 끌어올리는 것이 필자의 계획이다. 만약, 본문에서 모든 내용을 마무리하지 못한다면 다음글에서 계속 내용을 이어가도록 하겠다.

지금부터 구현해 볼 기능은 사용자가 INPUT 태그에 입력하는 값을 제한하는 기능이다. 이 기능은 실제 프로젝트에서도 매우 빈번하게 요구되는 기능으로, 올바른 전자우편 주소만 입력할 수 있도록 강제한다거나 금액을 입력해야 하는 입력란에 문자값을 입력하지 못하게 하는 등의 사례가 대표적인 경우다. 본문에서는 설명의 편의를 위해 간단히 숫자값 입력만 허용하는 Attached Behavior 유형의 HTCs를 구현해보려고 한다. 실제 코딩에 들어가기 전에 요구되는 기능들을 구현하기 위한 세부 사항부터 정의해보자. 필자가 정리한 세부 사항들은 다음과 같지만 이 정도로는 만족하지 못하는 분들도 계실 것이다. 그런 분들은 본문의 결과를 바탕으로 직접 원하는 기능들을 구현해보기 바란다. 본문의 뒷 부분쯤에서도 구현된 기능을 보다 발전시켜 나가면서 자연스럽게 일부 조건들은 변경되거나 새로 추가될 것이다.

  1. 타입이 TEXT인 INPUT 태그만을 대상으로 동작한다.
  2. 대상 태그가 로딩된 직후, 텍스트 정렬을 오른쪽 정렬로 설정한다.
  3. 대상 태그가 로딩된 직후, 입력된 값이 없다면 0으로 값을 초기화한다.
  4. 대상 태그에 포커스가 발생하면 입력값의 내용 전체를 반전시킨다.
  5. 오직 0부터 9까지의 숫자값만 입력할 수 있다.
  6. 제한되는 문자는 애초에 키 입력 자체가 되지 않는 방식으로 구현한다.
  7. 다음과 같은 특수키들은 모두 정상적으로 동작해야 한다.
    • 되돌리기(Ctrl+Z), 잘라내기(Ctrl+X), 복사(Ctrl+C), 붙여넣기(Ctrl+V)
    • 전체선택(Ctrl+A), 새로고침(Ctrl+R)
    • 모든 화살표 키 (←, ↑, →, ↓)
    • 페이지 이동 관련 키 (Home, End, PgUp, PgDn)
    • 삭제 관련 키 (Delete, Backspace)
    • 기타 관련 키 (Tab, Esc)
  8. 붙여넣기에 대비하여 포커스를 잃을 때 다시 한 번 유효성을 검사한다.

지금부터 이 세부 사항에 정의된 내용들을 한 가지씩 차례대로 구현해보자. 가장 먼저 해야할 작업은 HTCs 파일 자체를 생성하는 일이다. 각자의 작업 환경에 알맞게 적절한 경로에 텍스트 파일을 하나 생성하고 파일의 확장자를 .htc로 변경한다. HTCs 파일을 생성 위치에 관한 제약 사항은 존재하지 않지만 아무래도 웹 경로 상에 위치하는 것이 본문을 따라가는데 유리할 것이다. 물론, 실제 프로젝트에서는 공통 라이브러리들이 위치한 경로에 HTCs들을 모아 놓는 것이 대부분일 것이다. 그리고, 다음과 같이 파일에 PUBLIC:COMPONENT 엘레먼트 태그를 선언해주는데 일단 지금은 이 태그에서 제공하는 속성들이나 각각의 의미에 대한 자세한 설명은 생략하고 넘어가도록 하겠다. 이 부분은 본문에서 의도하는 모든 HTCs의 구현이 마무리 된 다음에 다시 논의하기로 한다.

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

자, 본격적인 HTCs의 구현이 시작되었다! 세부 사항 1번은 포괄적인 내용이므로 일단 구현을 보류하고 세부 사항 2번부터 구현을 해보도록 하자. 내용 자체는 매우 간단한 편이다. 단지, 텍스트 정렬을 오른쪽 정렬로 설정하기만 하면 되는데, 정작 문제는 어떤 시점에 해당 코드가 실행되야 하는지, 즉 어떤 이벤트 헨들러에 코드를 구현할 것인지가 문제가 된다. 이 작업을 처리하기에 가장 바람직한 시점은 HTCs가 추가된 대상 HTML 태그가 브라우저에 로딩된 직후라고 생각한다. 즉, 페이지 자체는 아직 로딩이 끝나지 않았더라도 해당 HTML 태그의 로딩은 완료된 시점이다. 따라서, 모든 HTML 태그에서 onLoad 이벤트가 제공되면 좋겠지만 대부분의 태그들은 이 이벤트를 지원하지 않으므로 뭔가 다른 대안이 필요하다. BODY 태그의 onLoad 이벤트나 각 HTML 태그의 onFocus 이벤트는 바람직한 대상이 아니라는 판단이다. 그렇다면 뭔가 좋은 대안이 없을까? 다행스럽게도 HTCs에서는 이런 목적을 위해서 다음과 같은 두 가지 이벤트를 추가적으로 제공해준다. 사실 이 외에도 두 가지 이벤트가 더 제공되지만 일단 본문에서는 논의하지 않는다.

  • onContentReady
  • onDocumentReady

각각의 이벤트 이름만 봐도 쉽게 예상할 수 있겠지만, 첫 번째 이벤트는 HTCs가 적용된 HTML 태그 자체가 파싱된 직후에 발생하고, 두 번째 이벤트는 HTML 문서 전체가 파싱된 직후에 발생한다. 즉, 두 번째 이벤트는 BODY 태그의 onLoad 이벤트와 거의 같은 역활을 한다. 다만, onLoad 이벤트 헨들러는 구현하는 작업의 대상 태그를 일일이 지정해줘야 하는 반면, onDocumentReady 이벤트는 작업이 HTCs 자신이 적용된 HTML 태그 자체에 집중되므로 논리적으로 구현이 용이하다는 장점을 갖고 있다. 결론적으로 세부 사항 2번을 구현하기에 가장 적합한 이벤트는 onContentReady 이벤트라고 말할 수 있다. 따라서, onContentReady 이벤트 헨들러를 구현해보도록 하자.

이벤트와 이벤트 헨들러 간의 연결은 PUBLIC:ATTACH 엘레먼트 태그를 사용해서 설정한다. 예를 들어서 onContentReady 이벤트의 경우, 이벤트 헨들러의 함수명을 evtContentReady()로 정했다면 다음과 같은 엘레먼트 태그를 작성하게 된다.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    
</PUBLIC:COMPONENT>

그런 다음, 이벤트 헨들러를 구현한 자바스크립트 함수를 작성한다.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        ...
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

뭔가 조금씩 프로그래밍을 하고 있다는 기분이 들기 시작한다. 본격적으로 필요한 로직을 구현해보도록 하자. HTCs에서 추가로 제공해주는 개체 중에 element라는 개체가 있는데, 이 개체는 해당 HTCs가 추가된 HTML 태그 자체에 대한 참조를 리턴해준다. 약간 차이점이 존재하긴 하지만 자바스크립에서 제공되는 this 키워드와 비슷한 개념이라고 볼 수 있으며, 코드를 작성할 때 매우 편리하게 사용할 수 있다. 다음 코드를 살펴보면 element 개체의 역활을 쉽게 이해할 수 있을 것이다.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        element.runtimeStyle.textAlign = "right";
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

단 한 줄의 코드로 구현하고자 했던 세부 사항 2번의 구현이 모두 마무리됐다. 코드가 동작하는 시점이나 그 기능에 있어 일말의 부족함도 존재하지 않는다. 참고로 element 키워드는 생략할 수도 있지만 코드 가독성 등을 이유로 그리 권장되지는 않는다. 나중에 따로 논의하겠지만, 특수한 경우 난해한 오류를 발생시킬 수도 있으므로 조금 귀찮더라도 항상 element 키워드를 입력하는 버릇을 들이도록 하자. 계속해서 세부 사항 3번도 세부 사항 2번에 바로 뒤이어 구현할 수 있을 것이다. 다음의 코드를 살펴보자.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = "0";
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

코드 몇 줄로 모든 세부 구현이 마무리됐다. 특이한 코드는 존재하지 않으며 정규 표현식을 사용해서 비베스크립트의 Trim() 함수 같은 기능을 구현한 trim() 함수가 정도가 눈에 띈다. 대부분의 코드는 일반적인 자바스크립트 구현에서 늘 사용하던 것들 뿐이다. 그러면 지금까지 구현한 결과를 중간 점검 차원에서 한 번 실제로 적용해보도록 하자. 다음 INPUT 태그가 바로 그 결과를 보여주고 있는데, 정상적으로 잘 작동하고 있다는 것을 알 수 있다.

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

그러면, 이번에는 세부 사항 4번을 구현해보도록 하자. 잠시 생각해보면 알겠지만 세부 사항 4번도 별다를 것이 없는 기능이다. 다만 이번에는 onFocus 이벤트가 로직을 구현할 위치로 적절할 것 같다는 판단인데, 결국 앞에서 살펴본 내용을 응용하면 다음과 같은 구현이 가능해진다. 재미있는 것은 작업 내용을 보면 볼수록 GUI 환경만 제공되지 않는다뿐이지 비주얼 베이직 등의 4GL 프로그래밍 언어에서 제공되는 이벤트 중심적인 프로그래밍 방식과 매우 유사하다는 점이다.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    <PUBLIC:ATTACH event="onfocus"        for="element" onevent="evtFocus();"        />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = "0";
    }
    
    function evtFocus()
    {
        element.select();
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

이번에는 5번, 6번, 7번 세부 사항들을 한꺼 번에 구현해보도록 하자. 비록 여러 가지 항목들로 나뉘긴했지만, 결국 이 세부 사항들은 모두 한 가지 공통의 목적을 달성하기 위한 것들로, 이 HTCs의 핵심이라고 말할 수 있는 부분이다. 세부 사항 정의에 따르면 0부터 9까지 숫자 이외의 값은 애초에 키보드가 눌려져도 입력 자체가 되지 않아야 한다. 따라서, 키보드 입력이 발생할 때마다 입력된 키 값을 검사해서 입력값에 따라 적절한 처리를 수행해야만 하는 것이다. 그런데, 한 가지 주의해야 할 부분이 있다. 쉽게 예상할 수 있는 것처럼 키 입력 자체는 onKeydown 등의 이벤트 헨들러를 구현해서 감지하면 된다. 그리고, 입력된 키의 값은 event 개체의 keyCode 프로퍼티 값을 검사하면 알 수 있으므로 아무런 문제가 없는 것처럼 보인다. 그런데, 문제는 난감하게도 한글 입력 모드에서는 키 입력 관련 이벤트가 발생하지 않는다는 점이다. 따라서, 입력된 키의 값을 검사할 수가 없기 때문에 원하지 않는 문자가 입력되는 것을 막을 방법이 없다. 다시 한 번 얘기하지만 이벤트가 발생하고 값이 넘어가지 않는 등의 현상이 일어나는 것이 아니라 아예 이벤트 자체가 발생조차 하지 않는다. 이 문제를 해결하기 위해서 다음과 같은 코드를 onFoucs 이벤트의 이벤트 헨들러에 추가해준다.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    <PUBLIC:ATTACH event="onfocus"        for="element" onevent="evtFocus();"        />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = "0";
    }
    
    function evtFocus()
    {
        element.runtimeStyle.imeMode = "disabled";
        element.select();
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

재미있게도 이 한 줄의 코드는 아예 한글 입력 기능을 제거해버린다. 다시 말하자면 한글 입력을 이벤트가 감지할 수 있도록 기능을 보정해주는 것이 아니라, 거꾸로 한글의 입력 자체를 불가능하게 만들어서 결과적으로는 키보드 입력과 관련된 이벤트 헨들러에서 감지할 수 있는 키 입력만을 허용해서 그 목적을 달성하게 된다. 물론, 이 코드는 해당 HTML 태그만을 대상으로 동작하므로 그 밖의 다른 HTML 태그들에서는 정상적으로 키보드 입력 작업을 처리할 수 있다. IME(Input Method Editor)에 관한 더 자세한 정보들은 여기를 참고하기 바란다. 조금만 살펴보면 몇 가지 재미있는 응용 방법들을 발견할 수 있을 것이다. 이제 한글 문제가 해결되었으므로 그 다음 단계로 넘어가자. 키보드의 입력을 감지하기 위해서는 onKeydown 이벤트의 헨들러를 구현하기로 한다. 사용자가 입력한 키는 event 개체의 keyCode 프로퍼티를 통해서 쉽게 파악이 가능한데, 그 결과에 따라 event 개체의 returnValue 프로터티를 사용해서 사용자가 입력한 값을 받아들일지 아니면 무시할지 결정한다. 다음 코드는 그 구현 결과다.

<PUBLIC:COMPONENT lightWeight="true">

    <PUBLIC:ATTACH event="oncontentready" for="element" onevent="evtContentReady();" />
    <PUBLIC:ATTACH event="onfocus"        for="element" onevent="evtFocus();"        />
    <PUBLIC:ATTACH event="onkeydown"      for="element" onevent="evtKeydown();"      />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = "0";
    }
    
    function evtFocus()
    {
        element.runtimeStyle.imeMode = "disabled";
        element.select();
    }
    
    function evtKeydown()
    {
        var nKey = event.keyCode;
        
        if (event.ctrlKey)
        {
            if (nKey == 65 || nKey == 67 || nKey == 82 || 
                nKey == 86 || nKey == 88 || nKey == 90)
                event.returnValue = true;
            else
                event.returnValue = false;
        }
        else
        {
            if ((nKey >= 48 && nKey <= 57)  || 
                (nKey >= 96 && nKey <= 105) || 
                (nKey >= 33 && nKey <= 40)  ||
                 nKey == 8  || nKey == 9  || nKey == 27  || nKey == 43  || 
                 nKey == 45 || nKey == 46 || nKey == 107 || nKey == 109
               )
                event.returnValue = true;
            else
                event.returnValue = false;
        }
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

단지 if 문이 조금 복잡해서 어려운 것처럼 보일 뿐이고 실제 구현 내용은 매우 간단하다. 굳이 설명을 한다면 입력을 허용하고 싶은 경우에는 event 개체의 returnValue 프로퍼티 값에 true를 설정하고 반대로 허용하고 싶지 않다면 false를 설정하면 그만인 것이다. 그러므로 비슷한 별도의 로직을 구현해야 한다거나 필요에 따라 약간의 수정이 필요한 경우에는 조건문의 일부 키 코드값 부분만 조정하면 된다. 그리고 참고로 한 가지 주의해야 할 것은 keyCode 프로퍼티가 리턴하는 값은 아스키 코드가 아니라 유니코드의 키 코드라는 점이다. 당연히 아스키 코드일 것이라고 짐작하고 코드를 작성하면 낭패를 보는 경우가 발생할 수도 있으므로 이 점에 주의하기 바란다. 대표적인 예로 키보드 상단에 위치한 숫자키와 키보드 우측 키 패드에 위치한 숫자키의 keyCode 프로퍼티 값은 서로 다르다.

다음으로 세부 사항 8번을 구현해보도록 하자. 사용자가 키보드로 입력한 값들은 앞에서 구현한 onKeydown 이벤트의 이벤트 헨들러 수준에서 적절하게 처리된다. 그러나, 지금 구현하고 있는 HTCs에서는 붙여넣기 기능이 허용되므로 원하지 않는 잘못된 값이 입력될 수 있는 여지를 남기고 있다. 따라서, 그에 대비해서 해당 HTML 태그가 포커스를 잃을 때, 즉 onBlur 이벤트가 발생할 때 한 번 더 유효성 검사를 수행해야만 하고, HTCs의 최종 코드는 다음과 같이 구현된다.

<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();"      />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = "0";
    }
    
    function evtFocus()
    {
        element.runtimeStyle.imeMode = "disabled";
        element.select();
    }
    
    function evtKeydown()
    {
        var nKey = event.keyCode;
        
        if (event.ctrlKey)
        {
            if (nKey == 65 || nKey == 67 || nKey == 82 || 
                nKey == 86 || nKey == 88 || nKey == 90)
                event.returnValue = true;
            else
                event.returnValue = false;
        }
        else
        {
            if ((nKey >= 48 && nKey <= 57)  || 
                (nKey >= 96 && nKey <= 105) || 
                (nKey >= 33 && nKey <= 40)  ||
                 nKey == 8  || nKey == 9  || nKey == 27  || nKey == 43  || 
                 nKey == 45 || nKey == 46 || nKey == 107 || nKey == 109
               )
                event.returnValue = true;
            else
                event.returnValue = false;
        }
    }
    
    function evtBlur()
    {
        var objRegEx = /^\d+$/ig;
        
        if (!objRegEx.test(trim(element.value)))
            element.value = "0";
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

정규 표현식으로 대상 HTML 태그가 포커스를 잃을 때 유효성 여부를 다시 한 번 검사하고 유효하지 않으면 값을 0으로 초기화하는 코드인데, 이런 경우 정규 표현식 대신 isNaN() 함수를 사용해도 문제는 없을 것이다. 마지막으로 모든 이벤트 헨들러 코드의 제일 첫 부분에 다음과 같은 코드를 추가하여 세부 사항 1번을 구현하도록 한다. 이 코드는 HTCs가 추가된 대상 HTML 태그가 타입이 TEXT인 INPUT 태그가 아닌 경우, 아무 작업도 실행하지 않고 함수를 빠져나간다. 이는 실수로 엉뚱한 HTML 태그에 해당 HTCs를 추가하는 경우를 대비하기 위한 것이다. 각자 성향에 따라 경고 메세지를 출력하는 등 추가적인 구현을 해도 괜찮을 것이다.

<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();"      />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        if (!checkTagType()) return;
        
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = "0";
    }
    
    function evtFocus()
    {
        if (!checkTagType()) return;
        
        element.runtimeStyle.imeMode = "disabled";
        element.select();
    }
    
    function evtKeydown()
    {
        if (!checkTagType()) return;
        
        var nKey = event.keyCode;
        
        if (event.ctrlKey)
        {
            if (nKey == 65 || nKey == 67 || nKey == 82 || 
                nKey == 86 || nKey == 88 || nKey == 90)
                event.returnValue = true;
            else
                event.returnValue = false;
        }
        else
        {
            if ((nKey >= 48 && nKey <= 57)  || 
                (nKey >= 96 && nKey <= 105) || 
                (nKey >= 33 && nKey <= 40)  ||
                 nKey == 8  || nKey == 9  || nKey == 27  || nKey == 43  || 
                 nKey == 45 || nKey == 46 || nKey == 107 || nKey == 109
               )
                event.returnValue = true;
            else
                event.returnValue = false;
        }
    }
    
    function evtBlur()
    {
        if (!checkTagType()) return;
        
        var objRegEx = /^\d+$/ig;
        
        if (!objRegEx.test(trim(element.value)))
            element.value = "0";
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    function checkTagType()
    {
        if (element.tagName.toUpperCase() == "INPUT" &&
            element.type.toUpperCase()    == "TEXT")
            return true;
                
        return false;
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

이로서 첫 번째 HTCs 코드 구현이 모두 마무리됐다. 그다지 어려운 부분도 없는 것 같고 이 정도면 그럭저럭 실제 프로젝트에서 사용할 수도 있을 것만 같다. 그러나, 과연 여기서 만족해도 되는 것일까? 그 대답은 당연히 '아니다.'이다. 먼저 지금까지 작성한 HTCs가 정상적으로 동작하는지 간단하게 테스트해보고 다시 보다 깊은 주제에 관해서 논의해보도록 하자.

<input type="text" id="HTCs2" style="behavior: url(/Content/Lecture/200507130001/asp_0016_02.htc);">

일단 위의 INPUT 태그 예제를 통해서 본문에서 구현한 HTCs가 문제 없이 동작한다는 점을 확인할 수 있다. 세부 사항 1번부터 8번까지 모든 동작들이 정확하게 구현되었다. 그렇다면 그 외에 뭔가 더 코드를 발전시킬 부분은 없을까? 필자는 다음과 같은 사례를 생각해 보았다. 현재 구현된 대로라면 대상 HTML 태그가 로딩된 직후, 입력값이 존재하지 않으면 값이 0으로 초기화된다. 또는, 붙여넣기로 숫자값이 아닌 문자가 입력되는 경우에도, 즉 유효하지 않은 값이 입력되는 경우에도 포커스를 잃는 시점에 역시 값이 0으로 초기화 된다. 그러나, 이 값이 항상 0이어야 할 필요가 있을까? 가령, 특정 프로젝트에서 같은 HTCs가 요구되는 열 번의 케이스 중에서 아홉 번은 이 값이 0으로 설정되야 하지만 마지막 한 번은 1로 설정되야 한다면, 그리고 단지 그 한 번을 위해서 별도의 HTCs를 다시 작성해야한다면 그것은 매우 심각한 리소스 낭비라는 것이 필자의 생각이다. 그러나, 다행스럽게도 HTCs에서는 이런 문제를 해결할 수 있는 매우 합리적이고 효율적인 방법을 이미 제시해주고 있으며 바로 PUBLIC:PROPERTY 엘레먼트 태그를 통해서 구현을 할 수 있다.

이 엘레먼트 태그는 클래스의 프로퍼티와 정확하게 같은 기능을 제공해준다. 기본값을 설정할 수도 있고 사용자, 즉 HTCs를 사용하는 프로그램 개발자가 임의로 값을 변경할 수도 있다. 심지어는 Put 속성이나 Get 속성을 통한 프로퍼티 함수의 구현까지 제공해준다. 일단 본문에서는 부담을 줄이기 위해 간단한 사용법부터 먼저 살펴보고 다음글에서 보다 깊은 부분까지 논의해보도록 하겠다. 그러면, 기존 코드에 다음과 같이 PUBLIC:PROPERTY 엘레먼트 태그를 추가해보자. 이는 basicValue라는 이름으로 프로퍼티를 생성하고 기본값을 0으로 설정하는 결과를 얻게 해준다.

<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" />
    
    ...

일단 이렇게 프로퍼티가 선언되고 나면, 다른 프로그램 언어에서 제공되는 클래스의 프로퍼티와 동일하게 다음처럼 마치 일반적인 변수인 것처럼 사용이 가능해진다.

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

결과적으로 이 모든 내용이 구현된 HTCs는 다음 코드와 같을 것이다. 그런데, 지금까지 살펴본 내용들만 가지고 생각해본다면 프로퍼티라고 해서 그다지 유용할 것 같지는 않다. 그렇지만, 자고로 프로퍼티라는 개념에는 디자인 타임이나 런타임에 외부에서 그 값을 편집할 수 있다는 의미가 담겨있다는 사실을 잊으면 안된다. 앞에서도 논의한 바와 같이 PUBLIC:PROPERTY 엘레먼트 태그도 역시 마찮가지 기능을 제공해준다.

<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" />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        if (!checkTagType()) return;
        
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = basicValue;
    }
    
    function evtFocus()
    {
        if (!checkTagType()) return;
        
        element.runtimeStyle.imeMode = "disabled";
        element.select();
    }
    
    function evtKeydown()
    {
        if (!checkTagType()) return;
        
        var nKey = event.keyCode;
        
        if (event.ctrlKey)
        {
            if (nKey == 65 || nKey == 67 || nKey == 82 || 
                nKey == 86 || nKey == 88 || nKey == 90)
                event.returnValue = true;
            else
                event.returnValue = false;
        }
        else
        {
            if ((nKey >= 48 && nKey <= 57)  || 
                (nKey >= 96 && nKey <= 105) || 
                (nKey >= 33 && nKey <= 40)  ||
                 nKey == 8  || nKey == 9  || nKey == 27  || nKey == 43  || 
                 nKey == 45 || nKey == 46 || nKey == 107 || nKey == 109
               )
                event.returnValue = true;
            else
                event.returnValue = false;
        }
    }
    
    function evtBlur()
    {
        if (!checkTagType()) return;
        
        var objRegEx = /^\d+$/ig;
        
        if (!objRegEx.test(trim(element.value)))
            element.value = basicValue;
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    function checkTagType()
    {
        if (element.tagName.toUpperCase() == "INPUT" &&
            element.type.toUpperCase()    == "TEXT")
            return true;
                
        return false;
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

다음은 지금까지 구현한 최종 결과를 적용한 INPUT 태그다. 간단하게 몇 가지 테스트를 해보면 쉽게 알 수 있겠지만 지금으로서는 기존 결과와 크게 다른 점이 눈에 띄지 않을 것이다. 매우 당연한 결과인데 HTCs의 구현 내용과는 별개로 그 사용 방법이 지금까지와 다른 점이 없기 때문이다. 현재까지는 구현한 코드 그대로 basicValue 프로퍼티에 기본값으로 설정된 0값이 코드 전반에 걸쳐서 사용되므로 기존 결과와 전혀 다를 바가 없는 것이다.

<input ... style="behavior: url(/Content/Lecture/200507130001/asp_0016_03.htc);">

따라서, 코드를 개선한 결과를 확인하려면 다음과 같이 개선한 기능을 사용하도록 그 사용법을 반영해줘야 한다. 다음은 값이 입력되지 않았거나 유효하지 않은 값이 입력된 경우 0 대신 0000이 기본값으로 설정되도록 지정한 코드다. 실로 간단명료한 코드가 아닐 수 없다. 선언한 프로퍼티 이름을 HTML 태그에서 직접 속성으로 접근하는 것이 가능한 것이다!

<input ... style="behavior: url(/Content/Lecture/200507130001/asp_0016_03.htc);" basicValue="0000">

만약 원한다면, 자바스크립트 코드를 통해서 해당 프로퍼티에 접근하는 것이 가능하다. 다음 자바스크립트 함수는 인자로 전달받은 값을 해당 태그의 basicValue 프로퍼티에 설정하는 함수를 간단하게 구현해본 것이다. 일단 프로퍼티 값이 변경된 다음부터는 모든 동작에서 변경된 값이 반영된다. 그냥 간단하게 생각해서 평소에 COM 컴포넌트 개체를 사용할 때 적용되는 개념과 완벽하게 동일한 개념으로 받아들이면 된다. HTCs에 선언된 프로퍼티가 인터넷 익스플로러에서 제공하는 개체와 완벽하게 결합되어 마치 본래부터 존재하던 프로퍼티인양 동작하는 것이 재미있기만 하다.

→ 클릭하면 basicValue 프로퍼티의 값이 1111로 설정된다.
→ 클릭하면 basicValue 프로퍼티의 값이 9999로 설정된다.

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

<SCRIPT language="javascript" type="text/javascript">
<!--
function setBasicValue(strValue)
{
    document.getElementById("HTCs5").basicValue = strValue;
}
//-->
</SCRIPT>

지금까지 논의한 내용만으로도 충분히 흥미롭지만 조금만 더 깊게 생각을 해보도록 하자. 지금 우리는 프로퍼티에 관해서 논의를 하고 있다. 그렇다면 메서드는? 당연한 얘기겠지만 메서드 구현 기능도 제공된다. 가령, 바로 위에서 샘플로 작성한 setBasicValue() 함수를 생각해보자. 함수 자체는 그다지 특이한 부분이 없지만 문제는 함수가 구현된 위치다. 이 함수는 HTCs 자체와는 별개로 HTML 문서의 내부에 정의되어 있다. 이 얘기는 곧 해당 HTCs가 필요한 모든 HTML 문서마다 이 함수가 반복해서 재정의되야 한다는 것을 뜻한다. 이런 방식으로는 굳이 HTCs를 작성하는 의미가 반감되어 버리게 된다. 클래스 추상화의 관점에서 본다면 이 함수는 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" />
    
    ...

메서드가 선언되었으므로 이제 구현만 하면 된다. 거창하게 구현이라고 해봐야 보통의 자바스크립트 함수를 작성하는 방법과 대동소이하지만 말이다. 아무튼 다음과 같은 HTCs 코드가 완성되는데 본문에서 우리들이 구현하는 HTCs의 최종 버전이다. 아직은 개선해야 할 부분들이 여기저기 상당히 많이 남아 있긴 하지만 일단 이 정도로 구현을 마무리하기로 한다. 다음 코드를 마우스로 선택하고 복사하여 .htc 파일을 생성하면 바로 사용이 가능하므로 직접 테스트해보기 바란다.

<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" />
    
    <SCRIPT language="javascript" type="text/javascript">
    <!--
    
    function evtContentReady()
    {
        if (!checkTagType()) return;
        
        element.runtimeStyle.textAlign = "right";
        
        if (trim(element.value).length == 0)
            element.value = basicValue;
    }
    
    function evtFocus()
    {
        if (!checkTagType()) return;
        
        element.runtimeStyle.imeMode = "disabled";
        element.select();
    }
    
    function evtKeydown()
    {
        if (!checkTagType()) return;
        
        var nKey = event.keyCode;
        
        if (event.ctrlKey)
        {
            if (nKey == 65 || nKey == 67 || nKey == 82 || 
                nKey == 86 || nKey == 88 || nKey == 90)
                event.returnValue = true;
            else
                event.returnValue = false;
        }
        else
        {
            if ((nKey >= 48 && nKey <= 57)  || 
                (nKey >= 96 && nKey <= 105) || 
                (nKey >= 33 && nKey <= 40)  ||
                 nKey == 8  || nKey == 9  || nKey == 27  || nKey == 43  || 
                 nKey == 45 || nKey == 46 || nKey == 107 || nKey == 109
               )
                event.returnValue = true;
            else
                event.returnValue = false;
        }
    }
    
    function evtBlur()
    {
        if (!checkTagType()) return;
        
        var objRegEx = /^\d+$/ig;
        
        if (!objRegEx.test(trim(element.value)))
            element.value = basicValue;
    }
    
    function trim(sourceString)
    {
        return sourceString.replace(/(?:^\s+|\s+$)/ig, "");
    }
    
    function checkTagType()
    {
        if (element.tagName.toUpperCase() == "INPUT" &&
            element.type.toUpperCase()    == "TEXT")
            return true;
                
        return false;
    }
    
    function setBasicValue(newValue)
    {
        element.basicValue = newValue;
    }
    
    //-->
    </SCRIPT>
    
</PUBLIC:COMPONENT>

이제 메서드도 프로퍼티처럼 대상 개체에서 본래 제공하던 메서드인 것처럼 사용할 수 있다. 결론적으로 HTCs로 프로퍼티와 메서드만 적절하게 구현해도 필요한 대부분의 클라이언트 측 업무 로직을 간단하게 처리할 수 있는 것이다.

→ 클릭하면 basicValue 프로퍼티의 값이 1111 로 설정된다.
→ 클릭하면 basicValue 프로퍼티의 값이 9999 로 설정된다.

<input type="text" id="HTCs6" style="behavior: url(/Content/Lecture/200507130001/asp_0016_final.htc);">

<a href="JavaScript:document.getElementById('HTCs6').setBasicValue('1111');" ...
<a href="JavaScript:document.getElementById('HTCs6').setBasicValue('9999');" ...

본문에서는 가장 기본적인 기능을 구현하는 HTCs 작성 방법에 대해서 살펴봤다. 그러나, 앞에서도 계속 얘기했던 것처럼 이것이 HTCs가 제공해주는 기능의 전부는 아니다. 본문에서 살펴본 프로퍼티나 메서드에 관한 세부적인 이슈들도 아직 많이 남아있으며, 아직까지 논의를 시작하지 않은 부분들도 상당히 많다. 가령, 새로운 이벤트를 정의해서 노출한다거나 퍼포먼스를 높이기 위한 자잘한 설정 등이 바로 그것인데 이런 주제들에 관해서는 다음글에서 본격적으로 논의해보도록 하겠다.