Outsider's Dev Story

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

자바스크립트 빌드 도구 Grunt

약간의 귀차니즘으로 노드등의 자바스크립트 프로젝트에는 그동안 빌드도구를 사용하지 않고 있었다. 자바스크립트쪽 빌드도구에는 Cake(이전 글 참조)나 Jake등이 있는데 Grunt를 사용하기로 결정했다.(Grunt를 선택하게 된 주된 이유는 스캐폴딩때문이긴 한데 이 얘기는 나중에...) Jake는 한번도 안써보았고 Cake는 약간 써보긴 했는데 Make류이기 때문에 아주 유연하게 테스크를 작성할 수 있기는 하지만 일일이 다 해야되는 귀차니즘도 좀 있는데 Grunt가 기본으로 제공하는 부분이나 확장성도 괜찮아 보여서 일단 써보기로 했다. jQuery도 1.8부터는 Grunt를 사용하고 있다. (분위기 파악을 다 못하긴 했는데 4.0을 준비하고 있어서인지 몰라도 문서화가 잘 안되어있다. 과거버전 기준인것도 있고 아예 안채워진것들도 있고 ㅠㅠ) 참고로 Grunt는 자바스크립트용 빌드도구다.


Grunt 설치
Grunt는 Node.js로 만들어졌고 npm에 등록이 되어 있으므로 일반적인 Node.js 커맨드라인 도구처럼 글로벌로 설치하면 된다.(당연히 Node.js는 설치가 되어 있어야 한다.)

$ npm install -g grunt
$ grunt --version
grunt v0.3.17

이 글을 쓰는 시점에 최신 버전은 0.3.17이고 곧 0.4가 나올듯하다.(0.4에 얼마나 바뀌는지는 잘 모르겠다.) 버전이 정상적으로 출력된다면 Grunt를 사용할 준비는 다 된 것이다.


템플릿
Grunt는 init 명령어를 통한 템플릿기능으로 프로젝트를 시작하기 위한 스캐폴딩 파일과 구조를 자동으로 생성해준다.  즉, 어떤 프로젝트에 기반이 되는 파일과 구조를 프로젝트에 맞게 자동으로 만들어주고 있는데 현재 Grunt(0.3.17) 기준으로 다음의 템플릿을 지원하고 있다.

  • commonjs - CommonJS 프로젝트
  • gruntplugin - Grunt Plugin 프로젝트
  • jquery - jQuery Plugin 프로젝트
  • node - Node.js 프로젝트
  • gruntfile - grunt.js
이렇게 5가지인데 앞의 4개는 프로젝트 특성에 맞게 소스파일, 테스트파일, 라이센스파일등을 자동으로 생성해주고 마지막 grunt.js는 Make를 사용하려면 Makefile이 필요하듯이 Grunt를 사용하려면 각 프로젝트에 grunt.js라는 파일이 있어야 하는데 이 파일을 기본적인 내용으로 만들어주는 것이다. 템플릿을 사용하려면 grunt init:템플릿이름의 명령어를 사용하면 된다.(위 리스트에서 앞에가 그런트가 사용하는 템플릿의 이름이고 뒤에는 설명이다.)

grunt init:node로 템플릿을 생성하는 화면

위 화면처럼 Node.js 프로젝트를 하려면 grunt init:node라고 하면 프로젝트에 필요한 정보를 묻는 프롬프트가 나오고 필요한대로 입력하면 그에 맞게 템플릿 파일이 생성된다. 괄호안에 있는 값이 기본값이므로 기본값을 그냥 사용하려면 엔터만 쳐도 되고 새로운 값을 입력하거나 빈값으로 하려면 none을 입력하면 된다. 템플릿 명이 기억이 나지 않으면 grunt init만 입력하면 이름이 지정안되었다고 오류 내면서 사용할 수 있는 템플릿이름을 보여준다.

  • .npmignore
  • LICENSE-MIT
  • README.md
  • grunt.js
  • package.json
  • lib/
    • example.js
  • test/
    • example_test.js

node 템플릿을 기준으로 위와같은 구조의 파일들을 자동으로 만들어준다. 기본적인 프로젝트 구조들은 매번 작업할 때마다 해야하는 귀찮은 작업이기 때문에 이 기능은 좋은 시작점이 된다.


grunt.js
템플릿 기능은 사실 좀 부가기능으로 보이고 Grunt가 빌드도구 이므로 프로젝트 중에 계속 반복해야하는 작업들을 Grunt로 편리하게 사용할 수 있다. 프로젝트에 Grunt를 사용하려면 프로젝트의 루트경로에 grunt.js라는 파일이 존재해야 한다. 다음은 grunt init:gruntfile을 실행하면 자동으로 생성되는 grunt.js 파일이다.

/*global module:false*/
module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    pkg: '<json:package.json>',
    meta: {
      banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
        '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
        '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' +
        '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
        ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */'
    },
    lint: {
      files: ['grunt.js', 'lib/**/*.js', 'test/**/*.js']
    },
    qunit: {
      files: ['test/**/*.html']
    },
    concat: {
      dist: {
        src: ['<banner:meta.banner>', '<file_strip_banner:lib/<%= pkg.name %>.js>'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    min: {
      dist: {
        src: ['<banner:meta.banner>', '<config:concat.dist.dest>'],
        dest: 'dist/<%= pkg.name %>.min.js'
      }
    },
    watch: {
      files: '<config:lint.files>',
      tasks: 'lint qunit'
    },
    jshint: {
      options: {
        curly: true,
        eqeqeq: true,
        immed: true,
        latedef: true,
        newcap: true,
        noarg: true,
        sub: true,
        undef: true,
        boss: true,
        eqnull: true,
        browser: true
      },
      globals: {}
    },
    uglify: {}
  });

  // Default task.
  grunt.registerTask('default', 'lint qunit concat min');

};

소스가 길기는 한데 일반적인 Node.js용 자바스크립트 파일이라고 생각하면 된다. Node.js로 만들어졌으므로 module.exports = function(grunt) {}로 전체 소스를 감싸주어야 하고 실제 프로젝트의 설정과 테스크설정은 grunt.initConfig()안에 모두 들어간다. grunt.js파일에서 grunt.initConfig()는 딱 한번만 와야한다. grunt.initConfig()의 각 설정부분을 하나씩 살펴보자.

pkg

pkg: '<json:package.json>',

pkg는 이름에서 알수 있듯이 패키지의 약자인데 CommonJS 스펙에 정의된 package.json 파일을 가리키는 용도로 사용한다. <json:package.json>와 같이 사용한 것은 grunt가 제공하는 json 디렉티브인데 실제로 현재 프로젝트의 package.json파일을 읽어온다. package.json에는 보통 프로젝트에 대한 메타정보가 들어있는데 이 정보를 그대로 읽어오기 때문에 <%= pkg.name %>와 같은 식으로 package.json에 정의된 내용을 가져와서 사용할 수 있다.

meta

meta: {
  banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
    '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
    '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' +
    '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
    ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */'
}

메타정보인데 내부 속성으로 banner만 가지고 있다. banner에는 자바스크립트 파일을 합치거나(concatenation) 압축했을 때(minify) 자바스크립트 파일 상단에 자동으로 넣을 주석을 지정한다. 앞에서도 얘기했듯이 <%= %>같은 스크립틀릿을 사용할 수 있고 여기서는 자바스크립트 함수도 실행할 수 있다.

lint

lint: {
  files: ['grunt.js', 'lib/**/*.js', 'test/**/*.js']
}

자바스크립트 파일을 lint할 대상을 배열로 지정한다. 여기서 lint는 더글라스 크록포드의 lint라기 보다는 코드를 검사해준다는 면에서 범용적인 의미의 동사로 사용한 것으로 보인다. 실제 검사는 JSHint를 사용해서 검사가 이뤄진다. 그리고 대상을 지정할 때 위에서는 files라는 이름을 주었는데 원하는 이름을 사용해서 여러가지로 지정할 수도 있다.

lint: {
  all: [],
  client: [],
  server: []
}

이런식으로 대상의 세트를 여러가지로 만들 수 있다. lint의 실행은 grunt lint:대상이름 으로 실행한다. grunt lint:all을 하면 all에 지정된 대상 파일들을 lint하고 이름을 주지않고 grunt lint하면 모든 대상을 다 lint한다.

qunit

qunit: {
  files: ['test/**/*.html']
}

jQuery의 테스트 프레임워크인 QUnit의 대상파일을 지정한다. grunt qunit으로 실행할 수 있는데 원래의 QUnit이 브라우저로 실행하는데 반해서 grunt에서는 브라우저 없이 PhantomJS를 사용해서 실행한다.(지정한 HTML파일을 띄운것처럼) 그러므로 Qunit을 사용하려면 PhantomJS가 설치되어 있어야 한다. lint와 마찬가지로 대상파일의 세트를 여러가지로 지정할 수 있다.

concat / min

concat: {
  dist: {
    src: ['<banner:meta.banner>', '<file_strip_banner:lib/<%= pkg.name %>.js>'],
    dest: 'dist/<%= pkg.name %>.js'
  }
},
min: {
  dist: {
    src: ['<banner:meta.banner>', '<config:concat.dist.dest>'],
    dest: 'dist/<%= pkg.name %>.min.js'
  }
}

concat은 자바스크립트 파일을 합치는 것을 의미한다. 개발과정에서는 모듈화로 파일을 분리해서 개발하지만 배포단계에서는 하나의 파일로 합쳐서 나가는것이 좋기 때문에 합치는 과정을 말하고  min은 불필요한 공백이나 줄바꿈을 없애서 파일사이즈를 줄이는걸 말한다.(난독화와는 다르다.) 그래서 concat과 min에서는 설정이 depth가 한단계 더 있다. src에 설정한 파일들을 dest로 내보내게 되고 이러한 설정을 당연히 여러세트로 만들 수 있다.

watch

watch: {
  files: '<config:lint.files>',
  tasks: 'lint qunit'
}

watch는 files에 지정된 파일들의 변경사항을 감시하다가 변경사항이 발생하면 tasks에 지정된 동작을 실행한다. 그러므로 앞의 설정과는 다르게 여러 세트를 설정할 수는 없다. 여기서는 Grunt의 디렉티브를 사용해서 lint 설정에서 files로 지정한 파일들을 감시하다가 변경되면 lint와 qunit을 실행하도록 했다.

jshint

jshint: {
  options: {
    curly: true,
    eqeqeq: true,
    immed: true,
    latedef: true,
    newcap: true,
    noarg: true,
    sub: true,
    undef: true,
    boss: true,
    eqnull: true,
    browser: true
  },
  globals: {}
}

처음에 볼때 헷갈리던 부분인데 JSHint 검사의 설정사항이고 이는 위에서 본 lint를 실행할때 적용된다. 즉, grunt jshint같은건 없다. 각 JSHint의 옵션은 JSHint의 문서를 참고하면 되고 globals에는 전역으로 사용할 변수등을 지정한다. 예를 들어 다른 라이브러리에서 자동으로 설정되는 전역변수나 require()등으로 불러와지는 변수들이 있는데 이런 경우 lint가 선언안되었다고 오류를 내보내기 때문에 globals에 설정해 주어야 한다. 예를 들어 mocha를 사용하는데 describe에서 오류가 난다면 globals: { describe: true } 처럼 설정하면 된다. lint에서 타겟을 여러개로 주었는데 JSHint의 설정도 다르게 적용하고 싶다면 다음처럼 하면 된다.

grunt.initConfig({
  lint: {
    a: [],
    b: []
  },
  jshint: {
    // Defaults.
    options: {},
    globals: {},
    a: {
      options: {},
      globals: {}
    },
    b: {
      options: {},
      globals: {}
    }
  }
});

uglify

uglify: {}

앞에서 지정한 min에서 사용할 UglifyJS의 설정이다. lint를 위해서 JSHint를 설정한 것이라 같다고 보면 된다.

test

test: {
  files: ['test/**/*.js']
}

위의 기본생성된 grunt.js에는 나오지 않았었지만 테스트파일에 대한 설정도 할 수 있다. 앞에서 qunit부분이 클라이언트측 자바스크립트 테스트라면 test는 서버사이드 자바스크립트 테스트이다. 테스트는 Nodeunit을 사용하는데 mocha를 쓰는 나로써는 테스트명령어를 임의로 할수 없는게 좀 불만이다.


태스크(Task)
설정을 간단히 살펴보았는데 앞에서도 좀 언급을 했지만 Grunt는 기본으로 제공하는 테스크들이 있다.

  • init
  • lint
  • concat
  • min
  • qunit
  • watch
  • test
  • server
이 태스크들은 grunt 태스크이름이나 grunt 태스크이름:타겟이름 으로 실행할 수 있다. 앞에서 설정이 테스트이름을 그대로 사용한 설정과 일반 설정이 함께 있어서 처음엔 좀 헷갈린다. 앞에서 설명하지 않은것은 server태스크뿐인데 이는 특정 폴더를 기준으로 정적서버를 띄워주는 기능인데 설계상 오류가 아닌가 싶을정도로 동작이 이상하다. grunt는 더이상 실행항 태스크가 없으면 종료가 되는데 그 때문에 grunt server를 하면 시작하자마자 바로 종료되므로 server 테스크의 문서에서는 소스를 넣어주라고 가이드하고 있는 그런짓을 하느니 그냥 Locally같은 걸 쓰고 말겠다.


플러그인
여기까지는 Grunt가 기본으로 제공하는 기능들이고 Grunt가 API를 제공하기 때문에 이를 이용해서 플러그인을 만들 수 있다. 플러그인 목록은 Grunt 홈페이지에 나와있고 Grunt가 제공하지 않는 기능들을 내장 태스크처럼 사용할 수 있도록 설정할 수 있다. 자세한 사용방법은 각 플러그인의 가이드를 참고해야 하는데 실제로 Grunt 플러그인은 NPM 모듈들이다. 그래서 프로젝트 로컬에 NPM 모듈을 설치한 뒤 다음과 같이 grunt.js에서 불러들어서 사용하는 구조이다.

grunt.loadNpmTasks('플러그인 이름');

이 방법이 그리 깔끔한 방법인지 약간 고민되긴 하지만 npm 모듈을 설치해야하니 package.json에 의존성을 명시해서 설치하도록 해놓고 함께 사용하는게 좋은 방법일 것 같다.

별도로 직접 작성한 플러그인은 다른 폴더에 작성한 후 다음 명령어로 불러와서 사용할 수도 있다.

grunt.loadTasks('폴더명')


별칭
Grunt에서는 테스크에 대한 별칭(Alias)을 지정할 수 있다. 위의 grunt.js 파일에서 마지막에 다음과 같은 라인이 있는 것을 볼 수 있다.

grunt.registerTask('default', 'lint qunit concat min');

기본 동작에 대한 등록인데 사실 이는 lint, qunit, concat, min 태스크에 대한 별칭을 default로 준 것이고 default는 예약된 별칭으로 grunt만 입력하면 grunt default와 동일하다. 그렇기 때문에 자주 사용하는 것을 default에 입력하면 편리하고 말그대로 별칭이기 때문에 한꺼번에 자주 사용하는 것들은 묶어서 별칭을 만들어두면 더 편리하게 사용할 수 있다.
2013/01/14 02:56 2013/01/14 02:56