클라이언트 측 코드를 결합하고 최소화한 후에도 성능에 영향을 미치지 않으면서 읽을 수 있고 더 중요한 것은 디버그할 수 있기를 바란 적이 있나요? 이제 소스 맵의 마법을 통해 가능합니다.
소스 맵은 결합된/축소된 파일을 빌드되지 않은 상태로 다시 매핑하는 방법입니다. 프로덕션용으로 빌드하면 JavaScript 파일을 축소하고 조합하는 작업 외에도 원본 파일에 대한 정보를 담은 소스 맵을 만들게 됩니다. 생성된 JavaScript에서 특정 행 및 열 번호를 쿼리하면 소스 맵에서 조회하여 원래 위치를 반환할 수 있습니다. 개발자 도구 (현재 WebKit Nightly 빌드, Chrome 또는 Firefox 23 이상)는 소스 맵을 자동으로 파싱하여 축소되지 않고 결합되지 않은 파일을 실행하는 것처럼 보이게 할 수 있습니다.
이 데모에서는 생성된 소스가 포함된 텍스트 영역의 아무 곳이나 마우스 오른쪽 버튼으로 클릭할 수 있습니다. '원래 위치 가져오기'를 선택하면 생성된 줄 및 열 번호를 전달하여 소스 맵을 쿼리하고 원본 코드의 위치를 반환합니다. 출력을 볼 수 있도록 콘솔이 열려 있는지 확인합니다.
실제
다음과 같은 실제 소스 맵 구현을 보기 전에 DevTools 패널에서 설정 톱니바퀴를 클릭하고 '소스 맵 사용 설정' 옵션을 선택하여 Chrome Canary 또는 WebKit Nightly에서 소스 맵 기능을 사용 설정했는지 확인하세요.
Firefox 23 이상에서는 기본적으로 내장된 개발자 도구에서 소스 맵이 사용 설정되어 있습니다.
소스 맵이 중요한 이유는 무엇인가요?
현재 소스 매핑은 비압축/결합된 JavaScript와 압축/결합되지 않은 JavaScript 간에만 작동하지만, CoffeeScript와 같은 JavaScript로 컴파일된 언어에 대한 논의와 SASS 또는 LESS와 같은 CSS 전처리기 지원 추가 가능성에 비추어 볼 때 미래는 밝습니다.
향후 소스 맵을 사용하여 브라우저에서 기본적으로 지원되는 것처럼 거의 모든 언어를 쉽게 사용할 수 있습니다.
- CoffeeScript
- ECMAScript 6 이상
- SASS/LESS 등
- JavaScript로 컴파일되는 거의 모든 언어
Firefox 콘솔의 실험용 빌드에서 디버그되는 CoffeeScript의 스크린캐스트를 살펴보세요.
Google 웹 도구 키트 (GWT)에 최근 소스 맵 지원이 추가되었습니다. GWT팀의 레이 크롬웰이 소스 맵 지원을 보여주는 멋진 스크린캐스트를 제작했습니다.
제가 만든 또 다른 예시에서는 ES6 (ECMAScript 6 또는 Next)를 작성하고 ES3 호환 코드로 컴파일할 수 있는 Google의 Traceur 라이브러리를 사용합니다. Traceur 컴파일러는 소스 맵도 생성합니다. 소스 맵 덕분에 브라우저에서 기본적으로 지원되는 것처럼 사용되는 ES6 트레잇과 클래스의 데모를 살펴보세요.
데모의 textarea를 사용하면 ES6를 작성할 수도 있습니다. 그러면 ES6가 즉시 컴파일되고 소스 맵과 상응하는 ES3 코드가 생성됩니다.
소스 맵은 어떻게 작동하나요?
현재 소스 맵 생성을 지원하는 유일한 JavaScript 컴파일러/축소기는 Closure 컴파일러입니다. (사용 방법은 나중에 설명해 드리겠습니다.) JavaScript를 결합하고 축소하면 그 옆에 소스 맵 파일이 생깁니다.
현재 Closure 컴파일러는 브라우저 개발 도구에 소스 맵을 사용할 수 있음을 나타내는 데 필요한 특별 주석을 끝에 추가하지 않습니다.
//# sourceMappingURL=/path/to/file.js.map
이를 통해 개발자 도구는 호출을 원래 소스 파일의 위치에 다시 매핑할 수 있습니다. 이전에는 주석 프래그마가 //@
였지만 이 주석과 IE 조건부 컴파일 주석에 몇 가지 문제가 있어 //#
로 변경하기로 결정했습니다. 현재 Chrome Canary, WebKit Nightly, Firefox 24 이상에서 새 주석 프래그마를 지원합니다. 이 문법 변경사항은 sourceURL에도 영향을 미칩니다.
이상한 주석이 마음에 들지 않는다면 컴파일된 JavaScript 파일에 특수 헤더를 설정할 수도 있습니다.
X-SourceMap: /path/to/file.js.map
이렇게 하면 주석을 추가한 것과 마찬가지로 소스 맵 소비자에게 자바스크립트 파일과 연관된 소스 맵을 찾으려면 어디를 찾아야 하는지 알려줍니다. 이 헤더는 한 줄 주석을 지원하지 않는 언어에서 소스 맵을 참조하는 문제도 해결할 수 있게 해줍니다.
소스 맵이 사용 설정되어 있고 개발 도구가 열려 있는 경우에만 소스 맵 파일이 다운로드됩니다. 또한 개발 도구에서 필요할 때 참조하고 표시할 수 있도록 원본 파일을 업로드해야 합니다.
소스 맵을 생성하려면 어떻게 해야 하나요?
Closure 컴파일러를 사용하여 JavaScript 파일을 축소하고 연결하며 소스 맵을 생성해야 합니다. 명령어는 다음과 같습니다.
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로만 작업하려면 이 작업이 필요합니다.
소스 맵의 구성
소스 맵을 더 잘 이해하기 위해 Closure 컴파일러에서 생성할 소스 맵 파일의 작은 예를 살펴보고 '매핑' 섹션의 작동 방식을 자세히 알아보겠습니다. 다음 예시는 V3 사양 예시와 약간 다릅니다.
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA"
}
위에서 소스 맵은 유용한 정보가 많이 포함된 객체 리터럴임을 알 수 있습니다.
- 소스 맵의 기반이 되는 버전 번호
- 생성된 코드의 파일 이름 (최소화된/결합된 프로덕션 파일)
- sourceRoot를 사용하면 소스에 폴더 구조를 추가할 수 있습니다. 이는 공간 절약 기법이기도 합니다.
- sources에는 결합된 모든 파일 이름이 포함됩니다.
- names에는 코드 전체에 표시되는 모든 변수/메서드 이름이 포함됩니다.
- 마지막으로 매핑 속성은 Base64 VLQ 값을 사용하여 마법이 실행되는 곳입니다. 여기에서 실제 공간 절약이 이루어집니다.
Base64 VLQ 및 소스 맵 작게 유지
원래 소스 맵 사양에는 모든 매핑에 관한 매우 상세한 출력이 포함되어 있어 소스 맵이 생성된 코드의 크기의 약 10배가 되었습니다. 버전 2에서는 이 크기를 약 50% 줄였고 버전 3에서는 다시 50% 줄였으므로 133KB 파일의 경우 소스 맵이 300KB 정도 됩니다.
복잡한 매핑을 유지하면서 크기를 줄이려면 어떻게 해야 할까요?
VLQ (가변 길이 수량)는 값을 Base64 값으로 인코딩하는 것과 함께 사용됩니다. 매핑 속성은 매우 큰 문자열입니다. 이 문자열 내에는 생성된 파일 내의 줄 번호를 나타내는 세미콜론 (;)이 있습니다. 각 줄에는 해당 줄의 각 구간을 나타내는 쉼표 (,)가 있습니다. 이러한 세그먼트는 가변 길이 필드에서 1, 4 또는 5입니다. 일부는 더 길게 보일 수 있지만 연속 비트가 포함되어 있습니다. 각 세그먼트는 이전 세그먼트를 기반으로 구축되므로 각 비트가 이전 세그먼트에 비례하므로 파일 크기를 줄이는 데 도움이 됩니다.
위에서 언급한 것처럼 각 세그먼트는 길이가 1, 4 또는 5일 수 있습니다. 이 다이어그램은 연속 비트 (g) 1개가 있는 가변 길이 4로 간주됩니다. 이 세그먼트를 분석하고 소스 지도에서 원래 위치를 파악하는 방법을 보여줍니다.
위에 표시된 값은 순수하게 Base64로 디코딩된 값이며, 실제 값을 가져오려면 추가 처리가 필요합니다. 각 세그먼트는 일반적으로 다음 다섯 가지를 계산합니다.
- 생성된 열
- 이 문제가 표시된 원본 파일
- 원래 행 번호
- 원본 열
- 원래 이름(있는 경우)
모든 세그먼트에 이름, 메서드 이름 또는 인수가 있는 것은 아니므로 전체 세그먼트가 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) 1번 열 16번 줄에 매핑된다는 것을 알 수 있습니다.
세그먼트가 디코딩되는 방식을 보여주기 위해 Mozilla의 소스 맵 JavaScript 라이브러리를 참조하겠습니다. JavaScript로 작성된 WebKit DevTools 소스 매핑 코드도 살펴볼 수 있습니다.
B에서 값 16을 가져오는 방법을 제대로 이해하려면 비트 연산자와 소스 매핑에 관한 사양이 작동하는 방식을 기본적으로 이해해야 합니다. 앞의 숫자(g)는 비트 AND(&) 연산자를 사용하여 숫자(32)와 VLQ_CONTINUATION_BIT(바이너리 100000 또는 32)를 비교하여 연속 비트로 플래그가 지정됩니다.
32 & 32 = 32
// or
100000
|
|
V
100000
이렇게 하면 두 값이 모두 표시되는 각 비트 위치에 1이 반환됩니다. 따라서 위의 다이어그램에서 볼 수 있듯이 32비트 위치만 공유하므로 Base64로 디코딩된 33 & 32
값은 32를 반환합니다. 그러면 이전의 각 연속 비트에 대해 비트 시프트 값이 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 문제
사양에서는 소스 맵 사용으로 인해 발생할 수 있는 교차 사이트 스크립트 포함 문제를 언급합니다. 이를 완화하려면 소스 맵의 첫 번째 줄 앞에 ')]}
'를 추가하여 자바스크립트를 의도적으로 무효화하여 문법 오류가 발생하도록 하는 것이 좋습니다. WebKit 개발 도구에서 이미 이 작업을 처리할 수 있습니다.
if (response.slice(0, 3) === ")]}") {
response = response.substring(response.indexOf('\n'));
}
위와 같이 처음 세 문자가 슬라이스되어 사양의 문법 오류와 일치하는지 확인하고 일치하는 경우 첫 번째 줄바꿈 엔터티 (\n)까지의 모든 문자를 삭제합니다.
sourceURL
및 displayName
사용: 평가 및 익명 함수
다음 두 가지 규칙은 소스 맵 사양의 일부가 아니지만 eval 및 익명 함수를 사용할 때 훨씬 쉽게 개발할 수 있도록 합니다.
첫 번째 도우미는 //# sourceMappingURL
속성과 매우 유사하며 실제로 소스 맵 V3 사양에 언급되어 있습니다. 코드에 eval 처리될 다음과 같은 특별 주석을 포함하여 DevTools에서 훨씬 논리적 이름으로 표시되도록 eval의 이름을 지정할 수 있습니다. CoffeeScript 컴파일러를 사용한 간단한 데모를 확인해 보세요.
데모: sourceURL을 통해 eval()
된 코드가 스크립트로 표시되는 것을 확인합니다.
//# sourceURL=sqrt.coffee
다른 도우미를 사용하면 익명 함수의 현재 컨텍스트에서 사용할 수 있는 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);
개발 도구 내에서 코드를 프로파일링하면 (anonymous)
와 같은 항목 대신 displayName
속성이 표시됩니다. 하지만 displayName은 거의 지원 중단되었으며 Chrome에 포함되지 않을 예정입니다. 하지만 아직 희망이 있습니다. 훨씬 더 나은 제안인 debugName이 제안되었습니다.
이 글을 작성하는 시점에서 eval 이름 지정은 Firefox 및 WebKit 브라우저에서만 사용할 수 있습니다. displayName
속성은 WebKit Nightly에만 있습니다.
함께 캠페인 참여하기
현재 CoffeeScript에 추가되는 소스 맵 지원에 관한 매우 긴 논의가 진행 중입니다. 문제를 확인하고 CoffeeScript 컴파일러에 소스 맵 생성을 추가하는 지원을 추가합니다. 이는 CoffeeScript와 그 열렬한 추종자에게 큰 도움이 될 것입니다.
UglifyJS에는 살펴봐야 할 소스 맵 문제도 있습니다.
coffeescript 컴파일러를 비롯한 많은 도구가 소스 맵을 생성합니다. 지금은 이 문제가 중요하지 않다고 생각합니다.
소스 맵을 생성할 수 있는 도구가 많을수록 좋습니다. 좋아하는 오픈소스 프로젝트에 소스 맵 지원을 요청하거나 추가해 보세요.
완벽하지는 않음
소스 맵에서 현재 지원하지 않는 한 가지는 보기 표현식입니다. 문제는 현재 실행 컨텍스트 내에서 인수나 변수 이름을 검사하려고 하면 실제로 존재하지 않으므로 아무것도 반환되지 않는다는 것입니다. 이를 위해서는 컴파일된 JavaScript의 실제 인수/변수 이름과 비교하여 검사하려는 인수/변수의 실제 이름을 조회하기 위한 일종의 역방향 매핑이 필요합니다.
물론 이 문제는 해결할 수 있으며 소스 맵에 더 많은 관심을 기울이면 놀라운 기능과 안정성을 개선할 수 있습니다.
문제
최근 jQuery 1.9에서 공식 CDN에서 제공할 때 소스 맵을 지원하도록 추가했습니다. 또한 jQuery가 로드되기 전에 IE 조건부 컴파일 주석 (//@cc_on)이 사용될 때 발생하는 특이한 버그도 지적했습니다. 이후 sourceMappingURL을 여러 줄 주석으로 래핑하여 이 문제를 완화하는 commit이 있었습니다. 조건부 주석을 사용하지 않는 것이 좋습니다.
이후 구문을 //#
로 변경하여 이 문제를 해결했습니다.
도구 및 리소스
다음은 확인해 볼 만한 추가 리소스와 도구입니다.
- 닉 피츠제럴드가 소스 맵 지원이 포함된 UglifyJS 포크를 보유하고 있습니다.
- Paul Irish가 소스 맵을 보여주는 유용한 데모를 제공합니다.
- 이 버전이 출시된 시점의 WebKit 체인지셋을 확인합니다.
- 이 전체 도움말의 시작점이 된 레이아웃 테스트도 체인지세트에 포함되어 있습니다.
- Mozilla에 내장 콘솔의 소스 맵 상태에 관해 따라야 하는 버그가 있습니다.
- Conrad Irwin이 모든 Ruby 사용자를 위해 매우 유용한 소스 맵 보석을 작성했습니다.
- eval 이름 지정 및 displayName 속성에 관한 추가 자료
- 소스 맵을 만들려면 Closure 컴파일러 소스를 확인하세요.
- GWT 소스 맵 지원에 관한 스크린샷과 대화목록이 있습니다.
소스 맵은 개발자 도구 모음에서 매우 강력한 유틸리티입니다. 웹 앱을 간결하면서도 쉽게 디버그할 수 있도록 유지하는 것은 매우 유용합니다. 또한 초보 개발자가 읽을 수 없는 축소된 코드를 살펴보지 않고도 경험 많은 개발자가 앱을 구성하고 작성하는 방법을 확인할 수 있는 매우 강력한 학습 도구입니다.
망설이지 마세요 지금 바로 모든 프로젝트의 소스 맵을 생성하세요.