사전 소스맵
포맷

소스맵

gabury1고친 사람 github-actions[bot]

소스맵은 빌드를 거쳐 바뀐 코드의 위치를 원래 코드의 위치로 되돌려 줍니다. 브라우저에 실리는 코드는 사람이 알아보기 어려운 모양으로 바뀌어 있습니다. 소스맵은 그 코드 곁에 따로 붙는 파일입니다. 오류가 나면 그곳이 원래 어느 파일의 몇째 줄인지 알려 줍니다.

쉽고 빠른 이해

소스맵은 바뀐 코드의 한 지점을 원래 코드의 한 지점으로 이어 주는 짝 목록입니다. 배포용 코드는 수천 줄이 한 줄로 줄어 있습니다. 그래서 오류가 배포된 app.min.js 의 1번째 줄 40213번째 칸에서 났다고 찍힙니다. 소스맵은 그곳을 src/cart.ts 의 42번째 줄로 바꿔 보여 줍니다.

배포용 코드는 이름도 n·d 처럼 짧게 바뀌어 있습니다. 소스맵이 없으면 오류 메시지가 가리키는 곳을 보고도 어느 코드를 고칠지 알 수 없습니다.

어떻게 도나:

  1. 빌드 도구가 코드를 바꾸면서 「바뀐 위치 → 원래 위치」 짝을 함께 적어 둡니다
  2. 바뀐 코드의 맨 끝에 그 짝 파일이 어디 있는지 한 줄 남깁니다
  3. 개발자 도구나 오류 수집 서비스가 그 파일을 읽어 원래 파일과 줄을 보여 줍니다

대가는 둘입니다. 빌드가 파일을 하나 더 만들어 시간과 용량이 듭니다. 그리고 소스맵을 누구나 받게 두면 원래 코드도 누구나 읽게 됩니다.

상세

이 절은 소스맵 파일 한 벌을 열어 봅니다. 짧은 TypeScript 함수 하나를 바꾼 결과를 놓고, 짝이 어떻게 적히는지와 누가 그 파일을 읽는지를 차례로 따라갑니다. 끝에서는 이 포맷이 담지 못하는 것을 짚습니다.

원래 코드와 어긋나는 까닭

브라우저에 실리는 자바스크립트는 사람이 쓴 코드와 다릅니다. 빌드가 그 사이에서 코드를 여러 번 바꾸기 때문입니다. 단계마다 위치가 조금씩 어긋납니다.

단계 하는 일 위치가 어긋나는 모양
트랜스파일 TypeScript 나 새 문법을 오래된 자바스크립트로 옮긴다 타입 표기가 빠져 칸이 밀린다
번들링 파일 수백 개를 하나로 합친다 원래 파일 이름이 사라진다
미니파이 공백과 줄바꿈을 지우고 이름을 짧게 바꾼다 수천 줄이 한 줄이 된다

세 단계를 다 거친 파일에서 오류가 나면 위치가 app.min.js:1:40213 처럼 찍힙니다. 한 줄짜리 파일의 40213번째 칸이라는 뜻입니다. 그 칸 근처에는 n·d 같은 줄인 이름만 보입니다. 원래 어느 함수인지 알아내기 어렵습니다.

백엔드 개발자는 이 문제를 덜 겪습니다. 자바 예외의 스택 트레이스에는 원래 소스의 줄 번호가 찍힙니다. 컴파일러가 바이트코드 위치와 소스 줄 번호의 짝을 클래스 파일 안에 같이 넣어 두기 때문입니다. 소스맵은 그 짝 목록을 코드 밖의 별도 파일로 뺀 것에 가깝습니다.

위치를 짝지은 목록

소스맵이 담는 것은 짝의 목록입니다. 짝 하나는 바뀐 코드의 한 지점과 원래 코드의 한 지점을 잇습니다. 앞의 오류라면 「app.min.js 1줄 40213칸 → src/cart.ts 42줄 7칸」 같은 짝이 목록 어딘가에 들어 있습니다.

짝을 모든 글자마다 적지는 않습니다. 이름이나 식이 시작하는 지점마다 하나씩 적습니다. 짝이 없는 칸을 찾을 때는 같은 줄에서 그 칸 앞쪽으로 가장 가까운 짝을 씁니다.

이 목록을 읽는 쪽은 사람이 아니라 도구입니다. 그래서 목록을 사람이 읽는 표가 아니라 아주 짧은 문자열로 줄여 적습니다. 그 문자열을 읽는 법은 두 소절 뒤에서 봅니다.

소스맵 파일 한 벌

예로 쓸 원래 코드는 src/add.ts 의 세 줄짜리 함수입니다.

TypeScript
function add(a: number, b: number) {
  return a + b;
}

변환 도구가 타입 표기를 지우고 공백을 줄이면 한 줄짜리 add.js 가 나옵니다. 원래 2번째 줄에 있던 return 이 결과에서는 1번째 줄 19번째 칸으로 옮겨 왔습니다.

JavaScript
function add(a,b){return a+b}

도구는 이 결과와 함께 add.js.map 이라는 소스맵을 냅니다. 안은 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) 객체 하나입니다.

JSON
{
  "version": 3,
  "file": "add.js",
  "sources": ["../src/add.ts"],
  "sourcesContent": ["function add(a: number, b: number) {\n  return a + b;\n}\n"],
  "names": [],
  "mappings": "AAAA,SAAS,SACP"
}

필드마다 맡은 일은 아래와 같습니다. mappings 가 짝 목록입니다. 나머지는 그 목록을 읽는 데 필요한 이름표입니다.

필드 담는 것
version 소스맵 형식의 판. 지금 쓰이는 판은 3 이다
file 이 소스맵이 설명하는 결과 파일의 이름
sources 원래 파일들의 경로 목록. 짝은 이 목록의 번호로 파일을 가리킨다
sourcesContent 원래 파일의 내용 전부. 비워 두면 도구가 sources 의 경로에서 원래 파일을 받아 온다
names 줄이기 전의 이름 목록. 미니파이가 total 을 n 으로 바꿨다면 total 이 여기 들어간다
mappings 짝 목록을 줄여 적은 문자열

번들러가 만든 번들이라면 sources 에 파일 수백 개가 들어갑니다. 짝은 파일 이름을 직접 적지 않고 이 목록의 번호만 적습니다. 같은 이름을 짝마다 되풀이하지 않으려는 것입니다.

이 파일은 결과 파일 곁에 하나 더 생깁니다. sourcesContent 에 원래 코드를 담으면 결과 파일보다 커지는 일이 많습니다. 빌드도 짝을 모아 적는 만큼 시간이 더 걸립니다.

mappings 를 읽는 법

mappings 문자열은 세 겹으로 쪼개집니다. 바깥부터 결과 코드의 줄, 줄 안의 조각, 조각 안의 숫자입니다. 조각 하나가 짝 하나입니다.

flowchart TD
    M["mappings 문자열"] -->|"세미콜론으로 나눈다"| L["결과 코드의 줄"]
    L -->|"쉼표로 나눈다"| S["조각 · 짝 하나"]
    S -->|"글자를 숫자로 푼다"| V["숫자 넷이나 다섯"]

세미콜론(;)은 결과 코드의 줄을 가릅니다. 세미콜론으로 나눈 첫 부분에 1번째 줄의 짝이 들어 있습니다. 둘째 부분에는 2번째 줄의 짝이 들어 있습니다. 줄 안에서는 쉼표(,)가 조각을 가릅니다.

조각을 풀면 숫자가 넷이나 다섯 나옵니다. 숫자의 순서는 늘 같습니다.

순서 뜻
1 결과 코드에서 몇째 칸인가
2 sources 의 몇 번째 파일인가
3 원래 파일의 몇째 줄인가
4 원래 파일의 몇째 칸인가
5 names 의 몇 번째 이름인가. 이름을 바꾸지 않았으면 없다

숫자가 하나뿐인 조각도 있습니다. 결과 코드의 그 칸에 대응하는 원래 코드가 없다는 표시입니다.

차이만 적는 까닭

조각의 숫자는 위치 그 자체가 아니라 앞 조각과의 차이입니다. 결과 칸만은 줄이 바뀌면 0 에서 다시 셉니다. 나머지 넷은 줄을 넘어서도 앞 조각에서 이어 셉니다.

차이로 적는 것은 파일을 작게 만들려는 것입니다. 한 줄짜리 번들에서 칸 번호는 수만까지 커집니다. 그런데 이웃한 두 조각의 차이는 대개 몇 칸이 안 됩니다.

조각의 숫자는 Base64 글자로 적습니다. Base64 는 영문 대소문자·숫자·+·/ 의 글자 64가지만으로 데이터를 적는 방식입니다. 글자마다 0번부터 63번까지 번호가 있어서 A 는 0번, B 는 1번입니다. 그래서 JSON 문자열 안에 그대로 넣을 수 있습니다.

숫자 하나는 글자 하나 이상으로 적힙니다. 작은 수는 한 글자로 끝납니다. 큰 수는 글자를 이어 붙입니다. 이렇게 수마다 길이가 달라지는 방식을 Base64 VLQ(Variable-Length Quantity, 가변 길이 수)라고 부릅니다. 차이로 적어 수가 작아진 덕에 숫자 대부분이 한 글자에 들어갑니다.

글자 하나가 담는 값은 -15부터 15까지입니다. Base64 글자 하나는 6비트입니다. 그중 한 비트는 「뒤에 글자가 더 붙는다」는 표시로 씁니다. 또 한 비트가 부호를 맡아서 크기에는 4비트, 곧 0부터 15까지만 남습니다.

한 글자로 끝나는 수는 글자 번호를 2로 나눠 풉니다. 몫이 크기입니다. 나머지가 1이면 음수, 0이면 양수입니다. 앞 예에 나오는 글자 넷을 풀면 아래와 같습니다.

글자 Base64 번호 2로 나누면 값
A 0 몫 0 · 나머지 0 0
C 2 몫 1 · 나머지 0 +1
P 15 몫 7 · 나머지 1 -7
S 18 몫 9 · 나머지 0 +9

이 표로 앞 예의 AAAA,SAAS,SACP 를 풉니다. 조각마다 네 글자가 숫자 넷이 됩니다.

AAAA   // 0, 0, 0, 0
SAAS   // +9, 0, 0, +9
SACP   // +9, 0, +1, -7

차이를 앞에서부터 더해 가면 짝 셋이 나옵니다. 파일 안의 숫자는 0 부터 셉니다. 도구는 화면에 보여 줄 때 1 을 더합니다. 아래 표도 사람이 세는 방식으로 1 부터 적었습니다.

결과 add.js 원래 src/add.ts 무엇이 시작하나
1줄 1칸 1줄 1칸 function
1줄 10칸 1줄 10칸 add
1줄 19칸 2줄 3칸 return

셋째 짝이 원래 2번째 줄의 return 이 옮겨 간 위치를 적은 것입니다.

결과 코드와 소스맵을 잇는 방법

결과 파일은 자기 소스맵이 어디 있는지 맨 끝 주석 한 줄로 알립니다. 브라우저의 개발자 도구는 이 줄을 보고 소스맵을 받아 옵니다.

JavaScript
function add(a,b){return a+b}
//# sourceMappingURL=add.js.map

CSS(Cascading Style Sheets, 웹 문서의 모양을 정하는 언어) 파일도 같은 소스맵 형식을 씁니다. 주석 꼴만 /*# sourceMappingURL=... */ 로 바뀝니다.

주석 말고도 잇는 방법이 둘 더 있습니다. 하나는 HTTP(HyperText Transfer Protocol, 웹의 요청·응답 규약) 응답 헤더입니다. 서버가 결과 파일을 보낼 때 SourceMap: /maps/add.js.map 헤더를 붙이면 결과 파일을 고치지 않아도 됩니다.

다른 하나는 소스맵 전체를 결과 파일 안에 넣는 인라인 방식입니다. 소스맵을 Base64 로 바꿔 주석의 주소 부분에 바로 적습니다. 이런 주소를 데이터 URL(Uniform Resource Locator, 자원의 주소)이라고 부릅니다. 파일 하나로 끝나서 개발 중에 편합니다. 대신 결과 파일이 소스맵만큼 커집니다.

개발자 도구가 소스맵을 읽으면 원래 파일이 파일 목록에 나타납니다. 원래 TypeScript 줄에 중단점을 걸 수도 있습니다. 도구가 짝을 거꾸로 찾아 결과 코드의 해당 칸에서 멈춥니다.

서버 쪽도 같습니다. Node.js 는 --enable-source-maps 옵션을 켜면 스택 트레이스를 원래 TypeScript 파일의 줄로 찍습니다.

오류 위치를 되돌리는 흐름

운영 중인 서비스는 사용자 브라우저에서 난 오류를 모아 봐야 합니다. 이 일을 맡는 오류 수집 서비스가 소스맵을 가장 많이 읽습니다.

흔한 방식은 소스맵을 사용자에게 내보내지 않는 것입니다. 빌드는 소스맵을 만듭니다. 결과 파일 끝의 주석은 넣지 않습니다. 소스맵은 오류 수집 서비스에만 올립니다. 브라우저는 줄인 코드만 받습니다.

sequenceDiagram
    participant 빌드
    participant 브라우저
    participant 수집 as 오류 수집 서비스
    participant 개발자
    빌드->>수집: 소스맵을 올린다
    Note over 빌드,브라우저: 배포에는 줄인 코드만 나간다
    브라우저->>수집: 오류 · app.min.js 1줄 40213칸
    수집->>수집: 소스맵에서 짝을 찾는다
    수집-->>개발자: src/cart.ts 42줄 7칸

브라우저가 보내는 것은 줄인 코드 기준의 위치뿐입니다. 서비스는 미리 받아 둔 소스맵으로 그 위치를 원래 파일과 줄로 바꿉니다. 개발자는 처음부터 원래 코드의 위치를 받아 봅니다.

이때 결과 파일과 소스맵은 같은 빌드에서 나온 짝이어야 합니다. 코드를 한 글자만 바꿔도 칸 번호가 밀리기 때문입니다. 다른 빌드의 소스맵을 대면 엉뚱한 줄을 가리킵니다.

변환이 여러 번 겹칠 때

빌드는 대개 한 번에 끝나지 않습니다. TypeScript 를 자바스크립트로 옮긴 뒤 번들로 합칩니다. 마지막에 줄입니다. 단계마다 도구가 다릅니다. 각 도구는 자기가 받은 코드를 기준으로만 짝을 압니다.

그래서 각 단계는 앞 단계의 소스맵을 받아 자기 짝과 이어 붙입니다. 「줄인 코드 → 번들」 짝과 「번들 → TypeScript」 짝을 이으면 「줄인 코드 → TypeScript」 짝이 됩니다. 마지막 단계가 내는 소스맵 하나만 있으면 맨 처음 코드까지 거슬러 갑니다.

중간 단계 하나가 앞의 소스맵을 받지 못하면 사슬이 거기서 끊깁니다. 그러면 오류 위치가 TypeScript 파일이 아니라 중간에 만들어진 자바스크립트를 가리킵니다.

이 포맷이 못 담는 것

소스맵이 담는 것은 위치와 이름의 짝뿐입니다. 코드가 무엇을 하는지는 적지 않습니다. 실무에서 겪는 불편 대부분이 거기서 나옵니다.

못 담는 것 그래서 생기는 일
변수가 살아 있는 범위 디버거의 변수 목록에 줄인 이름 n·d 가 섞여 보이기도 한다
짝과 짝 사이의 위치 앞쪽 가장 가까운 짝으로 어림하므로 오류 줄이 조금 어긋날 수 있다
누가 읽어도 되는지 파일을 받을 수 있는 사람은 원래 코드를 전부 읽는다

첫 줄은 이름 바꾸기에서 옵니다. 디버거의 변수 목록은 칸이 아니라 지금 실행 중인 코드에 살아 있는 변수를 보여 줍니다. 그 변수들의 이름은 줄인 이름입니다.

names 는 칸마다 원래 이름을 적습니다. 어느 변수가 어느 범위에 사는지는 적지 않습니다. 그래서 디버거는 목록의 n 이 원래 어느 이름인지 확실히 짚지 못합니다. 같은 n 이 함수마다 다른 변수일 수도 있습니다.

둘째 줄은 짝을 시작점에만 적어서 생깁니다. 한 식이 여러 줄에 걸쳐 있으면 오류 위치가 그 식의 첫 줄로 잡히기도 합니다. 짝을 촘촘히 적을수록 정확해집니다. 그만큼 파일도 커집니다.

셋째 줄은 sourcesContent 때문에 특히 아픕니다. 이 필드에는 원래 파일의 내용이 주석까지 고스란히 들어갑니다. 소스맵을 공개 서버에 함께 올리면 누구나 개발자 도구에서 원래 코드를 읽습니다. 그래서 소스맵을 오류 수집 서비스에만 올리는 방식이 흔합니다.

관련 항목

소스맵을 만들어 내는 빌드 단계

트랜스파일 · 번들러 · 미니파이 · 트리 셰이킹 · 코드 분할 · 빌드

소스맵을 내보내는 빌드 도구

TypeScript · tsc · Babel · webpack · esbuild · Vite · Rollup · Terser · SWC

소스맵을 읽어 원래 위치를 보여 주는 도구

개발자 도구 · 디버거 · 중단점 · 오류 수집 서비스 · Sentry · Node.js

소스맵 파일을 이루는 표기

JSON · Base64 · VLQ · 데이터 URL · 주석

소스맵이 위치를 되돌려 주는 오류 정보

스택 트레이스 · 예외 · 오류 추적 · 콘솔

소스맵으로 원래 위치를 되찾는 코드 형식

JavaScript · CSS · WebAssembly · Sass · Less

다른 언어에서 같은 짝을 담는 디버그 정보

클래스 파일 · 바이트코드 · 디버그 심볼 · DWARF · PDB

소스맵을 정의하는 표준과 기구

ECMA-426 · TC39 · ECMAScript · Ecma International

소스맵이 속하는 상위 분류

포맷 · 프론트엔드 · 디버깅 · 배포 · 웹 개발

다른 이름: source map · sourcemap · 소스 맵 · 소스맵 파일