Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

자바스크립트와 커피스크립트에서 소스맵(source map) 사용하기

최근 프로젝트에 적용을 좀 해볼까 하는 생각에 소스맵(Source Map)을 적용해 볼 생각으로 만져봤다. 다른 요인들로 인해서 바로 적용은 못했지만 만져본 김에 관련 내용을 좀 정리해 본다.


Source Map
소스맵의 스펙은 현재 구글독스에서 정리되어 있고(다른 표준화처럼 표준화 단체에서 진행하는 것은 아닌 것으로 보인다.) 현재 스펙은 리비전 3이다. 스펙상에 나온 내용으로 보면 구글이 만든 자바스크립트관련 도구인 Closure Inspector에서 Joseph Schorr가 적용한 것이 최초의 형식이다. 여기서 발전해서 현재의 리비전 3에 이르러 있다.

소스맵은 말 그대로 원본소스와 변환된 소스를 맵핑해 주는 방법을 제안한 것이다. 자바스크립트를 예로 들자면 Closure Tools가 지원해 주듯이 작성한 자바스크립트를 한 파일로 합치거나 사이즈를 줄이기 위해서 압축하거나 난독화해서 배포하는 방식을 많이 취하는데 이 방법은 성능에는 좋지만 사실 디버깅이 어려워지는 문제가 있는데 소스맵은 이 원본 소스와 최종소스를 매핑해서 추적할 수 있는 방법이다.

{
  version : 3,
  file: “out.js”,
  sourceRoot : "",
  sources: ["foo.js", "bar.js"],
  sourcesContent: [null, null],
  names: ["src", "maps", "are", "fun"],
  mappings: "AA,AB;;ABCDE;"
}

소스맵 파일은 위와 같이 JSON 형식으로 되어 있다.

  • version은 양수로 소스맵의 버전을 의미하고 항상 제일먼저 나와야 한다.
  • file은 변환된 파일명이다.
  • sourceRoot는 옵션값으로 소스 파일을 가져올 경로의 루트를 재조정하는데 사용한다.
  • sources는 mappings에서 사용할 원본 소스 파일명의 배열이다.
  • sourceContent은 옵션값으로 소스의 내용을 답고 있어야 하면 sources의 파일명으로 파일을 가져오지 못했을 때 사용하는 용도이다. null로 지정하면 반드시 소스피알이 필요하다.
  • names는 mappings에서 사용할 심볼 이름이다.
  • mappings는 인코딩된 매핑 데이터의 문자열이다.
mappings의 데이터는 먼저 생성된 파일의 각 라인을 세미콜론(;)을 구분자로 분리하고 구분된 각 부분을 다시 콤마(,)로 분리한다. 그리고 이렇게 구분된 각 부분은 몇가지 규칙으로(스펙에 나와있다.) base 64 VLQ 인코딩을 한다. 소스맵을 처음 보면 잘 이해가 안될 수 있는데 간단히 보았으니 예제를 통해서 보자.


자바스크립트 소스맵
가장 먼저 기본이라고 할 수 있는 JavaScript에서의 소스맵을 보자. 자바스크립트 소스맵을 사용하려면 Chrome의 개발자 도구 설정에서 다음과 같이 Enable source maps를 활성화해주어야 한다. 크롬은 23버전부터 지원되고 파이어폭스는 아직 안된다.

사용자 삽입 이미지

그럼 자바스크립트 소스를 보자.

// src/js/first.js
;function sum(a, b) {
  return a + b;
}

// src/js/second.js
;function multiply(a, b) {
  return a * b;
}

// src/js/third.js
;function minus(a, b) {
  return a - b;
}
var foo = sum(1,2);
var bar = multiply(4,5);
var baz = minus(6,2);

위처럼 3개의 자바스크립트가 있고 이를 하나의 파일로 압축해서 하나의 파일로 만드는 시나리오를 생각해 보자. 자바스크립트를 연결하고 압축하고 난독화하는 도구들은 많이 있는데 많이 알려진 도구들은 대부분 소스맵을 지원하고 있다. 요즘 많이 사용하는 UglifyJS2를 예로 들어보자.(UglifyJS는 2버전 부터 소스맵을 지원한다.)

uglifyjs src/js/first.js \
         src/js/second.js \
         src/js/third.js \
         -o out/script.min.js \
         --source-map out/script.js.map \
         --source-map-root http://localhost:8080/ \
         --source-map-url http://localhost:8080/out/script.js.map \
         -c -m

UglifyJS2가 설치되어 있으면 위와 같은 명령어로 원본 소스에서 하나의 JavaScript 파일을 생성하면서 소스맵을 만들 수 있다.(도구의 설치는 이 글과 상관없으니 생략한다.) 처음에 원본 소스를 지정하고 결과 파일은 out/script.min.js로 만든다. 소스맵은 --source-map옵션으로 같은 위치인 out/script.js.map에 만들고 --source-map-root로 소스맵을 가져올 루트경로를 지정한다. --source-map-url에서는 소스맵 파일의 URL을 지정했는데 설명상으로는 이 옵션이 없이도 사용가능해야 하는데 버그인지 잘 몰라서인지 없이는 안되서 그냥 절대경로를 지정했다. 이 옵션이 없을 경우 --source-map-root과 상관없이 --source-map에 지정한 out/script.js.map에서 소스맵 파일을 찾는데 script.min.js가 같은 폴더에 있으므로 경로가 잘못되서 안찾아진다. -c 옵션은 압축이고 -m 옵션은 난독화(magle)이다.(아직 초창기라서 그런지 자바스크립트 변환 도구들의 소스맵 지원 옵션은 유연하지 못한 느낌이 있다.)

{
  "version": 3,
  "file": "out/script.min.js",
  "sources": [
    "src/js/first.js",
    "src/js/second.js",
    "src/js/third.js"
  ],
  "names": [
    "sum",
    "a",
    "b",
    "multiply",
    "minus",
    "foo",
    "bar",
    "baz"
  ],
  "mappings": "AAAC,QAASA,KAAIC,EAAGC,GACf,MAAOD,GAAIC,ECDZ,QAASC,UAASF,EAAGC,GACpB,MAAOD,GAAIC,ECDZ,QAASE,OAAMH,EAAGC,GACjB,MAAOD,GAAIC,EAEb,GAAIG,KAAML,IAAI,EAAE,GACZM,IAAMH,SAAS,EAAE,GACjBI,IAAMH,MAAM,EAAE",
  "sourceRoot": "http://local.dev/"
}

이렇게 만들어진 script.js.map 파일은 위와 같다.(실제로는 띄어쓰기나 줄바꿈 없이 이어붙어져 있다.) 앞에서 본 스펙대로 만들어졌다.

function sum(u,n){return u+n}function multiply(u,n){return u*n}function minus(u,n){return u-n}var foo=sum(1,2),bar=multiply(4,5),baz=minus(6,2);
/*
//@ sourceMappingURL=http://local.dev/out/script.js.map
*/

자바스크립트는 위와 같이 만들어 진다. 일반적으로 많이 보는 압축된 JS코드이지만 마지막에 주석이 들어있다. //@ sourceMappingURL= 부분도 소스맵 스팩의 일부인데 이렇게 생성된 자바스크립트에서 주석으로 소스맵을 명시하면 소스맵을 지원하는 크롬같은 브라우저가 소스맵 파일을 가져와서 원본 파일과 매핑시켜준다. /* */은 스펙은 아니지만 주석을 한번더 감싸준 것으로 보인다.

사용자 삽입 이미지

이제 위의 JS 파일을 인클루드한 HTML 문서를 크롬 브라우저에서 열어보면 개발자도구에서 위와 같이 압축파일외에 원본파일도 모두 가져온 것을 볼 수 있다.

사용자 삽입 이미지

당연히 소스맵은 단순히 원본파일을 보기위한 용도가 끝이 아니기 때문에 위와 같이 원본소스에 Break Point를 걸어서 디버깅도 할 수 있다. 실제 JS는 압축되어 있지만 원본 소스를 인클루드 했듯이 디버깅 할 수 있는 것이다.

언제부터인지는 모르지만 jQuery도 CDN을 통해 제공하는 jquery.min.js 파일에서 소스맵을 지원하고 소스맵도 CDN으로 제공하고 있다. CDN을 통해서도 소스맵을 이용할 수 있지만 사실 자바스크립트는 크롬에서 압축된 코드를 다시 이쁘게 보여주는 기능을 제공하기 때문에 난독화까지 한 경우가 아니라면 소스맵을 사용하는 장점이 별로 없다.


CoffeeScript의 소스맵
아무래도 소스맵이 실제로 적용되기를 기다린 것은 자바스크립트 트랜스파일러인 CoffeeScript 진영일 것이다. 생각외로 커피스크립트 측에서  소스맵 지원이 그리 빠르지 못했지만 최근 릴리즈인 1.6.1 버전에서 드디어 소스맵을 지원하기 시작했다.

# src/coffee/script.coffee
sum = (a, b) -> a + b
multiply = (a, b) -> a * b
minus = (a,b) -> a - b

foo = sum 1, 2
bar = multiply 4, 5
baz = minus 7, 2

앞에서 본 자바스크립트와 동일한 커피스크립트이다.  커피스크립트 컴파일러를 통해서 다음과 같이 소스맵을 생성할 수 있다.

coffee -mc src/coffee/script.coffee

-c 옵션은 컴파일 옵션으로 커피스크립트를 자바스크립트로 변환해주고 -m 옵션을 주면 소스맵을 생성한다. 아무래도 첫 지원이라 옵션이 단순해도 너무 단순하다. 즉 앞에서 처럼 소스맵 경로나 ROOT를 지정할 수 없고 무조건 커피스크립트 파일과 같은 위치에 소스맵이 생성된다.

사용자 삽입 이미지

커피스크립트로 생성한 자바스크립트 파일을 인클루드한 HTML 파일을 열면 위처럼 자바스크립트와 함께 원본 파일인 커피스크립트 파일도 가져온 것을 볼 수 있고 마찬가지로 커피스크립트 코드 상에서 Breake Point를 걸어서 디버깅할 수 있다. 소스맵을 불러오는 주석은 당연히 동일하다. 커피스크립트를 사용할 때 더이상 추측해서 버그가 발생한 코드를 찾지 않다도 된다.


Node.js에서의 CoffeeScript 소스맵
소스맵을 지원하는 Chrome에서는 그렇다 치고 그럼 Node.js 환경에서 커피스크립트를 사용할 때는 어떠한가? 커피스크립트를 직접 실행할 때는 상관없지만 자바스크립트로 변환해서 하는 경우는 매핑이 필요하다. 하지만 Node.js 진영에서는 트랜스파일러를 별로 좋아하지 않는지 소스맵 지원에 대한 이슈를 열어놓기는 했지만 8개월째 처리가 안되고 있다. 그래서 Node.js에서는 바로 소스맵 기능을 사용할 수 있는데 아쉬운대로 이 문제를 해결할 수 있는 node-source-map-support 모듈이 존재한다.

커피스크립트를 사용하는 프로젝트에 node-source-map-support를 설치하고 다음과 같이 코드를 작성한다.

# test.coffee
require('source-map-support').install()
sum = (a, b) -> a + b

cosole.log 3, 4

console 대신 cosole이라고 작성한 것은 오류를 발생하기 위한 의도적인 오타이다.

$ coffee -mc test.coffee

이제 위와같이 커피스크립트 파일을 소스맵과 함께 컴파일한다.

$ node test.js
ReferenceError: cosole is not defined
  at Object.<anonymous> (test.coffee:4)
  at Object.<anonymous> (test.coffee:1)
  at Module._compile (module.js:449:26)
  at Object.Module._extensions..js (module.js:467:10)
  at Module.load (module.js:356:32)
  at Function.Module._load (module.js:312:12)
  at Module.runMain (module.js:492:10)
  at process.startup.processNextTick.process._tickCallback (node.js:244:9)

컴파일된 자바스크립트 파일을 노드로 실행하면 위처럼 매핑된 커피스크립트 파일로 매핑해서 오류메시지가 나오는 것을 볼 수 가 있다. 불필요한 require 코드가 들어가긴 하지만 Node.js가 지원해 주지 않는 상황에서 유일한 대안이라고 할 수 있다.
2013/03/08 03:10 2013/03/08 03:10