JavaScript용 Windows 라이브러리(WinJS)를 통한 사용자 지정 컨트롤 제작

JavaScript로 Windows 스토어 앱을 작성한 경험이 있는 개발자라면 대부분 JavaScript용 Windows 라이브러리(WinJS)를 접해 보았을 것입니다. 이 라이브러리에서 제공하는 CSS 스타일, JavaScript 컨트롤 및 유틸리티를 사용하면 Windows 스토어용 UX 지침을 충족하는 앱을 신속하게 제작할 수 있습니다. WinJS에서 제공하는 유틸리티 중에는 앱의 사용자 지정 컨트롤을 만드는 데 유용한 함수 집합이 있습니다.

WinJS에서 제공하는 라이브러리 함수는 단지 하나의 선택 사항일 뿐이며, 개발자는 자신이 원하는 다른 모든 패턴이나 라이브러리를 사용하여 JavaScript 컨트롤을 만들 수 있습니다. WinJS를 사용하여 컨트롤을 만들 때의 가장 큰 이점은 라이브러리의 다른 컨트롤과 일관되게 작동하는 자신만의 컨트롤을 제작할 수 있다는 점입니다. 자신만의 컨트롤을 개발하는 작업의 패턴은 WinJS.UI 네임스페이스의 다른 모든 컨트롤을 만드는 방법과 동일합니다.

이 글은 설정 가능한 옵션, 이벤트 및 공용 메서드에 대한 지원을 통해 자신만의 컨트롤을 제작하는 방법에 대해 설명합니다. 한편, 이와 동일한 주제인 XAML 컨트롤 개발에 관심이 많은 분들을 위해 이에 대한 글도 곧 게재할 예정입니다.

HTML 페이지에 JavaScript 기반 컨트롤 삽입하기

우선 페이지에 WinJS 컨트롤을 삽입하는 방법을 다시 알아보겠습니다. 여기에는 두 가지 방법, 즉 간섭되지 않는 방식으로 JavaScript만 사용하는 명령과 HTML 요소의 추가 특성을 사용하여 자신의 HTML 페이지에 컨트롤 삽입하는 선언이 있습니다. 선언은 도구 상자에서 컨트롤 끌기와 같은 디자인 타임 경험을 제공합니다. 자세한 내용은 WinJS 컨트롤 및 스타일 추가에 대한 MSDN 빠른 시작을 참조하세요.

이번 글에서는 WinJS에서의 선언적 프로세싱 모델의 장점을 취하는 JavaScript 컨트롤의 생성 방법에 대해 설명하겠습니다. 페이지에 선언적으로 컨트롤을 삽입하려면 다음과 같은 일련의 절차를 따르세요.

  1. 컨트롤이 파일로부터 API를 사용할 수 있도록 WinJS 레퍼런스를 HTML 페이지에 삽입합니다.

     <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
    
  2. 상기 레퍼런스의 스크립트 태그 처리 후에 자신의 컨트롤을 포함한 스크립트 파일을 레퍼런스에 삽입합니다.

     <script src="js/hello-world-control.js"></script>
    
  3. 앱의 JavaScript 코드에 WinJS.UI.processAll()을 호출합니다. 이 함수는 HTML을 분석하여 발견한 모든 선언적 컨트롤을 인스턴스화합니다. Visual Studio에서 앱 템플릿을 사용하는 경우 WinJS.UI.processAll()은 default.js 형식으로 호출됩니다.

  4. 페이지에 선언적으로 컨트롤을 삽입하세요.

     <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}"></div>
    

간단한 JavaScript 컨트롤

이제 아주 단순한 컨트롤을 하나 만들어 보겠습니다. Hello World 컨트롤. 이 컨트롤을 정의하는 데 사용되는 JavaScript는 다음과 같습니다. 프로젝트에 다음과 같은 코드의 새 파일을 만들고 이름을 hello-world-control.js라고 붙입니다.

 function HelloWorld(element) {
    if (element) {
        element.textContent = "Hello, World!";
    }
};

WinJS.Utilities.markSupportedForProcessing(HelloWorld);

그런 다음 페이지의 body에 아래의 마크업을 사용하여 컨트롤을 삽입합니다.

 <div data-win-control="HelloWorld"></div>

앱을 실행하면 컨트롤이 로드되어 페이지의 body에 “Hello, World!” 텍스트가 표시되는 것을 확인할 수 있습니다.

WinJS에만 해당되는 단 한 줄의 이 코드는 선언적 프로세싱으로 사용하도록 호환되는 코드를 표시하는 WinJS.Utilities.markSupportedForProcessing으로의 호출입니다. 이것이 바로 WinJS가 페이지에 콘텐츠를 삽입하는데 있어 믿을 만한 코드라고 할 수 있는 이유입니다. 이에 대해 자세한 내용은 WinJS.Utilities.markSupportedForProcessing 함수에 대한 MSDN 설명서를 참조하세요.

컨트롤을 만들 때 WinJS 유틸리티나 다른 라이브러리를 사용하는 이유

앞에서 WinJS를 실제로 사용하지 않고 선언적 컨트롤을 만드는 방법을 설명했습니다. 그럼 이제, WinJS를 사용하지 않고 구현한 아래의 코드 조각을 살펴보겠습니다. 이러한 코드 조각은 이벤트, 설정 가능한 옵션 및 공용 메서드로 구성된 보다 복잡한 컨트롤입니다.

 (function (Contoso) {
    Contoso.UI = Contoso.UI || {};

    Contoso.UI.HelloWorld = function (element, options) {
        this.element = element;
        this.element.winControl = this;

        this.blink = (options && options.blink) ? true : false;
        this._onblink = null;
        this._blinking = 0;

        element.textContent = "Hello, World!";
    };

    var proto = Contoso.UI.HelloWorld.prototype;

    proto.doBlink = function () {
        var customEvent = document.createEvent("Event");
        customEvent.initEvent("blink", false, false);

        if (this.element.style.display === "none") {
            this.element.style.display = "block";
        } else {
            this.element.style.display = "none";
        }

        this.element.dispatchEvent(customEvent);
    };

    proto.addEventListener = function (type, listener, useCapture) {
        this.element.addEventListener(type, listener, useCapture);
    };

    proto.removeEventListener = function (type, listener, useCapture) {
        this.element.removeEventListener(type, listener, useCapture);
    };

    Object.defineProperties(proto, {
        blink: {
            get: function () {
                return this._blink;
            },

            set: function (value) {
                if (this._blinking) {
                    clearInterval(this._blinking);
                    this._blinking = 0;
                }
                this._blink = value;
                if (this._blink) {
                    this._blinking = setInterval(this.doBlink.bind(this), 500);
                }
            },
            enumerable: true,
            configurable: true
        },

        onblink: {
            get: function () {
                return this._onblink;
            },
            set: function (eventHandler) {
                if (this._onblink) {
                    this.removeEventListener("blink", this._onblink);
                    this._onblink = null;
                }
                this._onblink = eventHandler;
                this.addEventListener("blink", this._onblink);
            }
        }
    });

    WinJS.Utilities.markSupportedForProcessing(Contoso.UI.HelloWorld);
})(window.Contoso = window.Contoso || {}); 

많은 개발자들이 이와 같은 방식(익명 함수, 생성자 함수, 속성, 사용자 지정 이벤트 사용)으로 컨트롤을 만들고 있습니다. 이 방식이 편하다면 그렇게 하셔도 됩니다. 하지만 이러한 코드가 다소 혼란스럽게 느껴지는 분들도 많이 있을 것입니다. 상당수의 웹 개발자들이 테크닉이 가미된 코드에 익숙하지 않기 때문입니다. 하지만 라이브러리는 이러한 코드를 작성할 때 발생하는 혼란을 없애주므로 이에 비해 훨씬 편리합니다.

또한 WinJS와 기타 라이브러리는 가독성을 높여주는 것은 물론, 많은 미묘한 문제까지 관리해 주기 때문에 개발자는 이러한 문제(프로토타입, 속성, 사용자 지정 이벤트를 효율적으로 사용)를 신경 쓸 필요가 없습니다. 또한 메모리 사용량을 최적화하고 개발자의 일반적인 실수를 방지해 줍니다. WinJS는 하나의 예일 뿐, 선택은 개발자의 몫입니다. 라이브러리가 어떻게 도움이 되는가에 대한 확실한 예로, 이 글을 모두 읽은 다음 이 단원의 코드를 다시 살펴보고, 이 글의 마지막 부분에서 WinJS 유틸리티를 사용해 구현한 동일한 컨트롤을 이전에 구현한 컨트롤과 비교해 보시기 바랍니다.

WinJS JavaScript 컨트롤의 기본 패턴

다음은 WinJS를 사용하여 JavaScript 컨트롤을 만들 때 가장 단순하면서도 모범 사례로 꼽히는 패턴입니다.

 (function () {
    "use strict";

    var controlClass = WinJS.Class.define(
            function Control_ctor(element) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                this.element.textContent = "Hello, World!"
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });
})();

또한 다음과 같이 페이지에 컨트롤을 선언적으로 삽입할 수 있습니다.

 <div data-win-control="Contoso.UI.HelloWorld"></div>

어떤 면에서 특히 WinJS를 처음 접하는 경우 이 방법이 익숙하지 않을 수도 있으니 차근차근 알아보도록 하겠습니다.

  1. 이 예제에서는 코드를 즉시 실행되는 함수로 알려진 JavaScript의 일반 패턴으로 래핑합니다.

     (function () {
    …
    })();
    

    이 예제는 코드가 자체 포함되도록 하고 모든 의도되지 않은 변수/전역 대입을 지나쳐 버리지 않도록 작성된 것입니다. 이 예제는 일반적인 모범 사례이며, 원본 소스에 취하고자 했던 변경 사항이라는 점에서 주목할 만한 가치가 있습니다.

  2. ECMAScript 5 고급 모드는 함수의 시작 부분에서 "use strict" 문을 사용함으로써 활성화됩니다. 이 방법은 Windows 스토어 앱 템플릿 전체에 걸쳐 오류 검사 및 JavaScript 차기 버전과의 호환성을 개선하는 모범 사례로 활용됩니다. 다시 말해, 이는 일반적인 모범 사례이며 원본 소스에 취하고자 하는 바로 그 예이기도 합니다.

  3. 이제 WinJS에 특화된 몇 가지 코드에 대해 알아보겠습니다. WinJS.Class.define() 은 무엇보다도 markSupportedForProcessing()으로의 호출을 처리하고 또한 컨트롤 속성의 향후 생성을 용이하게 하는 컨트롤에 대한 클래스를 생성하기 위해 호출됩니다. 이 코드는 표준 Object.defineProperties 함수 주변에서 실제로 간단하게 사용될 수 있는 도우미입니다.

  4. Control_ctor로 명명된 생성자가 정의됩니다. WinJS.UI.processAll()이 default.js로부터 호출될 때 이 코드는 페이지에서 data-win 컨트롤 특성을 사용하여 레퍼런스되는 모든 컨트롤에 대한 마크업을 검색하여 원하는 컨트롤을 찾아내고 이 생성자를 호출합니다.

  5. 생성자 내에 페이지의 요소에 대한 레퍼런스는 컨트롤 개체와 함께 저장되고 이 개체에 대한 레퍼런스는 이 요소와 함께 저장됩니다.

    • 요소 || document.createElement("div") 조각이 무엇에 대한 것인지 불확실한 경우 이 코드는 명령 모델을 지원하기 위해 사용됩니다. 사용자는 이 코드를 이용해 나중에 페이지의 요소에 컨트롤을 연결할 수 있습니다.
    • 이는 페이지의 요소에 레퍼런스를 유지하는 것은 물론, element.winControl 설정을 통해 컨트롤 개체에 이 요소의 레퍼런스를 유지하기에도 좋은 방법입니다. 이벤트와 같은 기능에 추가할 경우 일부 라이브러리 함수가 바로 작동하도록 할 수 있습니다. 순환 개체/DOM 요소 레퍼런스로 인해 발생할 수 있는 메모리 누설에 대해서는 걱정을 놓으셔도 됩니다. Internet Explorer 10이 이 현상을 방지해 주기 때문입니다.
    • 생성자가 컨트롤의 텍스트 콘텐츠를 수정하여 화면에 “Hello, World!” 텍스트가 표시되도록 설정합니다.
  6. 마지막으로, 컨트롤 클래스를 게시하고 앱의 모든 코드가 액세스할 수 있도록 컨트롤을 공개적으로 노출시키기 위해 WinJS.Namespace.define() 이 사용됩니다. 이 요소가 없었다면 현재 작업 중인 인라인 함수의 외부에서 코드에 전역 네임스페이스를 사용하여 컨트롤을 노출시키는 다른 해결책을 찾아야만 했을 것입니다.

컨트롤 옵션 정의하기

좀 더 흥미로운 예를 들기 위해 컨트롤에 설정 가능한 옵션에 대한 지원을 추가해 보겠습니다. 여기서 사용자가 콘텐츠를 깜박이게 할 수 있는 옵션을 추가해 보도록 하죠.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

이번에는 페이지에 컨트롤을 삽입할 때 data-win 옵션 특성을 사용하여 깜박임 옵션을 설정할 수 있습니다.

 <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}">
</div>

옵션을 위한 지원을 추가하기 위해 코드에 다음과 같은 변경 사항을 적용했습니다.

  1. 옵션은 생성자 함수의 매개 변수(옵션이라 명명)를 통해 컨트롤에 전달됩니다.
  2. 클래스의 개별 속성을 사용해 기본 설정이 구성됩니다.
  3. WinJS.UI.setOptions()가 호출되고, 컨트롤 개체에 전달됩니다. 이 호출은 컨트롤의 설정 가능한 옵션에 대해 기본 값을 오버라이드합니다.
  4. 새 옵션에 대해 공용 속성(blink로 명명)이 추가됩니다.
  5. 화면상의 텍스트를 깜박이게 하는 기능을 추가했습니다. (실제로 CSS 클래스를 토글링하는 것보다는 이처럼 스타일을 하드코딩하는 것이 더 좋습니다.)

이 예제에서 가장 어려운 부분은 WinJS.UI.setOptions()로의 호출입니다. 유틸리티 함수인 setOptions는 옵션 개체의 각 필드를 순환하며 setOptions에 대한 최초 매개 변수인 대상 개체의 동일 이름의 필드에 그 값을 할당합니다.

이 예제에서는 'blink' 필드에 대해 true 값을 전달하는 win-control에 대한 data-win 옵션 인수를 통해 옵션 개체를 구성합니다. 생성자 함수에서 setOptions()로의 호출은 이후 'blink'라 명명된 필드를 찾게 되며, 그 값을 컨트롤 개체에서의 동일한 이름의 필드에 복사합니다. 이와 같이 blink라 명명된 속성을 정의했으며, 이 속성은 setter 함수를 제공합니다. setter 함수는 setOptions()에 의해 호출되며 컨트롤의 _blink 멤버를 설정합니다.

이벤트에 대한 지원 추가하기

이번에 구현한 oh-so-useful blink 옵션을 가지고 필요할 때마다 깜박임 기능을 사용할 수 있도록 이벤트 지원을 추가해 보겠습니다.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

앞에서와 같이 페이지에 컨트롤을 삽입합니다. 나중에 요소를 검색할 수 있도록 이 요소에 ID를 추가했습니다.

 <div id="hello-world-with-events"
    data-win-control="Contoso.UI.HelloWorld"
    data-win-options="{blink: true}"></div>

이러한 변경을 통해 이제 'blink' 이벤트를 수신할 수 있는 이벤트 수신기를 연결할 수 있습니다. (참고: 이 예제에서는 document.getElementById를 $로 이름 붙였습니다.)

 $("hello-world-with-events").addEventListener("blink",
        function (event) {
            console.log("blinked element this many times: " + event.count);
        });

이 코드를 실행하면 Visual Studio의 JS Console에 매 500밀리초 마다 메시지가 출력됩니다.

이러한 동작을 지원하기 위해 세 가지 변경 사항이 컨트롤에 적용되었습니다.

  1. WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.Utilities.createEventProperties('blink'))로 호출됩니다. 이로써 사용자가 프로그래밍 방식으로 설정할 수도 있고 또는 HTML 페이지에서 선언적으로 바인딩할 수도 있는 'onblink' 속성이 생성됩니다.
  2. WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.UI.DOMEventMixin) 로의 호출은 컨트롤에 addEventListener, removeEventListener 및 dispatchEvent 함수를 추가합니다.
  3. 깜박임 이벤트는 that.dispatchEvent의 호출로 개시되며("blink", {element: that.element}), 요소 필드와 함께 사용자 지정 개체가 생성됩니다.
  4. 이벤트 처리기가 blink 이벤트용 수신기에 연결됩니다. 이에 대응하여 사용자 지정 이벤트 개체의 요소 필드를 액세스합니다.

여기서 컨트롤의 생성자에 this.element를 설정한 경우 dispatchEvent()only로의 호출이 작동한다는 점에 주목하세요. 이벤트 mix-in의 내부에서는 DOM의 요소에 액세스할 것을 요구합니다. 앞서 언급했던 사례 중 하나는 컨트롤 개체에 요소 멤버가 요구된다는 것입니다. 이로써 이벤트가 DOM Level 3 이벤트 패턴의 페이지에서 상위 요소를 포함할 수 있습니다.

공용 메서드 노출하기

컨트롤의 마지막 변경 사항으로, 깜박임 기능이 언제든지 실행되도록 호출할 수 있는 공용 doBlink() 함수를 추가합니다.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this.doBlink.bind(this), 500);
                        }
                    }
                },

                doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

이는 단지 형식적인 변경이며, _doBlink 함수의 이름을 doBlink로 바꿀 수 있습니다.

JavaScript를 통해 doBlink() 함수를 호출하려면 컨트롤에 대한 개체에 레퍼런스가 필요합니다. 컨트롤을 명령적으로 생성한 경우 이미 레퍼런스가 있을 것입니다. 선언적 프로세싱을 사용하는 경우 컨트롤에 대한 HTML 요소의 winControl 속성을 사용하여 컨트롤 개체를 액세스할 수 있습니다. 예를 들어, 이전과 동일한 마크업이라 가정할 경우 다음을 통해 컨트롤 개체에 액세스할 수 있습니다.

$("hello-world-with-events").winControl.doBlink();

결론

지금까지 다음과 같은 사항들을 구현하기 위해 필요한 컨트롤의 가장 일반적인 측면에 대해 알아보았습니다.

  1. 페이지에 컨트롤 가져오기
  2. 구성 옵션에 전달하기
  3. 이벤트를 개시하고 응답하기
  4. 공용 메서드를 통해 기능 노출하기

JavaScript로 간단한 사용자 지정 컨트롤을 만들 때 본 자습서가 많은 도움이 되기를 바랍니다. 컨트롤 제작 관련 질문이 있는 경우 Windows 개발자 센터의 포럼을 통해 문의하세요. 아울러 XAML 개발자를 위해 XAML 컨트롤 개발에 대한 동일한 주제의 글이 곧 게재될 예정이니 여러분의 많은 관심을 바랍니다.

Microsoft Visual Studio 프로그램 매니저, Jordan Matthiesen