Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

grunt-usemin을 사용한 JavaScript/CSS 파일 팩키징

요즘은 프론트앤드도 규모가 상당히 커졌기 때문에 프로젝트를 할 때 CSS나 자바스크립트의 패키징에 고민이 많이 되는데 적당한 해결책을 찾지 못하고 있었다. 내가 원하는 패키징 요구사항은 다음과 같았다.

  • 개발할 때는 원하는 대로 모듈화해서 파일별로 나누어서 개발한다.(CSS나 JS)
  • 프로덕션에 나갈 때는 모듈화된 파일을 하나의 파일로 합쳐야 한다.(의존 라이브러리도 포함해서)
  • 하나로 합쳐진 파일을 압축할 수 있어야 한다.

AMD나 require.js등의 접근방법도 있었지만 어차피 사용하는 거라면 모를까 패키징을 위해서 AMD를 도입하는 것은 좀 과도해 보였다. require.js를 패키징용으로만 쓸 수 있다고 들었는데 별로 안써봐서 그런지 정확히 어떻게 하는지 잘 모르겠다. 꽤 단순해 보이는 요구사항이지만 이렇게 패키징을 관리하기 어려운 이유는 HTML에서 JS나 CSS를 인클루드하고 있기 때문이다. 그래서 개발에서 사용할 때는 여러 JS 파일을 인클루드하고 있지만 프로덕션에 나갈때는 JS 파일이 하나로 합쳐질 것이므로 HTML도 하나의 파일만 인클루드 해야한다. 오랫동안 이러한 도구를 찾았지만 마땅히 맘에 드는 도구를 찾지 못하고 있었다. Ruby에는 Sprockets라는 좋은 패키징 도구가 있다고 들었는데 Ruby환경이 써보기 어려웠고 도구는 여러가지가 있었지만 내 필요사항에 과도해 보이거나 딱 맘에 들지 않았다.

grunt-usemin

그러다가 찾아낸 것이 usemin이다. usemin은 Yeoman에서 만든 Grunt 플러그인인데 프론트앤드 빌드에는 거의 Grunt(grunt는 이전 글 참고)를 쓰고 있었기 때문에 나에게는 쓰기 편했고 usemin에 기능도 내가 찾던 모습 그대로였다. usemin이 Grunt 플러그인이므로 usemin을 사용하려면 Grunt를 사용하고 있어야 하고 usemin에서 concat, uglify, cssmin, requirejs를 사용하므로 해당 플러그인이 이미 설치되어 있어야 한다.(물론 플로그인은 사용에 따라 선택해서 사용한다.)
다음과 같은 package.json이 있다고 하자.

{
  "name": "usemin-example",
  "version": "0.0.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib": "~0.7.0"
  }
}

Grunt를 사용하기 위해서 grunt를 설치하고 공식 플러그인인 grunt-contrib를 설치를 설치했다. Grunt의 공식플러그인은 모두 grunt-contrib라는 접두사가 붙는데 grunt-contrib-concat같은 식으로 필요한 것만 설치할 수도 있지만 여기서는 그냥 공식 플러그인을 모두 설치했다.

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
  });

  // Load the plugin.
  grunt.loadNpmTasks('grunt-contrib');
};

Gruntfile.js는 위와 같이 작성했다. 기본 뼈대만 있고 앞에서 설치한 grunt-contrib를 불러왔을 뿐 다른 설정은 추가하지 않았다. 이제 npm 명령어로 grunt-usemin을 설치한다.

$ npm install grunt-usemin --save-dev

usemin 사용

예를 들어 다음과 같은 HTML 파일이 있다고 하자.

<html>
  <head>
    <title>usemin example</title>
    <link href="../public/css/a.css" media="all" rel="stylesheet" type="text/css" />
    <link href="../public/css/b.css" media="all" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <!-- html code here -->
    <!-- html code here -->
    <!-- html code here -->
    <!-- html code here -->

    <script src="../public/js/a.js"></script>
    <script src="../public/js/b.js"></script>
    <script src="../public/js/c.js"></script>
  </body>
</html>

HTML에서 필요한 CSS 파일과 JavaScript파일을 인클루드하고 있고 개발할 때는 당연히 필요에 따라 모듈화해서 개발하는 것이 편하다. JavaScript와 CSS 파일은 예제를 위해서 다음과 같은 코드만 간단히 들어 있다.(주석은 파일을 구분하기 위해서 넣은것 뿐이다.)

// public/js/a.js
;var from_file_A = function() {
  return "A";
};

// public/js/b.js
;var from_file_B = function() {
  return "B";
};

// public/js/c.js
;var from_file_C = function() {
  return "C";
};
/* public/css/a.css */
html,
body {
  margin: 0;
  padding:0;
  border:0
}

/* public/css/b.css */
a {
  text-decoration: none;
}

여기서 usemin을 적용하려면 병합/압축할 JavaScirpt와 CSS 부분을 usemin이 인식할 수 있는 주석으로 감싼다.

<html>
  <head>
    <title>usemin example</title>

    <!-- build:css ../public/css/style.min.css -->
    <link href="../public/css/a.css" media="all" rel="stylesheet" type="text/css" />
    <link href="../public/css/b.css" media="all" rel="stylesheet" type="text/css" />
    <!-- endbuild -->
  </head>
  <body>
    <!-- html code here -->
    <!-- html code here -->
    <!-- html code here -->
    <!-- html code here -->

    <!-- build:js ../public/js/script.min.js -->
    <script src="../public/js/a.js"></script>
    <script src="../public/js/b.js"></script>
    <script src="../public/js/c.js"></script>
    <!-- endbuild -->

  </body>
</html>

usemin의 주석은 다음과 같은 형식을 취하고 있다.



<type>js아니면 css이고 alternate search path는 옵션값인데 기본적으로 현재 HTML 파일에 상대적으로 파일을 찾는데 추가적으로 찾아야 할 경로를 지정해 줄 수 있다. <path>는 병합/압축된 파일의 경로 및 파일명을 지정한다. 보다시피 단순히 HTML 주석일 뿐이므로 개발할 때 이상태로 그대로 사용할 수 있다.

usemin의 Grunt 설정

추가적으로 해야할 작업은 Grunt의 설정이다. usemin은 2가지 단계로 나누어 지는데 useminPrepareusemin이다. useminPrepare에서는 concat, uglify, cssmin, requirejs을 위한 설정을 추가하는 과정이고 usemin에서 실제 HTML 파일까지 변경한다. Grunt에서 usemin을 사용해야 하므로 다음과 같이 Gruntfile.jsgrunt-usemin을 추가해준다.

...

// Load the plugin.
grunt.loadNpmTasks('grunt-contrib');
grunt.loadNpmTasks('grunt-usemin'); // 추가!!

...

useminPrepare

먼저 useminPrepare를 설정하자.

...

// Project configuration.
grunt.initConfig({
  'useminPrepare': {
    html: 'views/index.html'
  },
  'usemin': {
    html: ['views/*.html']
  }
});

...

Grunt의 설정에서 useminPrepare부분을 추가해서 html에 사용할 HTML 파일을 지정한다. 여기서 HTML파일은 앞에서 usemin 주석을 사용한 HTML파일들이다. 위처럼 특정 파일을 지정해도 되고 배열([])을 사용해서 여러파일을 지정할 수도 있고 **/*.html처럼 한꺼번에 다수의 파일을 지정할 수도 있다.(파일 지정에 관한 규칙은 Grunt의 규칙이다.) 추가로 uglifycssmin 옵션도 지정할 수 있는데 js/css 압축에 다른 도구를 사용한다면 여기서 지정할 수 있다. dest로 최종 출력파일을 만들 기본 디렉토리를 지정할 수도 있다. grunt useminPrepare를 실행하면 다음과 같이 출력된다.

$ grunt useminPrepare
Running "useminPrepare:html" (useminPrepare) task
Going through views/index.html to update the config
Looking for build script HTML comment blocks

Found a block:
    
    <link href="../public/css/a.css" media="all" rel="stylesheet" type="text/css" />
    <link href="../public/css/b.css" media="all" rel="stylesheet" type="text/css" />
    <!-- endbuild -->
Updating config with the following assets:
    - public/css/a.css
    - public/css/b.css

Found a block:
    
    <script src="../public/js/a.js"></script>
    <script src="../public/js/b.js"></script>
    <script src="../public/js/c.js"></script>
    <!-- endbuild -->
Updating config with the following assets:
    - public/js/a.js
    - public/js/b.js
    - public/js/c.js

Configuration is now:

  cssmin:
  { 'public/css/style.min.css': 'public/css/style.min.css' }

  concat:
  { 'public/css/style.min.css': [ 'public/css/a.css', 'public/css/b.css' ],
  'public/js/script.min.js':
   [ 'public/js/a.js',
     'public/js/b.js',
     'public/js/c.js' ] }

  uglify:
  { 'public/js/script.min.js': 'public/js/script.min.js' }

  requirejs:
  {}

Done, without errors.

지정한 HTML파일에서 usemin 주석을 사용한 부분을 찾아내서 해당 파일들을 찾아온 것을 볼 수 있다. 이 출력내용을 보면 추측할 수 있지만 useminPrepareconcat, uglify, cssmin, requirejs을 위한 설정을 해주는 단계이다. 즉, 실제 useminPrepare만 실행하면 아무런 일도 일어나지 않고 concat, uglify, cssmin, requirejs을 실행할 대상 파일과 설정등을 useminPrepare가 HTML 파일에 설정한 내용을 바탕으로 자동으로 해준다. 그래서 실제로 사용할 때는 grunt useminPrepare concat uglify cssmin처럼 사용해야 실제로 병합되거나 압축된 파일이 만들어진다. grunt useminPrepare concat uglify cssmin를 사용한 뒤에 public/js/script.min.jspublic/css/style.min.css파일을 열어보면 다음과 같은 파일이 만들어진다.

var from_file_A=function(){return"A"},from_file_B=function(){return"B"},from_file_C=function(){return"C"};
html,body{margin:0;padding:0;border:0}a{text-decoration:none}

html에서 주석만으로 지정한 파일을 자동으로 한 파일로 합쳐준다.

usemin

usemin에서는 HTML에서 주석으로 지정한 부분을 병합/압축된 파일을 참조하도록 교체해준다.

...

grunt.initConfig({
  'useminPrepare': {
    html: 'views/index.html'
  },
  'usemin': {
    html: ['views/*.html']
  }
});

...

위처럼 usemin에 대한 설정을 추가한다. useminPrepare와 동일한 파일이라면 html: ['<%= useminPrepare.html%>']와 같이 지정해서 useminPrepare에 사용한 html파일설정을 그대로 가져올 수도 있다. css옵션으로 css파일에서도 동일한 교체작업을 수행할 수 있다는데 이건 안해봐서 정확히 어떻게 쓰는지 모르겠고 css에서는 어떤때 교체를 해야하는지도 잘 모르겠다. 그외에도 dirs로 참조파일을 검색할 폴더를 지정하거나 참조파일에 대한 기본 디렉토리를 basedir로 변경할 수 있다. 이제 grunt usemin을 사용하면 HTML이 다음과 같이 변경된다.

<html>
  <head>
    <title>usemin example</title>

    <link rel="stylesheet" href="../public/css/style.min.css" media="all">
  </head>
  <body>
    <!-- html code here -->
    <!-- html code here -->
    <!-- html code here -->
    <!-- html code here -->

    <script src="../public/js/script.min.js"></script>

  </body>
</html>

보다시피 깔끔하게 압축된 파일만 참조하도록 변경됐다.




실제로 써보니 상당히 유연해서 아주 사용하기 쉽다. 개발할때는 자연스럽게 모듈화해서 여러 파일로 나누어서 개발할 수 있고 단순히 HTML파일의 주석만으로 설정할 수 있기 때문에 병합/압축에 사용할 파일을 개발하면서 간단히 추가하고 뺄 수 있다. 그리고 병합/압축을 하는 과정과 준비하는 과정(useminPrepare)이 분리되어 있기 때문에 사용하는 기능만을 선택적으로 사용할 수 있다. 개발은 다수의 파일을 인클루드해서 개발하고 최종적으로 프로덕션에 내보낼 때만 Grunt로 변환해서 내보내면 된다.

Grunt 명령어를 많이 사용해야 하는데 Grunt가 별칭기능을 제공하므로 다음과 같이 build등의 이름으로 usemin을 사용하는 과정을 모두 적어두면 grunt build같은 명령어로 이 모든 과정을 한꺼번에 실행할 수 있다.

grunt.registerTask('build', ['useminPrepare', 'concat', 'uglify', 'cssmin', 'usemin']);
2013/06/17 01:17 2013/06/17 01:17