placeholder.js 소스 분석 스터디!

283줄이니까 화이팅하게 봐보도록 하겠습니다!

스터디 개념으로 한 번 쭉 훑어본 것이므로,,,, 정확성+전체파악에는 무리가 있습니다.

 

placeholder.js의 git주소입니다. -> https://github.com/mathiasbynens/jquery-placeholder

 

mathiasbynens/jquery-placeholder

A jQuery plugin that enables HTML5 placeholder behavior for browsers that aren’t trying hard enough yet - mathiasbynens/jquery-placeholder

github.com

w3.schools이나 mdn에서 설명을 잘~ 해주고 있기는 한데,

간단히 말해서 placeholder는 폼 요소에서 무엇을 작성해야 하는지 안내해주는 요소입니다.

 

예를 들어보겠습니다.

아래 캡쳐본은 네이버 로그인 화면입니다.

 

만약, 아래의 '아이디'와 같이 회색 글이 없다면 우리는 저 input이 무엇인지 알 길이 없습니다.

아래의 예시에는 두개의 칸만 있어서 추측이 가능하겠지만

만약 회원가입 폼 같이 조금만 복잡해져도 추측은 불 가능합니다.

그래서 사용하는 것이 placeholder입니다.

 

placeholder를 사용하는 것은 아주아주 쉽습니다.

HTML5가 지원되는 브라우저라면 말이죠.

<input type="text" placeholder="아이디">

이렇게 속성만 넣어주면, 위의 예시와 같이 '아이디'가 표현됩니다.

 

그런데,,,, 왜 라이브러리를 사용하냐구요?

 

바로, HTML5가 지원안되는 브라우저를 지원해주기 위해서 입니다.

우리나라는 ie 사용자가 많고, 또한 하위브라우저를 사용하는 사용자들이 있습니다.... OTL..

IE10부터 지원하고 있기 때문에, 사용자들을 위해서는 방법이 필요합니다. 

그래서 개발자 칭구들은 본인이 custom해서 사용하거나

라이브러리를 사용하여 하위 사용자들에게 제공해주고 있습니다.

 


git에서 소스를 다운받아서 확인해보았습니다. 

 

샘플 상황 입니다.

1. 아이디, 비밀번호의 input만 존재(type이 text, password)

2. $(input).placeholder(); 로 호출

(function(factory) {    
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof module === 'object' && module.exports) {
        factory(require('jquery'));
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function($) {

위의 코드에서 function이 파라미터로 받는 factory는 하위 function 입니다.  

(function(factory){}( 이곳에 있는 것을 factory로 넘겨주기 때문에! ));

 

typeof define 에서 define과 , module은 둘다 콘솔 찍어봤을 때 is not defined가 되므로 else가 실행됩니다.

else는 factory(jQuery)를 넘겨주므로 저 로직은 어떤 상황에서 jquery를 factory 함수에 넘겨주기 위하여 있는 것 같습니다.

 

}(function($) { 

function($) 함수 스코프 내부에서 $를 console.log 찍으면 return new.n.fn.init(a,b)가 찍히는데, 이건 무엇인지 아직 감이 안옵니다. 아래로 내려가겠습니다.

/****
* Allows plugin behavior simulation in modern browsers for easier debugging.
* When setting to true, use attribute "placeholder-x" rather than the usual "placeholder" in your inputs/textareas
* i.e. <input type="text" placeholder-x="my placeholder text" />
*/
var debugMode = false;

// Opera Mini v7 doesn't support placeholder although its DOM seems to indicate so
var isOperaMini = Object.prototype.toString.call(window.operamini) === '[object OperaMini]';
var isInputSupported = 'placeholder' in document.createElement('input') && !isOperaMini && !debugMode;
var isTextareaSupported = 'placeholder' in document.createElement('textarea') && !isOperaMini && !debugMode;
var valHooks = $.valHooks;
var propHooks = $.propHooks;
var hooks;
var placeholder;
var settings = {};

저는 in 연산자를 처음봤습니다.

 

in 연산자

[속성(속성 이름이나 배열의 인덱스를 넣어야 함) in 객체명] 으로 사용하고 명시된 속성이 객체에 존재하면 true를 반환한다고 합니다.

 

mdn에서 예제를 가져와봤는데, 

var trees = new Array('red', 'blue');
0 in trees // true
1 in trees // true
2 in trees // false = 인덱스는 1까지 있다
'red' in trees // false = 배열의 키 값은 포함안된다
'length' in trees // true = length는 array 객체의 속성이어서 해당된다.

출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/in

따라서

isInputSupported, isTextareaSupported의 값은 placeholder가 createElement한 input에 placeholder라는 속성이 존재하면 true입니다.

따라서 placeholder를 지원하는 브라우저일 경우 true가 됩니다.

if (isInputSupported && isTextareaSupported) {
    placeholder = $.fn.placeholder = function() {
        return this;
    };
    placeholder.input = true;
    placeholder.textarea = true;
} else {

placeholder를 지원하는 브라우저 일 경우 isInputSupported, isTextareaSupported값이 둘 다(&&) true이므로 if 문을 타게 됩니다. 

if 문에서 placholder, $.fn.placeholder에 this를 리턴하게 되는데 여기서 this는 input, textarea 객체입니다.

그리고 위에서 placeholder; 로 변수 선언만하고 아무것도 담지 않았던 변수에 .input과 .textaread의 값을 true로 각 각 넣어줍니다.

지원하는 브라우저에 사용되는 로직인 것 같습니다.

 

만약 지원하지 않는 브라우저일 경우에는 else 로직이 실행됩니다.

 else {
  placeholder = $.fn.placeholder = function(options) {
    var defaults = {customClass: 'placeholder'};
    settings = $.extend({}, defaults, options);

    return this.filter((isInputSupported ? 'textarea' : ':input') + '[' + (debugMode ? 'placeholder-x' : 'placeholder') + ']')
    .not('.'+settings.customClass)
    .not(':radio, :checkbox, [type=hidden]')
    .bind({
      'focus.placeholder': clearPlaceholder,
      'blur.placeholder': setPlaceholder
    })
    .data('placeholder-enabled', true)
    .trigger('blur.placeholder');
};

 

메서드 체이닝이 많이 되어 있습니다.;

 

else에서 처음으로 placeholder = $.fn.placeholder = 함수 로직이 있습니다.

if일 때도 placeholder 로직이 있었는데, 최종적으로는 같이 쓰나봅니다.

 

placeholder = 가 if 문에서는 단순하게 this를 반환했다면

else에서는 일단 파라미터로 options값을 받습니다.

 

options 값은 사용자가 $('input').placeholder를 호출할때, 파라미터로 넘겨주는 값이고 없을 경우 undefined가 된다.

$('#placeholderJs input').placeholder({ customClass: 'my-placeholder' });

위와 같이 placeholder.js를 호출 할 경우 options의 값은 [object object]이며

key, value 값이 콘솔에 찍히는 것을 확인할 수 있다. ( customClass, my-placeholder )

 

렌더링은 아래와 같이 됩니다.

customClass가 class로 들어간 것이 확인됩니다.

 

다시 코드로 돌아오면 options 값으로 넘긴 값은 아래의 defaults와 extend 되는 것을 볼 수 있습니다.

var defaults = {customClass: 'placeholder'};
settings = $.extend({}, defaults, options);

만약 사용자가 options 값을 넘겨주지 않으면 defaults인 customClass : 'placeholder' 값이 해당 input의 class로 삽입되게 됩니다.

 

.filter()메서드는 주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환합니다.

return this.filter((isInputSupported ? 'textarea' : ':input') + '[' + (debugMode ? 'placeholder-x' : 'placeholder') + ']')
.not('.'+settings.customClass)
.not(':radio, :checkbox, [type=hidden]')
.bind({
    'focus.placeholder': clearPlaceholder,
    'blur.placeholder': setPlaceholder
})
.data('placeholder-enabled', true)
.trigger('blur.placeholder');

debugmode는 false가 나오게 됩니다. 아마도 최초 선언을 false로 했으니까.?

debug할 때 사용하는 모드인 것 같습니다만, 잘 모르겠습니다;

 

지원안하는 브라우저 일 경우 isInputSupported가 false이니 this.filter ( ':input [placeholder]) 형태가 된다.

즉, input에 placeholder가 있는 obj?

 

메서드 체이닝을 이어가겠습니다.

.not() 에 들어가는 settings.customClass는 placeholder이고

.not('.placeholder') 가 포함되지 않고, 뒤의 .not은 radio나 checkbox, type=hidden 일 경우도 포함하지 않습니다.

 

그 이후 focus blur에 바인딩 해준다.

.bind({
    'focus.placeholder': clearPlaceholder,
    'blur.placeholder': setPlaceholder
})

focus.placeholder란 이벤트에 clearPlaceholder를 바인딩해주고.....

저거 하면 함수들 호출이 되는 건 알겠는데 ,이벤트는 언제 실행되는지 모르겠습니다.

 

일단 진행해보면 알게 될수도 있습니다.

코드를 보면 focus될때는 clearPlaceholder가 호출되고

blur 되면 setPlaceholder 되는 것 같으니 함수를 따라가보겠습니다.

 

먼저 clearPlaceholder는 아래와 같습니다.

function clearPlaceholder(event, value) {
  var input = this;
  var $input = $(this);

if (input.value === $input.attr((debugMode ? 'placeholder-x' : 'placeholder')) && $input.hasClass(settings.customClass)) {
  input.value = '';
  $input.removeClass(settings.customClass);

  if ($input.data('placeholder-password')) {
    $input = $input.hide().nextAll('input[type="password"]:first').show().attr('id', $input.removeAttr('id').data('placeholder-id'));
    // If `clearPlaceholder` was called from `$.valHooks.input.set`

    if (event === true) {
      $input[0].value = value;
      return value;
    }

    $input.focus();

    } else {
        input == safeActiveElement() && input.select();
    }
  }	
}

 

event, value값이 파라미터로 받는데

일단 focus 할 때 event는 아래와 같습니다,...

value는 undefined가 뜹니다. 왜죠? 일단 넘어가겠습니다.

var $input값은 해당 focus된 input이 됩니다.

 

if (input.value === $input.attr((debugMode ? 'placeholder-x' : 'placeholder')) && $input.hasClass(settings.customClass)) {

input.value가 placeholder 값과 같고 (아이디라는 placeholder를 넣었을때, input.value도 아이디가 나오게 됩니다)

input의 클래스에 customClass가 있으면(내가 셋팅했거나 최초의 위에서 선언된 값!) if가 실행됩니다.

 

근데 clearPlaceholder 함수에서 if 아니면 뭐 실행되는게 없습니다... placeholder-x를 잘 모르겠군요.

 

그 후에

input.value = '';

$input.removeClass(settings.customClass);

input.value를 비워주고 input의 class를 제거해줍니다.

 

포커스 전 렌더링

 

 

포커스 후 렌더링

 

보면 class가 add됐다가 remove되는게 보입니다.

value값도 추가됐다가 지워집니다.

 

if ($input.data('placeholder-password')) {

    $input = $input.hide().nextAll('input[type="password"]:first').show().attr('id', $input.removeAttr('id').data('placeholder-id'));
    // If `clearPlaceholder` was called from `$.valHooks.input.set`

    if (event === true) {
        $input[0].value = value;
        return value;
    }

    $input.focus();

} else {
    input == safeActiveElement() && input.select();
}

또 if, else의 기로에 놓이게 되는데, if를 먼저 보면 data로 placeholder-password가 있는지 분기합니다.

이것도 콘솔로 찍어보게 되면, text 타입이랑, password 타입이 다른 data를 가지고 있음을 알게됩니다.

 

text 타입

 

password 타입

 

그렇습니다, 이 if는 password 타입이 있을 때 분기하여 다른 로직을 실행하는데 사용합니다.

 

$input = $input.hide().nextAll('input[type="password"]:first').show().attr('id', $input.removeAttr('id').data('placeholder-id'));
// If `clearPlaceholder` was called from `$.valHooks.input.set`

if (event === true) {
    $input[0].value = value;
    return value;
}

$input.focus();

그 다음 $input에 현재 input을 숨기고,,,

nextAll()라는 jQuery API메소드를 사용합니다.

 

nextAll()은 해당 엘리먼트 이후에 오는 모든 엘리먼트라고 합니다.

예를 들어 아래와 같은 상황일 때, $('.first').nextAll()이면 2,3,4의 p가 됩니다.

<p class="first">1</p>
<p>2</p>
<p>3</p>
<p>4</p>

자 이어서 

$input = $input.hide().nextAll('input[type="password"]:first').show().attr('id', $input.removeAttr('id').data('placeholder-id'));

$input을 hide하고 다음에 오는 input password를 show 해줍니다.

렌더링된 객체를 확인해보면

왜 input이 두개가 되었을까요?

그렇습니다. 하나의 input을 만들었지만(type=password)

password타입 input이 특성 상 *****로 표시되기 때문에 text input이 필요했던 것입니다.

 

그래서 확인해 보면 $input은 새로 생긴 text 타입인 input이고, clearPlaceholder를 하게되면 새로 생긴 text input을 hide합니다.

nextAll()을 사용해서로 내가 html 마크업할 때 넣었던 password input을 show해주면서요.

(이것으로 보아 placeholder 생성 시에 반대로 해줄 것으로 보입니다.)

 

그 후 attr()로 id에 data로 저장되어 있던 placeholder-id 값을 넣어줍니다.(없으면 빈 값 인듯 싶네요)

 

if (event === true) {
    $input[0].value = value;
    return value;
}

$input.focus();

password 타입이 아닐 때의 경우 else를 봐보자
} else {
    input == safeActiveElement() && input.select();
}

로직을 내려가보면 if 문을 다시 만나는데, 여기서 event가 true일 때 실행이 됩니다.

event가 를 콘솔에 찍어보면 [object object]가 나옵니다. 

boolean 값이 아니군요,, 그러므로 if문은 실행이 안됩니다. (내가 테스트하는 시점)

 

그럼 넘어가서 input에 focus() 이벤트가 실행됩니다.

 

콘솔 결과입니다.

input은 해당 input 객체,

safeActiveElement도 해당 input 객체.

input.select()는 undefinde.

 

input과 safeActiveElement가 같습니다.

safeActiveElement()를 살펴보도록 하겠습니다.

 

function safeActiveElement() {
    // Avoid IE9 `document.activeElement` of death

    try {
        return document.activeElement;
    } catch (exception) {}
}

짧아서 좋습니다.

이건 찾아보니까 ie9에서 bug가 있는데, 이거 해결하려고 만든 소스인 것 같습니다.

<iframe>에서 activeElement하면 에러가 발생한다고 하는데, 테스트해보지 않아서 잘 모르겠습니다.

 

자 focus의 반대를 알아보도록 하겠습니다.

focusout되었을때,,

 

'blur.placeholder': setPlaceholder 

blue될 때, setPlaceholder()가 실행됩니다.

 

function setPlaceholder(event) {
    var $replacement;
    var input = this;
    var $input = $(this);
    var id = input.id;

event 파라미터를 받는데, 해당하는 input들이며

 input.id는 input이 나오게 됩니다.

 

// If the placeholder is activated, triggering blur event (`$input.trigger('blur')`) should do nothing.

if (event && event.type === 'blur' && $input.hasClass(settings.customClass)) {
    return;
}

event가 blur이며 customClass가 있기 때문에 if() 문을 타게되는데, if문은 return 시키는 용도로 사용됩니다.

 

if (input.value === '') {

    if (input.type === 'password') {

        if (!$input.data('placeholder-textinput')) {
            try {
                $replacement = $input.clone().prop({ 'type': 'text' });
            } catch(e) {
                $replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
            }

            $replacement
            .removeAttr('name')
            .data({
                'placeholder-enabled': true,
                'placeholder-password': $input,
                'placeholder-id': id
            })
            .bind('focus.placeholder', clearPlaceholder);

            $input
            .data({
                'placeholder-textinput': $replacement,
                'placeholder-id': id
            })
            .before($replacement);
        }

        input.value = '';
    $input = $input.removeAttr('id').hide().prevAll('input[type="text"]:first').attr('id', $input.data('placeholder-id')).show();
}

input에 value가 없으면 if()문이 실행됩니다. 

password일 경우 input.type === password if() 문이 실행됩니다.

 

password input에 focus하면 !$input.data('placeholder-texinput')은 flase가 됩니다.

그래서 실행이 안되는데, 그래도 코드를 보면 대충 data를 설정하는 부분? 인 것 같습니다.

 

여튼 실행이 안되니 else를 봐보도록 하겠습니다.

else {
    var $passwordInput = $input.data('placeholder-password');

    if ($passwordInput) {
        $passwordInput[0].value = '';
        $input.attr('id', $input.data('placeholder-id')).show().nextAll('input[type="password"]:last').hide().removeAttr('id');
    }
}

$input.addClass(settings.customClass);
$input[0].value = $input.attr((debugMode ? 'placeholder-x' : 'placeholder'));

passwordInput에 undefined가 되고.... 아래 if문은 실행되지 않습니다.

 

passwordinput이 아닐 경우

else {
    $input.removeClass(settings.customClass);
}

초 심플합니다, 그냥 class를 지워줍니다.

(클래스로 해당 js 실행하기 때문에 class가 없으면 실행이 안되겠죠..????)

 

자 다시 쭉쭉 위로 올라와서 보면

return this.filter((isInputSupported ? 'textarea' : ':input') + '[' + (debugMode ? 'placeholder-x' : 'placeholder') + ']')
    .not('.'+settings.customClass)
    .not(':radio, :checkbox, [type=hidden]')
    .bind({
        'focus.placeholder': clearPlaceholder,
        'blur.placeholder': setPlaceholder
    })
    .data('placeholder-enabled', true)
    .trigger('blur.placeholder');

.data로 placeholder-enabled를 true로 설정해줍니다, 그 후 trigger를 통해 blur.placeholder 실행합니다.

그렇다는건 setPlaceholder가 실행됩니다.

 

placeholder.input = isInputSupported;
placeholder.textarea = isTextareaSupported;

isInputSupported, isTextareaSupported를 각각 넣어주는데,

위에서 각각 false였으니 false가 들어가게됩니다.

 

hook부분을 보면, 일단 hook은 hooks 하단에서 사용이 되는데?

if (!isInputSupported) {
    valHooks.input = hooks;
    propHooks.value = hooks;
}

if (!isTextareaSupported) {
    valHooks.textarea = hooks;
    propHooks.value = hooks;
}

둘 다 false였으니까 !(not)를 만나서 true가 되면서 if문이 실행됩니다.

각각에 hooks을 담아주는데, 나중에 hooks 실행부분일 때 hooks 에 대해 알아보도록 하겠습니다.(지금 샘플에서는 없)

 

hooks 넘어가면 func가 보이고...

$(function() {

    // Look for forms
    $(document).delegate('form', 'submit.placeholder', function() {
        // Clear the placeholder values so they don't get submitted
        var $inputs = $('.'+settings.customClass, this).each(function() {
            clearPlaceholder.call(this, true, '');
        });

        setTimeout(function() {
            $inputs.each(setPlaceholder);
        }, 10);
    });
    
});

여기서 delegate() 메소드를 알아보겠습니다...

 

delegate()

저는 on에 익숙한데,,,

하위브라우저를 지원하니까 이런것을 사용하는 것 같습니다.

복제된 이벤트를 bind할 때 사용한다고 합니다.

 

이 시절 bind는 복제된 이벤트를 받을 수 없기 때문에 delegate를 썼다고 합니다.

this는 이벤트 발생시킨 항목.

 

form에 submit.placeholder 이벤트를 바인딩하였습니다.

$inputs에 해당하는 클래스의 this를 each 시켜서 submit할 때 input들 하나하나 clearPlaceholder해줍니다.

메소드로 this, true, '' 를 넘기는데... input을 clear하는 것으로 보입니다..(ajax때문?)

 

그 후 setTime을 통해 시간차를 두고 다시 바로 setPlaceholder를 실행해서 placeholder를 나타내어주는 로직입니다.

 

다음으로 라인으로 가보면~ 

간단히 보면 beforeunload에 bind 해줬는데, 이것도 샘플에서 실행이 안되므로 넘어가겠습니다.

beforeunload는 페이지를 떠날경우 발생하는 이벤트입니다. (창을 닫거나, 페이지 이동)

$(window).bind('beforeunload.placeholder', function() {
    var clearPlaceholders = true;

    try {
        // Prevent IE javascript:void(0) anchors from causing cleared values
        if (document.activeElement.toString() === 'javascript:void(0)') {
            clearPlaceholders = false;
        }
    } catch (exception) { }

    if (clearPlaceholders) {
        $('.'+settings.customClass).each(function() {
            this.value = '';
        });
    }
});

 

이제 args라는 함수가 남았는데, 이것도 샘플에서 실행안되니.. 담번에... .;;;;

function args(elem) {
    // Return an object of element attributes
    var newAttrs = {};
    var rinlinejQuery = /^jQuery\d+$/;

    $.each(elem.attributes, function(i, attr) {
        if (attr.specified && !rinlinejQuery.test(attr.name)) {
            newAttrs[attr.name] = attr.value;
        }
    });

    return newAttrs;
}

이렇게 대충 placeholder.js 분석이 끝났습니다.

어떻게 동작하고 무엇이 있는 지 정도만 파악한 것 같습니다.

 

text 타입 일 경우 value값을 넣어 표현하는것,

password 타입일 경우 엘리먼트 하나 만드는 것, 

그리고 예외처리들... 정도인 것 같습니다.

 

이렇게 읽기도 파악하기도 힘든데,,, 대단합니다 라이브러리 만드시는 분들....존경..

 

+ Recent posts