자바스크립트의 모듈화와 표준에 대하여

자바스크립트는 기본적으로 모듈화 기능이 없었다. 여러 .js 파일로 쪼개서 모든 파일들을 일일이 <script> 태그를 이용하여 불러오는 방법이 있긴 했지만, 이 방법에는 문제가 많았다.

  1. 파일을 불러들이는 순서가 중요하다.
  2. 따로 만든 오브젝트들이 모두 글로벌 변수에 저장이 된다.
  3. 변수의 이름이 겹치게 되면 에러가 난다.
  4. 그럼으로 다른 사람이 만든 코드를 그대로 불러들여서 사용하기가 복잡해진다.

#애초에 모듈화는 왜 중요한가?

모듈화가 중요한 이유는 무엇보다 코딩 작업이 편리해지고 코드의 유지 보수가 편해지기 때문이다. 하나의 .js 파일에 모든 코드가 다 들어있으면 그걸 뜯어보고 이해하고 고치는데 많은 시간이 걸리게 된다. 하지만 각 기능에 맞춰서 코드를 나누어서 관리한다면 코드를 고치거나 업데이트 할 때 해당 부분만 쉽게 찾아서 작업을 할 수가 있다.

한번 써둔 코드를 필요에 의해 다른 코드에서 불러와서 재활용하기도 편하고, 이러한 이유로 다른 사람이 만들어서 공개한 패키지들을 불러와서 사용하는 것도 매우 간편해진다.

#그렇다면 해결책은?

해결책은 기본적으로 자바스크립트의 표준의 하나인 ES6 (ECMAScript2015)가 재정되기 이전과 후로 나뉘우게 되지만, 이게 무엇인지 자세한건 나중에 다시 얘기하기로 하고, 우선 ES6 이전에 어떻게 해결했는지 부터 알아본다.

ES6 표준 이전에는 자바스크립트가 기본적으로 모듈화를 지원하지 않았기 때문에, 이러한 모듈화를 가능하게 해주는 CommonJS나 AMD 같은 Syntax(신택스; 일종의 문법)이 개발되었다. 이러한 신택스들을 지원하는 브라우저와 서버들이 늘어남으로서, 자바스크립트를 모듈화 하는 것이 가능해졌다. 일종의 에드온이 개발된 샘이다.

물론 이 신택스들을 지원하기 이전의 브라우저나 서버엔진들에서는 여전히 모듈화된 자바스크립트를 이해하지 못한다는 문제가 있긴 하지만, 그정도로 오래된 브라우저를 사용하는 사람은 요즘시대에 드물고, 서버쪽은 개발자가 컨트롤 할 수 있는 부분이기 때문에 문제 될 건 없다. 그냥 지원하는 최신 서버엔진으로 업그레이드 하면 그걸로 문제 해결.

CommonJS에서는 모듈화를 다음과 같이 실행할 수 있었다.

// a.js의 예제 코드

var sayHi = function() {
    console.log("Hi");
}

var sayBye = function() {
    console.log("Bye");
}

exports.hi = sayHi;
exports.bye = sayBye;

우선 간단하게 모듈화 시킬 a.js 라는 파일을 만들었다. 다른 모듈에서 sayHi 함수와 sayBye 함수를 사용할 수 있게 export 해주어야한다. exports.hiexports.bye 부분은 아래의 코드처럼 적는 것도 가능하다.

module.exports = {
    hi: sayHi,
    bye: sayBye
}

자신이 편한대로 골라서 사용하면 된다. 그럼 이제 b.js에서 a.js를 불러들이고 함수를 사용해본다.

// b.js 예제 코드

var say = require('./a.js');

say.hi();
// 결과 -> Hi

say.bye();
// 결과 -> Bye

이런식으로 불러들여서 해당 모듈의 함수를 실행할 수 있게 된다. CommonJS에서 다른 모듈을 불러들일 때는 require('파일경로')를 이용한다.

CommonJS는 서버사이드용으로 개발이 되었으며 NodeJS 같은 경우는 아직도 이런식으로 모듈화에 대응하고 있다. 브라우저에서 사용하려면 트렌스파일링을 해주어야 한다.

#ECMAScript란 무엇인가?

ECMA는 "European Computer Manufacturer's Association"의 약자다.

다른 프로그래밍 언어들 처럼 자바스크립트도 진화를 하고 있기 때문에 계속 새로운 네이티브 기능이 추가 되는데, 이 표준을 정하는 곳이 ECMA International 이란 곳이고, 여기서 정한 표준의 이름이 바로 ECMAScript가 되는 것이다. 새로운 표준이 정해지면 각 브라우저나 서버엔진(예를 들면 NodeJS)들이 스크립트 로더의 정의를 업데이트 함으로써, 새로운 기능들을 실행할 수 있게 된다.

그러한 이유로 새로운 표준으로 코드를 쓰면, 그 표준을 지원하지 않는 이전 브라우저 혹은 서버에서는 해당 코드를 이해하지 못한다. 그렇기 때문에 보다 많은 브라우저에 대응하기 위해서는 ES6 표준으로 적은 자바스크립트 코드를 이전 표준인 ES5으로 변환 (ES6의 문법을 번역) 해줄 필요가 있다.

이러한 변환 작업을 Transpile(트랜스파일)이라고 하는데, 대표적인 트랜스파일러로는 babel이라는 패키지가 있다. 이 패키지를 이용하면 ES6표준으로 적은 자바스크립트 코드를 ES5표준 코드로 바꾸어 줄 수가 있다.

다른 트랜스파일러로는 webpack이나 browserify 같은 번들링 (Bundling) 툴이 있다. 이러한 스크립트 번들러들은 모듈화 되어있는 코드들을 하나의 .js 파일로 만들어주는 역할을 한다. 이 때 모듈화 된 코드들이 ES6 표준으로 적은 것들이라면 이것들을 ES5 표준으로 변환하여 하나의 .js 파일로 번들링 하도록 설정하는 것이 가능하다.

자바스크립트 모듈화를 기본 지원하게 된 표준이 ES6(ECMAScript2015)인데, ES6이나 ECMAScript2015나 같은 걸 뜻한다. ES6은 6번째로 재정된 표준이라는 뜻이고, ECMAScript2015는 2015년에 재정된 표준이라는 뜻이다.

NodeJS에서는 8.5.0 버전 이후부터 실험적으로 ES6의 모듈화 신택스를 지원하고 있다. 대신 확장자를 .mjs로 해서 저장한 후 다음 커맨드로 실행해야한다.

node --experimental-modules <파일이름>.mjs

향후 ES6의 모듈화 신택스를 기본적으로 지원할 계획이 있지만 아직까지는 (2018년 7월) 지원이 되지 않는다. 그 전까지는 CommonJS 신택스를 사용하는 것이 좋다.

#브라우저 API

프로그래밍 언어가 진화하는 것 처럼 브라우저들도 진화를 하고 있다. 이전 버전에는 없는 기능들(API)가 추가되는 경우도 허다하다. 특히 HTML5가 재정된 이후에는 새롭게 탑재되는 기능들이 늘어났다.

이러한 브라우저 고유의 API들을 사용할 수 있도록 자바스크립트 또한 그에 맞춰 진화를 해왔다. 문제는 자바스크립트로 최신 브라우저 API를 이용하는 코드를 적었을 때, 그 코드가 해당 API가 지원되지 않는 브라우저 상에서 실행되지 않는다는 점이다.

이 문제를 해결하기 위해선 polyfill이라는 방법이 사용된다. 일종의 라이브러리 파일로, 브라우저들의 네이티브 API의 기능과 비슷한 것들을 포함하고 있다. 이전 브라우저들에 해당 API가 탑재되지 않은 경우, 수동으로 불러들인 polyfill 라이브러리에서 해당 API와 비슷한 기능을 실행시킴으로서 해결한다.

#ES6 모듈화 방법은?

ES6 표준에서는 모듈을 export 하는 방법이 두가지 있다.

1. export default

-> export할 모듈이 하나 밖에 없을 때는 이걸 이용하면 된다.

var a = function() {
    console.log("Hello");
}

export default a;

2. named export

-> export할 모듈이 여러개 있을 때는 이걸 이용하면 된다.

var a = function() {
    console.log("Hello");
}

var b = function() {
    console.log("Bye");
}

export const hi = a;
export const bye = b;

이 두가지의 차이점을 아는 것이 중요한 이유는, 어떤 export 방법을 택하느냐에 따라서 그 모듈을 import 하는 방식이 달라지기 때문이다.

#1번 방법으로 export했을 때 import 방법

import 할 때 이름을 임의로 원하는 것으로 지정할 수 있다.

import ABC from './a.js';

위 코드에서 볼 수 있듯이, export는 a라는 이름으로 했지만 import 할 때는 아무 이름이나 사용이 가능하다. 혹은 아래와 같은 코드로도 불러올 수 있다.

import {default as 원하는이름} from './a.js';

자신이 쓰기 편한 방법으로 사용하면 된다.

#2번 방법으로 export했을 때 import 방법

import 할 때 이름이 export에서 지정한 이름과 같아야하며 반드시 {} 안에 적어야한다.

import { hi, bye } from './a.js';

이렇게 원하는 부분만 골라서 불러 올 수 있다. 만약 모든 모듈을 한꺼번에 불러와야한다면 다음과 같은 방법도 가능하다.

import * as say from './a.js';
// say가 아닌 아무거나 자신이 원하는 이름을 지정하면 된다.
// say는 어디까지나 예제를 위해 임의로 설정한 이름

// 사용은 다음과 같이 하면 된다.
say.hi();
// 결과 -> Hello

이렇듯 불러들이려는 모듈이 어떠한 방식으로 export가 되었고, 그에 맞는 import 방식을 제대로 이해하는 것은 매우 중요한 일이다.


사실 이러한 것들을 몰라도 자바스크립트 어플리케이션을 개발할 수는 있다. 하지만 개발을 진행하다보면 여러가지 패키지들을 사용하게 되는데, 어떤 패키지는 잘 쓸 수 있고 어떤 패키지는 에러가 나는 등의 경우가 발생하는데, 대부분이 표준의 차이에서 일어난다.

예를 들면 서버사이드 렌더링(SSR)을 할 때, 클라이언트 사이드에서는 잘 실행되던 코드가 서버 사이드(NodeJS)에서는 실행이 되지 않는 경우가 있다. 여러 이유가 있을 수 있지만, 그 중 하나의 이유는 클라이언트 사이드에서 ES6 표준으로 모듈화를 한 코드를 사용했기 때문인 경우가 있다.

위에서 언급했듯이 NodeJS에서는 아직 완벽하게 ES6 표준 모듈화를 지원하지 않기 때문에, babel을 이용해서 ES6 코드를 CommonJS 코드로 변환해주어야 한다.

이러한 차이점을 이해하면 디버깅 능력도 올라가고, 여러사람들이 쉽게 사용할 수 있는 패키지도 만들 수 있게 되니, 시간이 날 때 공부를 해두는 것이 개발자로서의 능력을 향상 시키는데 도움이 될 것이다.