CodeSnippet과 함께하는 JavaScript 프로그래밍


codesnippet

웹 서비스에서 자바스크립트의 의존도는 계속 증가하는 추세다. github에 등록된 프로젝트들(2015년도 조사 자료)만 봐도 자바스크립트의 비중이 가장 높고, 실제로 여러분의 프로젝트들도 대부분이 그럴 것이다.

여러분은 혹시 다음의 경우를 겪지 않았는가?

1. 다른 사람이 이미 만들어 놓은 코드가 있는지 모르고 중복으로 작업했다.
   (trim 등의 유틸리티 메서드들은 특히 그러기 쉽다)

2. 같은 용도의 프레임웍을 여러개 또는 버전별로 추가했고, 심지어 한 페이지에서 같이 사용했다.
   (jQuery, prototype ...)

3. 새로 넣으려는 코드가 기존의 코드와 꼬여 문제가 발생했다.

4. 유지보수할 때 이미 개발된 HTML페이지의 자바스크립트를 어디서부터 손대야 할지 모르겠다.
   (페이지 진입과 동시에 실행되는 코드 파악이 어렵다)

물론 자바스크립트에 국한된 문제들은 아니지만, 유독 자바스크립트에서 두드러지게 나타나는 문제들이다.

이제 자바스크립트도 관리해야 하는 시대다.

이미 그랬어야 했을 수도 있다.

요점은 외부에 노출되는 자바스크립트 코드를 깔끔하게 정리하는 것이다. 다시 말해 window에 아무렇게 변수나 함수를 추가하지 말고 어떤 코드가 어디에 들어가야 할지를 명확하게 정리해서 쓸데없는 중복을 피하고 다른 코드와 꼬일 일이 없도록 출입구를 하나만 만드는 것이다. (jQuery는 window.$ 안에 코드들이 정리되어 있다. 우리라고 못할게 뭐 있겠는가! 전혀 어렵지 않다)

먼저 라이브러리 없이 코드를 정리하는 방법을 알아본다. 정리된 코드가 가져다주는 유지보수의 편의는 여러분이 겪어봐서 이미 알 것이다. (적어도 위와 같은 문제는 예방할 수 있다) 그 후 CodeSnippet의 유틸리티 메서드 defineNamespace, defineModule 을 통해 더 간단하게 정리해 본다.

기존의 방식과 문제점

전역을 오염시키는 문제점으로 즉시실행 함수를 사용해야 한다는 점에 대해서는 다들 알고 있을 것이다.

<body>
  <script>
    function login() {
      /* ... */
    }
  </script>
</body>

위의 코드는 login()으로 실행할수도 있지만 window.login()으로도 사용할 수 있으므로 다른 자바스크립트와 섞여 문제를 발생시킬 수 있는 여지가 있다. (이런 코드를 '전역을 오염시키는 코드' 라고 한다)

그래서 아래와 같이 즉시실행함수(IIFE)를 사용해 전역을 오염시키는 것을 방지했다.

<body>
  <script>
    (function() {
      function login() {
        /* ... */
      }
    })();
    login(); // ReferenceError
  </script>
</body>

문제는 login 함수가 익명함수 (이름 없는 함수) 내에 존재하기 때문에 외부에서 접근할 수 없다는 것이다.

때문에 window에 수동으로 외부에서 접근 가능해야 하는것들을 추가해야 한다. window에 프로젝트명으로 객체를 만들고 여기에 정리하기로 했다.

<body>
  <script>
    (function(w) {
      function login() {
        /* ... */
      }

      w.myProject = {
        login: login
      };
    })(window);

    w.myProject.login(); // OK
  </script>
</body>

login이란 메서드가 window.myProject라는 객체에 포함되었다. 하지만 불편한 점이 있다. namespace 가 여러 depth를 이루고 있을 텐데 익명함수 내에서 예외처리를 해야 한다. 예를 들면 myProject.common 을 만들 때 myProject객체가 있는지 말이다.

<body>
  <script>
    (function(w) {
      function saveData() {
        /* ... */
      }

      // myProject 가 있는지?
      if (w.myProject) {
        // myProject.user 가 있는지?
        if (w.myProject.user) {
          w.myProject.user.saveData = saveData;
        } else {
          w.myProject.user = {
            saveData: saveData
          };
        }
      }
    })(window);

    w.myProject.user.saveData();
  </script>
</body>

defineNamespace

CodeSnippet의 defindNamespace는 window에 자바스크립트 기능들을 쉽게 체계적으로 구조화하는 기능을 제공한다. 이름 그대로 Namespace를 정의하는 기능을 제공하는 유틸리티 메서드다.

이 메서드를 사용하면 앞서 이야기했던 불편한 문제 없이 계획적으로 기능들을 쉽게 추가할 수 있다.

<body>
  <script>
    var common = tui.util.defineNamespace("ne.myNote.common", {
      trim: function() {
        /* ... */
      }
    });

    tui.util.defineNamespace("ne.myNote", {
      login: function() {
        /* ... */
      }
    });

    ne.myNote.login();
    ne.myNode.common.trim("test");
    common.trim("test"); // 변수에 할당해 사용이 가능하다
  </script>
</body>

ne.myNotene.myNote.common네임스페이스를 정의했다. (사실 ne.로 시작하는 namespace는 사내 표준 가이드다.) 심지어 ne.myNote.common을 먼저 선언하더라도 문제 없이 동작한다.

defineNamspace를 사용해 자바스크립트 메서드들을 구조적으로 노출시키는 것만으로도 굉장히 깔끔한 산출물을 만들어 낼 수 있다.

혹시 여기서 더 나아가 스크립트가 로드되는 시점에 초기화 메서드를 실행하고 싶다면 defineModule 메서드를 사용할 수 있다.

<body>
  <script>
    tui.util.defineModule("ne.myNote.settings", {
      memberID: "<%= memberID %>",
      initialize: function() {
        // 페이지 로딩과 동시에 비동기 통신을 해야 함 (자동실행)
        $.ajax(/* ... */);
      }
    });

    ne.myNote.settings.memberID; // 사용자 ID
  </script>
</body>

defineNamespacedefineModule의 차이는 페이지 로딩 시점에 initialize라는 이름의 메서드 실행 여부 뿐이다.

defineModule은 한 페이지 단위의 자바스크립트 파일을 만드는 데 쓸 수 있다. initialize에서 페이지 로딩과 함께 실행되어야 하는 구현을 하는 형태로 사용할 수 있다. (실무 예제에서 다룬다)

여기까지만 해도 충분하지만. 혹시, 만약, 클래스 시뮬레이션이 필요하다면 defineClass를 사용할 수 있다. defineClass는 결과가 생성자 함수이므로, 인스턴스화 하여 사용할 수 있다.

<body>
  <script>
    var Comment = tui.util.defineClass({
      init: function(content) {
        this.content = content;
        this.like = 0;
      },
      likeIt: function() {
        this.like += 1;
      }
    });

    var comment1 = new Comment("I like it!");
    var comment2 = new Comment("I hate it!");
  </script>
</body>

defineClass는 내부적으로 prototype 패턴을 이용해 클래스를 시뮬레이팅한다. 따라서 프로퍼티는 인스턴스에, 메서드는 prototype객체에 추가하므로 브라우저 기반의 한정적인 자원에서 동작하는 자바스크립트를 효율적으로 다룰 수 있도록 해 준다. 물론 상속도 가능하다.

<body>
  <script>
    var PhotoComment = tui.util.defineClass(Comment, {
      init: function(content) {
        Comment.call(this, content);
        this.photoUrl = ""; // Comment클래스에 photoUrl 프로퍼티를 추가했다
      }
    });

    var comment1 = new PhotoComment("I like it!");
  </script>
</body>

실무 예제

로그인 페이지에서 사용할 모듈을 만들어 보자. 추가적으로 email 에 trim을 적용해야 하는 경우를 가정한다.

// util.js
tui.util.defineNamespace("ne.myNote.util", {
  trim: function(str) {
    if (str.trim) {
      return str.trim();
    }

    return str.replace(/^[\s]+|[\s]+$/g, "");
  },

  getElement: function(selector) {
    return document.querySelector(selector);
  }
});

util.js는 프로젝트 전체에서 사용할 유틸리티 메서드를 모은 모듈이다. getElement는 jQuery의 $()와 같은 것이라고 보면 된다.

<body>
  <form>
    <input type="text" name="email" placeholder="Enter email address" />
    <input type="password" name="password" placeholder="Enter password" />
    <input type="submit" value="login" />
  </form>
  <script src="./util.js"></script>
  <!-- ne.myNote.util -->
  <script>
    tui.util.defineModule("ne.myNote.page.login", {
      $email: ne.myNote.util.getElement("input[name=email]"),
      $form: ne.myNote.util.getElement("form"),

      initialize: function() {
        this.$form.addEventListener("submit", this.onSubmit.bind(this));
      },

      onSubmit: function(e) {
        this.$email.value = ne.myNote.util.trim(this.$email.value);
      }
    });
  </script>
</body>

각 페이지에서 사용하는 엘리먼트에 대해 모듈의 프로퍼티로 정의했다 ($email, $form) 자바스크립트가 사용하는 페이지 내 엘리먼트를 한눈에 볼 수 있어 관리하기 용이해졌다.

initialize 에서 페이지 초기에 실행되어야하는 스크립트를 구현했다. 이제 페이지 전체파일을 훑지 않아도 로딩 시점의 스크립트를 관리할 수 있게 되었다.

자바스크립트에서 문서에 바인딩하는 폼 엘리먼트 이벤트를 onSubmit에 설정했다. 이와 같이 이벤트 메서드의 컨벤션을 정의할 경우 페이지 내에서 어떤 이벤트를 구현하는지 쉽게 알아볼 수 있다.

요약 및 결론

CodeSnippet의 defineNamespace, defineModule, defineClass를 사용하면 서비스의 자바스크립트 코드를 쉽게 계획적으로 구조화할 수 있다.

  • defineNamespace: 예외처리 없이 window에 객체 형태의 네임스페이스를 쉽게 만들 수 있다.
  • defineModule: 페이지 로드 시점에 initialize 메서드를 실행하는것 빼고 defineNamespace와 같다
  • defineClass: 클래스 시뮬레이팅 유틸리티 메서드다.

서비스 개발 초기에 namespace 목록과 그 용도를 정리해 공유한다면 이미 있는 로직을 중복으로 추가한다거나, 같은 기능을 하는 다른 벤더의 프레임웍을 중복으로 추가한다거나 하는 불상사를 예방할 수 있다 (jQuery와 prototype 라이브러리를 한페이지에 같이 쓰는 프로젝트가 아직 많다...)

CodeSnippet은 이 외에도 여러 유용한 기능이 있다. 자바스크립트의 모호한 타입 체킹을 회피할 수 있는 유틸리티 메서드 부터, 사용하기 불편했던 window.open을 통한 팝업을 쉽게 사용하고 관리할 수 있는 메서드까지 말이다. 기능 목록은 CodeSnippet Github Repository에서 확인할 수 있다.

끝으로 스크립트의 전체 용량은 23KB이며 GZIP 압축 후 약 6.94KB이므로 부담없는 크기이다. 또 원하는 기능의 파일만 개별로 사용할수도 있다. 사용 중 발생하는 문제에 대해 github이나 dl_javascript@nhn.com으로 리포팅하면 큰 이슈가 없는 한 즉각 피드백을 받을 수 있다.