Typescript + Rollup + Sourcemap

woo94
dev-woo94
Published in
17 min readMay 20, 2024

Intro

Nodejs와 Typescript의 사용이 어느정도 무르익으면서 저도 같이 성장했습니다. 번들링(빌드파일 생성)에 대한 주제도 관심이 있어서 그 필요성과 그것을 도와주는 툴들을 마주하게 되었습니다.

Webpack은 알러지반응(?)이 있어서 최대한 쓰지 않으려고 했습니다.. 그러다 보니 Rollup이라는 훌륭한 번들러를 알게 되었습니다. 기존 몇몇 repo들은 Typescript + Babel + Rollup으로 전환을 했습니다. 하지만 번들링을 한 이후에 생기는 문제는.. 원본 소스 어디에서 문제가 발생한지 모르겠다는 점입니다.

아래는 예시입니다:

TypeError: global.nonExistentFunction is not a function
at /Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:265:152
at gt.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:145:216)
at s (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:956)
at Tt.dispatch (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:982)
at gt.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:145:216)
at /Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:4281
at Wt.process_params (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:5208)
at h (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:4231)
at /Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:6975
at gt.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:145:216)

발생한 에러는 nonExistentFunction이 함수가 아니라니깐, 위와 같은 함수가 존재하지 않는다는 말입니다. 원본 코드 파일이 어떤 파일이고 몇번째 줄인지는 모르겠지만, 해당 함수를 project 전역에 검색해서 디버깅을 하고 에러를 고칠 수 있겠습니다(대부분의 경우에는 error handler를 통해서 검출되므로 알 수 있습니다!).

하지만 이 방식은 정말 불편합니다… 번들링 된 코드가 에러가 발생했을 시 Typescript 파일과 line을 가리킬수는 없을까.. 대략 1년정도 틈틈히 덤볐던 주제였습니다. 계속 실패를 하다가 성공을 하게 되어서 기념할겸 글을 남기려고 합니다.

Sourcemap

Source map을 사용하는 이유는 디버깅과 성능 최적화에 있습니다.

디버깅

보통 JS 코드는 난독화와 압축과정을 거칩니다. 이로인해 최종 배포되는 코드는 사람이 읽기 힘듭니다. a, b, c, d, e와 같은 함수 이름이 사용되기 때문입니다. 디버깅 역시 힘듭니다. Source map은 이렇게 압축된 코드와 원본 코드를 매핑하여 디버깅을 도와줍니다.

성능 최적화

코드 난독화와 압축을 진행하게 되면 파일의 크기가 훨씬 줄어들기 때문에 성능에 좋습니다. 성능을 챙기면서 source map을 통해 디버깅도 용이하게 한다면 장점만을 취하는 좋은 전략이라고 볼 수 있습니다.

Source map을 통해 번들링된 코드에서 원본 코드를 정확하게 짚을 수 있다는 장점이 있지만 짚어주지 못한다면 디버깅이 힘들어집니다!

TL;DR

코드를 빠르게 보고 싶으신 분들은 글의 제일 마지막에 가시거나 github 링크로 이동하시면 됩니다.

프로젝트 설정

긴말을 뒤로하고 기본적인 Typescript 프로젝트를 만들면서 시작해보겠습니다.

아래의 명령어로 package.json 파일을 생성해줍니다:

npm init -y

Typescript도 devDependency로 추가해줍니다:

npm i -D typescript

아래 명령어를 실행해서 tsconfig.json 파일을 생성하여 Typescript 프로젝트를 초기화 해줍니다.

npx tsc --init

아래의 내용으로 파일의 내용을 교체해줍니다:

# tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "es6",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}
  • Rollup을 사용해주기 위해 "module": "ESNext" 로 해줍니다.
  • tsc 명령어로 transpile된 파일들을 만들어주지 않기 위해 "noEmit": true 로 해줍니다.
  • 여기에서 sourceMap: true 옵션을 켜주어 source map을 생성해주도록 합니다.

Express 서버 설정 및 라우터 생성

아래 명령어로 express를 설치해줍니다:

npm i express

npm i -D @types/express

프로젝트의 root에 src 폴더를 만들고 그 안에 index.ts 파일을 만들어줍니다:

// src/index.ts
import express from "express";

const app = express();

app.get("/", (req, res, next) => {
const result = (global as any).nonExistentFunction();
res.send("Hello, World!");
});

app.listen(3456, () => {
console.log("server is running on port 3456");
});

Express에는 default error handler가 있기 때문에, runtime error로 서버가 뻗는 상황을 고의로 만들어주는 것이 쉽지 않습니다. 따라서 아래의 코드가 추가되어 고의로 런타임 에러를 발생시킵니다:

const result = (global as any).nonExistentFunction();

Rollup 번들러 설정

이제 코드를 번들링해주는 Rollup 설정파일을 생성해주어야 합니다. tsconfig.json 파일의 설정에 의거하여 가장 유리한 형식은 rollup.config.mjs 파일이라고 판단했습니다.

우선, Rollup package를 설치해주겠습니다:

npm i -D rollup @rollup/plugin-commonjs rollup-plugin-typescript2 @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-terser

이제 rollup.config.mjs 파일을 생성해줍니다:

// rollup.config.mjs
import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import typescript from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
import terser from '@rollup/plugin-terser'

export default {
input: "./src/index.ts",
output: {
file: "./dist/bundle.js",
format: "cjs",
sourcemap: true
},
plugins: [
terser(),
resolve({
preferBuiltins: true
}),
commonjs(),
json(),
typescript()
]
}

Rollup을 제대로 사용하려면 많은 plugin들이 필요합니다 😓. 아래에서 하나씩 설명드리겠습니다

  • @rollup/plugin-commonjs
    Rollup은 ES module format의 코드만을 지원합니다. 우리는 물론 코드를 Typescript로 작성하기 때문에 ES module 방식의 import를 사용하겠지만, 사용하는 라이브러리들이 commonjs 방식으로 작성되어 있으면 Rollup은 이 코드를 해석하지 못합니다. 따라서 이 plugin을 사용하여 commonjs 스타일의 코드를 ES6로 변환해줍니다.
  • @rollup/plugin-node-resolve
    위에서 설명했듯 Rollup은 ES module format을 사용하기 때문에 require로 인한 path resolving을 제대로 수행하지 못합니다. 이 plugin을 사용하여 path resolving을 도와줍니다(node_modules를 찾는다던가 .yarn/cache를 찾는다던가)
  • @rollup/plugin-json
    사용하는 라이브러리에서 json 파일을 사용한다면, 이 plugin이 없으면 build 실패가 나옵니다.
  • rollup-plugin-typescript2
    TS -> JS로 transpile 할 때 tsconfig.json 파일을 참조하기 위해 사용합니다. 실제 프로젝트에서는 tsconfig.jsoncompilerOptions.paths 옵션을 사용하기 위해 Babel을 사용하지만.. 너무 장황해지기 때문에 Babel이 아닌 tsc를 사용하여 transpile을 진행하겠습니다.
  • @rollup/plugin-terser
    코드 난독화와 압축을 위해서 terser라는 라이브러리를 사용합니다.
    Terser recommends you use RollupJS to bundle your modules, as that produces smaller code overall.
    라고 쓰여있을 정도로 terser는 RollupJS의 사용을 권장합니다🐶!
    terser plugin을 사용하여 Rollup의 번들링시에 mangling(난독화의 일종)과 compressing(압축)을 진행해줍니다.
input: "./dist/bundle.js"

를 보니, 앱의 진입점은 이 파일 경로가 되겠습니다.

output: {
file: "./dist/bundle.js",
format: "cjs",
sourcemap: true
}

를 보니 결과물은 dist라는 폴더 아래에 bundle.js 파일에 생성됩니다.

Production 환경에서 node 명령어를 사용하여 js 런타임을 수행할것이기 때문에 format: "cjs" 로 설정해줍니다. 기본값은 “es”인데, 이러면 ES module format으로 결과물이 나오고, node 명령어로 런타임에 올려주자마자 터집니다.

sourcemap: true 옵션을 줬기 때문에 sourcemap 파일도 하나 dist 폴더에 생기게 됩니다.

번들링 진행

package.json 의 script에 다음의 명령어를 추가해줍니다:

"scripts": {
"build": "rollup -c",
"start": "node ./dist/bundle.js"
}

build 명령어를 통해서 번들링을 진행하고, start 명령어를 통해서 번들링된 파일을 실행시켜줍니다.

실행

아래의 명령어로 번들링과 실행을 수행해보겠습니다

npm run build && npm run start

그 다음 단순하게 브라우저에 localhost:3456 을 입력해서 들어가주시면 에러가 발생함을 알 수 있고, 터미널 혹은 브라우저를 통해서 에러 객체를 확인해줄수 있습니다.

TypeError: global.nonExistentFunction is not a function
at /Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:265:152
at gt.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:145:216)
at s (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:956)
at Tt.dispatch (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:982)
at gt.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:145:216)
at /Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:4281
at Wt.process_params (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:5208)
at h (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:4231)
at /Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:153:6975
at gt.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/dist/bundle.js:145:216)

번들링 된 코드에서 에러가 발생했다고 나오고 원본 코드를 가리키지 않습니다.

마지막 퍼즐(source map mapping)

그 이유는 우리는 source map을 만들기는 했지만, 이를 원본 코드에 매핑해주지 않았기 때문입니다. 1년간 실패한 이유가 source map을 만들기는 했지만 매핑을 해주지 않았기 때문이라니🙀… 여하튼 매핑을 하러 떠나보겠습니다.

아래의 명령어로 source-map-support package를 설치해줍니다.

npm i source-map-support

그 다음 src/index.ts 파일로 가서 제일 상단에 다음의 줄을 추가해줍니다:

import 'source-map-support/register'

다시 npm run build && npm run start 를 통해 빌드를 하고 빌드된 파일을 실행시켜줍니다. 이번에 localhost:3456 으로 접속해보니 아래와 같은 에러를 볼 수 있습니다:

TypeError: global.nonExistentFunction is not a function
at fn (/Users/woo94/Desktop/Dev/rollup-typescript/src/index.ts:8:34)
at po.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/layer.js:95:5)
at next (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/route.js:149:13)
at wo.fn [as dispatch] (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/route.js:119:3)
at po.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/layer.js:95:5)
at done (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/index.js:284:15)
at Function.process_params (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/index.js:346:12)
at next (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/index.js:280:10)
at fn (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/middleware/init.js:40:5)
at po.handle_request (/Users/woo94/Desktop/Dev/rollup-typescript/node_modules/express/lib/router/layer.js:95:5)

감격스럽게도 /src/index.ts:8:34 로 파일과 line을 정확하게 짚어내는 것을 볼 수 있었습니다 🥳.

전체코드

src/index.ts

// src/index.ts
import "source-map-support/register";
import express from "express";

const app = express();

app.get("/", (req, res, next) => {
const result = (global as any).nonExistentFunction();
res.send("Hello, World!");
});

app.listen(3456, () => {
console.log("server is running on port 3456");
});

tsconfig.json

# tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "es6",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}

rollup.config.mjs

// rollup.config.mjs
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import typescript from "rollup-plugin-typescript2";
import json from "@rollup/plugin-json";
import terser from "@rollup/plugin-terser";

export default {
input: "./src/index.ts",
output: {
file: "./dist/bundle.js",
format: "cjs", // CommonJS 형식으로 설정
sourcemap: true, // 소스맵 활성화
},
plugins: [
terser(),
resolve({
preferBuiltins: true,
}),
commonjs(),
json(),
typescript({
tsconfig: "./tsconfig.json",
}),
],
};

--

--