소스맵 업로드
빌드한 소스맵을 웹 버전별로 Sophonz에 올려, 프로덕션 에러의 스택 트레이스를 원본 파일과 줄 번호로 되돌립니다.
압축된 번들에서 난 에러는 onClick@chunks/36o8zuzlzx381.js:1:918처럼 보입니다. 같은 빌드의 소스맵을 올려 두면 로그와 트레이스 화면에서 onClick app/client-probes.tsx:43:19와 해당 줄의 코드로 바뀌어 보입니다. 소스맵은 웹 버전 단위로 보관되고, 스택 트레이스를 열 때 그 트레이스가 보고한 웹 버전의 소스맵으로 복원합니다.
웹 버전
웹 버전은 웹 빌드 자체의 버전입니다. Browser SDK(2.1.0 이상)는 init에 넘긴 appVersion을 리소스 속성 web.version으로 보내고, 소스맵은 이 값으로 매칭됩니다.
| 실행 환경 | service.version | web.version |
|---|---|---|
| 일반 브라우저 | appVersion | appVersion |
| 네이티브 앱의 WebView | 네이티브 앱 버전 (브릿지가 덮어씀) | 페이지에 설정한 appVersion |
WebView에서는 네이티브 브릿지가 service.version을 앱 버전으로 바꾸지만 web.version은 페이지가 설정한 값을 유지합니다. 웹 배포 주기가 앱보다 빨라도 웹 빌드마다 소스맵을 따로 올리고 찾을 수 있는 이유입니다. 자세한 내용은 WebView 연동을 참고하세요.
CAUTION — 업로드 버전과 SDK 버전은 같은 값이어야 합니다
소스맵은 그 빌드의 스크립트와만 맞습니다. CI에서 하나의 변수로 SDK의 appVersion과 업로드의 웹 버전을 함께 설정하세요. 값이 어긋나면 업로드는 성공해도 스택 트레이스는 복원되지 않습니다.
업로드 방법은 두 가지입니다.
@sophonz/cli: 번들러에 상관없이 빌드 결과 디렉터리를 올립니다. Next.js, webpack, Rollup, esbuild 등.@sophonz/vite-sourcemap: Vite 빌드가 끝나면 자동으로 올립니다.
@sophonz/cli
설치
npm install @sophonz/cli --save-devNode.js 20 이상이 필요합니다. 설치 없이 npx @sophonz/cli로 실행해도 됩니다.
사용법
export SOPHONZ_TOKEN=sophonz_pat_...
npx @sophonz/cli upload-sourcemaps \
--path dist \
--web-version 2026.09.15-1Next.js는 스크립트가 /_next/static 아래에서 제공되므로 접두사를 지정합니다. next.config에서 productionBrowserSourceMaps: true로 소스맵을 만들어야 합니다.
npx @sophonz/cli upload-sourcemaps \
--path .next/static \
--url-prefix _next/static \
--web-version "$WEB_VERSION"| 옵션 | 환경 변수 | 설명 |
|---|---|---|
-p, --path | — | 소스맵을 찾을 디렉터리 (필수) |
-w, --web-version | — | 웹 버전. SDK의 appVersion과 같은 값 (필수) |
--url-prefix | — | 스크립트가 제공되는 URL 경로. 예: _next/static |
-t, --token | SOPHONZ_TOKEN | 앱 액세스 토큰 |
--api-url | SOPHONZ_API_URL | 기본값 https://app.sophonz.ai |
--dry-run | — | 업로드하지 않고 대상만 출력 |
스크립트와 소스맵 짝짓기
CLI는 각 스크립트 끝의 //# sourceMappingURL= 주석을 읽어, 소스맵을 그 스크립트의 경로로 업로드합니다. 스택 트레이스에는 스크립트 이름만 남기 때문입니다.
번들러가 소스맵 이름을 스크립트와 다르게 짓는 경우도 이 방식으로 맞춰집니다. Next.js 16(Turbopack)은 36o8zuzlzx381.js의 소스맵을 1-1m44t1taogc.js.map으로 만듭니다. 인라인(data:) 소스맵도 추출해서 올리며, 어떤 스크립트도 참조하지 않는 .js.map은 자기 이름으로 올립니다.
--dry-run으로 짝이 어떻게 정해졌는지 확인할 수 있습니다.
[sophonz] 8 source map(s) for web version 1.4.0 → https://app.sophonz.ai
(dry run) _next/static/chunks/36o8zuzlzx381.js.map ← chunks/1-1m44t1taogc.js.map@sophonz/vite-sourcemap 플러그인
주요 기능
- Vite 빌드 완료 후 소스맵 자동 업로드
- 포함/제외 패턴으로 파일 필터링
- gzip 압축 지원
- 모든 소스맵을 tar.gz 하나로 묶어 한 번에 업로드하는 번들 모드
- 동시 업로드 제한 및 재시도
- CI 환경 감지, 업로드 후 로컬 소스맵 삭제
- 커스텀 경로 변환 및 URL 접두사 지원
설치
npm install @sophonz/vite-sourcemap --save-dev기본 설정
// vite.config.ts
import { defineConfig } from 'vite'
import sourcemapUpload from '@sophonz/vite-sourcemap'
export default defineConfig({
build: {
sourcemap: true, // 중요: 소스맵 생성을 활성화해야 합니다
},
plugins: [
sourcemapUpload({
endpoint: 'https://app.sophonz.ai/api/v1/web-sourcemaps',
apiKey: process.env.SOPHONZ_TOKEN,
// 웹 버전. SDK init의 appVersion과 같은 값
appVersion: process.env.WEB_VERSION,
}),
],
})NOTE — apiKey에 넣을 값
apiKey에는 앱 단위로 발급한 Sophonz 액세스 토큰을 넣습니다. 발급 방법은 액세스 토큰 발급을 참고하세요.
고급 설정
// vite.config.ts
import { defineConfig } from 'vite'
import sourcemapUpload from '@sophonz/vite-sourcemap'
export default defineConfig({
build: {
sourcemap: true,
},
plugins: [
sourcemapUpload({
endpoint: 'https://app.sophonz.ai/api/v1/web-sourcemaps',
apiKey: process.env.SOPHONZ_TOKEN,
// 웹 버전. 소스맵은 이 값으로 보관됩니다
appVersion: process.env.WEB_VERSION,
build: process.env.GIT_SHA,
// 파일 필터링 (출력 디렉터리 기준 상대 경로에 적용)
include: [/assets\/.+\.js\.map$/],
exclude: [/node_modules/],
// 스크립트가 제공되는 경로가 출력 디렉터리 구조와 다를 때
urlPrefix: 'static/js',
// 업로드 설정
concurrency: 3,
retries: 5,
gzip: true,
deleteAfterUpload: true,
// CI에서만 실행
ciOnly: true,
}),
],
})소스맵 개수가 많아 요청 수를 줄이고 싶다면 번들 모드를 사용합니다. 모든 소스맵을 tar.gz 하나로 묶어 한 번의 요청으로 업로드합니다.
sourcemapUpload({
endpoint: 'https://app.sophonz.ai/api/v1/web-sourcemaps',
apiKey: process.env.SOPHONZ_TOKEN,
appVersion: process.env.WEB_VERSION,
bundle: true,
bundleFilename: 'sourcemaps.tar.gz',
})설정 옵션
필수 옵션
| 옵션 | 타입 | 설명 |
|---|---|---|
endpoint | string | 소스맵을 업로드할 API 엔드포인트 |
선택 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
apiKey | string | - | 액세스 토큰. Authorization: Bearer <token> 헤더로 전송됩니다 |
appVersion | string | - | 웹 버전. 소스맵이 이 값으로 보관·매칭됩니다 |
release | string | - | appVersion이 없을 때 웹 버전으로 쓰입니다 |
build | string | - | 빌드 번호 또는 커밋 SHA. 빌드 ID로 기록됩니다 |
method | 'POST' | 'PUT' | 'POST' | HTTP 메서드. Sophonz 엔드포인트는 POST만 받습니다 |
headers | Record<string, string> | {} | 추가 HTTP 헤더 |
appType | 'ANDROID' | 'IOS' | 'WEB' | 'ALL' | null | null | 플랫폼 구분. 현재 엔드포인트는 사용하지 않습니다 |
fileFieldName | string | 'file' | 파일 업로드용 폼 필드명. Sophonz 엔드포인트는 file을 요구합니다 |
fields | Record<string, string | number | boolean> | {} | 추가 폼 필드 |
include | (string | RegExp)[] | - | 포함할 파일 패턴. 문자열은 glob이 아니라 정규식 소스로 해석됩니다 |
exclude | (string | RegExp)[] | - | 제외할 파일 패턴 |
urlPrefix | string | - | 논리 경로(name·path 필드) 앞에 붙일 접두사 |
transformPath | (path: string) => string | - | 논리 경로 변환 함수. 지정하면 urlPrefix는 무시됩니다 |
concurrency | number | 6 | 동시 업로드 수 |
retries | number | 3 | 업로드 중 예외가 발생했을 때의 재시도 횟수 (지수 백오프) |
gzip | boolean | true | 업로드 전 gzip 압축 |
bundle | boolean | false | 모든 소스맵을 tar.gz 하나로 묶어 한 번에 업로드 |
bundleFilename | string | 'sourcemaps.tar.gz' | 번들 모드에서 사용할 아카이브 파일명 |
deleteAfterUpload | boolean | false | 업로드 성공 후 로컬 .map 파일 삭제 |
ciOnly | boolean | false | CI 환경에서만 실행 |
verbose | boolean | CI에서 true, 로컬에서 false | 상세 로그 출력 |
CAUTION — 업로드 실패는 빌드를 멈추지 않습니다
서버가 오류 응답을 반환하면 플러그인은 로그만 남기고 빌드를 계속 진행합니다. 업로드를 빌드 성공 조건으로 두려면 @sophonz/cli를 별도 단계로 실행하세요. CLI는 실패하면 종료 코드 1로 끝납니다.
NOTE — Vite가 아닌 번들러의 소스맵 이름
플러그인은 .map 파일의 출력 경로를 그대로 논리 경로로 보냅니다. Vite처럼 소스맵 이름이 스크립트 이름 뒤에 .map만 붙는 번들러에서는 그대로 맞습니다. 이름이 다른 번들러는 @sophonz/cli를 사용하세요.
업로드 엔드포인트
POST https://app.sophonz.ai/api/v1/web-sourcemaps
Authorization: Bearer sophonz_pat_...
Content-Type: multipart/form-datahttps://app.sophonz.com/api/v1/web-sourcemaps도 같은 엔드포인트를 서비스합니다.
요청 필드
| 필드 | 설명 |
|---|---|
file | 소스맵 파일. contentEncoding=gzip이면 gzip 압축된 내용 |
path, name | 논리 경로. 스택 트레이스의 스크립트 URL과 이 경로의 끝부분을 비교합니다 |
webVersion | 웹 버전 |
appVersion | webVersion이 없을 때의 웹 버전 |
release | 둘 다 없을 때의 웹 버전. 셋 다 없으면 unreleased |
build | 빌드 ID |
contentEncoding | gzip 압축했을 때 gzip |
bundle, bundleFormat, fileCount | 번들 모드일 때만 |
요청에는 어느 앱의 소스맵인지 가리키는 값이 없습니다. 이 점이 아래 토큰 정책의 근거입니다.
응답
업로드가 성공하면 201과 함께 저장 결과를 돌려줍니다.
{
"ok": true,
"key": "sourcemaps/<orgId>/<projectId>/<appId>/web/1.4.0/assets/app.js.map",
"app": "next-sample",
"serviceNamespace": "sophonz",
"release": "unreleased",
"version": "1.4.0",
"bundle": false,
"fileCount": null
}key는 sourcemaps/<조직 id>/<프로젝트 id>/<앱 id>/web/<웹 버전>/<논리 경로> 형태입니다. 같은 웹 버전에 같은 경로를 다시 올리면 덮어씁니다.
제한과 오류
- 업로드 한 건은 200MB까지 허용됩니다. 번들 모드에서는 압축된 아카이브 전체가 이 한도에 걸립니다.
- 소스맵은 비공개로 저장됩니다. 소스맵에는 원본 소스가 그대로 들어 있으므로 공개 저장소나 CDN에 함께 배포하지 마세요.
| 상태 | 원인 |
|---|---|
401 | 토큰이 없거나, 잘못됐거나, 만료·폐기됨 |
400 | multipart 요청이 아니거나 file 필드가 없거나 파일이 비어 있음 |
413 | 파일이 200MB를 초과 |
500 | 서버가 소스맵을 저장하지 못함 |
액세스 토큰 발급
소스맵 업로드에는 Sophonz 액세스 토큰이 필요합니다. 토큰은 앱 단위로만 발급됩니다. 업로드 요청에는 앱을 가리키는 값이 없고, 소스맵이 어느 앱에 속하는지는 토큰이 정하기 때문입니다.
콘솔에서 발급
https://app.sophonz.ai에 로그인- 설정을 엽니다
- 대상 프로젝트와 앱을 선택합니다
- 액세스 토큰 섹션에서 토큰 이름과 만료 기간(만료 없음 / 30일 / 90일 / 1년)을 정해 생성합니다
- 토큰은 이때 한 번만 표시됩니다. 이 화면에서 복사하세요
발급된 토큰은 sophonz_pat_<임의 문자열> 형식입니다.
CAUTION — 토큰은 한 번만 표시됩니다
생성 화면을 벗어나면 토큰 원문은 다시 볼 수 없습니다. 목록에는 마지막 네 자리 힌트와 마지막 사용 시각, 만료일만 남습니다. CI/CD에서는 시크릿으로 관리하세요.
폐기는 즉시 적용되며 되돌릴 수 없습니다. 교체할 때는 새 토큰을 먼저 발급해 CI에 반영한 뒤 이전 토큰을 폐기하세요.
업로드 확인
설정 > 프로젝트 > 앱 > 매핑 파일에 앱의 버전 목록이 있습니다. 목록은 텔레메트리에서 자동으로 채워지며, 웹 버전마다 올라온 소스맵 개수가 표시됩니다.
- 텔레메트리를 보냈는데 소스맵이 없는 웹 버전에는 경고가 표시됩니다. 스택 트레이스가 복원되지 않는 버전입니다.
- 출시 전에 올린 웹 버전은 텔레메트리가 오기 전에도 목록에 나타납니다.
에러가 들어오면 로그나 트레이스에서 해당 행을 펼칩니다. 스택 트레이스 뷰어가 원본 파일, 줄 번호와 코드 한 줄을 함께 보여 줍니다. 소스맵이 없으면 "버전 X의 매핑 파일 없음"이 표시됩니다.
플랫폼별 소스맵 (appType)
appType 옵션은 'ANDROID' | 'IOS' | 'WEB' | 'ALL' | null을 받아 폼 필드로 함께 전송되지만, 현재 엔드포인트는 이 값을 사용하지 않습니다. 소스맵은 앱과 웹 버전 기준으로만 보관됩니다.
같은 앱에서 플랫폼별로 다른 번들을 배포한다면 웹 버전을 다르게 지정하세요. 예를 들어 2026.09.15-1-android처럼 구분하고, 각 번들의 SDK appVersion도 같은 값으로 맞춥니다.
GitHub Actions와 함께 사용
CLI를 별도 단계로 실행하는 예입니다. 웹 버전 변수 하나를 빌드와 업로드에 함께 넘깁니다.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
env:
WEB_VERSION: ${{ github.run_number }}-${{ github.sha }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- name: Build
# 앱 코드에서 이 값을 Sophonz.init({ appVersion })으로 넘깁니다
env:
NEXT_PUBLIC_WEB_VERSION: ${{ env.WEB_VERSION }}
run: npm run build
- name: Upload source maps
env:
SOPHONZ_TOKEN: ${{ secrets.SOPHONZ_TOKEN }}
run: npx @sophonz/cli upload-sourcemaps --path .next/static --url-prefix _next/static --web-version "$WEB_VERSION"Vite 플러그인을 쓴다면 업로드 단계 없이 npm run build에 SOPHONZ_TOKEN과 WEB_VERSION을 넘기면 됩니다.
CI 환경 감지
Vite 플러그인은 다음 환경 변수 중 하나라도 설정돼 있으면 CI 환경으로 판단합니다. 이 판정은 ciOnly와 verbose의 기본값에 쓰입니다.
CIGITHUB_ACTIONSGITLAB_CIBUILDKITECIRCLECITRAVISBITBUCKET_BUILD_NUMBER