사건의 발단
공통 패키지인 shared패키지에서 절대 경로를 사용하면 에러가 발생한다.
- 에러가 발생한 코드
// packages/shared/src/constants/config.ts
import { getSafeConfig } from '@src/utils';
사실 확인
차근차근 사실을 하나씩 확인해보자.
- 빌드에 문제가 발생했는가?
shared 패키지 단독으로 빌드했을 때 문제가 발생하지 않는다. dist 폴더 또한 정상적으로 생성이 되었다.
- 이 패키지를 사용하는 모든 패키지에서 빌드 에러가 발생한다.
원인 분석
빌드는 문제가 없었다. 그런데 사용하려는 측에서 에러가 발생한 것을 보니 빌드 파일에 문제가 있을 것이라 생각했다.
// packages/shared/dist/src/constants/config.js
import { getSafeConfig } from "@src/utils";
빌드 파일에 절대 경로로 코드가 작성되어있다. 프로젝트 실행 시 절대 경로는 tsconfig의 compileOptions > paths에 따라 해석이 된다. 즉 typescript가 이를 읽는다는 것을 의미한다. 그런데 다른 패키지에서 이 빌드된 패키지를 사용할 때는 dist폴더 내의 javascript파일을 읽게된다.
아래 파일은 shared 패키지의 package.json 파일이다. package.json의 exports에는 다른 패키지에서 shared 패키지의 각 모듈을 import할 때 어떤 경로에서 import할지를 명시할 수 있다.
// packages/shared/package.json
{
"name":"@extension/shared",
// ..
"exports":{
// ..
"./utils": {
"types": "./dist/utils/index.d.ts",
"default": "./dist/utils/index.js"
},
}
}
만약 다른 패키지에서 @extension/shared/utils로부터 import한다면 ./dist/utils/index.js의 파일을 import하는 것과 같다.
물론 typescript파일을 그대로 export해서 이 패키지를 사용하는 측에서 빌드하는 방식도 가능하다. 하지만 나는 javascript패키지에서도 이 패키지를 사용을 가능하기를 원했다. 그래서 패키지를 빌드할 때 tsc를 통해 javascript로 트랜스파일링을 하였고 이를 export하였다. 또한 빌드 후 만들어진 d.ts파일은 types로 내보내 typescript 프로젝트에서도 사용 가능하도록 하였다.
다시말해 위 절대 경로는 상대 경로로 변환이 되고 export되었어야함을 의미한다.
그렇다면 빌드 과정에서 왜 절대 경로는 상대 경로로 변환이 되지 않았으며, 빌드 과정에서는 문제가 없었던 걸까?
1. 빌드 과정에서 절대 경로가 문제되지 않은 이유
ref : https://github.com/evanw/esbuild/issues/2998
esbuild는 Node.js의 모듈 resolution 알고리즘을 따르고, TS Compiler 알고리즘 또한 따른다. 그렇기에 tsconfig.json의 compilerOptions > paths에 절대 경로를 지정하면 이를 esbuild는 인식한다.
다시말해 esbuild는 tsconfig.json의 paths를 인식하고 빌드를 수행한다. 그렇기에 빌드 시 에러가 발생하지 않은 것이다. 만약 다른 패키지가 아니라 이 패키지 내부에서 사용한다면 에러가 발생하지 않았을 것이다. 왜냐하면 tsconfig.json파일을 빌드할 때와 동일한 경로를 사용하기 때문이다.
그렇다면 이는 패키지 간의 tsconfig.json의 설정 차이에서 발생한 이슈라는 유추할 수 있다. 공통 패키지와 공통 패키지를 사용하는 패키지의 tsconfig.json은 당연히 다르기 때문이다.(루트로부터 extends를 했어도 compilerOptions > paths가 동일할 수는 없다)
2. 절대 경로가 상대 경로로 변환되지 않은 이유
ref : https://github.com/evanw/esbuild/issues/394
esbuild는 ts-node와 마찬가지로 절대 경로를 상대 경로로 변환해주지 않는다. 이를 해결하기 위해서는 https://github.com/dividab/tsconfig-paths같은 플러그인을 사용해야한다.
다시말해 esbuild는 원래 절대 경로를 상대 경로로 변환해주지 않는다.
해결 방법
코멘트와 같이 tsc-alias, fast-glob을 활용하여 절대 경로를 상대 경로로 수정해주는 방법도 있다. 하지만 esbuild에는 bundle옵션이 있다. 이 옵션은 여러 개의 파일을 하나로 합치고 import문을 상대 경로로 변환해준다. 이를 활용하면 절대 경로를 상대 경로로 변환하는 문제를 해결할 수 있다.
esbuild.build({
// ..
bundle: true,
})
결론
시간을 쓴 것에 비해 상당히 간단한 문제였다. esbuild는 애초에 ‘웹을 위한 빠른 번들러’라고 소개를 하고 있었다. 하지만 bundle옵션이 기본으로 false이기 때문에 하나의 파일로 합치는 번들링을 위해서는 추가 설정이 필요한 것이었다.
위 이슈에 대해 다시 한 번 정리해보자. esbuild는 tsconfig.json의 paths를 인식하여 빌드를 수행하기 때문에 빌드 자체에는 문제가 없었다. 하지만 esbuild는 기본적으로 절대 경로를 상대 경로로 변환해주지 않는다. 이는 패키지를 사용하는 측에서 문제가 될 수 있다. 다행히도 esbuild의 bundle 옵션을 사용하면 여러 파일을 하나로 합치면서 import문을 상대 경로로 변환해주기 때문에 이 문제를 해결할 수 있었다.
번들링은 파일만 합치는 게 아니라 경로 변환·최적화까지 해준다. 다음엔 esbuild처럼 bundle이 기본 false인 것부터 확인하고 써야겠다.