Web Tech/jQuery

필수적인 제이쿼리 플러그인 패턴

jaiyah 2016. 9. 28. 18:08

Essential jQuery Plugin Patterns


자바스크립트에서는 일반적인 개발 문제를 해결하기 위해 입증,검증된 좋은 방법인 Design Pattern(설계 패턴)을 사용함으로써 유용한 이익을 얻을 수 있습니다.

공식 jQuery 플러그인 작성가이드와 Widget 도 훌륭한 출발점을 제공하지만 또 다른 플러그인 디자인 패턴을 살펴보도록 하겠습니다.



플러그인의 개발은 지난 몇 년동안 많은 진전을 해 왔습니다. 우리는 더이상 플러그인을 하나의 방식으로 작성하지 않습니다. 

실제로, 플러그인의 특정 패턴은 다른 이슈에 있어서 더 나은 사용성을 제공할 수 있을 것입니다.


일부 개발자들은 jQuery UI widget factory 를 사용할 수 있습니다. 그것은 복잡하고 유연한 UI 구성 요소에 유용할 수도 있으며 다른 일부는 유용하지 않을 수도 있습니다. 

그리고 모듈(모듈 패턴과 유사)과 같이 자신의 플러그인을 구성하거나 AMD(asynchronous module definition) 등의 형식적인 모듈 형식(비동기 모듈정의)을 사용하기도 하며, 다른 일부는 자신의 플러그인 프로토타입 상속의 힘을 이용할 수도 있습니다.


이 포스팅에서 다루는 모든 패턴이 다양한 문제에 대한 모든 해결책을 제공하지는 않습니다.

그러나 현재 hello world 에서 가장 많이 사용하는 패턴을 다뤄보록 하겠습니다.


[참고]

jQuery Plugins/Authoring guide

Ben Alman’s plugin style guide

Remy Sharp’sSigns of a Poorly Written jQuery Plugin.




Design Patterns(설계 패턴)

jQuery 플러그인을 구현하는 방법은 다음과 같이 가장 기본적인 수준에서 정의된 규칙 안에서 작성합니다.


단순히 jQuery 의 $.fn 객체에 새로운 기능 속성을 추가하여 플러그인을 작성할 수 있습니다.

$.fn = $.prototype

JavaScript
$.fn.myPluginName = function() {
    // your plugin logic
};


그리고 다음과 같이 즉시실행함수(익명함수)로 플러그인의 로직을 포장했습니다.  

JavaScript
(function( $ ){
    $.fn.myPluginName = function() {
        // your plugin logic
    };
})( jQuery );


즉시실행 함수의 @param $ 의 사용은 jQuery 를 다른 자바스크립트 라이브러리 사이에서 충돌을 생성하지 않도록 하기 위함이며, 전역 공간의 오염도 줄일수 있는 방법입니다.


플러그인 패턴을 작성하는 다른 방법으로 다양한 사용자정의 플러그인 기능을 정의가 가능하도록 $.extend 를 사용하는 것입니다.


JavaScript
(function( $ ){
    $.extend($.fn, {
        pluginType01 : function(){
            // your plugin logic
        },       
        pluginType02: function(){
            // your plugin logic
        }
    });
})( jQuery );




A Lightweight Start

다음의 패턴은 각각의 플러그인을 개발하거나 간단하고 새로운 것(예:유틸리티 플러그인)을 작성하고자 하는 개발자들에게 이상적인 방법입니다.


모범적 관행

  1. 함수 작성 전에 semi-colon(;) 을 사용합니다.
  2. argument 로  window, document, undefined 를 설정합니다(jQuery 핵심 스타일 가이드라인 준수)
  3. 초기 생성에 관계된 플러그인 생성자 및 요소 할당을 합니다.
  4. 기본값과 옵셥값을 확장합니다.
  5. 여러개의 인스턴스 생성과 같은 문제를 방지하기 위해 생성자 주변을 감싸주도록 합니다.(조건처리)


다음 코드의 주석과 함께 좀 더 자세히 살펴보도록 하겠습니다.


JavaScript
/**
 * --------------------------------------------
 * 연결된 스크립트나 다른 플러그인에서 스코프가
 * 제대로 닫혀져 있지 않을 경우를 대비하여 세미콜론을
 * 함수 호출 전에 작성하면 보다 안전해집니다.
 * --------------------------------------------
 */
;(function ( $, window, document, undefined ) {

    /**
     * --------------------------------------------------------------
     * @param undefined
     * 글로벌 전역 변수인 undefined 사용합니다.
     * 단, ES3 에서는 다른 누군가에 의해서 전역 변수인 undefined 를
     * 변경할 수 있기 때문에 실제 undefined 인지 확신할 수 없습니다.
     * ES5 에서 undefined 는 더이상 값을 변경할 수 없습니다.
     * --------------------------------------------------------------
     */

    /**
     * ----------------------------------------------------------------------
     * @param window, document
     * window, document 는 전역 스코프가 아니라 지역스코프에서 사용하도록 설정
     * 이는 프로세스를 조금 더 빠르게 해결하고 능률적으로 minified 할 수 있다.
     * (특히, 해당 플러그인이 자주 참조될 경우에)
     * ----------------------------------------------------------------------
     */


    // * ------------------------------
    // * defaults 는 한번만 생성합니다.
    var pluginName = 'defaultPluginName',
            defaults = {
                propertyName: "value"
            };

    // * ------------------------------
    // * 실제 플러그인 생성자
    function Plugin( element, options ) {
        this.element = element;

        /**
         * ----------------------------------------------------------------
         * 제이쿼리는 두개 또는 그 이상의 객체들을 첫번째 객체에
         * 저장하여 병합,확장할 수 있는 $.extend 유틸리티를 소유하고 있습니다.
         * 일반적으로 첫번째 객체에 {}, 빈 객체를 설정하여
         * 플러그인 인스턴스에 대한 default option(기본옵션)을
         * 변경시키지 않도록 하기 위함입니다.
         * ----------------------------------------------------------------
         */

        this.options = $.extend( {}, defaults, options);

        this._defaults = defaults;
        this._name = pluginName;

        this.init();
    }

    Plugin.prototype.init = function () {
        // 이곳에 초기화 로직을 작성합니다.
        /**
         * ------------------------------------------------------------------
         * 이곳에 초기화 로직을 작성합니다.
         * 여러분은 이미 인스턴스를 통해 DOM 요소, options 을 사용할 수 있습니다.
         * (예. this.element, this.options)
         * ------------------------------------------------------------------
         */
    };

    /**
     * --------------------------------------------------------------------------
     * 생성자(예. new Plugin()) 주변에 여러개의 인스턴스 생성을 방지하기 위해
     * 가벼운 플러그인 래퍼를 설정합니다.
     * data 메소드를 이용하여 cache 해 두게 됩니다.
     * (한번 생성된 인스턴스는 더이상 같은 인스턴스를 생성하지 않도록 하기 위함입니다.)
     * --------------------------------------------------------------------------
     */
    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if (!$.data(this, 'plugin_' + pluginName)) {
                $.data(this, 'plugin_' + pluginName,
                        new Plugin( this, options ));
            }
        });
    }

})( jQuery, window, document );




'Complete'  Widget Factory

플러그인 제작 가이드는 플러그인을 개발하는데 적합한 소개를 하는데 반하여 규칙적인 기준을 가지고 다루는 일반적인 작업 그리고 모호한 작업을 하기 위한 많은 양의 편리성(편익)을 제공하지는 않습니다.


jQuery UI Widget Factory 에서는 객체 지향 원칙에 따른 복잡한 상태의 플러그인을 구축할 수 있습니다.

또한, 기본적인 플러그인에서 작업하는 반복되는 코드의 양을 줄이고 인스턴스와의 통신을 용이하게 합니다.


네트워크 연결상태를 추적할 수 있는 플러그인들은 그 플러그인 현재 상태를 추적하기 전인 경우는 물론 초기화가 된 이후에도 플러그인의 properties 를 변경할 수 있습니다.


위젯 팩토리의 좋은 점 중 하나는 실제 jQuery 의 컴포넌트에 기반한 다수의 jQuery UI 라이브러리입니다.

이것은 템플릿 구조 이상의 것인 더 많은 가이드라인을 볼 수 있으며 jQuery UI repository 볼 필요가 없다는 것을 의미합니다.



다음의 코드는 jQuery UI boilerplate 에 기반한 패턴입니다.

1. trigger 할 수 있는 이벤트는 물론이고 거의 모든 default methods 를 지원하고 이를 다룬다

2. 플러그인의 '상태관리(stateful)' 를 한다. 즉, 플러그인을 적용한 후 그 효과를 검사,수정하거나 심저어 이전 상태로 되돌릴 수 있다.

3. 사용자 지원 옵션이 자동으로 커스터마이징할 수 있는 기본 옵션과 통합된다.

4. 여러 개의 플러그인 메서드가 하나의 jQuery 메서드와 완벽하게 결합하므로, 원하는 하위 메서드 이름을 문자열로 지정해 호출할 수 있다.

5. 플러그인에 의해 발생하는 사용자 정의 이벤트 핸들러에서 위젯의 객체 데이터에 접근할 수 있다.

JavaScript
/*!
 * jQuery UI Widget-factory plugin boilerplate (for 1.8/9+)
 * Author: @addyosmani
 * Further changes: @peolanha
 * Licensed under the MIT license
 */


;(function ( $, window, document, undefined ) {

    // 사용자가 원하는 네임스페이스에 사용자 위젯을 정의
    // 추가로 작성할 수 있는 파라미터(Parmeters)
    // 예) $.widget( "namespace.widgetname", (optional) - an
    // existing widget prototype to inherit from, an object
    // literal to become the widget's prototype );
    // 즉, 기존의 위젯 프로토타입을 상속받으며,
    // 사용자가 정의하는 객체 리터럴은 위젯의 프로토타입이 된다.

    $.widget( "namespace.widgetname" , {

        // 기본값으로 사용할 옵션값을 정의
        options: {
            someValue: null
        },

        // 위젯 설정정
        // 요소 초기화 및 요소 생성 & 이벤트 바인딩 등등...
        _create: function () {

            // 해당 위젯이 호출되기 위해 _create는 자동으로 한번 실행된다.
            // 이곳에 초기 위젯 설정 코드를 작성한다.
            // this.element를 통해 호출의 대상이 되는 요소에 접근할 수 있다.
            // 위에 정의된 options도 this.options을 통해 접근 가능
        },

        // Destroy an instantiated plugin and clean up
        // modifications the widget has made to the DOM
        // 인스턴스화 플러그인을 제거하고 변경된 위젯을 청소하여 DOM을 구성
        destroy: function () {

            // this.element.removeStuff();
            // For UI 1.8, destroy must be invoked from the
            // base widget
            $.Widget.prototype.destroy.call(this);
            // For UI 1.9, define _destroy instead and don't
            // worry about
            // calling the base widget
        },

        methodB: function ( event ) {
            //_trigger dispatches callbacks the plugin user
            // can subscribe to
            // signature: _trigger( "callbackName" , [eventObject],
            // [uiObject] )
            // eg. this._trigger( "hover", e /*where e.type ==
            // "mouseenter"*/, { hovered: $(e.target)});
            this._trigger('methodA', event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger('dataChanged', event, {
                key: value
            });
        },

        // Respond to any changes the user makes to the
        // option method
        _setOption: function ( key, value ) {
            switch (key) {
                case "someValue":
                    //this.options.someValue = doSomethingWith( value );
                    break;
                default:
                    //this.options[ key ] = value;
                    break;
            }

            // For UI 1.8, _setOption must be manually invoked
            // from the base widget
            $.Widget.prototype._setOption.apply( this, arguments );
            // For UI 1.9 the _super method can be used instead
            // this._super( "_setOption", key, value );
        }
    });

})( jQuery, window, document );





Namespacing And Nested Namespacing

여러분의 코드를 네임스페이스에 작성하는 것은  글로벌 네임 스페이스의 다른 객체와 변수와의 충돌을 피할 수있는 방법입니다. 

이 네임스페이스가 중요한 이유는 페이지의 다른 스크립트가 당신과 같은 변수 또는 플러그인 이름을 사용하는 경우에도 플러그인을 보호할 수 있기 때문입니다. 

글로벌 네임스페이스라는 공간에서 거주하고 있는 시민(그 주민은 한 사람이어야 함으로)처럼, 주민간의 충돌을 막기위해서 사용자는 위와 같은 동일한 문제로 다른 개발자의 스크립트를 방해하지 말아야하는 이유도 있습니다.


자바스크립트는 다른 언어들처럼 내장된 네임스페이스를 지원하지는 않지만  네임스페이스와 같은 유사한 효과를 흉내낼 수는 있습니다. 

여러분은 사용하고자 하는 네임스페이스의 이름으로 최상위 객체를 초기화하고 사용자는 쉽게 동일한 이름을 가진 페이지에 다른 객체의 존재 여부를 확인할 수 있습니다. 

그러한 객체가 존재하지 않는다면, 우리는 그 네임스페이스를 확장하여 플러그인을 작성할 수 있습니다.


객체 (또는, 객체 리터럴) 등 namespace.subnamespace.pluginName 이름등과 같은 중첩된 네임스페이스를 만들 수 있습니다. 

그러나 더 간단히 작성하기 위해, 아래의 보일러플레이트와 같이 네임스페이스를 제공(작성)할 수 있을 것입니다.

JavaScript
/*!
 * jQuery namespaced 'Starter' plugin boilerplate
 * Author: @dougneiner
 * Further changes: @addyosmani
 * Licensed under the MIT license
 */

;(function ( $ ) {
    if (!$.myNamespace) {
        $.myNamespace = {};
    };

    $.myNamespace.myPluginName = function ( el, myFunctionParam, options ) {
        // To avoid scope issues, use 'base' instead of 'this'
        // to reference this class from internal events and functions.
        var base = this;

        // jQuery 객체나 DOM 노드에 접근
        base.$el = $(el);
        base.el = el;

        // DOM 객체에 역으로 플러그인 이름을 참조시킨다.
        base.$el.data( "myNamespace.myPluginName" , base );

        base.init = function () {
            base.myFunctionParam = myFunctionParam;

            base.options = $.extend({},
                    $.myNamespace.myPluginName.defaultOptions, options);

            // 여기에 여러분의 초기화 코드를 설정
        };

        // Sample Function, Uncomment to use
        // base.functionName = function( paramaters ){
        //
        // };
        // Run initializer
        base.init();
    };

    $.myNamespace.myPluginName.defaultOptions = {
        myDefaultValue: ""
    };

    $.fn.mynamespace_myPluginName = function
            ( myFunctionParam, options ) {
        return this.each(function () {
            (new $.myNamespace.myPluginName(this,
                    myFunctionParam, options));
        });
    };

})( jQuery );





Jaehee's WebClub