Gulp로 TypeScript 빌드하기

Gulp로 TypeScript 빌드하기

이 퀵 스타트 가이드는 gulp로 TypeScript를 빌드하고, 그 후에 Browserify, terser, Watchify를 gulp 파이프라인에 추가하는 방법을 알려줍니다. BabelifyBabel 기능을 추가하는 방법도 보여드릴게요.

이미 Node.jsnpm을 쓰고 있다고 가정합니다.

참고: 이 문서는 폐기(deprecated)된 문서입니다. 최신 빌드 도구 연동 방법은 빌드 도구와의 통합을 참고하세요.

출처: TypeScript 핸드북

최소 프로젝트 (Minimal project)

새 디렉터리에서 시작할게요. 지금은 proj라고 이름을 붙이지만, 원하는 대로 바꿔도 됩니다.

mkdir proj
cd proj

우선 프로젝트를 다음과 같이 구성하겠습니다.

proj/
   ├─ src/
   └─ dist/

TypeScript 파일은 src 폴더에서 시작해 TypeScript 컴파일러를 거쳐 dist로 나갑니다. 이 뼈대를 만들어 볼게요.

mkdir src
mkdir dist

프로젝트 초기화

이제 이 폴더를 npm 패키지로 만들겠습니다.

npm init

일련의 프롬프트가 나올 텐데, 엔트리 포인트(entry point) 외에는 기본값을 써도 됩니다. 엔트리 포인트는 ./dist/main.js로 하세요. 생성된 package.json 파일에서 언제든 다시 돌아와 바꿀 수 있어요.

의존성 설치

이제 npm install로 패키지를 설치할 수 있습니다. 먼저 gulp-cli를 전역으로 설치해요(Unix 시스템이라면 이 가이드의 npm install 명령에 sudo를 붙여야 할 수도 있어요).

npm install -g gulp-cli

그다음 프로젝트의 dev dependencies에 typescript, gulp, gulp-typescript를 설치합니다. Gulp-typescript는 TypeScript용 gulp 플러그인이에요.

npm install --save-dev typescript [email protected] gulp-typescript

간단한 예시 작성

Hello World 프로그램을 작성해 볼게요. srcmain.ts 파일을 만듭니다.

function hello(compiler: string) {
  console.log(`Hello from ${compiler}`);
}
hello("TypeScript");

프로젝트 루트(proj)에 tsconfig.json 파일을 만듭니다.

{
  "files": ["src/main.ts"],
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es5"
  }
}

gulpfile.js 만들기

프로젝트 루트에 gulpfile.js 파일을 만듭니다.

var gulp = require("gulp");
var ts = require("gulp-typescript");
var tsProject = ts.createProject("tsconfig.json");

gulp.task("default", function () {
  return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist"));
});

결과 앱 테스트

gulp
node dist/main.js

프로그램이 "Hello from TypeScript!"를 출력해야 합니다.

코드에 모듈 추가하기

Browserify로 넘어가기 전에, 코드를 만들어 나가면서 모듈을 섞어 봅시다. 이건 실제 앱에서 더 흔히 쓰게 될 구조예요.

src/greet.ts라는 파일을 만듭니다.

export function sayHello(name: string) {
  return `Hello from ${name}`;
}

이제 src/main.ts의 코드를 바꿔 greet.ts에서 sayHello를 import 하세요.

import { sayHello } from "./greet";

console.log(sayHello("TypeScript"));

마지막으로 tsconfig.jsonsrc/greet.ts를 추가합니다.

{
  "files": ["src/main.ts", "src/greet.ts"],
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es5"
  }
}

gulp를 실행한 뒤 Node에서 테스트해서 모듈이 동작하는지 확인하세요.

gulp
node dist/main.js

ES2015 모듈 문법을 썼음에도 TypeScript가 Node가 사용하는 CommonJS 모듈을 내보냈다는 점을 눈치채셨나요? 우리는 이 튜토리얼에서 CommonJS를 고수하겠지만, options 객체에서 module을 설정하면 바꿀 수 있어요.

Browserify

이제 이 프로젝트를 Node에서 브라우저로 옮겨 봅시다. 그러려면 모든 모듈을 하나의 JavaScript 파일로 묶고 싶을 거예요. 다행히 그게 정확히 Browserify가 하는 일입니다. 더 좋은 점은, Node가 쓰는 CommonJS 모듈 시스템(이건 TypeScript의 기본 emit이기도 해요)을 그대로 쓸 수 있다는 겁니다. 즉 우리의 TypeScript와 Node 설정이 기본적으로 그대로 브라우저로 옮겨간다는 뜻이에요.

먼저 browserify, tsify, vinyl-source-stream을 설치합니다. tsify는 gulp-typescript처럼 TypeScript 컴파일러에 접근할 수 있게 해 주는 Browserify 플러그인이에요. vinyl-source-stream은 Browserify의 파일 출력을 gulp가 이해하는 vinyl이라는 형식으로 다시 맞춰 줍니다.

npm install --save-dev browserify tsify vinyl-source-stream

페이지 만들기

srcindex.html이라는 파일을 만듭니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>
  </head>
  <body>
    <p id="greeting">Loading ...</p>
    <script src="bundle.js"></script>
  </body>
</html>

이제 main.ts를 바꿔 페이지를 업데이트합니다.

import { sayHello } from "./greet";

function showHello(divName: string, name: string) {
  const elt = document.getElementById(divName);
  elt.innerText = sayHello(name);
}

showHello("greeting", "TypeScript");

showHello를 호출하면 sayHello를 호출해 단락의 텍스트를 바꿉니다. 이제 gulpfile을 다음으로 바꾸세요.

var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var tsify = require("tsify");
var paths = {
  pages: ["src/*.html"],
};

gulp.task("copy-html", function () {
  return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});

gulp.task(
  "default",
  gulp.series(gulp.parallel("copy-html"), function () {
    return browserify({
      basedir: ".",
      debug: true,
      entries: ["src/main.ts"],
      cache: {},
      packageCache: {},
    })
      .plugin(tsify)
      .bundle()
      .pipe(source("bundle.js"))
      .pipe(gulp.dest("dist"));
  })
);

이건 copy-html 태스크를 추가하고 그것을 default의 의존성으로 넣은 것입니다. 즉 default가 실행될 때마다 copy-html이 먼저 실행되어야 한다는 뜻이에요. 또 default가 gulp-typescript 대신 tsify 플러그인과 함께 Browserify를 호출하도록 바꿨어요. 다행히 둘 다 TypeScript 컴파일러에 같은 options 객체를 넘겨줄 수 있답니다.

bundle을 호출한 뒤에는 source(vinyl-source-stream의 별칭)를 사용해 출력 번들을 bundle.js로 이름 붙입니다.

gulp를 실행한 다음 브라우저에서 dist/index.html을 열어 페이지를 테스트해 보세요. 페이지에 "Hello from TypeScript"가 보여야 합니다.

Browserify에 debug: true를 지정한 점을 눈치채셨을 거예요. 이 설정은 tsify가 번들된 JavaScript 파일 안에 소스 맵(source maps)을 내보내도록 합니다. 소스 맵 덕분에 번들된 JavaScript 대신 브라우저에서 원래 TypeScript 코드를 디버깅할 수 있어요. 브라우저의 디버거를 열고 main.ts 안에 브레이크포인트를 걸어 소스 맵이 동작하는지 테스트해 보세요. 페이지를 새로고침하면 브레이크포인트가 페이지를 멈추게 하고 greet.ts를 디버깅할 수 있게 해 줍니다.

Watchify, Babel, Terser

이제 Browserify와 tsify로 코드를 번들링하고 있으니, browserify 플러그인으로 빌드에 다양한 기능을 추가할 수 있어요.

  • Watchify는 gulp를 시작해서 계속 실행되게 하며, 파일을 저장할 때마다 증분 컴파일합니다. 덕분에 브라우저에서 편집-저장-새로고침 사이클을 유지할 수 있어요.
  • Babel은 ES2015 이상을 ES5와 ES3로 변환하는 매우 유연한 컴파일러입니다. TypeScript가 지원하지 않는 광범위하고 커스텀한 변환을 추가할 수 있게 해 줍니다.
  • Terser는 코드를 압축해서 다운로드 시간을 줄여줍니다.

Watchify

백그라운드 컴파일을 제공하는 Watchify부터 시작할게요.

npm install --save-dev watchify fancy-log

이제 gulpfile을 다음으로 바꾸세요.

var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var watchify = require("watchify");
var tsify = require("tsify");
var fancy_log = require("fancy-log");
var paths = {
  pages: ["src/*.html"],
};

var watchedBrowserify = watchify(
  browserify({
    basedir: ".",
    debug: true,
    entries: ["src/main.ts"],
    cache: {},
    packageCache: {},
  }).plugin(tsify)
);

gulp.task("copy-html", function () {
  return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});

function bundle() {
  return watchedBrowserify
    .bundle()
    .on("error", fancy_log)
    .pipe(source("bundle.js"))
    .pipe(gulp.dest("dist"));
}

gulp.task("default", gulp.series(gulp.parallel("copy-html"), bundle));
watchedBrowserify.on("update", bundle);
watchedBrowserify.on("log", fancy_log);

기본적으로 여기 세 가지 변경이 있는데, 코드를 조금 리팩터링해야 합니다.

  1. browserify 인스턴스를 watchify 호출로 감싼 뒤, 그 결과를 들고 있게 했어요.
  2. watchedBrowserify.on('update', bundle);를 호출해서 TypeScript 파일 중 하나가 바뀔 때마다 Browserify가 bundle 함수를 실행하게 했어요.
  3. watchedBrowserify.on('log', fancy_log);를 호출해서 콘솔에 로그를 남기게 했어요.

(1)과 (2)를 함께 보면, browserify 호출을 default 태스크 밖으로 옮겨야 한다는 뜻입니다. 또한 Watchify와 Gulp 둘 다 호출해야 하므로 default의 함수에 이름을 붙여야 하고요. (3)으로 로깅을 추가하는 건 선택 사항이지만 설정 디버깅에 아주 유용합니다.

이제 Gulp를 실행하면 시작한 뒤 계속 실행 상태를 유지할 겁니다. main.tsshowHello 코드를 바꿔서 저장해 보세요. 다음과 같은 출력이 보여야 합니다.

proj$ gulp
[10:34:20] Using gulpfile ~/src/proj/gulpfile.js
[10:34:20] Starting 'copy-html'...
[10:34:20] Finished 'copy-html' after 26 ms
[10:34:20] Starting 'default'...
[10:34:21] 2824 bytes written (0.13 seconds)
[10:34:21] Finished 'default' after 1.36 s
[10:35:22] 2261 bytes written (0.02 seconds)
[10:35:24] 2808 bytes written (0.05 seconds)

Terser

먼저 Terser를 설치합니다. Terser의 목적이 코드를 망가뜨리는(mangle) 것이므로, 소스 맵이 계속 동작하도록 vinyl-buffer와 gulp-sourcemaps도 설치해야 해요.

npm install --save-dev gulp-terser vinyl-buffer gulp-sourcemaps

이제 gulpfile을 다음으로 바꾸세요.

var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var terser = require("gulp-terser");
var tsify = require("tsify");
var sourcemaps = require("gulp-sourcemaps");
var buffer = require("vinyl-buffer");
var paths = {
  pages: ["src/*.html"],
};

gulp.task("copy-html", function () {
  return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});

gulp.task(
  "default",
  gulp.series(gulp.parallel("copy-html"), function () {
    return browserify({
      basedir: ".",
      debug: true,
      entries: ["src/main.ts"],
      cache: {},
      packageCache: {},
    })
      .plugin(tsify)
      .bundle()
      .pipe(source("bundle.js"))
      .pipe(buffer())
      .pipe(sourcemaps.init({ loadMaps: true }))
      .pipe(terser())
      .pipe(sourcemaps.write("./"))
      .pipe(gulp.dest("dist"));
  })
);

terser 자체는 호출이 하나뿐이라는 점을 눈치채셨을 거예요 — buffersourcemaps에 대한 호출은 소스 맵이 계속 동작하도록 보장하기 위해 존재합니다. 이 호출들 덕분에 이전처럼 인라인 소스 맵을 쓰는 대신 별도의 소스 맵 파일을 얻을 수 있어요. 이제 Gulp를 실행하고 bundle.js가 읽을 수 없는 덩어리로 최소화되는지 확인해 보세요.

gulp
cat dist/bundle.js

Babel

먼저 Babelify와 ES2015용 Babel preset을 설치합니다. Terser처럼 Babelify도 코드를 망가뜨리므로 vinyl-buffer와 gulp-sourcemaps가 필요해요. 기본적으로 Babelify는 .js, .es, .es6, .jsx 확장자의 파일만 처리하므로, .ts 확장자를 Babelify의 옵션으로 추가해야 합니다.

npm install --save-dev babelify@8 babel-core babel-preset-es2015 vinyl-buffer gulp-sourcemaps

이제 gulpfile을 다음으로 바꾸세요.

var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var tsify = require("tsify");
var sourcemaps = require("gulp-sourcemaps");
var buffer = require("vinyl-buffer");
var paths = {
  pages: ["src/*.html"],
};

gulp.task("copy-html", function () {
  return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});

gulp.task(
  "default",
  gulp.series(gulp.parallel("copy-html"), function () {
    return browserify({
      basedir: ".",
      debug: true,
      entries: ["src/main.ts"],
      cache: {},
      packageCache: {},
    })
      .plugin(tsify)
      .transform("babelify", {
        presets: ["es2015"],
        extensions: [".ts"],
      })
      .bundle()
      .pipe(source("bundle.js"))
      .pipe(buffer())
      .pipe(sourcemaps.init({ loadMaps: true }))
      .pipe(sourcemaps.write("./"))
      .pipe(gulp.dest("dist"));
  })
);

또한 TypeScript가 ES2015를 대상으로 해야 합니다. 그러면 Babel이 TypeScript가 내보낸 ES2015 코드에서 ES5를 만들어냅니다. tsconfig.json을 수정해 볼게요.

{
  "files": ["src/main.ts"],
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es2015"
  }
}

이렇게 단순한 스크립트라면 Babel의 ES5 출력은 TypeScript의 출력과 매우 비슷해야 합니다.

더 알아보기 (Learn more)