gruntfile에 충전하기

빌드 구성을 최대한 활용하는 방법

소개

Grunt를 처음 접하는 경우 Chris Coyier의 훌륭한 글 'Grunt for Grunt is Weird and Hard'를 읽어보는 것이 좋습니다. Grunt에 대해 소개한 후에는 직접 Grunt 프로젝트를 설정하고 Grunt가 제공하는 기능을 경험해 봅니다.

이 도움말에서는 수많은 Grunt 플러그인이 실제 프로젝트 코드에서 수행하는 작업에 중점을 두지 않고 Grunt 빌드 프로세스 자체에 중점을 둡니다. 다음과 같은 실용적인 아이디어를 제공해 드립니다.

  • Gruntfile을 깔끔하고 깔끔하게 유지하는 방법
  • 빌드 시간을 극적으로 개선하는 방법
  • 빌드 발생 시 알림을 받는 방법도 포함됩니다.

간단한 면책 조항: Grunt는 작업을 수행하는 데 사용할 수 있는 여러 도구 중 하나일 뿐입니다. Gulp가 내 스타일에 딱 맞으면, 좋습니다. 옵션을 설문조사한 후에도 자체 도구 모음을 빌드하고 싶더라도 괜찮습니다. 이 글에서는 Grunt에 초점을 맞추기로 했는데, 이는 강력한 생태계와 오랜 사용자층의 사용자 기반이었습니다.

Gruntfile 구성

Grunt 파일을 많이 포함하거나 Gruntfile에 많은 수작업을 작성해야 하는 경우 관리가 매우 힘들고 유지 관리가 어려워질 수 있습니다. 다행히 이 문제에 초점을 맞추는 플러그인이 꽤 많이 있습니다. Gruntfile을 다시 깔끔하게 만드는 것입니다.

최적화 전 Gruntfile

최적화를 수행하기 전의 Gruntfile은 다음과 같습니다.

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

예를 들어, '이봐요! 기대는 안 했는데! 사실 관리가 용이한 일입니다."라고 말합니다. 편의상 많은 맞춤설정이 없는 플러그인 세 개만 포함했습니다. 실제 프로덕션 Gruntfile을 사용하여 중간 크기의 프로젝트를 빌드하려면 이 문서에서 무한 스크롤이 필요합니다. 그럼 무엇을 할 수 있는지 알아보겠습니다.

Grunt 플러그인 자동 로드

프로젝트에 사용할 새 Grunt 플러그인을 추가할 때는 두 파일을 모두 package.json 파일에 npm 종속 항목으로 추가한 다음 Gruntfile 내에서 로드해야 합니다. 'grunt-contrib-concat' 플러그인의 경우 다음과 같을 수 있습니다.

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

이제 npm을 통해 플러그인을 제거하고 package.json을 업데이트했지만 Gruntfile을 업데이트하는 것을 잊은 경우 빌드가 중단됩니다. 이때 유용한 플러그인인 load-grunt-tasks를 사용하면 됩니다.

이전에는 다음과 같이 Grunt 플러그인을 수동으로 로드해야 했습니다.

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

load-grunt-tasks를 통해 다음 한 줄로 축소할 수 있습니다.

require('load-grunt-tasks')(grunt);

플러그인을 요청하면 package.json 파일을 분석하고 종속 항목 중 Grunt 플러그인인지 확인한 후 자동으로 로드합니다.

Grunt 구성을 여러 파일로 분할

load-grunt-tasks는 Gruntfile의 코드와 복잡성이 약간 줄어들지만 대규모 애플리케이션을 구성하면 여전히 파일이 매우 커집니다. 여기서 load-grunt-config가 사용됩니다. load-grunt-config를 사용하면 Gruntfile 구성을 작업별로 나눌 수 있습니다. 또한 load-grunt-tasks와 그 기능을 캡슐화합니다.

중요: Gruntfile을 분할하는 것이 모든 상황에서 작동하지 않는 경우도 있습니다. 작업 간에 공유된 구성이 많은 경우 (예: Grunt 템플릿을 많이 사용하는 경우) 약간 주의가 필요합니다.

load-grunt-config를 사용하면 Gruntfile.js가 다음과 같이 표시됩니다.

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

예, 맞습니다. 파일이 전부입니다. 이제 작업 구성은 어디에 있을까요?

Gruntfile 디렉터리에 grunt/라는 폴더를 만듭니다. 기본적으로 플러그인은 사용하려는 작업 이름과 일치하는 파일들을 해당 폴더 내에 포함합니다. 디렉터리 구조는 다음과 같습니다.

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

이제 각 작업의 작업 구성을 각 파일에 직접 넣겠습니다 (대부분 원본 Gruntfile에서 새 구조에 복사하여 붙여넣는 것을 볼 수 있음).

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

JavaScript 구성 블록을 사용하지 않는 경우 load-grunt-tasks를 통해 YAML 또는 CoffeeScript 구문을 대신 사용할 수 있습니다. YAML에 최종 필수 파일인 'aliases' 파일을 작성해 보겠습니다. 이 파일은 작업 별칭을 등록하는 특수 파일로, 이전에는 registerTask 함수를 통해 Gruntfile의 일부로 실행해야 했습니다. Google의 경우는 다음과 같습니다.

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

작업이 끝났습니다. 터미널에서 다음 명령어를 실행합니다.

$ grunt

모든 것이 제대로 작동했다면 이제 'default' 작업을 확인하고 모든 작업을 순서대로 실행합니다. 이제 기본 Gruntfile을 건드릴 필요가 없는 세 줄의 코드로 제거하고 모든 작업 구성을 외부화했으므로 완료되었습니다. 하지만 모든 것을 만드는 것은 여전히 꽤 느립니다. 이를 개선할 수 있는 방법을 살펴보겠습니다.

빌드 시간 최소화

웹 앱의 런타임 및 로드 시간 성능은 빌드를 실행하는 데 필요한 시간보다 훨씬 업무상 중요함에도 불구하고 느린 빌드는 여전히 문제가 됩니다. grunt-contrib-watch와 같은 플러그인을 사용하거나 Git 커밋이 완료된 후에는 자동 빌드를 실행하기 어렵게 되며, 실제로 빌드를 실행하기 위한 '페널티'가 발생합니다. 빌드 시간이 빠를수록 워크플로가 더 민첩해집니다. 프로덕션 빌드를 실행하는 데 10분 이상 걸리는 경우 빌드는 꼭 필요할 때만 실행해야 하므로 실행되는 동안 커피를 마시러 돌아다녀야 합니다. 이는 생산성 저하를 초래하는 것입니다. 속도를 높여야 할 요소가 있습니다.

실제로 변경된 파일만 빌드: grunt-newer

사이트를 처음 빌드한 후에는 다시 빌드할 때 프로젝트의 파일 몇 개만 터치했을 가능성이 높습니다. 이 예에서 src/img/ 디렉터리에서 이미지를 변경했다고 가정해 보겠습니다. 이미지를 다시 최적화하기 위해 imagemin을 실행하는 것이 합리적이지만 단일 이미지에서만 concatuglify를 재실행하면 소중한 CPU 주기만 낭비됩니다.

물론 언제든지 $ grunt 대신 터미널에서 $ grunt imagemin를 실행하여 당면한 작업만 선택적으로 실행할 수 있지만 더 스마트한 방법이 있습니다. 이를 grunt-newer라고 합니다.

Grunt-newer에는 실제로 변경된 파일에 대한 정보를 저장하는 로컬 캐시가 있으며 실제로 변경된 파일에 대해서만 작업을 실행합니다. 활성화하는 방법을 살펴보겠습니다.

aliases.yaml 파일을 기억하시나요? 다음과 같이 변경합니다.

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

다음과 같이 변경합니다.

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

작업에 'newer:'를 추가하기만 하면 먼저 grunt-newer 플러그인을 통해 소스 및 대상 파일을 파이핑한 다음 작업을 실행해야 하는 파일(있는 경우)을 결정합니다.

여러 작업을 동시에 실행: grunt-concurrent

grunt-concurrent는 서로 독립적이고 많은 시간을 소비하는 작업이 많을 때 매우 유용하게 사용할 수 있는 플러그인입니다. 기기의 CPU 수를 활용하고 여러 작업을 병렬로 실행합니다.

무엇보다도 구성이 매우 간단합니다. load-grunt-config를 사용한다고 가정하고 다음과 같은 새 파일을 만듭니다.

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

이름이 'first'와 'second'인 병렬 실행 트랙을 설정하기만 하면 됩니다. concat 작업이 먼저 실행되어야 하며 이 예에서는 그 동안 실행할 다른 작업이 없습니다. 두 번째 트랙에는 uglifyimagemin을 입력합니다. 이 둘은 서로 독립적이며 둘 다 상당한 시간이 소요되기 때문입니다.

이 작업 자체는 아직 아무것도 하지 않습니다. 직접 작업 대신 동시 작업을 가리키도록 default 작업 별칭을 변경해야 합니다. 다음은 grunt/aliases.yaml의 새로운 콘텐츠입니다.

default:
  - 'concurrent:first'
  - 'concurrent:second'

이제 grunt 빌드를 다시 실행하면 동시 플러그인이 먼저 연결 작업을 실행한 다음 imagemin과 uglify를 병렬로 실행하기 위해 서로 다른 두 CPU 코어에 두 개의 스레드를 생성합니다. 목표를

하지만 조언 한마디입니다. 이 기본 예에서는 grunt-concurrent를 사용하면 빌드 속도가 크게 빨라지지 않을 수 있습니다. 그 이유는 서로 다른 스레드에서 Grunt의 여러 인스턴스를 생성하여 오버헤드가 발생했기 때문입니다. 제 경우에는 최소 300ms 이상이 생성될 것입니다.

시간이 얼마나 걸렸나요?

이제 모든 작업을 최적화하고 있으므로 각 작업을 실행하는 데 필요한 시간을 파악하면 큰 도움이 될 것입니다. 다행히 이를 위한 플러그인인 time-grunt도 있습니다.

time-grunt는 npm 작업으로 로드하는 기존의 grunt 플러그인이 아니라 load-grunt-config와 유사하게 직접 포함하는 플러그인입니다. load-grunt-config에서와 마찬가지로 time-grunt에 대한 요구를 Gruntfile에 추가합니다. 이제 Gruntfile이 다음과 같이 표시됩니다.

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

실망하셨다니 안타깝게도 여기까지입니다. 터미널에서 Grunt를 다시 실행해 보세요. 모든 작업 (및 전체 빌드)에 대해 실행 시간에 관해 잘 정돈된 정보 패널이 표시됩니다.

그런트 시간

자동 시스템 알림

이제 Grunt 빌드가 신속하게 실행되고 어떤 방식으로든 (예: grunt-contrib-watch를 사용하여 파일을 보거나 커밋한 후) 자동 빌드할 수 있다면 새로운 빌드를 사용할 준비가 되었을 때 또는 문제가 발생했을 때 시스템에서 알림을 보낼 수 있다면 좋지 않을까요? grunt-notify를 확인합니다.

기본적으로 grunt-notify는 OS에서 사용 가능한 알림 시스템 (OS X 또는 Windows용 Growl, Mountain Lion’s 및 Mavericks 알림 센터, Notify-send)을 사용하여 모든 Grunt 오류 및 경고에 대해 자동 알림을 제공합니다. 놀랍게도 이 기능을 사용하려면 npm에서 플러그인을 설치하고 Gruntfile에 로드하기만 하면 됩니다. 위의 grunt-load-config를 사용하는 경우 이 단계가 자동화됩니다.

운영체제에 따라 다음과 같이 표시됩니다.

Notify

오류 및 경고 외에도 마지막 작업의 실행이 완료된 후에 실행되도록 구성해 보겠습니다. grunt-load-config를 사용하여 작업을 파일 간에 분할한다고 가정할 때 필요한 파일은 다음과 같습니다.

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

구성 객체의 첫 번째 수준에서 키는 연결하려는 작업의 이름과 일치해야 합니다. 이 예에서는 빌드 체인의 마지막 작업인 imagemin 작업이 실행된 직후에 메시지가 표시됩니다.

요약

위에서부터 따랐다면 여러분은 이제 매우 깔끔하고 체계적인 빌드 프로세스의 소유자입니다. 병렬화와 선택적 처리 덕분에 엄청나게 빠른 속도를 자랑하며, 문제가 발생하면 알려주고 계십니다.

Grunt와 플러그인을 더욱 개선하는 다른 보석을 발견하면 Google에 알려 주세요. 감사합니다.

업데이트 (2014년 2월 14일): 실제로 작동하는 Grunt 프로젝트 전체의 사본을 얻으려면 여기를 클릭하세요.