Node.js 20 Next.js Promise.withResolvers polyfill 사용
//_components/PDFViewer.tsx
"use client"
import { pdfjs } from "react-pdf";
//...
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
//...
pdfjs worker을 적용하는 중 Promise.withResolvers is not a function Error을 마주했다. 이렇게 Promise.withResolvers가 내부 코드에서 사용되는 모습을 볼 수 있다.(pdf.worker.min.mjs는 pdf.worker.mjs의 빌드된 압축 코드일텐데 왜 안뜨는건지 모르겠다.)
//grep -r "withResolvers" node_modules/pdfjs-dist/
node_modules/pdfjs-dist//build/pdf.worker.mjs: this._loadedStreamCapability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: const capability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: } = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: const capability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: const startCapability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: const pullCapability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: const cancelCapability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: this.sinkCapability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: sinkCapability: Promise.withResolvers(),
node_modules/pdfjs-dist//build/pdf.worker.mjs: this._capability = Promise.withResolvers();
node_modules/pdfjs-dist//build/pdf.worker.mjs: const pdfManagerCapability = Promise.withResolvers();
왜냐하면 Promise.withResolvers는 Node.js에서 기본적으로 지원되지 않아(브라우저에서만 지원된다)생긴 문제이다. 이 상황에서 Promise.withResolvers()는 undefined이다. Next.js를 사용했기 때문이고 Vite React와 같은 CSR환경이라면 생기지 않는 에러라고 생각한다.
정확히는, Node.js 22에서 지원된다. 이 글을 쓸 시점에 LTS는 20이고, 프로젝트에도 Node.js 20을 사용중이다.
이를 위해 polyfill을 사용해야한다. 이런 이슈는 여러 사람들에게 나타나 polyfill code는 쉽게 얻을 수 있었다.
//util/pdfWorkerPolyfill.ts
"use client";
if (typeof Promise.withResolvers === "undefined") {
if (typeof window !== "undefined") {
// @ts-expect-error This does not exist outside of polyfill which this is doing
window.Promise.withResolvers = function () {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
} else {
// @ts-expect-error This does not exist outside of polyfill which this is doing
global.Promise.withResolvers = function () {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
}
}
직접 로직을 지정해줘서 동작하게 한다.
그렇다면 이를 어떻게 적용하는 것일까?
//page.tsx
"use client";
import "./_util/pdfWorkerPolyfill";
import PDFViewer from "./_components/PDFViewer";
export default function Page() {
return(
<>
{/* ... */}
<PDFViewer />
{/* ... */}
</>
);
}
이렇게 적용하면 된다.
이를 위해 es6 module import 실행 순서에 대해 생각해봐야한다.
0. 모듈 트리 생성
import와 export 를 파싱해 종속성을 파악한다. 모듈은 비동기적으로 로드되지만 파싱은 싱글쓰레드에서 동기적으로 발생한다.
이렇게 최상위 모듈부터 시작해 종속성 트리를 생성한다. 모듈은 한번만 로드된다.(캐시돼 같은 모듈을 나중에 로드해도 아무일이 발생하지 않는다)
1. 모듈 인스턴스화
import 및 export 구문을 분석해 모듈을 메모리에 할당한다. 이 과정에서 변수 및 함수 선언이 초기화된다.
2. 모듈 초기화
최상위 모듈부터 시작해 의존하는 모듈이 먼저 초기화한 뒤 의존하는 코드가 실행된다
3. 실행
최상위 문장부터 순차적으로 실행된다. 초기화가 완료된 모듈에서 import한 것들을 사용가능하다.
위 코드에 대해 생각해보자면,
page.tsx에선 polyfill과 pdfviewer을 import받는다.
page.tsx는 polyfill을 먼저 import받아
1. polyfill의 코드를 실행한다. 이때 Promise.withResolvers가 정의된다.
2. PDFviewer.tsx를 import받는다.
3. PDFviwer.tsx는 react-pdf를 import받아 react-pdf의 내부 코드를 실행한다.
4. pdfjs의 workerSrc를 설정한다.
5. cdn으로 받은 모듈을 실행한다.
이 로직으로 polyfill을 적용할 수 있다