본문 바로가기

Develop/Frontend 가이드

[FE] webpack 에서 Source Map 이 동작하는 원리

반응형

Source Map
Source Map

Webpack 에서 Source Map 이 동작하는 원리

Visual Studio Code 에서 디버깅하려고 break point 를 잡으면 break point 가 활성화가 되지 않거나 엉뚱한 코드에서 break 가 되는 경우을 종종 겪게 됩니다. 개발을 하다보면 이게 꽤 거슬립니다. 일단 디버깅을 제대로 할 수 없으니 런타임에 어떤 데이터가 저장되고 어디서 어떤 함수가 호출되는지를 console.log() 로 파악하는데 이게 너무나도 고역입니다. 그래서 저는 Source Map 이 어떻게 구성되고 동작하는지를 조사했고 이를 공유드리고자 합니다.

Source Map 은 개발하는 코드와 번들링된 코드 사이의 관계를 표현하는 데이터입니다. 이 Source Map 이 필요한 이유는 webpack 을 사용해서 번들링을 하게 되면 작성한 코드가 bundle.js 라는 하나의 JavaScript 파일로 만들어지기 때문입니다. Source Map 이라는 기능을 사용하지 않으면 브라우저에서 디버깅할 때 번들링된 코드 즉 bundle.js 파일을 디버깅해야 합니다. 왜냐하면 실제로 브라우저에 로드되어 실행하는 코드가 bundle.js 파일이기 때문입니다. 번들링된 코드로부터 소스 코드를 유추하는건 너무나 비효율적이기 때문에 개발하는 코드와 번들링된 코드를 연결하는 Source Map 이 등장하게 됩니다. TypeScript 도 컴파일하여 JavaScript 파일을 만들기 때문에 마찬가지의 이유로 디버깅을 원활하게 하려면 Source Map 이 필요합니다.

webpack.config.js 에서 devtool 로 source map 을 설정하면?

일단 webpack 을 통해서 번들링된 bundle.js 파일 끝에 보면 아래와 같은 주석이 있음을 확인할 수 있습니다.

//# sourceMappingURL=<url>

이 특별한 주석을 브라우저가 읽고 Source Map 이 저장한 파일 찾아, 그 파일을 바탕으로 원래 코드와 번들된 코드를 연결합니다. webpack Devtool 문서를 보면 source map 을 만드는 여러 방법이 나열되어 있는데, 각 옵션이 다 위 Source Map 주석을 어떻게 표현할지와 연관되는 옵션입니다.

source-map
가장 기본적인 옵션입니다. map 파일을 만들고 url 에 파일 경로를 추가합니다.

//# sourceMappingURL=bundle.js.map
{"version":3,"sources":["webpack:///./example.coffee"],"names":[],"mappings":";;;;;;;;;AAEU;;;AAAA;;AACV,OACE;EAAA,MAAQ,IAAI,CAAC,IAAb;EACA,QAAQ,MADR;EAEA,MAAQ,SAAC,CAAD;WAAO,IAAI,OAAO,CAAP;EAAX;AAFR,EAFQ;;;AAOV,OAAO,SAAC,MAAD,KAAS,OAAT;SACL,MAAM,MAAN,EAAc,OAAd;AADK","file":"./bundle-source-map.js","sourcesContent":["# Taken from http://coffeescript.org/\n\n# Objects:\nmath =\n  root:   Math.sqrt\n  square: square\n  cube:   (x) -> x * square x\n\n# Splats:\nrace = (winner, runners...) ->\n  print winner, runners\n"],"sourceRoot":""}

eval 옵션
eval 이 붙는 source map 옵션은 JavaScript 의 함수인 eval() 을 사용하여 source map 을 만듭니다. eval 함수로 각 모듈을 따로 실행시키기 때문에 수정된 모듈만 재빌드해서 빠르지만 정확한 소스 코드 위치를 매핑하지 못하는 경우가 종종있습니다.

eval("// Taken from http://coffeescript.org/\n\n// Objects:\nvar math, race;\n\nmath = {\n  root: Math.sqrt,\n  square: square,\n  cube: function(x) {\n    return x * square(x);\n  }\n};\n\n// Splats:\nrace = function(winner, ...runners) {\n  return print(winner, runners);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vLy4vZXhhbXBsZS5jb2ZmZWU/MjQxNiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFVTs7O0FBQUEsSUFBQSxJQUFBLEVBQUE7O0FBQ1YsSUFBQSxHQUNFO0VBQUEsSUFBQSxFQUFRLElBQUksQ0FBQyxJQUFiO0VBQ0EsTUFBQSxFQUFRLE1BRFI7RUFFQSxJQUFBLEVBQVEsUUFBQSxDQUFDLENBQUQsQ0FBQTtXQUFPLENBQUEsR0FBSSxNQUFBLENBQU8sQ0FBUDtFQUFYO0FBRlIsRUFGUTs7O0FBT1YsSUFBQSxHQUFPLFFBQUEsQ0FBQyxNQUFELEVBQUEsR0FBUyxPQUFULENBQUE7U0FDTCxLQUFBLENBQU0sTUFBTixFQUFjLE9BQWQ7QUFESyIsInNvdXJjZXNDb250ZW50IjpbIiMgVGFrZW4gZnJvbSBodHRwOi8vY29mZmVlc2NyaXB0Lm9yZy9cblxuIyBPYmplY3RzOlxubWF0aCA9XG4gIHJvb3Q6ICAgTWF0aC5zcXJ0XG4gIHNxdWFyZTogc3F1YXJlXG4gIGN1YmU6ICAgKHgpIC0+IHggKiBzcXVhcmUgeFxuXG4jIFNwbGF0czpcbnJhY2UgPSAod2lubmVyLCBydW5uZXJzLi4uKSAtPlxuICBwcmludCB3aW5uZXIsIHJ1bm5lcnNcbiJdLCJmaWxlIjoiMC5qcyJ9\n//# sourceURL=webpack-internal:///0\n");

inline 옵션
map 파일을 만들지 않고 주석에 파일을 data URL 로 적습니다. Source Map 이 독립된 파일로 존재하지 않고 bundle.js 파일 내에 포함되게 됩니다.

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9leGFtcGxlLmNvZmZlZSJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7QUFFVTs7O0FBQUE7O0FBQ1YsT0FDRTtFQUFBLE1BQVEsSUFBSSxDQUFDLElBQWI7RUFDQSxRQUFRLE1BRFI7RUFFQSxNQUFRLFNBQUMsQ0FBRDtXQUFPLElBQUksT0FBTyxDQUFQO0VBQVg7QUFGUixFQUZROzs7QUFPVixPQUFPLFNBQUMsTUFBRCxLQUFTLE9BQVQ7U0FDTCxNQUFNLE1BQU4sRUFBYyxPQUFkO0FBREsiLCJmaWxlIjoiLi9idW5kbGUtaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzQ29udGVudCI6WyIjIFRha2VuIGZyb20gaHR0cDovL2NvZmZlZXNjcmlwdC5vcmcvXG5cbiMgT2JqZWN0czpcbm1hdGggPVxuICByb290OiAgIE1hdGguc3FydFxuICBzcXVhcmU6IHNxdWFyZVxuICBjdWJlOiAgICh4KSAtPiB4ICogc3F1YXJlIHhcblxuIyBTcGxhdHM6XG5yYWNlID0gKHdpbm5lciwgcnVubmVycy4uLikgLT5cbiAgcHJpbnQgd2lubmVyLCBydW5uZXJzXG4iXSwic291cmNlUm9vdCI6IiJ9

cheap 옵션
cheap 옵션은 라인 넘버만 매핑하고 라인에서 몇 번째 글자인지는 매핑하지 않습니다. 그래서 빠르게 빌드가 가능한 반면 정확한 매핑은 포기하는 옵션입니다.

Source Map 이 소스코드를 찾는 원리

bundle.js.map

{
    version : 3,
    file: "bundle.js",
    sourceRoot : "",
    sources: ["a.js", "b.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

bundle.js.map 파일은 json 포맷으로 만들어집니다.
sources 는 bundle.js 를 만드는데 활용된 소스 코드 파일 목록입니다.
names 는 소스 코드의 모든 변수와 함수 이름이 기록됩니다.
mappings 가 실제 코드와 매핑할 수 있도록 하는 데이터이며 Base64 VLQ 로 인코딩되어 기록됩니다.

Base64 VLQ 와 관련된 자세한 내용은 이 글 을 참고해주시길 바랍니다.

따라서 디버깅하다가 매핑이 비활성화되어 있거나 매핑이 잘못된 경우 map 파일에서 sources 에 소스 파일이 포함되어 있는지 그리고 디버깅하려는 변수 또는 함수가 names 에 포함되어 있는지 확인해보고 잘못되어 있으면 다시 빌드해서 확인해보면 됩니다.

mappings 가 잘못되었는지는 Mozilla 에서 제공하는 base64 VLQ 디코더로 디코딩해보시고 확인하면 됩니다. 만약 mappings 가 잘못되었다면 webpack.config.js 에서 devtool 옵션을 source-map 으로 변경해보시고 시도해보시길 추천드립니다.

참고

Offical Specification : Source Map
Introduction to JavaScript Source Maps
Github webpack/examples/source-map/

반응형