이번 글에서는 예제 코드를 통해 jQuery 소스에서 많이 사용되는 each() 메서드에 대해서 알아보자.

다음 예제 코드의 실행 결과를 추측해보자.이 예제는 jQuery API 사이트에서 each 메서드를 설명한 http://api.jquery.com/each/의 예제 코드다. 예제는 비록 간단하지만, 이를 제대로 이해하기 위해서는 몇가지 지식이 필요하다.

  • 예제1 - HTML 파트
<ul>
  <li>foo</li>
  <li>bar</li>
</ul>
  • 예제2 - JS 파트
$('li').each(function(index) {
  alert(index + ': ' + $(this).text());
});

이 예제를 jQuery 소스를 살펴보면서 하나씩 살펴보자.

$('li') 살펴보기

가장 먼저 $(‘li’)라는 부분이 실행될 것이다. $() 함수는 결국 jQuery 생성자 함수를 통해 jQuery 객체를 생성하는 역할을 한다. 아래 코드는 jQuery 객체의 생성자 함수를 나타낸다. 참고로 jQuery 생성자 함수(init 함수)는 context라는 인자를 받는 데, jQuery 소스에서 context는 주로 document 객체를 가리킨다.

  • 예제3 - jQuery 생성자 함수
// jQuery 객체의 초기화
jQuery.fn = jQuery.prototype = {
  // jquery 객체의 사실상 생성자
  init: function( selector, context ) {
    // Make sure that a selection was provided
    selector = selector || document;

    // Handle $(DOMElement)
    // jQuery 객체로 넘어온 인자의 nodeType파악

    // 여기서 this는 jQuery 객체를 의미
    if ( selector.nodeType ) {
      this[0] = selector;
      this.length = 1;
      // jQuery 객체를 돌려줌.
      return this;

    // Handle HTML strings
    } else if ( typeof selector == "string" ) {
      // Are we dealing with HTML string or an ID?
      var match = quickExpr.exec( selector );

      // Verify a match, and that no context was specified for #id
      if ( match && (match[1] || !context) ) {

        // HANDLE: $(html) -> $(array)
        if ( match[1] )
          selector = jQuery.clean( [ match[1] ], context );

        // HANDLE: $("#id")
        else {
          var elem = document.getElementById( match[3] );

          // Make sure an element was located
          if ( elem )
            // Handle the case where IE and Opera return items
            // by name instead of ID
            if ( elem.id != match[3] )
              return jQuery().find( selector );

            // Otherwise, we inject the element directly into the jQuery object
            else {
              this[0] = elem;
              this.length = 1;
              return this;
            }

          else
            selector = [];
        }

      // HANDLE: $(expr, [context])
      // (which is just equivalent to: $(content).find(expr)
      } else
        return new jQuery( context ).find( selector );

    // HANDLE: $(function)
    // Shortcut for document ready
    } else if ( jQuery.isFunction( selector ) )
      return new jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );

    return this.setArray(
      // HANDLE: $(array)
      selector.constructor == Array && selector ||

      // HANDLE: $(arraylike)
      // Watch for when an array-like object, contains DOM nodes, is passed in as the selector
      (selector.jquery || selector.length && selector != window && !selector.nodeType && selector[0] != undefined && selector[0].nodeType) && jQuery.makeArray( selector ) ||

      // HANDLE: $(*)
      [ selector ] );
  },

$(‘li’)의 경우는 ‘li’가 “string” 타입이지만, html이나 id 문자열이 아니기 때문에 위 생성자 코드에서 다음과 같은 부분이 실행된다.

  • 예제4 - jQuery 생성자 함수 일부
// HANDLE: $(expr, [context])
// (which is just equivalent to: $(content).find(expr)
return new jQuery( context ).find( selector );

이 경우 jQuery(context)라는 jQuery 객체를 먼저 생성 후, 이 객체의 find() 메서드를 실행하게 된다. 따라서 다시 jQuery 객체를 생성하기 위한 생성자 함수가 실행되는데, 이때는 다음과 같은 부분이 실행된다. jQuery( context )에서 context 인자값이 undefined이므로 아래 생성자 함수의 selector 매개변수에 undefined가 전달된다. selector는 undefined의 경우는 document(DOM element)가 저장되면서 jQuery 객체를 리턴하게 된다.

  • 예제5 - jQuery 생성자 함수 일부
function( selector, context ) {
        // Make sure that a selection was provided
  selector = selector || document;

  // Handle $(DOMElement)
  if ( selector.nodeType ) {
    this[0] = selector;
          this.length = 1;
    return this;
        }
    ...

이제 document를 포함하는 jQuery 객체를 만들었으니, find()메서드를 살펴보자. 코드는 다음과 같으며, selector에는 여전히 ‘li’가 전달된다. 코드가 상당히 복잡하다. 그래도 하나씩 살펴보도록 하자.

  • 예제6 - $().find() 메서드 코드
find: function( selector ) {
  var elems = jQuery.map(this, function(elem){
    return jQuery.find( selector, elem );
  });

  return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ?
    jQuery.unique( elems ) :
    elems );
},

우선 jQuery.map() 메서드의 실행 결과 값을 elems 변수에 저장한다. 따라서, jQuery.map() 메서드를 다시 분석해야 한다. 코드는 다음과 같다. 위 코드에서 인자로 넘긴 this는 find 메서드를 호출한 객체, 즉 위에서 생성한 Document를 가리키는 jQuery 객체다.

  • 예제7 - jQuery.map() 메서드
map: function( elems, callback ) {
  var ret = [];

  // Go through the array, translating each of the items to their
  // new value (or values).
  for ( var i = 0, length = elems.length; i < length; i++ ) {          
    // 이 부분은 예제8에서 살펴보자.           
    var value = callback( elems[ i ], i );               
    if ( value !== null && value != undefined ) {              
      if ( value.constructor != Array )                  
        value = [ value ];                         
      // concat() 메서드는 배열을 합치는 기능을 한다 .              
      ret = ret.concat( value );                  
    }      
  }           
  return ret; 
 } 

jQuery.map() 메서드를 살펴보면, 인자로 elems와 callback 함수를 받는다. elems는 배열이고, callback은 콜백 함수가 넘겨질 것이다. jQuery.map() 메서드 내부에서는 elems 배열과 인덱스 값을 callback 함수를 호출해서 생성되는 리턴값을 ret 배열로 만든 다음 이를 다시 리턴한다. 다시 말하면, jQuery.map() 메서드는 ‘콜백 함수(기존 배열) ⇒ 새로운 배열’ 이렇게 생각하면 된다. 우리가 살펴보고 있는 예제에서는 elems는 this를 가리키는 데, this는 length가 0인 유사 배열 객체로 document를 가리키는 jQuery 객체다. 때문에 jQuery.map() 메서드 내에서는 for 루프가 한번만 돌며 다음과 같은 콜백 함수를 실행시킬 것이다. 여기서 인자로는 this가 전달된다.

  • 예제8 - jQuery.map() 메서드로 전달되는 콜백 함수
function(elem) {
  return jQuery.find( selector, elem );
}

결국 위 콜백 함수 jQuery.find(“li”, this)의 결과값이 jQuery.map() 메서드의 실행 결과가 된다. 이 결과값을 알아보기 위해서는 jQuery.find() 메서드 코드를 살펴보자.

  • 예제9 - jQuery.find() 메서드의 일부
find: function( t, context ) { 
 ...      
  // Set the correct context (if none is provided)    
  context = context || document;         
  
  // Initialize the search      
  var ret = [context], done = [], last, nodeName;         
  
  // Continue while a selector expression exists, and while      
  // we're no longer looping upon ourselves      
  while ( t && last != t ) {          
    var r = [];          
    last = t;          
    t = jQuery.trim(t);     
    var foundToken = false;     
    
    // See if there's still an expression, and that we haven't already     
    // matched a token     
    if ( t && !foundToken ) {       
      // Handle multiple expressions       
      if ( !t.indexOf(",") ) { 
....       
      } else {
        // Optimize for the case nodeName#idName 
        var re2 = quickID;         
        var m = re2.exec(t);         
      
        // Re-organize the results, so that they're consistent         
        if ( m ) {           
          m = [ 0, m[2], m[3], m[1] ];          
        } else {           
          // Otherwise, do a traditional filter check for
          // ID, class, and element selectors           
          re2 = quickClass;           
          m = re2.exec(t);         
        }         
        
        m[2] = m[2].replace(/\\/g, "");         
        var elem = ret[ret.length-1];         
        
        // selector가 id값일 경우는 id를 통해 원하는 DOM 요소를 검색한다.         
        if ( m[1] == "#" && elem && elem.getElementById && !jQuery.isXMLDoc(elem) ) {  
...         
        } else {           
          // We need to find all descendant elements           
          // 여기 예제에서는 ret[0]은 document 객체이다.           
          for ( var i = 0; ret[i]; i++ ) {             
            // 태그를 구한다. --> 예제에서는 tag에 "li" 문자열이 저장
            var tag = m[1] == "#" && m[3] ? m[3] : m[1] != "" || m[0] == "" ? "*" : m[2];

            // document의 getElementsByTagName() 메서드를 통해 tag가 포함된 DOM 요소를 찾는다.
            // 그리고 찾은 DOM 요소의 배열을 r에 합친다.
            r = jQuery.merge( r, ret[i].getElementsByTagName( tag ));
          }
...
          // r 배열 객체의 레퍼런스를 ret에 저장.
          ret = r;
        }
        // 이미 처리한 셀렉터 문자열을 없앤다.
        t = t.replace( re2, "" );
      }
     }
 ...

  } // while문 끝

  // And combine the results
  done = jQuery.merge( done, ret );

  return done;
}

jQuery.find() 함수는 주어진 컨텍스트 내에서 셀렉터 값을 가진 DOM 요소를 찾는 메서드이다. jQuery는 다양한 셀렉터 형식을 지원하고 있다. 우리가 살펴볼 예제에서는 단순히 ‘li’라는 html 태그명만을 넘겼다. 이해를 돕기위해 예제9는 jQuery.find() 메서드 코드에서 태그명을 셀렉터로 넘겼을 때 처리되는 주요 부분만을 간추린 것이다.

전체적인 동작은 다음과 같다. 1. 정규표현식을 통해 셀렉터 형식 체크 –> 예제에서는 셀렉터가 태그임을 인식 2. 주어진 컨텍스트(여기서는 document)내에서 해당 태그를 가진 DOM 요소 검색 –> getElementsByTag() 메서드 이용 3. 검색된 DOM 요소 배열을 리턴

따라서 이번 예제에서 우리가 찾으려는 jQuery.find(“li”, this);의 결과값은 맨 처음 HTML 코드 예제1에서 <li> 태그를 가진 두 개의 DOM 요소가 될 것이다.

이제 다시 예제6으로 돌아가보자. 원래 구하려고 했던 jQuery.map() 메서드의 결과값은 결국 예제1의 <li> 태그를 가진 DOM 요소 객체이며, 이는 elems 변수에 저장된다. 이후 pushStack() 메서드가 호출된다.

  • 예제10 - .pushStack() 메서드
// Take an array of elements and push it onto the stack
// (returning the new matched element set)
pushStack: function( elems ) {

  // elems 인자에 포함된 DOM 요소를 가진 jQuery 객체를 생성한다.
  var ret = jQuery( elems );

  // 새로 생성된 객체의 prevObject 프로퍼티에 현재 jQuery 객체(this)를 추가한다.
  ret.prevObject = this;

  // 새로 생성된 jQuery 객체를 반환한다.
  return ret;
},

예제10의 코드를 통해 .pushStack()의 기능을 살펴보면, elems 인자로 전달된 DOM 요소에 매핑되는 jQuery 객체를 새로 생성하고 생성된 객체의 prevObject 프로퍼티에 현재 jQuery 객체를 설정한다. (일종의 스택 추가 작업) 그리고 새로 생성된 jQuery 객체를 반환한다. 이 값은 결국 예제4의 리턴값이 된다.

이제 예제2에서 $(‘li’) 부분에 대해 jQuery 소스 코드의 내부 동작 과정에 대해서 살펴봤다. 이를 그림으로 정리하면 다음과 같다.

$('li').each() 살펴보기

이제 다시 예제2로 돌아가보자. 이 부분을 이해하기 위해서는 each() 설명 부분을 먼저 알아야 한다. 간단히 설명하자면 이 메서드는 jQuery 객체에 매핑된 DOM 요소들을 루프를 돌면서 콜백 함수를 실행하는 역할을 한다. 우리는 앞부분에서 $(‘li’)가 나타내는 jQuery 객체가 예제1의 두개의 li 태그를 매핑하고 있다는 것을 살펴봤다. 그러므로 예제2는 이 매핑된 두개의 DOM 객체에 대해서 아래 콜백 함수를 실행하는 역할을 한다.

  • 예제11 - 콜백함수
function(index) {
    alert(index + ': ' + $(this).text());
}

이제 마지막으로 콜백 함수를 살펴보자. this가 가리키는 것은 무엇일까?? this는 콜백 함수 내부에 있기 때문에, 콜백 함수를 호출하는 객체가 바로 this일 것이다. each() 에서 설명했듯이, .each() 메서드 실행 시에 콜백 함수를 호출하는 주체는 바로 jQuery 객체에 매핑되어 있는 DOM 객체들이다. 따라서, 이 예제의 경우는 ‘li’ 태그 DOM 객체가 this에 해당한다. 이제 위 예제의 $(this)는 DOM 객체를 selector로 넘겨서 jQuery 객체를 생성한다는 것을 알 수 있다. 예제12는 jQuery 생성자 함수중 selector가 DOM 객체일 때, jQuery 객체를 생성하는 부분이다.

  • 예제12 - jQuery 생성자 함수 일부
// 여기서 this는 jQuery 객체를 의미 
if ( selector.nodeType ) {
  this[0] = selector;
  this.length = 1;
  
  return this;
}

이 과정을 정리하면 아래 그림과 같이 나타낼 수 있다.

이제 마지막 .text() 메서드를 호출하면 된다. 이 메서드는 jQuery 객체와 매핑된 DOM 요소들(자식 노드 포함)의 text 부분을 묶어서 출력하는 역할을 한다. 자세한 설명은 text() 메서드 기사를 참조해라.

이제 지금까지의 상황을 종합해서 출력 결과를 알아보면 다음과 같다. 우선 예제1의 출력 결과는 다음과 같다.

  • foo
  • bar

예제2의 출력 결과로 다음과 같은 경고창이 출력된다.