JavaScript 소스 맵 소개

라이언 세던

클라이언트 측 코드를 결합하고 축소한 후에도 성능에 영향을 미치지 않고 가독성이 높고 디버그가 더 중요해지고 싶었던 적이 있으신가요? 이제 소스 맵의 마법을 사용해 볼 수 있습니다.

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

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

작동 중인 Mozilla JavaScript 소스 맵 라이브러리의 예

현실

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

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

Firefox 23 이상에는 기본 제공되는 개발자 도구에서 소스 맵이 기본적으로 사용 설정되어 있습니다.

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

소스 맵이 왜 중요한가요?

현재 소스 매핑은 비압축/압축된 JavaScript와 압축/비결합된 JavaScript 사이에서만 작동하지만, 앞으로는 CoffeeScript와 같은 컴파일된 JavaScript 언어와 SASS, LESS 등의 CSS 전처리기 지원을 추가할 수 있는 기술까지 발전할 것으로 보입니다.

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

  • CoffeeScript
  • ECMAScript 6 이상
  • SASS/LESS 및 기타
  • JavaScript로 컴파일되는 거의 모든 언어

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

최근 Google Web Toolkit (GWT)에 소스 맵에 대한 지원이 추가되었습니다. GWT팀의 Ray Cromwell은 소스 맵 지원이 실제로 사용되는 모습을 보여주는 멋진 스크린캐스트를 제작했습니다.

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

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

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

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

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

현재 소스 맵 생성을 지원하는 유일한 자바스크립트 컴파일러/축소 요소는 클로저 컴파일러입니다. (사용 방법은 나중에 설명하겠습니다.) JavaScript를 결합하고 축소하면 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 예

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

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

클로저 컴파일러를 사용하여 자바스크립트 파일의 소스 맵을 축소하고 연결하고 생성해야 합니다. 명령어는 다음과 같습니다.

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일 수 있습니다. 이 다이어그램은 1개의 연속 비트 (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(파일 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 value를 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'));
}

위에 표시된 것처럼 처음 3개의 문자를 잘라서 사양의 구문 오류와 일치하는지 확인하고 일치하는 경우 첫 번째 줄바꿈 항목 (\n)에 이르는 모든 문자를 삭제합니다.

sourceURLdisplayName 실제 작동: 평가 및 익명 함수

다음 두 규칙을 사용하면 소스 맵 사양에 속하지 않지만 eval 및 익명 함수를 사용할 때 훨씬 더 쉽게 개발할 수 있습니다.

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

데모: eval() 코드가 sourceURL을 통해 스크립트로 표시되는 방식 참고

//# 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에는 또한 살펴봐야 할 소스 맵 문제도 있습니다.

Cofscript 컴파일러를 포함해 많은 tools가 소스 맵을 생성합니다. 지금은 논란의 여지가 있는 상황이라고 생각해요.

소스 맵을 생성할 수 있는 도구가 많을수록 더 나은 선택이 될 것이므로 좋아하는 오픈소스 프로젝트에 소스 맵 지원을 요청하거나 추가하세요.

완벽하지 않음

현재 소스 맵에서 지원하지 않는 한 가지는 watch 표현식입니다. 문제는 현재 실행 컨텍스트 내에서 인수나 변수 이름을 검사하려고 해도 실제로 존재하지 않으므로 아무것도 반환하지 않는다는 것입니다. 이 경우 검사하려는 인수/변수의 실제 이름을 컴파일된 자바스크립트의 실제 인수/변수 이름과 비교하여 조회하려면 일종의 역방향 매핑이 필요합니다.

이것은 물론 해결 가능한 문제이며 소스 맵에 더 많은 관심을 기울이면 몇 가지 놀라운 기능과 안정성이 향상되는 것을 보게 될 것입니다.

문제

최근 jQuery 1.9는 공식 CDN에서 제공될 때 소스 맵에 대한 지원을 추가했습니다. 또한 jQuery가 로드되기 전에 IE 조건부 컴파일 주석 (//@cc_on)이 사용되는 경우 특이한 버그를 지적했습니다. 이후 sourceMappingURL을 여러 줄 주석으로 래핑하여 이 문제를 완화하기 위한 커밋이 있습니다. 교훈을 얻을 때 조건부 코멘트를 사용하지 않습니다.

이 문제는 구문을 //#로 변경하여 해결되었습니다.

도구 및 리소스

다음과 같은 리소스 및 도구를 확인해 보세요.

  • 닉 피츠제럴드는 소스 맵을 지원하는 UglifyJS를 사용합니다.
  • Paul Eyeish는 소스 맵을 보여주는 유용한 데모를 제공합니다.
  • 드롭이 발생한 시점의 WebKit 변경 집합을 확인하세요.
  • 변경 세트에는 레이아웃 테스트도 포함되어 이 문서 전체를 시작할 수 있었습니다.
  • Mozilla에는 기본 제공 콘솔에서 소스 맵의 상태를 따라야 하는 버그가 있습니다.
  • 콘래드 어윈은 모든 Ruby 사용자를 위해 매우 유용한 소스 지도 보석을 작성했습니다.
  • 평가 이름 지정displayName 속성에 관한 추가 자료
  • 소스 맵을 만드는 방법은 클로저 컴파일러 소스를 확인할 수 있습니다.
  • 몇 가지 스크린샷이 있으며 GWT 소스 맵 지원에 관한 설명이 있습니다.

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

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