RecoPick



안녕하세요. RecoPick팀의 김군우입니다.

JavaScript, CSS 등을 이용해 Front-end 개발을 하시는 분들도 많지만, CoffeeScript, TypeScript, Stylus, LESS 등 간결한 문법, 확장된 기능을 좋아해 이런 언어들을 Front-end 개발에 활용하시는 분들도 많이 계시죠? 저도 CoffeeScript, Stylus 참 좋아합니다.

그런데 이런 언어들을 활용해 개발할 때는 참 즐거운데, 막상 그렇게 개발한 코드들을 브라우저에서 확인하거나 배포하려고 할 때 매번 변환 작업이라든지, HTML에 <script>, <link> 요소들을 개발 환경, 배포 환경 각각 다르게 될 때에 대한 처리 등등의 귀찮은 작업들이 필요합니다. 이런 귀찮은 작업들로 인해, 이런 언어들의 편리함에도 불구하고 다시 JavaScript, CSS로 돌아가 보신 적은 없으신가요? 이 글은 그런 분들을 위한 팁입니다!

grunt란?



grunt는 JavaScript/CoffeeScript용 빌드 도구입니다. 이는 JavaScript/CoffeeScript 등으로 grunt가 제공하는 형식에 맞추어 어떤 작업을 코드로 만든 후 커맨드 라인 환경에서 실행할 수 있게 해줍니다.

예를 들자면, 내가 만든 main.coffee라는 CoffeeScript 파일을 main.js라는 JavaScript로 변환하자면 보통 다음과 같이 파일명을 명시해주어야 합니다.

$ coffee -c main.coffee

그리고 CoffeeScript 파일이 두 개 이상이라면

$ coffee -c main.coffee
$ coffee -c sub1.coffee
$ coffee -c sub2.coffee

이와 같이 여러 개의 명령이 필요하며 매번 파일명을 명시해야 하는데, grunt를 활용하면

$ grunt coffee

이 하나의 명령으로 위의 작업들을 간단히 끝낼 수 있습니다.

위의 예제를 비롯하여 CoffeeScript, Stylus 등을 JavaScript, CSS 개발하듯 grunt를 활용하는 방법을 살펴보겠습니다.

grunt 설치

grunt 설치는 이미 공식 사이트나 한글로 정리된 사이트들도 많으니 따로 다루지 않을 예정입니다. 공식 사이트에서는 gruntjs.com Getting started 페이지를 참고하세요.

참, grunt 설치를 위해서는 node.js의 설치가 선행되어야 합니다. node.js의 설치도 마찬가지로 생략할게요.

CoffeeScript 변환하기

grunt를 설치하셨다면 이제 CoffeeScript 변환하기부터 시작해보죠.

gruntjs.com Getting started 페이지를 따라 grunt 설치 및 예제 Gruntfile을 만들어보셨다면 프로젝트 폴더에 Gruntfile.js라는 파일을 만드셨을것입니다. 테스트를 위해 저는 다음과 같이 프로젝트 폴더를 구성해보았습니다.

scripts/
  main.coffee
styles/
  main.styl
public/
  main.html
  dist/
Gruntfile.coffee
package.json

scripts 디렉토리에는 작업할 CoffeeScript, styles 디렉토리에는 작업할 Stylus가 들어가고 dist 폴더에는 변환하여 생성될 JavaScript, CSS 파일이 들어갈 예정입니다.

자 이제 scripts/main.coffee 파일이 public/dist/main.js로 변환되도록 Gruntfile의 task를 만들어보겠습니다. scripts/main.coffee는 다음과 같이 간단하게 작성했습니다.

alert 'Hello'

다음은 Gruntfile의 골격입니다. (Gruntfile은 CoffeeScript로도 잘 동작합니다.) 주석 처리해놓은 grunt-contrib-uglify 관련 코드는 gruntjs.com의 Getting started 페이지에 포함된 코드이며, 이후 작성할 코드에 참고할 목적으로 남겨두었습니다.

module.exports = (grunt) ->
  # Project configuration.
  grunt.initConfig {
  }

  # Load the plugin that provides the "uglify" task.
  # grunt.loadNpmTasks('grunt-contrib-uglify')

  # Default task(s).
  # grunt.registerTask('default', ['uglify'])

우선 grunt의 CoffeeScript 변환 플러그인인 grunt-contrib-coffee를 설치합니다.

$ npm install grunt-contrib-coffee --save-dev

그리고 grunt-contrib-coffee을 사용하여 CoffeeScript 변환 Task를 만듭니다.

module.exports = (grunt) ->
  # Project configuration.
  grunt.initConfig {
    coffee:
      compile:
        files:
          'public/dist/main.js': 'scripts/main.coffee'
  }

  # Load the plugin that provides the "coffee" task.      
  grunt.loadNpmTasks('grunt-contrib-coffee')

  # Default task(s).
  # grunt.registerTask('default', ['uglify'])

이제 grunt coffee라는 명령으로 scripts/main.coffee 파일이 public/dist/main.js로 변환됩니다.

$ grunt coffee
Running "coffee:compile" (coffee) task
File public/dist/main.js created.

Done, without errors.

그럼 생성된 JavaScript 파일을 브라우저에서 테스트해볼까요? public/main.html 파일을 간단히 만들어보죠.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="utf-8">
    <title>테스트</title>
  </head>
  <body>
    <p>안녕</p>
    <script src="dist/main.js"></script>
  </body>
</html>

만든 main.html 파일을 브라우저에서 열어봅니다.




이제 다음과 같은 순서로 CoffeeScript로 작성한 파일을 브라우저에서 확인할 수 있게 되었습니다.

  1. scripts/main.coffee CoffeeScript 파일 작성
  2. grunt-contrib-coffee task 생성
  3. HTML 파일에 dist/main.js 경로의 script include 처리
  4. grunt coffee 명령으로 public/dist/main.js 생성
  5. 브라우저에서 확인
  6. 이후 1, 4, 5 순으로 CoffeeScript 파일 개발 진행

watch를 통해 불필요한 반복 작업 제거

이제 매번 4번(grunt coffee 명령)을 실행해야 하는 불편함을 해결해보겠습니다. 바로 grunt-contrib-watch 플러그인을 통해서인데요. 이 플러그인은 파일의 변경을 지켜보고 있다가 변경이 되면 특정 grunt task를 실행해주는 플러그인입니다.

플러그인의 설명을 보셨으면 아마 어떻게 불편한 4번 명령을 없앨지에 대해 느낌이 오실 것 같은데요. 바로 scripts/main.coffee 파일이 변경되면 자동으로 grunt coffee 명령이 실행되도록 grunt-contrib-watch를 통해 tasks를 작성하는 것입니다.

우선 grunt-contrib-watch를 설치하고요.

$ npm install grunt-contrib-watch --save-dev

Gruntfile을 업데이트합니다.

module.exports = (grunt) ->
  # Project configuration.
  grunt.initConfig {
    watch:
      scripts:
        files: ['scripts/**']
        tasks: ['coffee']
        options:
          spawn: false
    coffee:
      compile:
        files:
          'public/dist/main.js': 'scripts/main.coffee'
  }

  # Load the plugin that provides the "coffee" task.
  grunt.loadNpmTasks('grunt-contrib-coffee')

  # Load the plugin that provides the "watch" task.
  grunt.loadNpmTasks('grunt-contrib-watch')

  # Default task(s).
  # grunt.registerTask('default', ['uglify']);

watch라는 task를 만들었습니다.

자, 이제 watch task를 실행해보겠습니다.

$ grunt watch
Running "watch" task
Waiting...

명령이 끝나지 않은 채 위와 같은 메시지가 표시됩니다. 저 커맨트 창은 그대로 둔 채 scripts/main.coffee 파일을 한번 고쳐보겠습니다.

alert 'Hello, watch'

그럼 위의 커맨드 창에서 다음과 같은 메시지가 표시됩니다.

$ grunt watch
Running "watch" task
Waiting...OK
>> File "scripts/main.coffee" changed.


Running "coffee:compile" (coffee) task
File public/dist/main.js created.

Running "watch" task
Completed in 0.043s at Sun Dec 29 2013 15:12:16 GMT+0900 (KST) - Waiting...

자동으로 coffee task가 실행되어 public/dist/main.js가 다시 생성되었습니다.

만든 main.html 파일을 브라우저에서 다시 열어보면,


grunt watch를 실행해놓은 상태라면 매번 grunt coffee를 실행할 필요가 없어지게 되었습니다.

실전 CoffeeScript 개발

이제 원리는 알았고 실전에서 쓰는 예제를 한번 보시죠. 실전이라는게 별건 아니고 jQuery 등의 라이브러리를 쓰고, CoffeeScript 파일을 여러개로 쪼개 개발하는 경우입니다.

디렉토리 구조를 보면

scripts/
  lib/
    jquery-1.10.2.min.js
  main.coffee
  sub1.coffee
styles/
  main.styl
public/
  main.html
  dist/
Gruntfile.coffee
package.json

scripts/sub1.coffee, 그리고 jQuery 라이브러리(scripts/lib/jquery-1.10.2.min.js)가 추가되었습니다.

이에 따라 Gruntfile도 업데이트가 필요하게 되었습니다.

module.exports = (grunt) ->
  # Project configuration.
  grunt.initConfig {
    watch:
      scripts:
        files: ['scripts/**']
        tasks: ['coffee']
        options:
          spawn: false
    coffee:
      compile:
        files:
          'public/dist/main.js': ['scripts/main.coffee', 'scripts/sub1.coffee']
  }

  # Load the plugin that provides the "coffee" task.
  grunt.loadNpmTasks('grunt-contrib-coffee')

  # Load the plugin that provides the "watch" task.
  grunt.loadNpmTasks('grunt-contrib-watch')

  # Default task(s).
  # grunt.registerTask('default', ['uglify']);

scripts/main.coffee, scripts/sub1.coffee CoffeeScript를 변환 후 하나로 합쳐 main.js가 생성되도록 하였습니다.

jQuery 라이브러리의 경우 HTML 파일에 <script> 태그를 하나 추가하는 방법도 있겠습니다만, 저는 HTML 파일을 고치기가 귀찮으니 public/dist/main.js 파일에 jQuery 라이브러리도 포함되도록 하려고 합니다.

CoffeeScript를 변환 후 하나로 합친 JavaScript 파일과 jQuery를 이어 붙이면(concat) 되겠네요. 여기서는 grunt-contrib-concat 라이브러리를 사용하면 됩니다.

$ npm install grunt-contrib-concat --save-dev

Gruntfile에 concat task도 만들어줍니다. 최종 결과물이 public/dist/main.js여야 하니 CoffeeScript 변환 시 생성되는 JavaScript의 파일명은 바꾸어줍니다. watch task도 coffee, concat task를 차례대로 실행하도록 고쳐줍니다.

module.exports = (grunt) ->
  # Project configuration.
  grunt.initConfig {
    watch:
      scripts:
        files: ['scripts/**']
        tasks: ['coffee', 'concat']
        options:
          spawn: false
    coffee:
      compile:
        files:
          'public/dist/coffee.js': ['scripts/main.coffee', 'scripts/sub1.coffee']
    concat:
      dist:
        src: ['scripts/lib/jquery-1.10.2.min.js', 'public/dist/coffee.js']
        dest: 'public/dist/main.js'
  }

  # Load the plugin that provides the "coffee" task.
  grunt.loadNpmTasks('grunt-contrib-coffee')

  # Load the plugin that provides the "watch" task.
  grunt.loadNpmTasks('grunt-contrib-watch')

  # Load the plugin that provides the "concat" task.
  grunt.loadNpmTasks('grunt-contrib-concat')

  # Default task(s).
  # grunt.registerTask('default', ['uglify']);

이제 public/dist/main.js는 jQuery 라이브러리와 작업한 두 개의 CoffeeScript가 모두 포함되도록 되었네요. grunt watch가 실행된 상태라면 scripts/main.coffee, scripts/sub1.coffee가 변경되면 public/dist/main.js가 바로 생성되는 상태가 되었고 grunt coffee concat이라는 명령으로 수동으로 public/dist/main.js를 생성해낼 수도 있게 되었습니다.

배포 전략

주제에는 살짝 벗어난 이야기지만 grunt 이야기가 나온김에, 배포시에 grunt-contrib-uglify 플러그인을 이용하여 JavaScript 코드를 minify하도록 하는 uglify task도 추가해보겠습니다.

배포 시의 파일 경로도 개발 시와 동일하게 public/dist/main.js가 되도록 grunt-rename 플러그인을 통해 minify 되기 전의 main.js의 파일명을 임시로 바꾼 후 minify 시 다시 main.js가 되도록 할 것입니다.

$ npm install grunt-rename --save-dev
$ npm install grunt-contrib-uglify --save-dev

Gruntfile을 수정합니다.

module.exports = (grunt) ->
  # Project configuration.
  grunt.initConfig {
    watch:
      scripts:
        files: ['scripts/**']
        tasks: ['coffee', 'concat']
        options:
          spawn: false
    coffee:
      compile:
        files:
          'public/dist/coffee.js': ['scripts/main.coffee', 'scripts/sub1.coffee']
    concat:
      dist:
        src: ['scripts/lib/jquery-1.10.2.min.js', 'public/dist/coffee.js']
        dest: 'public/dist/main.js'
    rename:
      compile:
        files:
          'public/dist/main-uncompressed.js': 'public/dist/main.js'
    uglify:
      dist:
        files:
          'public/dist/main.js': 'public/dist/main-uncompressed.js'
  }

  # Load the plugin that provides the "coffee" task.
  grunt.loadNpmTasks('grunt-contrib-coffee')

  # Load the plugin that provides the "watch" task.
  grunt.loadNpmTasks('grunt-contrib-watch')

  # Load the plugin that provides the "concat" task.
  grunt.loadNpmTasks('grunt-contrib-concat')

  # Load the plugin that provides the "rename" task.
  grunt.loadNpmTasks('grunt-rename')

  # Load the plugin that provides the "uglify" task.
  grunt.loadNpmTasks('grunt-contrib-uglify')

  # Default task(s).
  # grunt.registerTask('default', ['uglify']);

이제 배포 시에 grunt coffee concat rename uglify 명령을 실행하면 minify된 main.js를 생성해낼 수 있을 것입니다.

grunt.registerTask로 개발시에는 grunt, 배포시에는 grunt dist와 같이 명령어를 간결하게 만들수도 있습니다.

module.exports = (grunt) ->
  # Project configuration.
  grunt.initConfig {
    watch:
      scripts:
        files: ['scripts/**']
        tasks: ['default']
        options:
          spawn: false
    coffee:
      compile:
        files:
          'public/dist/coffee.js': ['scripts/main.coffee', 'scripts/sub1.coffee']
    concat:
      dist:
        src: ['scripts/lib/jquery-1.10.2.min.js', 'public/dist/coffee.js']
        dest: 'public/dist/main.js'
    rename:
      compile:
        files:
          'public/dist/main-uncompressed.js': 'public/dist/main.js'
    uglify:
      dist:
        files:
          'public/dist/main.js': 'public/dist/main-uncompressed.js'
  }

  # Load the plugin that provides the "coffee" task.
  grunt.loadNpmTasks('grunt-contrib-coffee')

  # Load the plugin that provides the "watch" task.
  grunt.loadNpmTasks('grunt-contrib-watch')

  # Load the plugin that provides the "concat" task.
  grunt.loadNpmTasks('grunt-contrib-concat')

  # Load the plugin that provides the "rename" task.
  grunt.loadNpmTasks('grunt-rename')

  # Load the plugin that provides the "uglify" task.
  grunt.loadNpmTasks('grunt-contrib-uglify')

  # Default task(s).
  grunt.registerTask('default', ['coffee', 'concat'])
  grunt.registerTask('dist', ['default', 'rename', 'uglify'])

마치며

TypeScript 등 CoffeeScript와 같이 JavaScript로 변환되는 언어를 좋아하시는 분, 혹은 LESS, Stylus 등 CSS 전처리 언어를 좋아하시는 분도 이 팁을 활용하면 멋진 개발 및 배포 환경을 구성하실 수 있으리라 생각합니다.

이 글에서 사용된 grunt는 맛만 보셨을 뿐 테스트 케이스 실행, CSS Sprites 이미지 제작 등 무궁무진하게 활용할 수 있습니다. 어떻게 활용하시느냐에 따라 귀찮은 반복작업을 쉽게 줄이고 즐거운 개발 생활을 하실 수 있지 않을까 생각됩니다.

이 글에 사용된 예제 코드는 github의 grunt-test 저장소에 올려두었으니 참고해주세요!

Posted by recopick