0. 들어가며…
이거 무슨 에러지?
개발을 하다 보면, 다음과 같은 에러를 마주치게 된다.
app-abc123.js:1:120
Uncaught Error: Something went wrong
에러는 분명히 발생했고 콘솔에도 찍혔다. 그런데 이상하다…?
에러 메시지를 가만히 들여다보고 있으면 몇 가지 답답함이 밀려온다.
- 어떤 파일에서 발생한 에러인지 감이 안 오고
- 어떤 코드 줄에서 터졌는지도 모르겠고
- 심지어 이게 내가 작성한 코드인지조차 확신이 안 든다
에러는 났는데 정작 어디를 봐야 할지 모르는 상태. 이쯤 되면 자연스레 이런 생각이 든다.
*"아… 이거 sourcemap 없어서 그런가?"*
sourcemap, 들어는 봤는데 설명은 애매한 그 단어
sourcemap. 대부분의 개발자가 한 번쯤은 들어봤을 단어다.
하지만 막상 누군가 "sourcemap이 정확히 뭐야?"라고 물어보면 대답이 조금씩 흐려진다.
보통은 이런 답이 돌아온다.
- "난독화된 코드를 원래 코드로 바꿔주는 거 아닌가요?"
- "개발 모드에서는 있고, 운영에서는 끄는 그 옵션이요."
- "없으면 디버깅이 불편해지는 거?"
전부 틀린 말은 아니다. 하지만 핵심이 빠져 있다.
- sourcemap은 단순한 디버깅 옵션이 아니다.
- sourcemap은 빌드 결과물의 일부이며, 보안과 직접적으로 연결되는 파일이다.
이걸 모른 채 설정을 만지면 운영에서 에러가 나는데 재현이 안 되거나,
에러 위치가 엉뚱하게 나오거나, 심지어 의도치 않게 원본 소스 코드가 외부에 노출되는 사고가 발생할 수 있다. 즉, sourcemap은 "있으면 좋은 것"이 아니라 "잘 다뤄야 하는 것"이다.
이번 글에서는…
그래서 이번 시간에는 막연하게 알고 있던 sourcemap에 대해 차근차근 알아보려고 한다.
- sourcemap은 정확히 무엇인가?
- 무엇을 해주고, 무엇을 해주지 않는가?
- sourcemap은 언제, 어떻게 만들어지는가?
- 빌드 파이프라인의 어느 지점에서 생성될까?
- 왜 빌드 결과물과 항상 한 쌍일까?
- .map 파일 하나로 어떻게 원본 코드의 에러 위치를 찾을 수 있을까?
- 이게 가능한 원리는 뭘까?
- 왜 .map 파일이 노출되면 위험한가?
- 정말로 .js와 .map만 있으면 원본 소스를 볼 수 있을까?
단, VLQ 수학 공식 암기나 스펙 문서 해설 같은 내용은 다루지 않는다. 대신 이 글을 읽으면
sourcemap이 왜 필요하며, 운영 환경에서 어떻게 다뤄야 안전한지 알 수 있을 것이다.
1. Source Map이란 무엇인가
1) Source Map이란?
(1) 코드를 복구하는 툴이 아니다
- sourcemap을 처음 접하면 다음과 같이 생각한다.
“sourcemap은 난독화된 자바스크립트를 원래 코드로 되돌려주는 파일이지 않나?”
- 반은 맞고 반은 틀린 말이다.
- 정확히 말하면, sourcemap은 코드를 변환하거나 로직을 '되돌리는' 도구가 아니다.
- 그럼 sourcemap은 정확히 무엇을 할까? 역할은 생각보다 단순하다.
💡 빌드된 자바스크립트 코드의 위치를 원본 소스 코드의 위치로 연결(Mapping)해 주는 것
- 상황과 함께 더 자세히 살펴보자.
(2) 빌드가 끝난 코드에 남아 있는 정보
- 에러가 발생했을 때, 빌드된(minified) 자바스크립트 파일 기준으로 얻을 수 있는 정보는 다음과 같다.
app-abc123.js:1:120
- 이 로그가 우리에게 알려주는 정보는 딱 두 가지다.
- 이 에러는 app-abc123.js 파일에서 발생했고, 1번째 줄 120번째 컬럼에서 터졌다는 사실!
- 하지만 여기엔 디버깅에 필요한 결정적인 정보가 빠져 있다.
- 이 코드가 원래 어떤 파일에서 왔는지
- 내가 작성한 코드인지
- 아니면 라이브러리 / 프레임워크 내부 코드인지
- 즉, "어디서 에러가 났는지(물리적 좌표)"는 알 수 있지만
- "무슨 코드에서 에러가 났는지(맥락)"는 알 수 없다. 바로 이 간극을 메워주는 것이
sourcemap이다.
(3) 좌표를 바꿔주는 지도 (Coordinate Map)
sourcemap은 ‘코드 복원기’라기보다‘좌표 변환 지도’에 가깝다.- 만약 다음과 같은 (a) 입력이 있다면, sourcemap은 그에 대응되는 (b) 위치 정보를 찾아준다.
(a) 입력: 빌드된 JS 파일의 위치 (line, column)
⬇️
(b) 출력: 원본 소스 파일의 위치 (file, line, column)
즉, sourcemap은 “이 코드가 어디서 왔는지”를 계산해내는 도구가 아니라,
빌드 과정에서 미리 기록해 둔 좌표 관계를 조회하기 위한 데이터다.브라우저는 이 좌표 관계를 이용해, 사람이 이해할 수 있는 에러 위치를 복원한다.
그래서 브라우저는 sourcemap에게 이런 질문을 던진다.
Q. 브라우저:
"이 번들 파일(app.js)의 1번째 줄, 120번째 컬럼은
원래 어떤 파일의 어디였어?"
A. Source Map:
"원본 코드와 빌드된 코드 사이의 좌표 관계를 기준으로 보면,
그 위치는 ExampleComponent.vue 파일의 15번째 줄이야."
(4) .map 파일을 뜯어보자
- 실제 .map 파일을 열어보면 보통 다음과 같은 JSON 구조를 띄고 있다.
{
"version": 3,
"file": "app-abc123.js",
"sources": ["ExampleComponent.vue"],
"sourcesContent": ["<template>...</template>"],
"mappings": "oGAIA,SAASA,GAAM,..."
}
처음 보면 알 수 없는 문자열(mappings) 때문에 이해하기 어렵다.
하지만 여기서 주목해야 할 포인트는 명확하다. 핵심은 "텍스트 치환"이 아니라 "위치 관계"다
sourcemap은 전체 코드를 1:1로 치환하는 도구가 아니다.
본질은 좌표에 있다. sourcemap은 다음 질문에 답하기 위해 존재한다.
이 에러가 발생한 위치(line, column)는 원본 코드 기준으로 어디이며,
그 위치에 있던 변수의 원래 이름은 무엇인가?"
- 즉, sourcemap이 제공하는 정보의 핵심은 좌표(Coordinate)다.
- 빌드된 JS의 몇 번째 줄, 몇 번째 컬럼이 원본 소스의 어떤 파일, 어떤 위치에 대응되는지 그 관계를 기록해 둔 것이 바로 .map 파일이다.
- 여기서
sourcesContent필드에 원본 코드의 전체 텍스트가 함께 포함되는 경우도 많다. - 브라우저가 원본 파일을 별도로 요청하지 않고도 개발자 도구에서 바로 코드를 보여주기 위해서다.
- 하지만 이건 어디까지나 편의 기능이다.
- sourcemap의 본질적인 역할은 여전히 하나다.
- 빌드된 코드와 원본 코드 사이의 "위치 관계"를 알려주는 디버깅용 지도!
- 이 구조를 이해하면, 왜 .map 파일이 노출되면 위험한지,
- 그리고 왜 운영 환경에서는 이 파일을 함부로 다루면 안 되는지도 알게 된다.
2) sourcemap은 언제, 어떻게 만들어질까
(1) sourcemap은 빌드 과정에서 '필연적으로' 생성된다
sourcemap은 빌드 도구가 코드를 변환하는 과정에서 자동으로 생성해내는 산출물이다.
그래서 sourcemap의 생성 원리를 이해하려면 빌드 과정을 알아야 한다.
우리가 평소에 작성하는 코드는 보통 이런 모습이다. 사람이 읽고 이해하기 좋은 형태다.
// TypeScript
function boom() {
throw new Error("Test error");
}
- 혹은 Vue를 쓴다면 이런 형태일 것이다.
<!-- Vue SFC -->
<script setup lang="ts">
function boom() {
throw new Error("Test error");
}
</script>
- 하지만 중요한 사실이 하나 있다.
- 브라우저는 이 코드를 그대로 실행할 수 없다. (혹은 실행할 수 있어도 매우 비효율적이다.)
(2) 빌드 도구는 코드를 '실행용 언어'로 번역한다
Vite, Webpack, Rollup 같은 빌드 도구들은 브라우저를 대신해 '번역' 역할을 수행한다.
- 변환(Transpile): TypeScript나 최신 JS 문법을 구형 브라우저도 이해할 수 있는 JS로 바꾼다.
- 번들링(Bundling): 수백 개의 파일을 로딩하기 좋게 몇 개의 파일로 합친다.
- 압축(Minify): 공백, 줄바꿈을 제거하고 긴 변수명을
a,b처럼 짧게 줄여 용량을 줄인다.
이 과정을 거치고 나면 우리는 이런 결과물을 얻게 된다.
// ExampleComponent-1B51Wyje.js
function n(){throw new Error("Test error")}...
(3) 빌드 결과물에는 "맥락(Context)"이 없다
- 빌드가 끝난 JS 파일을 들여다보면, 처음에 작성했던 코드를 보기 어렵다.
- 이 코드가 원래 어떤 파일이었는지 모름
- 원본 코드의 몇 번째 줄이었는지 모름
- 어떤 함수나 변수명을 가지고 있었는지 모름
- 빌드 도구의 목표는 단 하나, "브라우저가 가장 빠르고 효율적으로 실행할 수 있는 코드"를 만드는 것이다.
- 브라우저 입장에서는 원본 파일명이 무엇인지, 개발자가 주석을 어떻게 달았는지는 전혀 중요하지 않다.
- 그래서 빌드 결과물에는 '실행'에 필요 없는 모든 정보가 제거된다.
(4) 여기서 문제가 발생한다
- 문제는 에러가 났을 때다.
ExampleComponent-1B51Wyje.js:1:120개발자는 이 로그만 보고는 아무것도 할 수 없다.
"120번째 글자에서 에러가 났다"는 사실만으로는 디버깅이 어렵기 때문이다.
바로 이 지점에서 sourcemap이 등장한다.
빌드 도구는 코드를 최적화하면서, 원본과의 관계를 기록한 '별도의 설명서'인 .map을 만들어낸다.
즉, 빌드는 실행을 위해 맥락을 버리고, sourcemap은 디버깅을 위해 그 맥락을 따로 보존한다.
(5) sourcemap은 빌드의 입력이 아니라 '결과'다
- 가장 중요한 포인트는 sourcemap은 빌드 과정이 끝나야 완성되는 결과물이라는 점이다.
- 빌드 파이프라인을 단순화하면 다음과 같다.

- 코드를 변환(Transpile)할 때마다 "이 줄은 원래 저기서 왔다"는 기록을 남기고,
- 마지막에 이를 모아
.map파일로 내보낸다. - 그래서 sourcemap은 항상 번들된 파일과 '한 쌍(Pair)'으로 존재한다.
(6) 빌드 옵션의 의미: "기록을 남길 것인가?"
- Webpack의
devtool이나 Vite의build.sourcemap옵션은 결국 빌드 도구에게 이런 지시를 내리는 것이다.
"코드를 변환할 때, 원본 위치 정보도 같이 기록해서 파일로 만들어줄래?"
- 이 옵션은 크게 세 가지 동작으로 나뉘는데, 이는 뒤에서 다룰 보안 및 운영 전략의 핵심이 된다.
| 옵션 | 동작 방식 | 특징 |
|---|---|---|
false |
기록하지 않음 | - .map 파일이 생성되지 않는다. - 디버깅 불가. |
true |
기록함 + 꼬리표 부착 | - .map 파일을 생성한다.- JS 파일 끝에 //# sourceMappingURL=... 주석을 달아 브라우저가 원본 파일을 자동으로 찾게 한다. |
hidden |
기록함 + 꼬리표 없음 | - .map 파일은 생성한다.- 하지만, JS 파일에 연결 고리(주석)를 남기지 않는다. 그래서 브라우저는 자동으로 원본 파일을 찾을 수 없다. |
3) map 파일은 어떻게 원본 위치를 찾아낼까?
(1) 좌표값으로 위치를 찾는다.
- 다시 처음 상황으로 돌아가 보자. 브라우저 콘솔에 다음과 같은 에러가 찍혔다.
ExampleComponent-1B51Wyje.js:1:120
- 이 로그가 알려주는 정보는 매우 제한적이다.
- 파일(ExampleComponent)에서 에러가 났는데, 위치가 1번째 줄, 120번째 컬럼이라는 것만 안다.
🤔 이 숫자(1:120)을 보고 브라우저는 어떻게 원본 파일의 정확한 위치를 찾아내는 걸까?
- 원리는 생각보다 단순하다. sourcemap의 본질은 좌표 대응표(Lookup Table)다.
- 복잡한 알고리즘으로 추론하는 게 아니라, "이 위치는 원래 여기야"라고 기록해 둔 장부(Map)를 펼쳐보는 것이다.
(빌드된 코드의 좌표) ⇄ (원본 코드의 좌표)
- 즉, 디코딩이란 다음 질문의 답을 찾아가는 과정이다.
"이 난독화된 JS 파일의 1행 120열은, 원래 어떤 파일의 몇 번째 줄이었어?"
(2) .map 파일의 핵심 3대장
- 실제 .map 파일의 내부를 다시 살펴보자.
- 수많은 필드가 있지만, 실제 위치 추적(디코딩)에 사용되는 것은 딱 세 가지다.
{
"version": 3,
"file": "ExampleComponent-1B51Wyje.js",
"sources": ["../../src/components/ExampleComponent.vue"],
"names": ["boom", "_openBlock", "_createElementBlock"],
"mappings": "oGAIA,SAASA,GAAM,..."
}
a. sources: "어디서 왔는가 (원본 파일 목록)"
"sources": ["../../src/components/ExampleComponent.vue"]
- 이 번들 파일 하나를 만드는 데 사용된 원본 파일들의 경로 리스트다.
- 배열의 인덱스로 관리된다. 즉, sources[0]은 ExampleComponent.vue를 가리킨다.
b. names: "무엇이었는가 (이름 사전)"
"names": ["boom", "_openBlock", "_createElementBlock"]
- 선택 사항이지만 가독성을 위해 매우 중요한 필드다.
- 빌드 과정에서 boom이라는 함수명이 n으로 난독화되었다면, 에러 스택에는 at n (...)이라고 뜰 것이다.
- 이때 이 사전을 참조해 n이 원래 boom이었다는 것을 복구해 준다.
c. mappings: "어떻게 연결되는가 (압축된 좌표)"
"mappings": "oGAIA,SAASA,GAAM,..."
- 그리고 핵심 중의 핵심.
- 이 암호 같은 문자열 속에 빌드된 코드와 원본 코드의 모든 좌표 연결 정보가 압축되어 들어있다.
(3) mappings는 어떻게 읽히는가?
디코딩 라이브러리(브라우저 내장 기능)가 하는 일을 순서대로 풀어보면 다음과 같다.
- 입력: 에러가 발생한 JS 좌표를 받는다. (Line: 1, Column: 120)
- 탐색: mappings 문자열을 해석하여 해당 좌표가 속한 구간(Segment)을 찾는다.
- 해독: 그 구간에 저장된 정보를 바탕으로 다음 4가지를 알아낸다.
- Source Index (몇 번째 소스 파일인가?)
- Original Line (원본 몇 번째 줄인가?)
- Original Column (원본 몇 번째 칸인가?)
- Name Index (원래 이름은 무엇인가?)
- 출력: 최종적으로 우리가 보는 정렬된 에러 로그를 반환한다.
// 디코딩 결과
{
source: "ExampleComponent.vue",
line: 4,
column: 8,
name: "boom"
}
(4) 왜 mappings는 암호처럼 생겼을까?
- 여기서 자연스럽게 의문이 생긴다.
🤔 그냥 JSON으로 { line: 4, column: 8 } 이렇게 저장하면 안 되나? 왜 이상한 문자로 저장하지?
- 이유는 파일 크기 때문이다.
- 만약 모든 글자마다 매핑 정보를 객체로 저장한다면 .map 파일의 크기는 원본 JS 파일보다 수십 배, 수백 배 커질 것이다.
- 그래서 sourcemap은 VLQ(Variable-length quantity)라는 방식을 사용해 이전 값과의 차이(Delta)만을 기록한다.
- "좌표 120번"이라고 쓰는 대신 "아까 거기서 +1칸 옆"
- "4번째 줄"이라고 쓰는 대신 "아까 그 줄 그대로(+0)"
- 이 방식을 통해 파일 용량을 획기적으로 줄일 수 있었다.
(5) 효율적이지만, 치명적인 단점: "부서지기 쉽다"
- mappings 구조는 용량 면에서는 효율적이지만, 디버깅 관점에서 '부서지기 쉬운(Fragile)' 구조다.
- 모든 위치 정보가 이전 위치에 의존적이기 때문이다.
- 즉, 맨 앞의 글자 하나만 달라져도 그 뒤에 오는 모든 좌표 계산이 틀어진다.
- 이것이 의미하는 바는 명확하다.
💡 sourcemap에는 "비슷한 버전"이란 없다. "정확한 버전" 아니면 "완전한 오답" 뿐이다.
- 빌드 시점이 다르거나, 아주 사소한 플러그인 설정 변경으로 JS 파일의 공백 하나가 달라졌다고 가정해 보자.
- Delta 인코딩 특성상, 그 지점부터 뒤에 있는 모든 에러 스택의 줄 번호가 밀려버린다.
- 그래서 Sentry 같은 에러 모니터링 도구는 릴리즈(Release) 버전을 중요하게 여긴다.
- "이 버전의 JS 파일에는 반드시, 정확히 이 버전의 sourcemap만 적용한다"는 원칙이 지켜지지 않으면, 디버깅 자체가 불가능하기 때문이다.
4) 왜 .map 파일 노출은 위험한가
(1) .map만 있으면 원본 소스를 볼 수 있다
- 여기까지 이해했다면, 자연스럽게 하나의 질문이 떠오를 것이다.
😟 "잠깐… .map 파일에 원본 파일 경로랑 위치 정보가 다 들어 있다면, 이거 위험한 거 아닌가?"
- 맞다. .map 파일만 있으면, 제3자가 원본 소스 코드를 복원할 수 있다.
- 다시 .map 파일의 구조를 들여다보자.
{
"sources": ["../../src/components/ExampleComponent.vue"],
"sourcesContent": [
"<template>\n <button @click=\"boom\">Throw Error</button>\n</template>\n..."
],
"mappings": "oGAIA,SAASA,GAAM,..."
}
- 여기서 가장 치명적인 필드는 바로
sourcesContent다. - 이 필드에는 원본 파일의 전체 텍스트가 그대로 담겨 있는 경우가 많다.
- 브라우저가 원본 파일을 별도로 요청하지 않고도 개발자 도구에서 코드를 보여줄 수 있는 이유가 바로 이 데이터 덕분이다.
(2) 그럼 진짜로 파일까지 복원할 수 있다는 건가?
- .map 파일 하나만 있으면 된다.
- 아래는 .map 파일을 입력으로 받아 원본 소스 파일을 복원하는 스크립트의 핵심 로직이다. (전체 코드는 여기서 볼 수 있다.)
import fs from 'fs';
import { SourceMapConsumer } from 'source-map';
async function restoreSource(mapFilePath) {
const mapContent = fs.readFileSync(mapFilePath, 'utf8');
const sourceMap = JSON.parse(mapContent);
const consumer = await new SourceMapConsumer(sourceMap);
// sources 배열을 순회하며 원본 코드를 꺼냄
sourceMap.sources.forEach((sourcePath) => {
// 🔥 핵심: sourceContentFor 메서드가 원본 코드를 리턴함
const sourceContent = consumer.sourceContentFor(sourcePath);
if (sourceContent) {
fs.writeFileSync(`./restored/${path.basename(sourcePath)}`, sourceContent);
console.log(`✅ 복원 완료: ${sourcePath}`);
}
});
consumer.destroy();
}
- 이 스크립트가 하는 일은 단순하다.
- 소스맵 내부에 저장된 sourcesContent (원본 코드 텍스트)를 읽어서 파일로 저장하는 것뿐이다.
- 예시로 ExampleComponent.js.map 파일 하나만 스크립트에 넣고 돌려보았다.
// ExampleComponent.map.js (입력: .map 파일)
{
"version":3,
"sources":["../../src/components/ExampleComponent.vue"],
"sourcesContent":["<template>\n <button @click=\"boom\">Throw Error</button>..."],
"mappings":"oGAIA..."
}
- 그러면 다음과 같이 주석, 공백, 변수명(boom)까지 포함괸 원본 파일이 출력된다.
<!-- 복원된 파일: ExampleComponent.vue (출력: 원본 코드) -->
<template>
<button @click="boom">Throw Error</button>
</template>
<script setup lang='ts'>
function boom(){
throw new Error("Test error");
}
</script>
(3) 난독화해도 안전하지 않을까?
- 여기서 알 수 있는 사실은 명확하다.
- sourcemap이 함께 배포되었다면, 난독화된 JS는 보안상 아무런 의미가 없다.
- 왜냐하면 .map 파일과 빌드 파일만 있다면, .map 안에 들어 있는 "원본"을 꺼낼 수 있기 때문이다.
5) 그래서 운영 환경에서는 어떻게 해야 할까?
이제 다시 빌드 옵션 이야기로 돌아가 보자. 운영(Production) 환경에서 어떤 전략을 취해야 할까?
(1) 위험한 설정: sourcemap: true
- 동작: .map 파일을 생성하고, JS 파일 맨 끝에
//# sourceMappingURL=app.js.map이라는 "안내 표지판(주석)"을 달아둔다. - 결과: 브라우저 개발자 도구는 이 주석을 보고 자동으로 .map 파일을 찾아 다운로드한다.
- 문제점: 개발자뿐만 아니라, 사용자의 브라우저도 똑같이 소스맵을 다운로드해서 원본 코드를 보여준다.
(2) 현실적인 절충안: sourcemap: 'hidden'
- 동작: .map 파일은 똑같이 생성한다. 하지만 JS 파일 끝에 "안내 표지판(주석)"을 달지 않는다.
- 결과: 브라우저는 .map 파일이 존재하는지조차 모른다. 그래서 다운로드를 시도하지 않고, 원본 코드도 노출되지 않는다.
- 활용: "연결 고리"가 끊겨 있을 뿐 파일은 존재한다.
- Sentry 같은 도구에 이 .map 파일을 업로드하면, 도구가 내부적으로 매칭해서 에러를 복원해 준다.
- 앞서 소개한 로컬 CLI 도구를 사용해 수동으로 복원할 수도 있다.
(3) 가장 안전하지만 불편한 설정: sourcemap: false
- 동작: .map 파일 자체를 아예 만들지 않는다.
- 결과: 원본 코드로 돌아갈 방법이 아예 사라진다. 보안은 완벽하지만, 개발자조차 디버깅을 포기해야 한다.
6) 그럼 개발자는 어떻게 디버깅할까?
(1) 에러 모니터링 도구를 쓰면 된다! 환경이 허락한다면…
- 여기서 현실적인 문제가 하나 남는다.
🤔sourcemap을 hidden으로 숨겼다면,
우리는 운영 에러를 어떻게 원본 기준으로 확인해야 할까?
- 일반적인 환경이라면 답은 간단하다.
- Sentry, Datadog 같은 에러 모니터링 서비스(SaaS)를 사용하면 된다.
- 우리가 .map 파일을 그쪽 서버로 업로드해주면, 그들이 매칭한 에러 로그를 보여준다.
“하지만 모든 환경이 그럴 수 있는 건 아니다.”
- 금융권, 공공기관, 혹은 엄격한 내부망 환경에서는 다음과 같은 제약이 있을 수 있다.
- ❌ .map 파일을 외부 서버로 업로드할 수 없다. (소스코드 유출 간주)
- ❌ 외부 SaaS 서비스를 사용할 수 없다. (망분리 정책)
- ❌ 프론트엔드 에러 로그를 네트워크 밖으로 전송하는 것 자체가 금지다.
- 이 경우 대게 자체 로그툴이 있으나, 그 도구 역시 직접 로그를 남긴 코드에만 의존하는 경우가 많다.
- 즉, 로그를 심어둔 지점에는 추적 가능하지만, 로그가 없는 코드는 난독화된 JS 기준으로만 보인다.
- 로그를 심지 않은 경우, 운영 에러가 보통 이런 형태로 남는다.
at ExampleComponent-Cq_iF_Ko.js:1:120
at index-CWMXbtJ-.js:13:38
- 결국 sourcemap 기반 디버깅이 필요하지만, 이를 사용할 수 있는 수단이 없는 상태다.
(2) decode-sourcemap-cli, sourcemap을 로컬에서만 디코딩하기
- 이 문제를 해결하기 위해, 나는 로컬에서 sourcemap을 직접 디코딩하는 CLI 도구를 만들었다.
decode-sourcemap-cli의 핵심은 단순하다.
💡 운영 환경의 에러를, 서버 없이, 네트워크 없이, 오직 빌드 산출물만으로 디버깅하자.
- 이 도구는 다음을 전제로 한다.
- 운영에 배포된 JS 파일과
.map파일이 로컬에 존재하고 - 해당 파일들은 동일한 릴리즈 기준으로 생성되었으며
- 외부 서비스로 어떤 데이터도 업로드하지 않는다
- 운영에 배포된 JS 파일과
- 즉, 디버깅의 기준을 ‘에러 로그’가 아니라 ‘로컬에 보관된 릴리즈 산출물’로 옮기는 전략이다.
(3) 어떻게 사용하는가?
사용 방식은 매우 단순하다.
1. 준비: 운영 서버에 배포된 것과 동일한 릴리즈 기준으로 로컬 빌드를 수행한다. (로컬에서 .map이 산출되게)
2. 실행: 프로젝트 루트에서 다음 명령어를 입력한다.
npx dsm
3. 선택: 만약 모노레포 환경이라면, 어떤 앱을 디버깅할지 선택하는 화면이 나온다.

4. 입력: 브라우저 콘솔이나 로그 파일에서 에러 스택 트레이스(Minified)를 복사해서 그대로 붙여 넣는다.


5. 실행: 도구는 내부적으로 다음 과정을 수행한다.
에러 스택에 등장하는 JS 파일명(index-abc.js)을 파싱하고
로컬 dist 폴더에서 짝이 맞는 .map 파일을 찾아낸 뒤
source-map 라이브러리로 좌표를 역추적한다.
6. 결과: 잠시 후, 터미널에 원본 파일 경로와 정확한 라인 넘버가 출력된다. (Cmd+Click로 바로 이동도 가능)

(4) 왜 이 도구는 'Strict' 할까?
- 이 도구를 만들 때 가장 신경 쓴 부분은 앞서 4번 챕터에서 다룬 "소스맵의 버전 민감성"이다.
- 소스맵은 조금만 버전이 달라도 전체 좌표가 어긋나는 '부서지기 쉬운' 구조라고 했다.
- 그래서 decode-sourcemap-cli는 기본적으로 strict 전략을 사용한다.
- 단순히 파일명(app.js)만 같은 게 아니라, 해시값(app-a1b2c.js)까지 정확히 일치해야 디코딩을 시도한다.
물론 예외적인 상황도 고려했다.
- 코드가 100% 동일하더라도, 로컬 환경(Mac/Windows)과 CI/CD 서버(Linux)의 환경 차이로 인해 빌드 해시값이 미세하게 달라지는 경우가 종종 발생한다.
- 이런 상황을 위해, 해시값 검증을 건너뛰고 파일명만으로 매칭하는 filename 전략(--strategy=filename)도 만들었다.
- 단, 이 옵션은 정확도를 보장할 수 없기에 '힌트'를 얻는 용도로만 사용하는 게 좋다.
- 자세한 사용법과 전략, 단일 앱 / 모노레포 환경에서의 설정 방법은 아래 저장소에 정리해 두었다.
👉 https://github.com/KumJungMin/sourcemap-tools/tree/main/packages/decode-sourcemap-cli
2. 마치며…
지금까지 sourcemap의 원리부터 보안 이슈, 그리고 Sentry의 철학까지 살펴보았다.
긴 글을 세 줄로 요약하면 다음과 같다.
- sourcemap은 코드 복원기가 아니라 '좌표 변환 지도'다.
- 운영 환경에 sourcemap: true로 배포하는 건 원본 코드 노출의 위험이 크다.
- 소스맵은 버전 민감도가 매우 높으므로, CI/CD 파이프라인에서 릴리즈 단위로 철저히 관리해야 한다.
개발자에게 디버깅 경험(DX)은 포기할 수 없는 가치다. 하지만 그것이 서비스와 사용자의 보안을 위협하는 핑계가 되어서는 안 된다.
가장 이상적인 그림은 "사용자에게는 코드를 숨기되(hidden), 개발자는 언제든 원본을 볼 수 있는" 환경을 만드는 것이다. 상황에 따라 Sentry 같은 SaaS를 쓰든, 오늘 소개한 decode-sourcemap-cli 같은 로컬 도구를 쓰든, 보안과 효율이라는 모두 챙길 수 있는 환경을 구축하길 바란다.
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
| 이미지 리스트 성능 문제는 DOM에서 끝나지 않는다 (1) | 2026.01.13 |
|---|---|
| 브라우저 렌더링 최적화를 위한 Virtual Scroll - 구현 코드 살펴보기 (1) | 2025.11.16 |
| 브라우저 렌더링 최적화를 위한 Virtual Scroll - 원리/성능 비교 (0) | 2025.11.16 |
| [Vue] Fragment의 함정: 왜 $el은 Text Node가 될까? (0) | 2025.10.26 |
| [JS] 모바일 웹뷰에서 가상 키보드 감지하는 법: visualViewport·디바운스·rAF (8) | 2025.08.17 |
댓글