소스맵(Source Map) 완전 해부
브라우저 개발자 도구에서 Sources 패널을 열어보면 원본 TypeScript 파일이 깔끔하게 표시되곤 합니다. 분명 브라우저가 실행하는 건 번들링되고 압축된 JavaScript일 텐데, 어떻게 원본 코드가 나타나는 걸까요? 바로 소스맵(source map) 덕분이에요.
소스맵은 웹 개발에서 빠질 수 없는 디버깅 도구지만 그 내부를 들여다볼 일은 좀처럼 없는데요. 이 글에서는 소스맵의 JSON 구조와 VLQ 인코딩 원리를 깊이 살펴보고 주요 빌드 도구별 설정 방법도 정리해보겠습니다. 나아가 프로덕션 환경에서 소스맵이 뜻하지 않게 소스코드를 노출하는 보안 위험과 대응법도 함께 다룹니다.
소스맵이 필요한 이유
현대 웹 개발에서 우리가 작성한 코드는 브라우저에서 그대로 실행되지 않습니다. TypeScript는 JavaScript로 변환되고 JSX는 createElement 호출로 바뀌며 SCSS는 CSS로 컴파일되죠. 사실 여기까지만 해도 브라우저에서 실행하는 데는 문제가 없는데요. 실제 프로덕션에서는 한 발 더 나아갑니다. 번들러가 수백 개의 파일을 하나로 합쳐 네트워크 요청 수를 줄이고, 압축기(minifier)가 공백과 주석을 제거해서 파일 크기를 최소화하죠. 사용자가 내려받아야 할 파일이 작아질수록 페이지 로딩이 빨라지기 때문입니다. 여기에 난독화(obfuscation)까지 적용하면 변수명이 a, b, c로 바뀌고 코드 흐름이 뒤섞여서 소스코드를 들여다봐도 원래 의도를 파악하기 어렵게 됩니다. 브라우저에서 누구나 소스코드를 볼 수 있는 웹의 특성상, 비즈니스 로직을 보호하기 위해 많이 사용하죠.
이렇게 변환된 코드에서 에러가 발생하면 어떻게 될까요? 브라우저 콘솔에는 bundle.min.js:1:28432처럼 의미를 알 수 없는 위치 정보만 표시됩니다. 압축된 한 줄짜리 코드에서 28,432번째 문자가 어디인지 찾아야 하니, 사실상 디버깅이 불가능하죠 😅
소스맵은 바로 이 문제를 해결합니다. 변환된 코드의 각 위치가 원본 코드의 어느 파일, 몇 번째 줄, 몇 번째 열에 해당하는지를 담은 매핑 정보 파일이에요. 브라우저 개발자 도구가 이 파일을 읽으면 마치 원본 코드를 직접 실행하는 것처럼 디버깅할 수 있게 됩니다.
소스맵의 구조
소스맵은 .map 확장자를 가진 JSON 파일입니다. 구조를 살펴볼까요?
{
"version": 3,
"file": "example.min.js",
"sources": ["../src/math.ts", "../src/index.ts"],
"sourcesContent": [
"export function add(a: number, b: number) {\n return a + b;\n}",
"import { add } from './math';\nconsole.log(add(1, 2));"
],
"names": ["add", "console", "log"],
"mappings": "AAAA,SAASA,IAAIC,EAAWC;AACE,OAAO,EAAID;..."
}
version은 소스맵 형식 버전으로 현재 표준은 3입니다. 2023년에 ECMA-426으로 공식 표준화되었지만 버전 번호는 그대로 유지되고 있죠. file은 변환된 결과 파일의 이름이고, sources는 원본 소스 파일들의 경로 배열입니다.
여기서 가장 주목해야 할 필드가 바로 sourcesContent입니다. 이 필드에는 sources에 나열된 각 파일의 전체 원본 코드가 그대로 들어갑니다. 소스맵 파일만 있으면 원본 파일에 접근하지 않고도 전체 소스코드를 복원할 수 있다는 뜻이에요. 다시 말해, .map 파일 하나가 외부에 노출되면 전체 코드베이스가 그대로 유출될 수 있습니다.
names에는 원본 코드에서 사용된 식별자 이름이 배열로 들어가고, mappings에는 변환된 코드와 원본 코드 사이의 위치 대응 관계가 VLQ(Variable-Length Quantity) Base64 인코딩으로 압축되어 저장됩니다.
매핑은 어떻게 인코딩되는가
mappings 필드의 문자열이 조금 낯설어 보일 수 있는데요. 이 문자열에는 변환된 코드의 모든 위치 정보가 촘촘하게 압축되어 있습니다. 간단한 예제로 살펴볼게요.
원본 TypeScript 코드가 이렇다고 해보죠.
function add(a, b) {
return a + b;
}
이 코드를 압축하면 한 줄이 됩니다.
function add(n, d) {
return n + d;
}
이때 생성되는 소스맵의 mappings 값은 이런 모습이에요.
AAAA,SAASA,IAAIC,EAAEC;AACf,OAAO,EAAID;AACb
세미콜론(;)은 변환된 코드의 줄 구분자입니다. 위 예제는 압축된 코드가 한 줄이라 세미콜론이 없지만, 원본이 3줄이었으니 세미콜론 2개로 원본의 줄 경계를 나타내고 있죠. 각 줄 안에서는 쉼표(,)로 개별 매핑 구간(segment)을 구분합니다. 이 구조를 풀어보면 다음과 같습니다.
AAAA,SAASA,IAAIC,EAAEC ← 원본 1번째 줄: function add(a, b) {
AACf,OAAO,EAAID ← 원본 2번째 줄: return a + b;
AACb ← 원본 3번째 줄: }
각 구간은 최대 5개의 VLQ 값으로 이루어집니다. 예를 들어 첫 번째 구간 AAAA는 4개의 값을 담고 있어요.
A A A A
│ │ │ └─ 원본 코드의 열 번호
│ │ └──── 원본 코드의 줄 번호
│ └─────── 원본 파일 인덱스 (sources 배열에서의 위치)
└────────── 변환된 코드에서의 열 위치
5번째 값이 있으면 names 배열에서의 식별자 인덱스를 나타냅니다. 예를 들어 SAASA의 마지막 A는 names[0], 즉 add를 가리키죠.
여기서 핵심은 모든 값이 이전 값과의 차이(delta)로 기록된다는 점입니다. 예를 들어 원본 코드의 10번째 줄 다음에 12번째 줄이 나오면 12가 아니라 2만 저장하는 거죠. 대부분의 매핑이 바로 직전 위치 근처를 가리키기 때문에 차이 값은 아주 작고, VLQ 인코딩은 작은 숫자를 적은 바이트로 표현하는 데 최적화되어 있어서 전체 파일 크기를 크게 줄일 수 있습니다.
빌드 도구별 소스맵 설정
대부분의 빌드 도구가 소스맵 생성을 지원합니다. 주요 도구별 설정 방법을 살펴볼게요.
웹팩(Webpack)에서는 devtool 옵션으로 소스맵 유형을 제어합니다.
module.exports = {
// 완전한 소스맵 (프로덕션 디버깅용)
devtool: "source-map",
// sourceMappingURL 주석 없이 생성
// devtool: "hidden-source-map",
// 빠른 리빌드 (개발 전용)
// devtool: "eval-source-map",
};
Vite에서는 build.sourcemap 옵션을 사용합니다.
export default defineConfig({
build: {
sourcemap: true, // .map 파일 생성
// sourcemap: "hidden", // sourceMappingURL 주석 없이 생성
},
});
TypeScript 컴파일러는 tsconfig.json에서 설정합니다.
{
"compilerOptions": {
"sourceMap": true
}
}
Bun 번들러에서는 --sourcemap 플래그를 사용합니다. 여기서 주의할 점이 하나 있는데요, Bun은 번들링 시 소스맵을 기본으로 생성합니다. 프로덕션 배포 시에는 none으로 명시적으로 꺼주는 것이 안전하죠.
# 별도 .map 파일로 생성
bun build ./src/index.ts --outdir ./dist --sourcemap=external
# 번들 파일 안에 인라인으로 삽입
bun build ./src/index.ts --outdir ./dist --sourcemap=inline
# 소스맵 생성 비활성화
bun build ./src/index.ts --outdir ./dist --sourcemap=none
소스맵은 어떻게 연결되는가
소스맵 파일이 있다는 것을 브라우저에게 알려주는 방법은 두 가지입니다.
첫 번째는 변환된 파일 끝에 특별한 주석을 추가하는 방식입니다.
// 번들링된 코드...
//# sourceMappingURL=example.min.js.map
CSS 파일도 비슷한 형식으로 연결합니다.
/* 압축된 스타일... */
/*# sourceMappingURL=styles.min.css.map */
두 번째는 HTTP 응답 헤더를 사용하는 방식입니다.
SourceMap: /path/to/example.min.js.map
브라우저의 개발자 도구는 이 두 가지 방법 중 하나를 감지하면 자동으로 소스맵 파일을 내려받습니다. 그리고 Sources 패널에 원본 코드를 표시해주죠. 중단점(breakpoint)을 설정하거나 에러 스택 트레이스를 확인할 때도 압축된 코드 대신 원본 코드의 파일명과 줄 번호가 표시되어 훨씬 수월하게 디버깅할 수 있습니다.
소스맵으로 인한 소스코드 유출 사건들
소스맵이 프로덕션에 노출되어 소스코드가 유출되는 사고는 한두 번이 아닙니다. 규모나 기술력과 관계없이 반복적으로 발생해 왔죠.
- Bitbucket Cloud / Imgur (2020) — 프론트엔드 소스맵이 공개 접근 가능한 상태로 노출, 버그 바운티를 통해 보고
- Stripe API 키 유출 (2023) — 소스맵에서 하드코딩된 결제 API 비밀 키가 발견되어 2만 5천 달러 바운티 지급
- Astro CVE-2024-56159 (2024) — 빌드 버그로 서버 측 소스맵이 브라우저에 노출, CVSS 7.8
- Apple App Store (2025) — 웹 기반 App Store에서 Svelte/TypeScript 전체 코드 유출, 8,000개 이상의 GitHub 포크 생성 후 DMCA 대응
그리고 가장 최근인 2026년 3월 31일, 클로드 코드(Claude Code)에서 사건이 터졌습니다. 한 보안 연구원이 npm 저장소에 발행된 패키지(@anthropic-ai/claude-code)에서 거대한 소스맵 파일을 발견한 건데요. cli.js.map이라는 이름의 이 파일에는 Claude Code의 전체 TypeScript 소스코드가 sourcesContent 필드에 그대로 담겨 있었죠.
왜 이런 일이 벌어졌을까요? 디버깅 목적으로 의도적으로 켠 소스맵이 배포 과정에서 제외되지 않은 채 npm에 올라갔을 가능성이 높습니다. 일각에서는 Bun 번들러의 소스맵 관련 버그 때문이라는 추측도 있었지만, 앤트로픽의 Claude Code 책임자는 “개발자 실수”라고 밝혔고 .npmignore 누락이 원인이라는 데 중론이 모아졌습니다. .npmignore에 *.map 패턴을 추가하거나 package.json의 files 필드에서 명시적으로 제외하는 것만으로 막을 수 있었던 일이었죠.
유출된 소스코드에는 시스템 프롬프트, 도구 정의, 권한 모델, 아직 공개되지 않은 기능까지 포함되어 있었습니다. 사건이 알려지자 유출된 코드를 올린 GitHub 저장소는 순식간에 폭발적인 관심을 받았고 이를 악용한 2차 피해도 뒤따랐습니다. 유출 코드를 분석한 것처럼 위장한 악성 저장소를 통해 멀웨어를 배포하거나, 비슷한 이름의 npm 패키지를 등록해 의존성 혼동(dependency confusion) 공격을 시도하는 사례까지 보고되었죠.
앤트로픽은 이 사건을 “사람의 실수로 인한 패키징 문제”라고 밝혔습니다. npm 저장소에서 해당 버전을 삭제하고 소스맵이 제거된 새 버전을 곧바로 배포했으며, 유출된 코드를 미러링하는 GitHub 저장소에는 DMCA 삭제 요청을 보냈습니다. 하지만 한 번 공개된 소스코드는 되돌릴 수 없었고, 이 사건은 프로덕션 환경에서 소스맵 관리가 얼마나 중요한지 업계에 경각심을 불러일으켰습니다.
보안 업체 Escape.tech에 따르면, 테스트한 조직의 약 70%에서 소스맵이 노출된 상태로 발견된다고 합니다. 소스맵 유출은 소수의 불운한 사고가 아니라 업계 전반에 걸친 만연한 문제인 셈이죠.
프로덕션 소스맵 보안
위 사례들에서 볼 수 있듯이, 소스맵이 외부에 노출되면 심각한 보안 위험으로 이어질 수 있습니다. 특히 오픈 소스가 아닌 상업용 프로젝트에서는 심각한 비즈니스 피해로 이어질 수 있죠.
가장 직접적인 위험은 sourcesContent를 통한 전체 소스코드 복원입니다. 아무리 코드를 압축하고 난독화해도 소스맵만 있으면 원본을 그대로 되살릴 수 있죠. 노출된 소스코드에는 API 엔드포인트, 내부 로직, 보안 메커니즘이 담겨 있을 수 있어서 공격자에게 매우 유용한 정보가 됩니다. sources 필드에 나열된 파일 경로로 프로젝트의 디렉토리 구조와 의존성까지 드러나면서 공급망 공격의 실마리를 제공할 수도 있고요.
이런 위험을 방지하려면 어떻게 해야 할까요?
우선 npm 패키지를 배포할 때는 반드시 소스맵 파일을 제외해야 합니다.
*.map
또는 package.json의 files 필드에서 배포할 파일을 명시적으로 지정하는 방법도 있습니다. 이 방식이 허용 목록(allowlist) 방식이라 더 안전하죠.
{
"files": ["dist/cli.js", "README.md"]
}
웹 서비스라면 CDN이나 웹 서버 설정에서 .map 파일에 대한 외부 접근을 차단하는 것이 좋습니다.
그런데 프로덕션에서 소스맵을 완전히 없애면 에러가 발생했을 때 추적이 어려워지지 않을까요? 이때 활용하는 것이 hidden source map과 에러 모니터링 서비스의 조합입니다. Webpack의 hidden-source-map이나 Vite의 sourcemap: "hidden" 옵션을 사용하면 소스맵 파일은 생성하되 sourceMappingURL 주석은 추가하지 않습니다. 브라우저가 소스맵의 존재를 알 수 없으니 외부 노출 위험이 사라지죠. 이렇게 만들어진 소스맵을 Sentry 같은 에러 모니터링 서비스에 비공개로 업로드하면, 사용자에게는 소스맵을 노출하지 않으면서도 에러 발생 시 원본 코드의 위치를 확인할 수 있습니다.
마지막으로, CI/CD 파이프라인에 검증 단계를 추가하는 것도 효과적입니다. 사람의 실수는 언제든 발생할 수 있으니까요.
# 배포 산출물에 소스맵이 포함되지 않았는지 검증
if find dist -name "*.map" | grep -q .; then
echo "소스맵 파일이 배포 산출물에 포함되어 있습니다!"
exit 1
fi
이렇게 자동화된 안전장치를 마련해두면 .npmignore 설정이 실수로 빠지더라도 소스맵이 배포되는 것을 막을 수 있습니다.
마치며
소스맵은 현대 웹 개발에서 디버깅을 위해 꼭 필요한 도구입니다. 변환되고 압축된 코드를 원본에 매핑해주는 이 JSON 파일 덕분에 TypeScript 변환, 번들링, 압축 같은 빌드 과정의 이점을 누리면서도 편안하게 디버깅할 수 있죠.
하지만 Claude Code 유출 사건이 보여주듯, 소스맵의 sourcesContent 필드에는 원본 코드가 통째로 들어간다는 사실을 항상 기억해야 합니다. 프로덕션 환경에서는 hidden source map과 에러 모니터링 서비스를 조합하여 디버깅 역량은 유지하면서 소스코드 노출은 방지하는 전략이 바람직합니다. 그리고 무엇보다 .npmignore나 CI 검증 같은 안전장치를 통해 사람의 실수가 보안 사고로 이어지지 않도록 예방하는 것이 중요하고요.
소스맵의 공식 표준은 ECMA-426에서 확인할 수 있습니다.
This work is licensed under
CC BY 4.0