블로그 내 검색

2012. 2. 27.

자바스크립트 패턴 #2 - 모듈 패턴 (Module Pattern) (수정)

모듈 패턴은 전통적인 소프트웨어 공학에서 클래스 사용에 private 과 public 으로 나뉜 캡슐화를 제공하는 방법이다.

자바스크립트에서의 모듈 패턴은 전역 영역에서 특정 변수영역을 보호하기 위해 단일 객체 안의 public/private의 변수를 포함할 수 있는 각 클래스 형식의 개념을 구현하는데 사용된다. 이 패턴으로 당신이 페이지에 추가한 추가적인 자바스크립트가 다른 스크립트와 이름이 충돌하는 것을 줄여줄 수 있다.


(발번역이지만 내용 전달은 되었길 빕니다;)

자바스크립트는 private, public 등의 접근 제한자를 언어차원에서 지원하지 않습니다.
하지만 모듈 패턴을 사용하여 그런 접근 제한을 구현해낼 수 있습니다. 요새 CommonJS, AMD (Asyncronouse Module Definition) 등의 자바스크립트의 표준화의 진행은 자바스크립트의 모듈화에 주안점을 두고 있지요. 그 근간은 이 모듈 패턴입니다.

고급 사용자라면 더욱 미려하고 시적인 모듈 패턴을 구사할 수도 있겠지만 저의 지식이 그에 못미치는 관계로 기초중에 기초만 설명해볼까 합니다.

모듈 패턴을 이해하려면 클로저와 컨텍스트에 대한 지식이 조금은 필요합니다.

그럼 간단한 코드를 예로 들어보죠.
서버에서 데이터를 얻어와 반환하는 코드의 한 예시입니다.
이때 한번 요청한 데이터는 캐싱되어야 합니다.
// 데이터 캐시
var dataCache = {};

// 데이터 캐시 아이디
var id = 0;
    
var url = '/default/data';
// ... 기타 사용 변수

var connectServer = function() { ... }
var sendRequest = function() { ... }
var parseData = function(data) { ... }
var getData = function() {
    connectServer();
    var data = sendRequest();
    dataCache[id++] = data;
    return parseData(data);
}
위와 같은 코드는 물론 잘 동작합니다.
하지만 전역 공간에 변수가 무분별하게 선언되어 있습니다. 이는 위에서 모듈 패턴의 정의에서 말한것과 같이 추가적인 스크립트(외부 라이브러리든 다른 개발자에 의해) 가 있을 경우 이름이 충돌할 수 있습니다. 함수 이름도 getData같은 아주 흔한 이름이기에 출동 가능성은 더욱 높습니다.
(혹시 이를 대비해 함수 이름을 어렵고 길게 만들자는 생각을 하신분은 없으시겠죠)

그리고 모든 변수들은 public 접근제한 상태입니다.
connectServer,  sendRequest,  parseData 이 세개는 특별한 경우 외엔 다른 곳에서 쓰일 필요가 없습니다.

private 접근제한이 적당할 것 같습니다

전역에 변수를 선언하고 싶지 않으려면 다음과 같이 익명 함수를 통한 선언을 해 볼 수 있습니다.
// 데이터 캐시
var dataCache = {};

// 데이터 캐시 아이디
var id = 0;

// 익명 함수로 감싸 전역 객체를 더럽히지(?)
// 않는다.
// 하지만 여전히 캐시는 전역에...
(function() {
    
    var url = '/default/data';
    // ... 기타 사용 변수
    
    var connectServer = function() { ... }
    var sendRequest = function() { ... }
    var parseData = function(data) { ... }
    
    var getData = function() {
        connectServer();
        var data = sendRequest();
        dataCache[id++] = data;
        return parseData(data);
    }
    
    getData();
    
})();
자. 어떻습니까. 모든 변수들이 private 스코프가 되었군요. 익명 함수로 인해 전역 스코프 접근도 없게 되었습니다.
그런데 이런 방식으로는 코드의 재사용을 전혀 할 수가 없습니다. 매 데이터 요청 시 저런 긴 코드를 쓸 생각이 아니라면 좀더 생각해 봅시다.
// 데이터 캐시
var dataCache = {};

// 데이터 캐시 아이디
var id = 0;

// 별도의 네임스페이스 적용. 
// 역시 캐시는 밖에 있지만 함수는 재활용이 가능하다.
var getData = function(url) {        
    
    url = url || '/default/data';
    // ... 기타 사용 변수
    
    var connectServer = function() { ... }
    var sendRequest = function() { ... }
    var parseData = function(data) { ... }
    
    connectServer();
    var data = sendRequest();
    dataCache[id++] = data;
    return parseData(data);
}
이건 나름 좋은 방법 같습니다.

필요한 함수들이 private되어 감춰졌습니다.

최소한으로 전역 영역에 자신을 노출하면서 기능을 재사용할 수도 있습니다. GetData라는 것은 일종의 네임스페이스 역할을 한다고 볼 수 있겠네요.그러나 아직 부족합니다.

저 개념을 좀더 발전시켜 봅시다.

데이터를 일관된 방식으로 요쳥하고 데이터 파서를 지정할 수 있는 객체를 제공하는 모듈을 만듭니다.
그리고 같은 요청시의 캐시 문제도 해결해 봅시다.
var spec = {
    url: '/some/path/data',
    callback: function(data) { ... }, // 콜백 지정
    parser: function jsonParser(data) { ... } // 파서 지정
};
// 모듈화. 생성 인자로 객체를 받는다.
// spec 객체를 바탕으로 객체 생성.
var dataModule = (function(spec) {
    
    // private 영역 시작

    // 데이터 캐시
    var dataCache = {};
    
    // 데이터 캐시 아이디
    var id = 0;
    
    var url = spec.url || '/default/data';
    // ... 기타 사용 변수
    
    var connectServer = function() { ... }
    var sendRequest = function(opt) { ... }
    var parseData = spec.parser || function(data) { ... };
    
    var callback = spec.callback || function() { };    
    var headers = spec.headers || {};

    // private 영역 끝.
    
    
    // 필요한 것만 공개. 접근 제한은 public이 된다
    // 리턴되는 객체의 메서드들은 클로저로서
    // private 영역의 변수에 접근이 가능하다.
    return {
        send: function() {
            connectServer(spec.url, spec.method);
            var data = sendRequest(headers);
            dataCache[id++] = data;
            return parseData(data, callback);
        },
        cache: function(id) { return dataCache[id]; },
        getLastCacheId: function() { return id; }
    } 
    
})(spec); // 익명 함수를 바로 실행

// @Test 코드
// 데이터 요청
var rs = dataModule.send();
console.log(dataModule.getLastCacheId()) // 마지막 요청 아이디

무명 함수의 결과를 받는 객체의 이름은 뭘로 지정해도 상관 없습니다. dataModule로 지정해 두었을 뿐 다른 프로그래밍시에는 다른 이름이 될 수도 있겠죠.

모듈은 별도의 정의된 이름 공간에 두면 문제가 없습니다. 한번 사용하고 말 것이라면 내부에 선언하고 바로 처리해도 되겠지요.
모듈 패턴은 사용하기에 따라 굉장한 용도가 있습니다.

어떻게 보면 new 를 사용한 객체의 생성보다 더욱 자바스크립트스러운 객체 생성방법이라고도 생각됩니다.

영어에 자신이 있으신 분은 고수가 쓰신 이 포스팅을 읽어보시길 추천드립니다.

댓글 28개:

  1. 위 코드의 "||" 연산자는 어떤 의미를 갖는건가요?

    var url = spec.url || '/default/data';
    var callback = spec.callback || function() { };

    답글삭제
    답글
    1. '||' 연산자는 두개의 표현식을 받는데 앞의 표현식이 참 판단일 경우에 앞의 것을 리턴합니다.
      그러니까

      var url = spec.url || '/default/data';

      의 뜻은 spec.url이 값이 있다면 url 에 할당하고 아니라면 '/default/data' 값을 기본값으로 주라는 뜻이지요.

      흔히 자바스크립트 객체가 생성될 때 기본값 지정할때 유용하게 쓰입니다.

      추가로...
      '&&' 연산자도 있습니다. 동작은 반대로 한다고 보면 됩니다.

      && 연산자는 두 조건중 앞의 표현식의 값이 거짓이면 그 값을 리턴하고 거짓이 아니면 뒤의 표현식의 값을 리턴합니다.

      삭제
    2. OR, AND 연산자를 이렇게 활용할 수 있군요. 3항 연산자를 자주 사용했었는데 참고 해야겠습니다 .

      삭제
    3. 3항 연산자보다 간결해서 쓰기 참 편하죠.

      && 나 || 로 앞 표현식이 처리된다면 뒤 표현식이 처리안된다는 점을 이용해서 함수와 함수를 저런 논리 연산자로 연결하는 기법도 있습니다.

      물론 코드 리딩은 힘들어지지만 코드 량은 좀더 줄일 수 있죠;

      var ok = usuallyTrue() && extraWork();

      이런식;

      삭제
  2. OR의미 입니다.A||B A이거나 B..

    답글삭제
    답글
    1. 이 연산자 쓰다보면 참 편하다는걸 느끼죠.

      삭제
  3. 그런데 왜 self execution을 하는 걸까요? 그냥 생성자 함수 형식으로 생성한 다음에 new를 사용해서 인스턴스화 해서 쓰는거랑 차이가 없어 보이는데, 어떤 목적이나 용도가 있는건가요?

    답글삭제
    답글
    1. 어려운 리플이네요 ㅠㅠ;
      저도 아직 둘의 차이와 장점을 잘 모릅니다;
      그래도 제가 느낀 점을 몇가지 적자면...

      둘다 전역 스코프를 오염시키지 않고 반환된 객체가 public과 private 둘다 잘 된 캡슐화 구현을 할 수 있다는건 같습니다.

      반면 차이점을 찝어보면,

      일단 생성자 함수가 선언되어 버리면, 그 함수 이름이 전역에 선언됩니다.
      사용하려면 생성자 함수 하나로 일단 전역에 식별자가 하나 생성되고, 그 다음에는 그 모듈을 new로 생성한뒤 그 결과를 받은 모듈을 식별할 식별자가 하나 더 필요해지죠. 사소하다면 사소할 수 있겠습니다만;
      모듈 사용시마다 new 하는것도 번거롭고, 중복생성된 객체 사용으로 문제가 생길수도 있구요.

      중요한건, 일단 모듈은 익스포트가 되지 않는다면 별 의미가 없습니다.
      가져다 쓰지 못하는 모듈은 의미가 없는데, 식별할 변수가 없다면 더 의미가 없죠;

      하지만 익스포트하려면 전역 식별자가 필요합니다.
      self execution(익명클로저함수 - anonymous closure function 이라고도 합니다)로 실행한 결과를 어디에서든 식별가능한 특정 변수에 담는거나,
      말씀대로 생성자 함수를 선언하고 new 해서 생성하여 어디에서든 식별가능한 변수로 사용하는 방법이나 별 차이가 없게 됩니다...

      이런 경우 보통 네임스페이스를 사용합니다.
      jQuery는 jQuery를 전역에 선언하고 모든 것을 그것으로 접근하게 하죠.
      야후의 YUI는 YUI를, Sencha는 Ext를 전역에 선언하고 모든 모듈을 그 네임스페이스로 접근하게 합니다.

      그리고, 진행중인 자바스크립트 모듈화 표준안에서도 도움이 됩니다.
      CommonJS를 예로 들면 작성한 모듈을 익스포트할땐 exeport라는 전역 변수를 하나 씁니다.
      위 포스트의 코드의 return이랑 거의 같은 일을 하는 함수죠;
      사용할땐 require로 파일명을 적어 가져옵니다.
      내부적인 식별 네임스페이스는 아마도 파일명이 될 것 같습니다...

      RequireJS를 예로 들면 모듈을 정의할땐

      define(function() {

      // 모듈 코드

      return {

      // 공개할 코드

      };

      });

      식인데, 저 define 함수 안에 익명함수를 넣어두는 것만으로 모듈이 정의되죠;

      앞서도 써놨지만, 사실 일반적인 자바스크립트 코딩에서, self execution 형식의 코딩이 어떤 점에서 딱 확실히 유리한지는 잘 모릅니다;

      다만 코드가 훨씬 깔끔하고 변수 하나를 덜 선언할 수 있으며, 중복 생성의 위험이 덜하다는 점만으로도 사용할 가치는 있다고 봅니다...

      삭제
    2. 좀 다른 내용이지만 혹시 자바스크립트 완벽 가이드 책이 있으시다면 10장을 읽어 보는것도 좋을거 같아요~ 모듈화에 대해 일반적인 내용이 나와 있습니다.

      정의의 편의성이라면 아무래도 익명 클로저 실행이 나을것 같네요!

      삭제
    3. 기왕 질문드린김에 하나 더 여쭤도 될까요? ^^;;
      모듈패턴을 사용하면서 다음과 같이 네임스페이스를 인자로 넘기는 경우를 가끔 봤습니다. 이유가 무얼까요? 저렇게 인자로 넘기지 않아도 해당 모듈에 접근하는 방법은 똑같지 않나요?
      var app = app || {};
      app.service = (function(app){
      ...........
      })();

      삭제
    4. 모듈 패턴에서 네임스페이스를 주는 이유는 역시 전역공간 오염에 대한 문제랑 비슷합니다.

      최근 추세가 되는 복잡한 자바스크립트일 경우에는 외부(혹은 전역)공간은 말그대로 혼돈의 카오스입니다(?)뭐가 선언되어 있을지, 뭘 만지면 큰일날지 잘 모르죠.
      (애초에 철저하게 관리한다면 모르지만요...)

      외부에 변수가 무엇이 있는지 잘 모를땐 애초에 외부에서 필요한 변수만 가져와서(import) 그 외부변수만 사용하겠다! 라고 명시적으로 선언해두는 겁니다.

      외부모듈 YUI가 있고, 나의 모듈에서 YUI에 접근하려고 한다면,

      (function(Y) {

      // XXX: 이 모듈 코드에서는 외부 변수로 Y만 사용하겠다

      })(YUI);

      라는 식이죠.
      이렇게 하면 코드를 읽는 사람은 이 코드는 외부변수에는 YUI만 사용한다라는 사실을 알 수 있어 코드 가독성이 높아지고, 코딩하는 사람도 외부변수 접근에 암묵적 제한이 걸리게 됩니다.

      어차피 접근은 똑같지만 일종의 관례라고 생각하시면 됩니다.

      또 한가지, 최적화에 관련된 문제인데요.
      외부변수를 저렇게 인자로 받을 경우 불필요한 스코프 체인 탐색이 줄어들어 속도도 빨라집니다. 변수 탐색을 외부까지 확장할 필요가 없어지니까요.

      답변이 되었는지 모르겠네요...

      삭제
    5. 감사합니다... 명쾌한 설명이네요...

      삭제
    6. 다른말로 하면 의존성이라고도 할 수 있겠네요...이 모듈은 YUI에 의존성이 있다 를 잘 표현할 수 있죠.

      만일 표준 구현 모듈을 쓴다면 모듈간 로딩 순서도 명확하게 알 수 있구요.

      삭제
    7. self execution 을 하는 이유????
      첫째 변수의 전역화를 막고 원하는 범위에 한정 시킬 수 있다.
      둘째 해당 코드가 어떠한 독립적인 의미를 지니며, 이것을 강조하고 싶을때.(부차적인 효과 이겠군요.)

      삭제
  4. 자세하고 이해하기 쉬운 설명 정말 감사드려요^^
    음.. 굳이 꼽자면 말씀대로 글로벌 변수에 추가되는 정도가 되겠군요.
    말씀해주신 책도 함 찾아봐야겠네요...
    앞으로도 좋은 글 많이 부탁드려요^^

    답글삭제
  5. 깔끔한 설명 감사합니다

    답글삭제
  6. 굉장히 좋은 글이네요. 클로저를 공부해야할듯 싶네요. 참 좋은것같네요 소스가!

    답글삭제
  7. 캡슐화에서 궁금한점이 있습니다.
    자바스크립트의 prototype을 사용하면서 캡슐화 사용이 가능한가요?
    제가 아는선에서 구현해볼려고 했는데 prototype멤버에서 생성자함수내의 private멤버에 접근할 방법을 모르겠더라구요..

    답글삭제
    답글
    1. 프로토타입을 사용한다는 뜻은 일단 다수 객체 생성을 고려한다는 것으로 볼 수 있습니다.
      그럼 프로토타입과 생성자 함수를 모듈 안에 아예 집어넣는 방법으로 해결하면 됩니다. :)

      삭제
  8. 글을 남겼는데 사라져버렸네요; 열심히 썼는데 간략하게 하자면.. 자바스크립트를 공부하고 있는데 마지막 예제의 var dataModule = ( .. ) 와 같이 해버리면 한번 초기화 된 이후에는 모든 메소드에서 dataModule을 싱글톤으로 접근하는게 아닌가 하는게 질문입니다. A()에서도 B()에서도 dataModule을 접근하면 단일 인스턴스만 접근하니까 static 변수처럼 되어버릴것 같은데.. java의 new 처럼 인스턴스별로 사용할순 없는건가요? 아님 제가 코드를 이해못한부분이 있다면 말씀부탁드립니다.

    답글삭제
    답글
    1. 물론 문제는 없습니다. :)
      위의 데이터모듈은 여러개 생성할 필요가 없는 모듈이라 저렇게 처리했을 뿐이죠. new 로 '여러개의 상태를 갖는' 모듈을 원한다면 객체를 직접 반환하지 않고 생성자 함수를 반환하시면 될 것 같습니다.

      중요한 건 원하시는 객체가 여러개의 상태가 존재해야 하는지, 아니면 히니의 상태만 필요한지를 고려하시면 될 것 같네요

      삭제
  9. 모듈패턴할때 즉시실행함수를 쓰는다른이유가 있나요?
    즉시실행 안하고 new로 인스턴스 만들어서 생성해도 되나요?

    답글삭제
    답글
    1. 즉시 실행함수를 쓰는 이유는 전역에 불필요한 변수/프로퍼티 를 선언하는것을 피하기 위해서입니다 :)
      네이밍이 겹쳐 타 코드에 영향을 줄 수 있기 때문이죠.

      삭제
    2. 그냥 이렇게 짜는게 가독성면에서 더 좋을거 같은데요

      function dataModule(spec){
      // 데이터 캐시
      var dataCache = {};

      // 데이터 캐시 아이디
      var id = 0;

      var url = spec.url || '/default/data';

      var connectServer = function() { ... }
      var sendRequest = function(opt) { ... }
      var parseData = spec.parser || function(data) { ... };

      var callback = spec.callback || function() { };
      var headers = spec.headers || {};

      // 퍼블릭 함수
      function send(){
      connectServer(spec.url, spec.method);
      var data = sendRequest(headers);
      dataCache[id++] = data;
      return parseData(data, callback);
      }
      function cache(id){ return dataCache[id]; }
      function getLastCacheId (id) { return id; }

      return{
      send:send,
      cache:cache,
      getLastCacheId:getLastCacheId
      }
      }

      삭제
  10. 자바스크립트를 테스트하기 쉽게 모듈화하는 것을 고민하다가 여기까지 왔습니다. 링크 걸어주신 것 중에 모듈 패턴에 점진적으로 로직을 증가하는 것이 가장 눈에 들어오네요.

    답글삭제
  11. 포스팅 정말 감사합니다 ㅜ

    답글삭제