Outsider's Dev Story

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

Grunt에 사용자 템플릿 추가하기

Grunt에 대한 간단한 소개글을 올렸었는데 개인적으로는 Grunt를 선택하게 된 주된 이유 중 하나가 템플릿 기능이었다.

개인 프로젝트를 이것저것 하는데 제대로 만들지 못하고 테스트하고 버리는게 많아서 프로젝트를 셋팅하는데 계속해서 반복해야 하는 작업들이 무척 귀찮았다. 프로젝트 특성에 따라서 이런 작업들이 다양하게 있겠지만 아주 간단하게는 git 저장소로 만든 후 .gitignore를 매번 만드는 게 싫었고(뭐뭐 적어야 하는지 매번 생각도 나지 않는다) jQuery나 bootstrap 같은 파일을 매번 받는게 귀찮아서 이런걸 관리해 줄 도구를 오랫동안 찾고 있었다.

처음에는 Typesafe 스택에서 사용하는 giter8을 좀 봤는데 템플릿 기능의 용도이기는 하지만 매번 github에 저장소를 만들어야 하는게 싫었다. 좀 규모가 큰 템플릿이라면 다른 사람과 공유할 수도 있어서 좋겠지만 간단한 용도에 일일이 Github 저장소를 만드는 것은 번잡스럽게 느껴졌고 네트워크가 안될때도 사용하고 싶었다. 그 다음에는 트위터의 bower를 보았는데 어느정도 내가 생각하던 기능이기도 한데 클라이언트용만 되는게 내 목적에는 딱 맞아보이지 않았다.(사실 많이 써보진 않았다.) 그러다가 눈에 띈게 Grunt의 init기능이었다. 이전 글에서 grunt의 템플릿 기능을 좀 살펴보았지만 여기에 추가적으로 커스텀 템플릿을 만들어서 사용할 수 있다.


내장 템플릿 덮어쓰기
Grunt는 전역으로 설치한 npm 모듈이므로 Grunt설치경로/tasks/init에 내장 템플릿의 파일들이 있는데 다음과 같은 구조로 되어 있다.

  • commonjs.js
  • commonjs/
    • rename.json
    • root/
    gruntfile.js
  • gruntfile/
    • root/
    gruntplugin.js
  • gruntplugin/
    • rename.json
    • root/
    jquery.js
  • jquery/
    • rename.json
    • root/
    node.js
  • node/
    • rename.json
    • root/

뒤에 슬래시(/)가 있는 것을 폴더다. 템플릿 이름과 동일한 js파일과 디렉토리가 한 쌍으로 존재하고 있다. grunt init을 실행하면 실행한 템플릿이름의  js 파일이 실행되고 그에 따라 사용하는 템플릿 파일들은 같은 이름의 폴더 아래 root 폴더에 존재한다. rename.json 파일들은 root안에 있는 템플릿 파일이름을 프롬프트에서 입력받은 속성값으로 대체하고 싶을때 그 매핑규칙을 작성하는 파일이다. 이 파일들을 보면 템플릿을 어떻게 작성하는지 대충이나 감을 잡을수 있다. 아쉽게도 이부분에 대한 문서화는 거의 안되어 있어서 API를 추측성으로 하거나 소스를 살펴봐야한다.

내장된 템플릿이 맘에 들지 않을 경우 저 파일들을 직접 수정해도 되지만 Grunt도 계속 업데이트하고 해야하니 별로 좋은 방법은 아니고 대신 Grunt가 덮어쓸수 있도록 제공하고 있다. 사용자 홈디렉토리에 ~/.grunt/tasks/init 디렉토리를 생성하고 위의 템플릿파일을 복사해서 수정하면 기존의 템플릿이 덮어써진다.


사용자 정의 템플릿 추가하기
위에 덮어쓴 것과 마찬가지로 ~/.grunt/tasks/init 디렉토리에 원하는 템플릿을 생성해서 넣으면 개인적으로 필요한 템플릿을 추가할 수 있다.

gitignore 템플릿
그래서 만들었다. 앞에서도 얘기했지만 git 저장소를 만들때마다 매번 .gitignore 파일을 만드는게 무척 귀찮았다. 물론 ~/.gitignore_global을 만들어도 되겠지만 혼자하는게 아니면 프로젝트에 각각 포함되는게 더 맞아보인다. 뭐뭐 추가하는지 다 외우고 있지도 않기 때문에 매번 이전 프로젝트꺼를 복사하곤 했는데 이를 한곳에서 관리하면서 복사만 해다가 썼으면 좋겠다. 물론 Github에서 저장소를 만들때 gitignore파일을 추가해주지만  나는 보통 Github에서 만들고 클론받아서 쓰기보다는 로컬에서 만들어서 작업하다가 버리던가 Github에 올리던가 하기 때문에 나에게는 해결책이 되지 않았는데 Grunt를 이용하면 해결할 수 있다.(물론 비슷한 쉘스크립트같은걸 써도 되겠지만...)

다음은 ~/.grunt/tasks/init/git.js 파일이다.

// Basic template description.
exports.description = 'Create basic files for git repository.';

// Template-specific notes to be displayed before question prompts.
exports.notes = 'added git files like .gitignore'

// Any existing file or directory matching this wildcard will cause a warning.
exports.warnOn = '.gitignore';

// The actual init template.
exports.template = function(grunt, init, done) {

  grunt.helper('prompt', {}, [
  ], function(err, props) {
    // Files to copy (and process).
    var files = init.filesToCopy(props);

    // Actually copy (and process) files.
    init.copyAndProcess(files, props);

    // All done!
    done();
  });

};

다른 템플릿파일을 참고해서 간단히 만들었다. exports.notes부분은 실행시 상단에 나오는 안내메시지고 exports.warnOn는 특정 파일이나 디렉토리가 있을 경우 경고하도록 한 것이다. 여기서는 .gitignore 파일을 만들 거라서 이 파일이 이미 있을 경우에는 바로 덮어쓰지 않도록 한 것이다.(--force옵션을 주면 덮어쓸 수 있다.) exports.template에서 실제로 프롬프트와 실행할 작업을 작성하는데 딱히 프롬프트로 입력받을 값을 없기 때문에 모두 지우고 그대로 root 폴더 아래 있는 파일들을 복사하도록 했다.(init.filesToCopy로 복사대상을 찾고 init.copyAndProcess에서 복사를 한다.) ~/.grunt/tasks/init/git/root/ 아래에는 .gitignore파일만 넣어놨다.

grunt init:git을 실행한 화면

이제 grunt init:git 이라고 하면 위처럼 현재위치에 .gitignore 파일을 복사한다.

jQuery 템플릿
내친김에 필요하다고 생각하던걸 하나 더 만들었다. 웹프로젝트를 하면 거의 jQuery를 쓰기 때문에 매번 jQuery사이트에 가서나 JQuery CDN에서 wget으로 다운받는게 귀찮아서 Grunt 템플릿으로 만들었다.

다음은 ~/.grunt/tasks/init/jq.js 파일이다. 이름을 jq라고 한 이유는 내장 템플릿에 jquery가 있기 때문이고 ~/.grunt/tasks/init/root/ 아래에는 아무것도 두지 않았다.

var path = require('path')
  , fs = require('fs')
  , http = require('http');

// Basic template description.
exports.description = 'Download jquery file';

// Template-specific notes to be displayed before question prompts.
exports.notes = 'Downlode jquery you specify';

// Any existing file or directory matching this wildcard will cause a warning.
exports.warnOn = 'jquery.*.js';
exports.warnOn = '*/jquery.*.js';

// The actual init template.
exports.template = function(grunt, init, done) {

  var LASTEST_VERSION = '1.8.3';

  grunt.helper('prompt', {}, [
    // Prompt for these values.
    {
      name: 'version',
      message: 'What version for jQuery do you need?',
      default: LASTEST_VERSION
    },
    {
      name: 'min',
      message: 'Do you want minified jQuery file?',
      default: 'Y/n',
      warning: 'Yes: Minified file. No: Non-minified file.'
    },
    {
      name: 'targetPath',
      message: 'Where do you want to download a jQuery file? (relative path)',
      default: './'
    }
  ], function(err, props) {
//    props.version = semver.valid(props.version) || LASTEST_VERSION;
    props.min = /y/i.test(props.min);
    props.targetPath = path.relative('./', props.targetPath);

    // initialize constants
    var JQUERYPATH = path.join(__dirname, 'jq', 'root', props.version + (props.min ? '-min' : ''));
    var JQUERYFILE = 'jquery-' + props.version + (props.min ? '.min' : '') + '.js'

    fs.exists(JQUERYPATH, function(exists) {
      if (exists) {
        writeFileThenEnd();
      } else {
        getJQueryFile(writeFileThenEnd);
      }
    });

    var writeFileThenEnd = function() {
      // Files to copy (and process).
      var files = init.filesToCopy(props);
      for (file in files) {
        var toRemovePrefix = path.join(props.version + (props.min ? '-min' : ''), '/');
        if (file.indexOf(toRemovePrefix) === 0) {
          var newKey = file.replace(toRemovePrefix, path.normalize(props.targetPath) + '/');
          files[newKey] = files[file];
          delete files[file];
        } else {
          delete files[file];
        }
      }

      // Actually copy (and process) files.
      init.copyAndProcess(files, props);

      // All done!
      done();
    };

    var getJQueryFile = function(callback) {
      http.get('http://code.jquery.com/' + JQUERYFILE, function(response) {
        if (response.statusCode === 200) {
          fs.mkdir(JQUERYPATH, function(err) {
            var file = fs.createWriteStream(JQUERYPATH + '/' + JQUERYFILE);
            response.on('data', function(chunk) {
              file.write(chunk);
            }).on('end', function() {
              file.end();
              callback();
            });
          });
        } else {
          grunt.log.writeln().fail("Aborted due to can't find the jQuery file. Check the version - " + props.version);
        }
      });
    }
  });
};

소스가 훨씬 복잡해 졌는데 실행하면 사용할 jQuery 버전과 압축버전을 사용할 것인지를 묻고 어디에 파일을 둘 것인지를 묻는다. 입력받은 jQuery파일을 jQuery CDN에서 다운받아서(압축버전이면 min.js를...) root폴더 아래에 버전마다 폴더를 만들어서 복사한다. 그리고 해당 버전을 지정한 위치에 복사한다.(전체복사를 안하기 위해서 복사대상 파일을 약간 조작했다.) 기본에 한번 사용한 버전은 root 폴더아래 존재하기 때문에 jQuery CDN에서 받아오지 않고 로컬에 있는 파일을 그냥 사용한다.

사용자 삽입 이미지

이제 grunt init:jq라고 실행하면 위처럼 jQuery 파일을 복사한 것을 볼 수 있다.


좀 더 써봐야 알겠지만 일단은 만족스러워 보인다. 개인용도로는 프롬프트가 약간 귀찮기도 하지만 일일이 옵션으로 하는것도 헷갈릴것 같기도 해서 프롬프트도 괜찮은것 같다. 뭐 써보면서 bootstrap용이나 비슷한 용도로 추가하면서 써 볼 생각이다.
2013/01/15 17:41 2013/01/15 17:41