개발 기술/사소하지만 놓치기 쉬운 개발 지식

sourcemap? 운영에서는 그냥 끄는 옵션 아니에요?

by GicoMomg 2025. 12. 13.

0. 들어가며…

이거 무슨 에러지?

개발을 하다 보면, 다음과 같은 에러를 마주치게 된다.

app-abc123.js:1:120
Uncaught Error: Something went wrong

에러는 분명히 발생했고 콘솔에도 찍혔다. 그런데 이상하다…?
에러 메시지를 가만히 들여다보고 있으면 몇 가지 답답함이 밀려온다.

  • 어떤 파일에서 발생한 에러인지 감이 안 오고
  • 어떤 코드 줄에서 터졌는지도 모르겠고
  • 심지어 이게 내가 작성한 코드인지조차 확신이 안 든다

에러는 났는데 정작 어디를 봐야 할지 모르는 상태. 이쯤 되면 자연스레 이런 생각이 든다.

*"아… 이거 sourcemap 없어서 그런가?"*

sourcemap, 들어는 봤는데 설명은 애매한 그 단어

sourcemap. 대부분의 개발자가 한 번쯤은 들어봤을 단어다.
하지만 막상 누군가 "sourcemap이 정확히 뭐야?"라고 물어보면 대답이 조금씩 흐려진다.
보통은 이런 답이 돌아온다.

  • "난독화된 코드를 원래 코드로 바꿔주는 거 아닌가요?"
  • "개발 모드에서는 있고, 운영에서는 끄는 그 옵션이요."
  • "없으면 디버깅이 불편해지는 거?"

전부 틀린 말은 아니다. 하지만 핵심이 빠져 있다.

  • sourcemap은 단순한 디버깅 옵션이 아니다.
  • sourcemap은 빌드 결과물의 일부이며, 보안과 직접적으로 연결되는 파일이다.

이걸 모른 채 설정을 만지면 운영에서 에러가 나는데 재현이 안 되거나,
에러 위치가 엉뚱하게 나오거나, 심지어 의도치 않게 원본 소스 코드가 외부에 노출되는 사고가 발생할 수 있다. 즉, sourcemap은 "있으면 좋은 것"이 아니라 "잘 다뤄야 하는 것"이다.

이번 글에서는…

그래서 이번 시간에는 막연하게 알고 있던 sourcemap에 대해 차근차근 알아보려고 한다.

  1. sourcemap은 정확히 무엇인가?
    • 무엇을 해주고, 무엇을 해주지 않는가?
  2. sourcemap은 언제, 어떻게 만들어지는가?
    • 빌드 파이프라인의 어느 지점에서 생성될까?
    • 왜 빌드 결과물과 항상 한 쌍일까?
  3. .map 파일 하나로 어떻게 원본 코드의 에러 위치를 찾을 수 있을까?
    • 이게 가능한 원리는 뭘까?
  4. 왜 .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는 어떻게 읽히는가?

디코딩 라이브러리(브라우저 내장 기능)가 하는 일을 순서대로 풀어보면 다음과 같다.

  1. 입력: 에러가 발생한 JS 좌표를 받는다. (Line: 1, Column: 120)
  2. 탐색: mappings 문자열을 해석하여 해당 좌표가 속한 구간(Segment)을 찾는다.
  3. 해독: 그 구간에 저장된 정보를 바탕으로 다음 4가지를 알아낸다.
    • Source Index (몇 번째 소스 파일인가?)
    • Original Line (원본 몇 번째 줄인가?)
    • Original Column (원본 몇 번째 칸인가?)
    • Name Index (원래 이름은 무엇인가?)
  4. 출력: 최종적으로 우리가 보는 정렬된 에러 로그를 반환한다.
// 디코딩 결과
{
  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 파일이 로컬에 존재하고
    • 해당 파일들은 동일한 릴리즈 기준으로 생성되었으며
    • 외부 서비스로 어떤 데이터도 업로드하지 않는다
  • 즉, 디버깅의 기준을 ‘에러 로그’가 아니라 ‘로컬에 보관된 릴리즈 산출물’로 옮기는 전략이다.

(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의 철학까지 살펴보았다.

긴 글을 세 줄로 요약하면 다음과 같다.

  1. sourcemap은 코드 복원기가 아니라 '좌표 변환 지도'다.
  2. 운영 환경에 sourcemap: true로 배포하는 건 원본 코드 노출의 위험이 크다.
  3. 소스맵은 버전 민감도가 매우 높으므로, CI/CD 파이프라인에서 릴리즈 단위로 철저히 관리해야 한다.

개발자에게 디버깅 경험(DX)은 포기할 수 없는 가치다. 하지만 그것이 서비스와 사용자의 보안을 위협하는 핑계가 되어서는 안 된다.

가장 이상적인 그림은 "사용자에게는 코드를 숨기되(hidden), 개발자는 언제든 원본을 볼 수 있는" 환경을 만드는 것이다. 상황에 따라 Sentry 같은 SaaS를 쓰든, 오늘 소개한 decode-sourcemap-cli 같은 로컬 도구를 쓰든, 보안과 효율이라는 모두 챙길 수 있는 환경을 구축하길 바란다.


반응형

댓글