패키지 매니저, 자동화 도구

패키지 매니저, 자동화 도구(Transpiler, Task Runner, Module Bundler) 등 생산성을 높히는 도구들에 대해서 배웁니다.
모듈 번들러, 패키지 매니저, 자동화 도구, 트랜스파일러, 태스크 러너, 생산성 도구, 웹팩, Webpack


패키지 매니저

NPM (Node Package Manager)NPM (Node Package Manager)

NPM을 통해 Node.js의 모듈이나 JavaScript 라이브러리를 설치 할 수 있었습니다. 이처럼 패키지(하나의 완성된 소프트웨어나, 부품으로 쓰이는 라이브러리 및 모듈)들의 설치, 업데이트 및 패키지간의 의존성과 버전 정보를 관리해주는 프로그램을 패키지 매니저(Package Manager)라고 합니다.

분류예시
엔드유저용 프로그램 배포Apple App Store, Google Play Store, MS Windows Apps, …
OS별 프로그램 및 라이브러리 배포cygwin/Windows, yum,apt/Linux, brew/macOS, …
플랫폼별 패키지 배포npm/Node.js, pip/Python, composer/PHP, gem/Ruby, nuget/.NET, pod/iOS,macOS, gradle,maven/Java,Android …

패키지 매니저에는 엔드유저가 사용 할 실행 가능한 프로그램을 배포하는 프로그램도 있고, yum, apt, brew처럼 셸에서 OS별로 쓰이는 여러 프로그램 및 각종 라이브러리의 설치를 돕는 CLI 프로그램도 있으며, 특정 플랫폼의 라이브러리 및 모듈만을 모아둔 CLI 프로그램들도 있습니다.

Package ManagerPackage Manager

이중 소프트웨어 개발에 있어서 자주 쓰게될 패키지 매니저는 플랫폼별로 제공되는 패키지 매니저가 되겠습니다. Node.js의 NPM을 생각해보시면 되겠습니다.

패키지 매니저는 개발중인 패키지의 의존성 정보를 기반으로(Node.js의 경우 package.json), 원격 저장소(Repository)로부터 의존 패키지들을 다운로드 및 업데이트 할 수 있도록 합니다. 이 때문에 프로그램을 배포 할 때 의존하고 있는 패키지들을 모두 소스코드에 포함 할 필요 없이 그 메타 정보만을 포함(ex; package.json에)하는 것으로 충분합니다. 패키지 매니저를 사용하는 가장 큰 목적은, 이처럼 의존 라이브러리들의 설치 및 관리의 편리함에 있다고 볼 수 있겠습니다.

누구도 응용프로그램을 바닥부터 만들어나가지는 않습니다. 낯선 플랫폼에서 개발을 시작 할 때, 우선적으로 그 플랫폼의 공식적인 또는 생태계를 장악하는 패키지 매니저가 있는지 알아보고, 개발에 필요한 라이브러리 및 프레임워크를 검색해보면 생산성을 높힐 수 있겠습니다.

자동화 도구

웹 프론트엔드의 각종 도구들웹 프론트엔드의 각종 도구들

JavaScript 생태계와 웹의 UI/UX가 발전하면서, 웹 서비스에서 프론트엔드의 비중이 비약적으로 높아졌습니다. 애초에 JavaScript는 전문적인 IDE 없이 HTML/CSS에 곁다리 느낌으로 쓰이던 스크립트인지라, JS로 복잡한 시스템을 만드는 것을 돕기 위해 많은 도구들이 함께 등장했습니다.

프론트엔드에 큰 관심이 있다면, 위 도구들에 대해서 조금씩 공부해 보는 것도 나쁘지 않겠습니다. 아래에서는 주요한 자동화 도구들의 개념에 대해서만 소개합니다.

트랜스파일러

프로그래밍 언어로 작성된 소스코드를 실행 가능한 바이너리로 번역해주는 프로그램을 컴파일러(Compiler)라고 했습니다. 트랜스파일러(Transpiler; Transcompiler; Source-to-Source Compiler)는 특정 언어로 작성된 소스코드를 다른 언어의 소스코드로 번역해주는, 즉 텍스트를 텍스트로 번역하는 프로그램입니다.

TranspilersTranspilers

위처럼 아예 서로 다른 플랫폼간의 트랜스파일을 구현하는 경우도 있습니다만, 아래에서는 웹 생태계에서 쓰이는 트랜스파일러에 대해서 알아보겠습니다.

CSS Transpiler

프로젝트가 복잡해지면 CSS 역시 복잡해지기 마련입니다. CSS가 변수나 함수(mixin), 또는 nesting 같은 기능을 제공하지 않기 때문에, CSS 작성의 생산성을 높히기 위해서 LESS, SASS, PostCSS 등의 CSS Transpiler들이 등장했습니다.

style.less

/* nesting */
div {
    color: black;

    p {
        font-weight: bold;

        &.danger {
            color: red;
        }
    }
}

/* minxin */
.rounded (@radius : 10px) {
  border-radius: @radius;
  -moz-border-radius: @radius;
  -webkit-border-radius: @radius;
}
#ball { .rounded(50%); }
#footer { .rounded; }

전용 트랜스파일러로 LESS로부터 CSS를 생성

~$ lessc style.less style.css

style.css

/* nesting */
div { color: black; }
div p { font-weight: bold; }
div p.danger { color: red; }

/* minxin */
#ball {
  border-radius: 50%;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
}
#footer {
  border-radius: 10px;
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
}

CSS 트랜스파일러는 위처럼 CSS 작성을 돕기 위해 등장했습니다. 최근에는 PostCSS라고 불리는 트랜스파일러가 상승세에 있습니다. PostCSS를 이용하면 변수, 함수(mixin), nesting, 테마, grid 및 모듈화, 에러 검증 등 수 많은 기능들 뿐만 아니라 차기 CSS의 문법(W3C 에서 준비 중인)을 이용 할 수 있습니다.

PostCSS 예시

:root { 
  --red: #d33;
}
a { 
  &:hover {
    color: color(var(--red) a(54%));
  }
}

웹 프론트엔드의 생태계는 워낙 거미줄처럼 서로 엮여 있어서, 배보다 배꼽이 커지는 경우가 많습니다. 위의 PostCSS, LESS, SASS, SCSS, … 무엇이 되었든 CSS3에 충분히 통달 한 후, 생산성 향상이 절실 할 때 시작해보시길 바랍니다.

JavaScript Transpiler

이번엔 JavaScript의 문제점에 대해서 생각해보겠습니다. JS의 가장 큰 골칫거리는 호환성이 되겠습니다. 호환성은 CSS3나 HTML5에도 적용되는 문제지만, 특히 JS의 경우에는 호환성으로 인한 오류가 전체 코드를 종료시키는 문제를 발생시킬 수 있습니다.

ECMAScript 호환성 표ECMAScript 호환성 표

이런 와중에 Chrome이나 Firefox, Safari 같은 에버그린 웹 브라우저는 ECMAScript 표준을 바로 바로 적용시키면서 빠르게 최신 문법들을 지원하기도 합니다. 이런 문제는 시간이 많이 흘러, 구형 웹 브라우저들이 사장되면 자연히 해결되겠지만, 제품을 만드는 개발자 입장에서는 당장 최신 문법을 쓰고 싶어도 호환성 문제가 걱정되기 마련입니다.

BabelBabel

이러한 문제를 해결해주는 Babel최신 문법의 JS를 구형 브라우저에서 실행 가능하도록 번역해주는 트랜스파일러입니다. 물론 웹 브라우저 뿐만 아니라 Node.js 용도로도 이용 할 수 있습니다.

New JS

[1, 2, 3].map(n => n ** 2);

let [a,,b] = [1,2,3];

const x = [3, 4, 5];
var y = [1, 2, ...x];

let name = "Guy Fieri";
let place = "Flavortown";
let hi = `Hello ${name}, ready for ${place}?`;

Old JS

[1, 2, 3].map(function(n) {
    return Math.pow(n, 2);
});

var _ref = [1,2,3];
var a = _ref[0];
var b = _ref[2];

var x = [3, 4, 5];
var y = [1, 2].concat(x);

var name = "Guy Fieri";
var place = "Flavortown";
var hi = 'Hello ' + name + ', ready for ' + place + '?';

TypeScript

다음으로, JavaScript가 Dynamically Typed Language라는 점은 생산성을 높히는 장점이 될 수도 있지만, 큰 시스템을 작성할 때는 견고하지 못한 약점으로 작용 할 수 있습니다. JavaScript의 쓰임새가 최초의 단촐한 용도를 넘어서, SPA처럼 거대한 시스템을 작성하는데 확장되면서 동적인 타이핑이 시스템의 안정성이나 개발을 취약하게 하는 경우가 생겼습니다.

TypeScriptTypeScript

TypeScript 예시

let obj: string;
obj = 'yo';
// Error: Type 'number' is not assignable to type 'string'.
obj = 10;

// types can be inferred (return type)
function sayIt(what: string) {
    return `Saying: ${what}`;
}
const said: string = sayIt(obj);

class Sayer {
    // mandatory
    what: string;

    constructor(what: string) {
        this.what = what;
    }

    // return type if you want to
    sayIt(): string {
        return `Saying: ${this.what}`;
    }
}

이를 해결하기 위해서 등장한 TypeScript는 JS의 최신 문법 및 정적 타입을 지원하는 TypeScript라는 언어를 JS로 번역해주는 트랜스파일러입니다. MS에서 개발한 TypeScript는 Angular.js, React.js 등 유명 SPA 프레임워크에서 지원하며 높은 인기를 보이고 있습니다. 또한 최근에는 Facebook에서 개발한 Flow라는 정적 타입 언어도 있습니다.

태스크 러너

태스크 러너(Task Runner)는 프로그램 개발에 수반되는 반복적인 작업(Task)들을 스크립트로 작성해 한번에 실행 할 수 있게끔 도와주는 도구입니다. 예를 들어서 어떤 웹 프론트엔드 개발자는 아래와 같은 작업을 수도 없이 반복 할 수 있습니다.

  • LESS 파일들을 CSS로 트랜스파일
  • TypeScript 파일들을 JS로 트랜스파일
  • 기존에 작성한 JS 코드들의 말단이 모두 잘 작동하는 지 테스트(Unit Test)하고, JS 문법 및 코딩 스타일 검사(Lint)
  • 작성한 JS/CSS 및 라이브러리 JS/CSS 파일들을 한개의 JS/CSS 파일로 합치기(Bundling)
  • JS/CSS 파일의 불필요한 공백 및 주석 등을 제거해(Minification) 파일 크기 줄이기
  • JS 파일을 난독화(Uglification)하여 소스코드의 로직 유출 방지하기

개발자가 코드를 조금씩 수정 할 때마다, 손 수 위 작업들을 반복한다면 말그대로 배보다 배꼽이 크겠습니다. Task Runner는 이런 Task들을 순차적으로 배치하여, 마치 컴파일 언어의 빌드 과정처럼 구성 할 수 있도록 돕습니다. 또한 소스코드 파일들의 변경을 감지하여, 그 때마다 자동으로 스크립트를 수행하게 할 수 도 있습니다.

GulpGulp

Gulp, Grunt는 Node.js로 작성된 인기있는 Task Runner입니다. 제공되는 API 및 플러그인을 이용해서 Task 스크립트를 Node.js로 작성하고 실행 할 수 있습니다.

Gulp 스크립트 예시

const gulp = require('gulp');
const rename = require('gulp-rename');
const uglify = require('gulp-uglify');

const DEST = 'build/';

gulp.task('default', function() {
  return gulp.src('foo.js')
    .pipe(uglify())
    .pipe(rename({ extname: '.min.js' }))
    .pipe(gulp.dest(DEST));
});

JavaScript 모듈화

HTML에서 로드된 모든 JavaScript는 전역 공간에서 순차적으로 실행될 뿐, 애초에 JavaScript에는 모듈이라는 개념이 존재하지 않습니다. JavaScript 생태계가 발달하면서, JS에 인위적으로 모듈을 도입하는 CommonJS, AMD (Asynchronous Module Definition)라는 두 진영이 등장했습니다.

CommonJS

var someModule = require('someModule');

exports.doSomethingElse = function() {
  return someModule.doSomething() + "bar";
};

Node.js의 모듈 패턴은 CommonJS 방식을 따릅니다. CommonJS는 백엔드, 서버사이드를 위한 방식입니다.

AMD

define(
  // 이 모듈은 jquery와 underscore 두 모듈에 의존합니다.
  ['jquery', 'underscore'],
  
  // 의존 모듈이 로드되면 인자로 모듈들이 전달되며 이 함수가 실행됩니다.
  function ($, _) {

  // 격리된 스코프
  function a(){};
  function b(){};
  function c(){};

  // 함수의 리턴 값이 모듈이 exports하는 값이 됩니다.
  return {
    b: b,
    c: c
  };
});

반면 웹 브라우저 환경에 좀 더 적합하게 구현된 방식을 AMD라 합니다. AMD 방식은 콜백을 이용해서 모듈을 동기적으로 로드 할 수 없는 웹 브라우저 환경에서의 문제점을 해결합니다.

UMD

// jquery 모듈에 의존하는 모듈을 UMD 방식으로 정의
(function (root, factory) {
  // AMD
  if (typeof define === 'function' && define.amd) {
    define(['jquery'], factory);

  // CommonJS (Node.js)
  } else if (typeof exports === 'object') {
    module.exports = factory(require('jquery'));

  // Web Browser (root is window)
  } else {
    root.returnExports = factory(root.jQuery);
  }
}(this, function ($) {
  // ...
  return ...;
}));

CommonJS, AMD 방식에 모두 호환되는 모듈을 작성하고 싶은 경우에는 UMD (Universal Module Definition) 방식으로 모듈을 배포 할 수 있습니다.

모듈 번들러

대형 프로그램을 개발하다보면, 스코프를 분리하고 코드의 재사용성을 높히기 위해서, 소스코드를 여러 파일로 분리하며 모듈화하기 마련입니다. 이 때 Node.js 같은 플랫폼에서는 모듈 패턴을 지원하므로 단순히 엔트리 스크립트를 실행하면 되겠지만, 웹 브라우저에서 동작 할 JavaScript에는 모듈 패턴을 웹 브라우저가 이해 할 수 있도록, 의존 모듈들의 코드를 모두 인라인으로 복사하고, 소스코드에 모듈 로더 코드(ex; require 함수 등)를 추가하고, 모듈 간의 스코프를 분리하고, 여러 모듈의 소스코드를 적은 수의 파일로 합쳐주는(쓰는 모듈마다 script 태그를 쓰기 싫다면) 등의 부가적인 작업들이 필요합니다.

Webpack

WebpackWebpack

모듈 번들러(Module Bundler)는 위처럼 모듈 패턴이 적용된 JavaScript 파일들을 웹 브라우저에서 실행가능한 번들(Bundle)로 생성해주는 프로그램입니다. Webpack은 Node.js로 작성된, 대표적인 JavaScript 모듈 번들러입니다. 번들링 기능 뿐만 아니라, 위에 소개한 트랜스파일 및 태스크 러너의 기능도 포함 수 있어서 프론트엔드 개발자들에게 높은 인기를 얻고 있습니다. 현재 Angular.js, React.js 등 대표적인 SPA 프레임워크의 생태계에서도 널리 쓰이고 있습니다.

webpack.config.js (설정 파일 예시)

module.exports = {
  entry: './app.js', // 엔트리 스크립트 파일
  output: {
    filename: 'bundle.js' // 생성될 번들 파일
  }
}
```js

### app.js
```js
import bar from './bar'; // ES6에서 고안된 import/export 문법을 제공 (아직 웹 브라우저 상에서는 지원되지 않음)

bar();
bar.js
export default function bar() {
  // ...
}

bundle.js (웹 브라우저에서 로드 가능)

/******/ (function(modules) { // webpackBootstrap
/******/  // The module cache
/******/  var installedModules = {};

/******/  // The require function
/******/  function __webpack_require__(moduleId) {

/******/    // Check if module is in cache
/******/    if(installedModules[moduleId])
/******/      return installedModules[moduleId].exports;

/******/    // Create a new module (and put it into the cache)
/******/    var module = installedModules[moduleId] = {
/******/      i: moduleId,
/******/      l: false,
/******/      exports: {}
/******/    };

/******/    // Execute the module function
/******/    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/    // Flag the module as loaded
/******/    module.l = true;

/******/    // Return the exports of the module
/******/    return module.exports;
/******/  }


/******/  // expose the modules object (__webpack_modules__)
/******/  __webpack_require__.m = modules;

/******/  // expose the module cache
/******/  __webpack_require__.c = installedModules;

/******/  // identity function for calling harmory imports with the correct context
/******/  __webpack_require__.i = function(value) { return value; };

/******/  // define getter function for harmory exports
/******/  __webpack_require__.d = function(exports, name, getter) {
/******/    Object.defineProperty(exports, name, {
/******/      configurable: false,
/******/      enumerable: true,
/******/      get: getter
/******/    });
/******/  };

/******/  // getDefaultExport function for compatibility with non-harmony modules
/******/  __webpack_require__.n = function(module) {
/******/    var getter = module && module.__esModule ?
/******/      function getDefault() { return module['default']; } :
/******/      function getModuleExports() { return module; };
/******/    __webpack_require__.d(getter, 'a', getter);
/******/    return getter;
/******/  };

/******/  // Object.prototype.hasOwnProperty.call
/******/  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

/******/  // __webpack_public_path__
/******/  __webpack_require__.p = "";

/******/  // Load entry module and return exports
/******/  return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

"use strict";
/* harmony export (immutable) */ exports["a"] = bar;
function bar() {
  // ...
}


/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {

"use strict";
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__bar__ = __webpack_require__(0);


__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__bar__["a" /* default */])();


/***/ }
/******/ ]);

자바스크립트를 사랑해야하는 이유자바스크립트를 사랑해야하는 이유

JavaScript 생태계를 공부하다보면 배보다 배꼽이 크다는 생각이 듭니다. 위에 거론한 도구들이 뭔가 거창한 일을 하는 것도 아닌데, 웹 생태계와 오픈소스의 동시다발적인 발전에 의해서 이런 복잡한 모습을 띄게 됐다고 생각합니다. 위에서 다룬 내용들은, 일반적인 컴파일 언어에서는 빌드 한번으로 해결되는 문제들에 불과합니다.

첫 시간에 언어는 단순한 도구에 불과하는 말씀을 드린 기억이있습니다. 재차 말씀드리지만, 일단 위 도구들에 대한 개념 정도만 인지하시고, 필요 할 때마다 필요한 만큼만 배워나가시길 바랍니다.

추가 자료: How it feels to learn JavaScript in 2016 (번역본)

이 강의를 포함한 커리큘럼
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.