Isomorphic 환경 구성하기 1

서두

Isomorphic 모듈이란, Universal 모듈이라고도 하는데 브라우저와 서버에서 모두 구동 가능한 모듈을 의미한다. 플랫폼 독립적인 기능을 하는 모듈들이 해당될 것이다.

그런데 같은 자바스크립트 모듈이라고 하더라도 몇가지 차이점으로 인해 동작이 달라질 수 있다.

  • ES5, ES6등 언어 규격과 구동 엔진의 구현 여부
  • 기본 내장 모듈의 차이(window, process 등)
  • 모듈 로딩 방식(AMD, CommonJS 등)

이 글에서는 이런 차이점을 극복하고 비즈니스 로직 개발에 집중할 수 있게 보일러플레이트(스캐폴딩?)를 만드는게 목적이다.

즉, 서버와 브라우저에서 테스트 가능하고 ES6 수준의 문법과 import 구문을 사용한 모듈 제작을 바로 시작할 수 있게 하는게 목적이다.

  • ESM 방식의 모듈 로딩
  • 브라우저 호환성 향상을 위한 Babel 적용
  • 유닛 테스트 환경 구축
  • Node.js를 사용한 테스팅과 커버리지
  • 브라우저를 사용한 테스팅과 커버리지
  • Github Circle CI 연동

환경은 윈도우였으나 리눅스와의 차이는 없을 것으로 생각된다.

패키지 설정

먼저 폴더를 만들고 package.json을 생성한다.

> mkdir isomorphic-test-environment
> cd isomorphic-test-environment
> npm init -y
1
2
3

매우 간단한 모듈 하나를 작성한다.

// src/lib.js
const add = (a, b) => a + b

module.exports = {
  add
}
1
2
3
4
5
6

그리고 수동 테스트를 해보자. 간단한 모듈이므로 어려울건 없다.

// temp.js
const lib = require('./src/lib')
console.log(lib.add(1, 2))

// 3
1
2
3
4
5

테스트 환경 구성

테스트를 위해 mocha, chai 조합을 설치하자.

> npm i -D mocha chai
1

테스팅 프레임워크도 다양하지만 이번 구축에서는 두가지 환경을 조합해야하는 호환성이 중요한 문제이므로 최신보다는 널리 쓰이고 호환성이 좋은 도구들 위주로 선정하였다.

아래도 계속 나오지만 호환성과 버전 문제가 꽤나 중요했다.

테스트 코드를 작성해보자.

// test/lib.test.js
let { assert, expect } = require('chai')
let lib = require('../src/lib')

describe('lib', function() {
  describe('lib.add()', function() {
    it('tdd style', function() {
      assert.equal(lib.add(1, 2), 3)
    })

    it('bdd style', function() {
      expect(lib.add(1, 2)).to.equal(3)
    })
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

테스트를 수행하면 아래처럼 나올 것이다.

> npx mocha
 MOCHA  Testing...

  lib
    lib.add()
      √ tdd style
      √ bdd style

  2 passing (6ms)

 MOCHA  Tests completed successfully
1
2
3
4
5
6
7
8
9
10
11

npx는 mocha 같은 도구를 글로벌 설치하지 않고 패키지 내부 설치용을 사용할 수 있게 해준다. 아래처럼 변환해준다고 생각하면 된다.

npx mocha --> ./node_modules/.bin/mocha
1

npm처럼 기본 설치되는걸로 기억한다.

TDD/BDD 환경이 구성되었다!

ESM 모듈 로딩

현재 코드는 ESM방식이 아닌 CommonJS 방식이다. `import, export'를 적용해보자.




 



// src/lib.js
const add = (a, b) => a + b

export {
  add
}
1
2
3
4
5
6

 
 













// test/lib.test.js
import { assert, expect } from 'chai'
import * as lib from '../src/lib'

describe('lib', function() {
  describe('lib.add()', function() {
    it('tdd style', function() {
      assert.equal(lib.add(1, 2), 3)
    })

    it('bdd style', function() {
      expect(lib.add(1, 2)).to.equal(3)
    })
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

이젠 테스트를 수행해보면 문법이 잘못됐다며 에러를 낼 것이다. 이후로는 즉시 코드 실행이나 테스트가 불가능하고 호환이 가능하도록 babel의 전처리 과정을 거쳐야 실행이 가능해진다.


먼저 서버를 살펴보자.

Node.js는 기본적으로 CJS(CommonJS) 방식을 사용한다. ES6에서 표준으로 지정한 ESM방식을 아직까지 공식적으로 지원하지 못하고 있다. (현재 Node v11에서도 Experiment 상태이다)

여러가지 복잡한 문제가 아직 해결되지 않은 것으로 알고 있다. 자세히 알고 싶다면 다음 글을 참고하자. ES Modules와 Node.js: 쉽지 않은 선택 | Node.js 한국 커뮤니티


브라우저는 어떨까?

브라우저에서는 아래와 같은 형태로 최신 브라우저들에서 지원하기 시작했다. 자세한 정보는 다음 글을 참고하자. ECMAScript modules in browsers | Jake Archibald

<script type="module" src="..."></script>
1

서버와 브라우저 모두에서 동일한 형태로 사용되기 위해서는 번들링이 가장 현실적인 선택이다. 번들링을 통해 모듈 로딩 방법의 고민에서 벗어날 수 있다. 물론 동작 원리나 개념은 알고 있어야 하겠다.

번들링을 위해 가장 널리 쓰이는 webpack을 설치하고 모듈 로딩 방식을 바꿔보도록 하겠다.

참고로 매우 간단하게 사용 가능한 번들링 도구인 parcel이 있으나 뒤에 나올 커버리지를 위한 Code Instrument가 잘 되지 않아서 webpack으로 선택하였다.

Webpack 설정

webpack 설치를 하자.

> npm i -D [email protected]
1

아직 mocha-webpack 패키지가 webpack v4를 지원하지 않으므로 webpack v3를 사용하였다.

webpack init로 기본 설정 파일을 만들 수도 있지만 너무 복잡하므로 샘플에서 따온 기본 설정을 만들자.

// webpack.config.js
var path = require('path')

module.exports = {
  entry: './src/lib.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'build.js'
  }
}
1
2
3
4
5
6
7
8
9
10
11

이제 번들링을 통해 수동 테스트가 가능하다.

> npx webpack test/lib.test.js
> npx mocha dist/build.js
1
2

잘 동작한다. 하지만 매번 번들링 후에 테스트를 수행하는 것이 생산성을 저해한다. mocha-webpack을 통해 두가지 작업을 한번에 해결할 수 있다.

> npm i -D mocha-webpack
1

하는 김에 npm script도 설정해보자.

// package.json
  ...
  "scripts": {
    "test": "./node_modules/.bin/mocha-webpack test/**/*.test.js"
  },
  ...
1
2
3
4
5
6

이제 간단한 명령어로 번들링과 테스트가 동시 수행 가능하다.

> npm test
1

빌드 결과물도 생기지 않으니 훨씬 편해졌다.

Babel 적용

이제 ES6 코드를 대부분의 브라우저에서 동작 가능하도록 Babel을 적용해보자. Node에서만 동작하고 ESM이 필요없다면 사실 Webpack이나 Babel은 불필요하다.

Babel을 설치하자.

> npm i -D [email protected] [email protected] babel-preset-env
1

babel 최신 버전은 @babel 네임스페이스로 바뀐것 같다. 다만 webpack v3와의 궁합에 문제가 있어서 한단계 낮은 버전으로 선택했더니 동작했다.

webpack 수행시 babel을 거쳐서 트랜스파일 될 수 있게 해준다.











 
 
 
 
 
 
 
 
 


// webpack.config.js
var path = require('path')

module.exports = {
  entry: './src/lib.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'build.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

babel 설정도 만들어주자. 프리셋 babel-preset-env를 사용하게 해준다.

// .babelrc
{
  "presets": ["env"]
}
1
2
3
4

눈에 보이는 변화는 없다. webpack 빌드시 나오는 dist/build.js의 내용이 ES5 호환 가능하도록 달라졌을 뿐이다. 하지만 실제 브라우저에서 동작 시켜보면 낮은 버전의 브라우저에서는 동작하지 않던 코드가 동작 가능해질 것이다.

더욱 낮은 버전이나 Explorer 8 이하등에서는 babel-polyfill이 추가로 필요할 수 있다.

커버리지 적용

이제 서버 테스트의 마지막 단계이다. 커버리지를 위한 babel-plugin-istanbul, cross-env, nyc을 설치하자.

> npm i -D babel-plugin-istanbul cross-env nyc
1

cross-env는 윈도우, 리눅스 환경변수값 설정 방식을 동일하게 할 수 있게 해준다. nyc는 커버리지 도구인 istanbul의 CLI이다.

babel 설정에서 환경변수 NODE_ENV=test일때 커버리지 플러그인을 적용해준다.




 
 
 
 
 


// .babelrc
{
  "presets": ["env"],
  "env": {
    "test": {
      "plugins": ["istanbul"]
    }
  }
}
1
2
3
4
5
6
7
8
9

기존 npm script도 수정해주고, 추가 몇가지 설정을 해준다.




 

 
 
 
 


// package.json
  ...
  "scripts": {
    "test": "./node_modules/.bin/cross-env NODE_ENV=test ./node_modules/.bin/nyc --reporter=text --reporter=html ./node_modules/.bin/mocha-webpack test/**/*.test.js"
  },
  "nyc": {
    "sourceMap": false,
    "instrument": false
  }
  ...
1
2
3
4
5
6
7
8
9
10

nyc 관련 설정을 해주지 않으면, 불필요한 js 확장자 설정 파일(webpack.config.js 등)이 커버리지에 포함되게 된다. 궁금하면 설정을 제거하고 실행해보면 바로 알 수 있다.

기존 테스트 결과 아래에 추가로 아래와 같은 커버리지 결과가 나온다.

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 lib.js   |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|
1
2
3
4
5
6

또한 새로 생성된 coverage/index.html을 브라우저에서 열어보자. 코드단위로 커버된 라인이 표시된다.

커버리지

코드의 어느 부분이 실행되었는지 확인 가능하므로 더욱 견고한 테스트를 작성할 수 있게 된다.

nyc 파라미터로 --reporter=???? 를 추가하면 다양한 리포트를 생성할 수 있다. Using Alternative Reporters | IstanbulJS

결론

여기까지, 노드 엔진으로 구동하는 테스트와 커버리지 환경을 구성해 보았다. 추가로 브라우저에서도 동작 가능한 모듈의 테스트 환경 구성은 다음 글에서 다루겠다.

최종 수정: 2018-12-27 04:12:54