JavaScript 소스 맵 소개

클라이언트 측 코드를 결합하고 축소한 후에도 성능에 영향을 미치지 않고 클라이언트 측 코드를 읽을 수 있고 더 중요하게 디버그할 수 있기를 바란 적이 있으셨나요? 이제 소스 맵의 기능을 활용할 수 있습니다.

소스 맵은 결합/축소된 파일을 빌드되지 않은 상태로 다시 매핑하는 방법입니다. 프로덕션용으로 빌드할 때는 JavaScript 파일을 축소하고 결합하는 동시에 원본 파일에 대한 정보가 포함된 소스 맵을 생성합니다. 생성된 JavaScript에서 특정 줄과 열 번호를 쿼리할 때 원래 위치를 반환하는 소스 맵에서 조회를 수행할 수 있습니다. 개발자 도구 (현재 WebKit 나이틀리 빌드, Chrome 또는 Firefox 23 이상)는 소스 맵을 자동으로 파싱하여 축소되지 않은 파일과 결합되지 않은 파일을 실행하는 것처럼 보이게 할 수 있습니다.

데모를 사용하면 생성된 소스가 포함된 텍스트 영역의 아무 곳이나 마우스 오른쪽 버튼으로 클릭할 수 있습니다. '원래 위치 가져오기'를 선택하면 생성된 줄과 열 번호를 전달하여 소스 맵을 쿼리하고 원본 코드의 위치를 반환합니다. 출력을 볼 수 있도록 콘솔이 열려 있는지 확인합니다.

실제 사용 중인 Mozilla JavaScript 소스 맵 라이브러리의 예

실제

다음과 같은 소스 맵의 실제 구현을 보기 전에 개발자 도구 패널에서 설정 톱니바퀴 아이콘을 클릭하고 '소스 맵 사용 설정' 옵션을 선택하여 Chrome Canary 또는 WebKit Nightly에서 소스 맵 기능을 사용 설정해야 합니다.

WebKit 개발자 도구에서 소스 맵을 사용 설정하는 방법

Firefox 23 이상에는 기본으로 제공되는 개발 도구에서 소스 맵이 기본적으로 활성화되어 있습니다.

Firefox 개발자 도구에서 소스 맵을 사용하는 방법

소스 맵이 왜 중요한가요?

현재 소스 매핑은 압축되지 않은/결합된 JavaScript에서 압축/결합되지 않은 JavaScript 사이에서만 작동하지만, CoffeeScript와 같은 JavaScript로 컴파일된 언어에 대한 논의와 심지어 SASS 또는 LESS와 같은 CSS 전처리기에 대한 지원을 추가할 가능성까지 밝아지고 있습니다.

미래에는 소스 맵을 통해 브라우저에서 기본적으로 지원되는 것처럼 거의 모든 언어를 쉽게 사용할 수 있습니다.

  • CoffeeScript
  • ECMAScript 6 이상
  • SASS/LESS 등
  • JavaScript로 컴파일되는 거의 모든 언어가 지원

Firefox 콘솔의 실험용 빌드에서 디버깅되는 CoffeeScript 스크린캐스트를 살펴보세요.

Google 웹 툴킷 (GWT)에 최근 소스 맵에 대한 지원이 추가되었습니다. GWT 팀의 Ray Cromwell은 소스 맵이 실제로 어떻게 지원되는지 보여주는 멋진 스크린캐스트를 진행했습니다.

제가 준비한 또 다른 예는 Google의 Traceur 라이브러리를 사용합니다. 이 라이브러리를 사용하면 ES6 (ECMAScript 6 또는 Next)를 작성하고 ES3 호환 코드로 컴파일할 수 있습니다. Traceur 컴파일러는 소스 맵도 생성합니다. 소스 맵 덕분에 브라우저에서 기본적으로 지원되는 것처럼 사용되는 ES6 트레잇 및 클래스에 관한 이 데모를 살펴보세요.

또한 데모의 텍스트 영역을 사용하면 즉석 컴파일되는 ES6을 작성하고 이에 상응하는 ES3 코드와 소스 맵을 생성할 수 있습니다.

소스 맵을 사용한 Traceur ES6 디버깅

데모: ES6 작성, 디버그, 실제 소스 매핑 보기

소스 맵은 어떻게 작동하나요?

현재 소스 맵 생성을 지원하는 유일한 JavaScript 컴파일러/최소자는 클로저 컴파일러입니다. (사용 방법은 나중에 설명하겠습니다.) 자바스크립트를 결합하고 축소하면 함께 소스 맵 파일이 생성됩니다.

현재 클로저 컴파일러는 소스 맵을 사용할 수 있음을 브라우저 개발자 도구에 나타내는 데 필요한 특수 주석을 끝에 추가하지 않습니다.

//# sourceMappingURL=/path/to/file.js.map

이렇게 하면 개발자 도구가 호출을 원본 소스 파일의 위치로 다시 매핑할 수 있습니다. 이전에는 코멘트 pragma가 //@이었지만 이 문제와 IE 조건부 컴파일 주석으로 인해 //#로 변경하기로 결정되었습니다. 현재 Chrome Canary, WebKit Nightly, Firefox 24 이상에서는 새로운 댓글 pragma를 지원합니다. 이 구문 변경은 sourceURL에도 영향을 줍니다.

이상한 주석이 마음에 들지 않으면 컴파일된 JavaScript 파일에 특수 헤더를 설정할 수 있습니다.

X-SourceMap: /path/to/file.js.map

주석과 마찬가지로 이는 소스 맵 소비자에게 JavaScript 파일과 연결된 소스 맵을 찾을 위치를 알려줍니다. 이 헤더는 한 줄 주석을 지원하지 않는 언어에서 소스 맵을 참조하는 문제도 해결할 수 있습니다.

소스 맵이 켜져 있고 소스 맵이 꺼져 있는 것을 보여주는 WebKit Devtools 예

소스 맵 파일은 소스 맵이 사용 설정되어 있고 개발자 도구가 열려 있는 경우에만 다운로드됩니다. 또한 개발 도구가 필요할 때 원본 파일을 참조하고 표시할 수 있도록 원본 파일을 업로드해야 합니다.

소스 맵은 어떻게 생성하나요?

JavaScript 파일의 소스 맵을 축소, 연결, 생성하려면 Closure 컴파일러를 사용해야 합니다. 명령어는 다음과 같습니다.

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

중요한 두 가지 명령어 플래그는 --create_source_map--source_map_format입니다. 이는 기본 버전이 V2이고 V3만 사용하고자 하므로 필요합니다.

소스 맵 분석

소스 맵을 더 잘 이해하기 위해 클로저 컴파일러로 생성되는 소스 맵 파일의 작은 예를 살펴보고 '매핑' 섹션의 작동 방식에 관해 자세히 알아보겠습니다. 다음 예는 V3 사양 예의 약간 변형한 것입니다.

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

위에서 소스 맵은 다양한 정보가 포함된 객체 리터럴임을 확인할 수 있습니다.

  • 소스 맵의 기반이 되는 버전 번호
  • 생성된 코드의 파일 이름 (미니페스트/결합된 프로덕션 파일)
  • sourceRoot를 사용하면 소스 앞에 폴더 구조를 추가할 수 있으며 공간을 절약하는 기법이기도 합니다.
  • 소스 에는 결합된 모든 파일 이름이 포함되어 있습니다.
  • name에는 코드 전체에 나타나는 모든 변수/메서드 이름이 포함됩니다.
  • 마지막으로 매핑 속성에서 Base64 VLQ 값을 사용할 때 마법이 발생합니다. 여기서 실제 공간이 절약됩니다.

Base64 VLQ 및 소스 맵을 작게 유지

원래 소스 맵 사양은 모든 매핑에 대해 매우 상세한 출력을 가지고 있었으며, 그 결과 소스 맵은 생성된 코드의 약 10배 크기가 되었습니다. 버전 2는 이를 약 50% 줄였고 버전 3은 다시 50% 더 줄였습니다. 따라서 133KB 파일의 경우 약 300kB의 소스 맵이 생깁니다.

그렇다면 복잡한 매핑을 어떻게 유지하면서 크기를 줄였을까요?

VLQ (가변 길이 수량)는 값을 Base64 값으로 인코딩하는 것과 함께 사용됩니다. 매핑 속성은 매우 큰 문자열입니다. 이 문자열에는 생성된 파일 내의 줄 번호를 나타내는 세미콜론 (;)이 있습니다. 각 줄 내에는 해당 줄 내의 각 부분을 나타내는 쉼표 (,)가 있습니다. 각 세그먼트는 가변 길이 필드에서 1, 4 또는 5입니다. 일부는 더 길게 표시될 수 있지만 연속 비트가 포함됩니다. 각 세그먼트는 이전 세그먼트를 기반으로 빌드되므로 각 비트가 이전 세그먼트에 상대적이므로 파일 크기를 줄이는 데 도움이 됩니다.

소스 맵 JSON 파일 내 세그먼트 분석

위에서 언급했듯이 각 세그먼트는 가변 길이가 1, 4 또는 5일 수 있습니다. 이 다이어그램은 하나의 연속 비트 (g)가 있는 4의 가변 길이로 간주됩니다. 이 부분을 세분화하여 소스 맵이 원래 위치에 어떻게 적용되는지 보여드리겠습니다.

위에 표시된 값은 순전히 Base64로 디코딩된 값이며, 실제 값을 얻기 위해서는 약간 더 많은 처리가 필요합니다. 각 세그먼트는 일반적으로 다음과 같은 5가지 요소를 제시합니다.

  • 생성된 열
  • 이 표시된 원본 파일
  • 원래 행 번호
  • 원본 열
  • 그리고 가능한 경우 원래 이름

모든 세그먼트에 이름, 메서드 이름 또는 인수가 있는 것은 아니므로 세그먼트는 4~5개의 가변 길이 간에 전환됩니다. 위 세그먼트 다이어그램의 g 값은 연속 비트라고 하며 Base64 VLQ 디코딩 단계에서 추가 최적화를 허용합니다. 연속 비트를 사용하면 세그먼트 값을 기반으로 하여 큰 숫자를 저장할 필요 없이 큰 숫자를 저장할 수 있습니다. 이는 미디 형식에 기반하는 매우 영리한 공간 절약 기술입니다.

위의 다이어그램 AAgBC가 추가로 처리되면 0, 0, 32, 16, 1을 반환합니다. 32는 다음 값 16을 빌드하는 데 도움이 되는 연속 비트입니다. Base64에서만 디코딩된 B는 1입니다. 따라서 중요한 값은 0, 0, 16, 1입니다. 그러면 생성된 파일의 1번 줄 (줄은 세미콜론으로 개수가 유지됨)이 파일 0 (파일 0의 배열은 foo.js)에 매핑되고, 16번 줄은 열 1에 매핑된다는 것을 알 수 있습니다.

세그먼트가 디코딩되는 방식을 보여주기 위해 Mozilla의 Source Map JavaScript 라이브러리를 참조하겠습니다. 또한 JavaScript로 작성된 WebKit 개발자 도구 소스 매핑 코드를 확인할 수 있습니다.

B에서 값 16을 얻는 방법을 올바르게 이해하려면 비트 연산자와 소스 매핑에서 사양이 작동하는 방식에 대한 기본적인 이해가 필요합니다. 앞의 숫자 g는 비트 AND(&) 연산자를 사용하여 숫자 (32)과 VLQ_CONTINUATION_BIT(바이너리 100000 또는 32)를 비교하여 연속 비트로 플래그가 지정됩니다.

32 & 32 = 32
// or
100000
|
|
V
100000

그러면 둘 다 표시된 각 비트 위치에 1이 반환됩니다. 따라서 33 & 32의 Base64로 디코딩된 값은 위 다이어그램에서 볼 수 있듯이 32비트 위치만 공유하기 때문에 32를 반환합니다. 그러면 선행하는 각 연속 비트에 대해 비트 Shift 값을 5씩 증가시킵니다. 위의 경우 5만큼만 이동하므로 왼쪽으로 1 (B)만큼 5만큼 이동합니다.

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

그런 다음 이 값은 숫자 (32)를 한 스팟 오른쪽으로 이동하여 VLQ 부호가 있는 값에서 변환됩니다.

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

이것이 1을 16으로 바꾸는 방법입니다. 지나치게 복잡한 과정으로 보일 수 있지만 수치가 커지기 시작하면 타당성이 드러납니다.

잠재적 XSSI 문제

사양에는 소스 맵 사용 시 발생할 수 있는 교차 사이트 스크립트 포함 문제가 언급되어 있습니다. 이 문제를 완화하려면 구문 오류가 발생하도록 의도적으로 JavaScript를 무효화할 수 있도록 소스 맵의 첫 번째 줄 앞에 ')]}'를 추가하는 것이 좋습니다. WebKit 개발자 도구는 이 작업을 이미 처리할 수 있습니다.

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

위에 나와 있는 것처럼 처음 세 문자는 사양의 구문 오류와 일치하는지 확인하기 위해 슬라이스로 분할되며, 일치하는 경우 첫 번째 줄바꿈 항목 (\n)으로 이어지는 모든 문자가 삭제됩니다.

sourceURLdisplayName 실행: Eval 함수 및 익명 함수

소스 맵 사양의 일부는 아니지만 다음 두 규칙을 사용하면 eval 및 익명 함수로 작업할 때 훨씬 더 쉽게 개발할 수 있습니다.

첫 번째 도우미는 //# sourceMappingURL 속성과 매우 비슷하며 소스 맵 V3 사양에 실제로 언급되어 있습니다. 코드에 평가될 다음 특수 주석을 포함하면 개발 도구에서 보다 논리적인 이름으로 표시되도록 eval의 이름을 지정할 수 있습니다. CoffeeScript 컴파일러를 사용하는 간단한 데모를 확인해 보세요.

데모: sourceURL을 통해 eval()의 코드가 스크립트로 표시되는 것을 참고하세요.

//# sourceURL=sqrt.coffee
개발자 도구에서 sourceURL 특수 주석 표시

다른 도우미에서는 익명 함수의 현재 컨텍스트에서 사용할 수 있는 displayName 속성을 사용하여 익명 함수의 이름을 지정할 수 있습니다. 다음 데모를 프로파일링하여 실제 displayName 속성을 확인하세요.

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
displayName 속성의 실제 작동 방식 보기

개발 도구 내에서 코드를 프로파일링할 때 (anonymous) 같은 속성이 아닌 displayName 속성이 표시됩니다. 그러나 displayName은 거의 죽어버린 상태이므로 Chrome에 반영되지 않을 것입니다. 하지만 희망을 잃지 않고 debugName이라고 하는 훨씬 더 나은 제안이 제안되었습니다.

이 문서를 작성하는 시점 기준으로, 에발 명명은 Firefox와 WebKit 브라우저에서만 사용할 수 있습니다. displayName 속성은 WebKit 나이틀리에만 있습니다.

함께 힘을 모아요

현재 CoffeeScript에 추가되는 소스 맵 지원에 관해서는 긴 논의가 진행 중입니다. 문제를 확인하고 CoffeeScript 컴파일러에 소스 맵 생성을 추가하기 위한 지원을 추가하세요. 이는 CoffeeScript와 헌신적인 팔로어들에게 큰 이득이 될 것입니다.

UglifyJS에는 소스 맵 문제도 있으므로 살펴봐야 합니다.

많은 tools는 커피 스크립트 컴파일러를 비롯한 소스 맵을 생성합니다. 저는 이제 논란의 여지가 있다고 생각합니다.

소스 맵을 생성할 수 있는 도구가 많을수록 Google에서 더 나은 결과를 얻게 될 것입니다. 원하는 오픈소스 프로젝트에 소스 맵 지원을 요청하거나 추가하시기 바랍니다.

완벽하지 않음

현재 소스 맵이 적합하지 않은 한 가지는 watch 표현식입니다. 문제는 현재 실행 컨텍스트 내에서 인수나 변수 이름을 검사하려고 할 때 실제로 존재하지 않는 것이기 때문에 아무것도 반환하지 않는다는 점입니다. 이렇게 하려면 컴파일된 JavaScript의 실제 인수/변수 이름과 비교하여 검사하려는 인수/변수의 실제 이름을 조회하기 위한 일종의 리버스 매핑이 필요합니다.

물론 이는 해결할 수 있는 문제이며, 소스 맵에 더 많은 관심을 기울이면 몇 가지 놀라운 기능과 안정성이 개선되는 것을 확인할 수 있습니다.

문제

최근 jQuery 1.9에는 공식 CDN에서 제공될 때 소스 맵에 대한 지원이 추가되었습니다. 또한 jQuery가 로드되기 전에 IE 조건부 컴파일 주석 (//@cc_on)이 사용될 때의 특별한 버그도 있었습니다. 이후 sourceMappingURL을 여러 줄 주석으로 래핑하여 이를 완화하기 위한 커밋이 있었습니다. 조건부 주석을 사용하지 않는 방법을 배웁니다.

이 문제는 문법이 //#로 변경되어 해결되었습니다.

도구 및 리소스

다음은 확인해야 할 추가 리소스와 도구입니다.

  • 소스 맵을 지원하는 UglifyJS의 포크를 보유한 닉 피츠제럴드
  • Paul Irish에서 제공하는 간단한 데모를 통해 소스 맵을 확인할 수 있습니다.
  • 이 항목이 드롭될 때의 WebKit 변경사항 집합을 확인하세요.
  • 변경 세트에는 전체 도움말이 시작되는 레이아웃 테스트도 포함되어 있었습니다.
  • Mozilla에는 기본 제공 콘솔의 소스 맵 상태에 대해 따라야 하는 버그가 있습니다.
  • Conrad Irwin이 모든 Ruby 사용자를 위해 매우 유용한 소스 맵 젬을 작성했습니다
  • 평가 이름 지정displayName 속성에 대한 추가 자료
  • 소스 맵을 만드는 방법은 클로저 컴파일러 소스를 참고하세요.
  • GWT 소스 맵 지원에 대한 스크린샷과 안내가 있습니다.

소스 맵은 개발자의 툴셋에서 매우 강력한 유틸리티입니다. 웹 앱을 가볍게 유지하면서 쉽게 디버그할 수 있으면 매우 유용합니다. 또한 초보 개발자가 읽을 수 없는 축소된 코드를 살펴보지 않고도 앱을 구성하고 작성하는 방법을 확인할 수 있는 매우 강력한 학습 도구입니다.

망설이지 마세요 이제 모든 프로젝트의 소스 맵을 생성해 보세요.