tsup 라이브러리에서 es5 타겟으로 사용 시 겪었던 문제
안녕하세요, 오랜만에 글을 적어보네요.
오늘은 최근에 Sentry를 통한 모니터링 중에 이슈가 하나 발생해서 그 이슈를 디버깅해 보고 해결한 내용을 간단하게 적어보려고 합니다.
문제 발생
기존에 tsup을 통해 라이브러리를 개발해서 사용하고 있는데요.
레거시 브라우저 지원을 위해서 브라우저 지원 타겟을 es5로 지정해서 CommonJS와 ESM 두 가지 형태로 결과물을 제공했습니다.
// tsup.config.ts 예시 코드
import { defineConfig } from 'tsup';
export default defineConfig((options) => ({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
target: 'es5',
}));
그러다 최근 Sentry에서 Unexpected token
에러가 나타났고, tsup을 통해 개발했던 라이브러리를 next.js에서 ESM으로 불러와서 사용 중인 것을 볼 수 있었습니다.
원인
우선 해당 에러를 재현하기 위해 안드로이드 에뮬레이터 셋업을 한 뒤 해당 브라우저 버전 (Chrome 70)으로 디버깅한 결과, index.mjs(ESM) 파일에서 ?.
옵셔널 체이닝 코드가 있었고 해당 스펙을 브라우저에서 지원하지 않아 에러가 발생했습니다.
위에서 언급했듯이 브라우저 지원 타겟을 es5로 지정했을 경우 옵셔널 체이닝(?.
)은 자동으로 트랜스파일이 돼야 했을 텐데 그대로 남아있는 것이 의아하여 tsup 공식 문서와 코드를 디버깅 해봤습니다.
찾아본 결과 tsup에서는 target을 es5로 지정하면 우선 esbuild로 es2020으로 빌드 후 swc를 통해 es5로 한 번 더 빌드해서 제공해주는데 이 과정에서 CommonJS는 es5
로 되었으나, ESM의 경우 es2020
으로 변환되어 있었습니다.
당연하게도… es2020
타겟으로 변환된 코드를 레거시 브라우저에서 사용했으니 에러가 발생할 수밖에 없었던 거죠.
TMI) 왜 esbuild와 swc를 같이 쓰는지?
esbuild의 경우 기본적으로es5
를 지원하지 않아 swc를 같이 사용해es5
로 변환하는 작업을 거친 것 같습니다.
해결 방법과 논의
우선 이 문제를 해결하는 방법 중에 ESM을 es6
로 변환하여 해결하려면 CommonJS와 ESM을 서로 다르게 타겟을 지정해주면 됩니다.
// tsup.config.ts 예시 코드
import { defineConfig, Options } from 'tsup';
export default defineConfig((options) => {
const commonOptions: Partial<Options> = {
entry: ['src/index.ts'],
};
return [
{
...commonOptions,
format: ['cjs'],
target: 'es5'
},
{
...commonOptions,
format: ['esm'],
target: 'es6'
},
]
});
이렇게 하면 ESM에서도 옵셔널 체이닝이 트랜스파일 되어 Chrome 70에서도 사용할 수 있습니다.
라이브러리의 타겟을 es5에서 es6로 변경하면 안 되는가?
사실 위에 방식으로 하면 CommonJS는 es5
, ESM은 es6
가 됩니다. 서로 타겟이 다르게 되는 거죠. 목표하는 타겟은 es5
인데 next.js에서는 ESM을 사용하니 es6
가 지원되지 않는 브라우저는 또 다른 이슈가 발생할 것입니다.
여기서 es6
의 경우 이제 어느 정도 사용자들에게 제공해도 되지 않을까 하여 관련해서 한번 논의를 진행했습니다만... 아직까지 저희가 지원하는 최소 버전들이 낮아 레거시 브라우저에 대한 의존도가 높은 것으로 판단되어 이번에는 아쉽지만 es6
의 변경은 시도해 볼 수 없었습니다. (다음번엔 꼭...!!)
그래서 ESM의 경우 es5
를 지원하지 않기에 라이브러리에서 ESM 포맷을 제거하고 CommonJS만 제공하도록 변경하였습니다.
es5를 라이브러리에서 제공해야 할까? 아니면 서비스 측에서 변환해서 사용해야 할까?
라이브러리에서 제거하기 전에 혹여나 행복회로를 돌리며(?) target을 es6
로 지정하고 사용하는 서비스 측에서 (next.js의 경우 transpile-modules 와 같은) 지원 브라우저에 맞게 변환하여 사용하는 건 어떤지에 대해 얘기를 나눠봤습니다.
이 부분에 대해선 라이브러리에서 미리 es5
로 제공하는 편이 좋을 것 같단 의견으로 모여 결론적으로는 라이브러리에서 es5
형태의 CommonJS를 사용하는 방식으로 정하게 됐습니다.
마치며
다행히 Sentry 덕분에 이번 이슈를 파악하고 해결할 수 있었던 것 같습니다.
라이브러리를 개발하면서 minify를 지정하지 않고 빌드된 결과물의 코드를 훑어봤다면 es2020의 이슈를 미리 알아챌 수 있었을까 싶기도 하고 혹은 tsup에서 OUTPUT: CommonJS(ES5), ESM(ES2020)
과 같은 느낌으로 터미널에서 알려 줬다면 어땠을까 싶기도 했습니다.
esm에선 es6로 되어있겠지? 와 같은 안일한 생각한 나 반성해 😔