Node 20.19.0과 Assetpack

nijuy·2025년 6월 30일

🍿 스낵게임

목록 보기
3/3

오늘은 스낵게임 개발 중 node 버전을 변경하면서 에셋 최적화 파이프라인이 깨졌던 경험과 해결 과정을 공유하려고 합니다. 문제가 발생했던 환경은 다음과 같습니다.

  • Node.js 20.19.0
  • pnpm 10.10.0
  • assetpack 0.8.0

문제 상황

개발을 이어가던 중 또 pnpm assets 스크립트에서 에러가 났습니다.
제가 ‘또’라고 하는 건 최근에 pnpm assets 스크립트가 안 돌아갔던 적이 있기 때문입니다.
패키지 버전도 패키지 매니저도 건드리지 않았는데 어떻게 이런 일이 생기나 싶어 에러 메시지를 확인했습니다.

이번에는 /static 폴더를 읽으려고 했는데 그런 폴더가 없다는 내용이었습니다.

당연히 없지. 안 만들었으니까.
왜 만들지도 않은 폴더를 읽으려고 했는지 알아보겠습니다.

assetpack을 사용하려면 반드시 프로젝트 루트에 .assetpack.js 파일을 만들어야 합니다. (공식 문서)

이 파일에 각종 설정을 지정할 수 있는데, 스낵게임에서 사용한 설정 파일은 이렇습니다.

export default {
  entry: './raw-assets',
  output: './public/assets/',
  cache: false,
  plugins: {
    /** 각종 플러그인들. . . */
  },
};

해석하자면 raw-assets 폴더에 있는 에셋을 최적화해서 public/assets 폴더에 저장한다는 의미입니다.

이전 글에서 assetpack의 이미지 에셋 최적화 과정을 이런 그림으로 표현한 적이 있는데요.

여기서 왼쪽의 원본들이 raw-assets 폴더, 스프라이트 이미지가 public/assets 폴더에 위치한다고 생각하시면 됩니다.

만약 설정 파일에서 entry/output을 따로 지정하지 않으면 assetpack의 기본 설정이 적용됩니다.

// @assetpack/core/dist/es/index.js
const defaultConfig = {
    entry: './static',
    output: './dist',
    ...
};

이 지점에서 에러 메시지의 /static 은 제가 설정한 entry가 아니라 default 값임을 알 수 있습니다. 만들지도 않은 폴더를 읽으려고 했던 건 설정한 config 값이 제대로 읽히지 않았다는 뜻이겠네요.


왜 config 파일이 읽히지 않나 고민해봤는데, 제가

패키지 버전도 패키지 매니저도 건드리지 않았는데!

라고 했지만 진짜 아무런 변화도 없는데 멀쩡한 Assetpack이 안 돌아갈리는 없습니다.

생각해보면 저는 종종 nvm(node version manager)를 사용해서 노드 버전을 변경할 때가 있습니다.

문제가 생겼던 환경은 node 20.19.0이고, node 20.9.0를 사용 중인 노트북에서는 정상적으로 동작하는 걸 보고 여기가 원인이라는 확신이 234098245% 들었습니다.

그래서

  • assetpack에서 config를 불러오는 과정을 파악하고
  • Node.js 릴리즈 노트를 통해, 이 과정에 영향을 줄 만한 변경사항이 있었는지 확인했습니다.

Assetpack에서 config를 불러오는 과정

assetpack/cli 코드로 들어가서 .assetpack.js 파일이 어떻게 로드되는지 보겠습니다.
예전엔 node_modules 내부를 보려면 식은땀부터 났는데 이제는 조금 익숙해졌습니다.

아래는 config 파일 로드와 관련된 부분만 발췌한 코드입니다.

// @assetpack/cli/dist/index.js
async function main() {
  const configPath = options.config ? 
                       path.resolve(process.cwd(), options.config)
                       : await findUp('.assetpack.js', { cwd: process.cwd() });
  
  if(!configPath) { /** 설정 파일 없을 때 에러 처리 */ }
  
  let config;
  let AssetPack;
  
 // We try to load cjs first, if that fails we try to load esm
  try {
    config = require(configPath);
    AssetPack = require('@assetpack/core').AssetPack;
  } catch (error) {
    if (error.code === 'ERR_REQUIRE_ESM') {
      const esmLoader = dynamicImportLoader();
      const urlForConfig = pathToFileURL(configPath);
      config = (await esmLoader(urlForConfig)).default;
      AssetPack = (await esmLoader('@assetpack/core')).AssetPack;
      // ...
    }
  }
}
  1. configPath 변수에 .assetpack.js 파일의 경로를 저장

  2. 먼저 cjs 방식으로 로드

  3. ERR_REQUIRE_ESM 에러가 생기면 esm 방식으로 다시 로드

    ERR_REQUIRE_ESM 에러
    ES 모듈을 require()로 불러오려고 시도할 때 생기는 에러입니다.

스낵게임은 package.json에 type: module을 명시했기 때문에, .assetpack.js 파일도 ES 모듈로 동작합니다. 따라서 아래처럼 파일 로드가 이뤄졌을 것으로 예상됩니다.

  try {
    config = require(configPath); // 💥 ERR_REQUIRE_ESM 발생
    // ...
  } catch (error) {
    if (error.code === 'ERR_REQUIRE_ESM') {
      const esmLoader = dynamicImportLoader();
      const urlForConfig = pathToFileURL(configPath);
      
      config = (await esmLoader(urlForConfig)).default; // ESM 방식으로 로드됨
      AssetPack = (await esmLoader('@assetpack/core')).AssetPack;
      ...
    }
  }

이렇게 config 파일의 경로를 찾고, 순차적으로 로드하는 구조까지는 파악이 끝났습니다.
다음으로 Node.js의 변경사항이 이 과정에 어떤 영향을 미쳤는지 살펴볼 차례입니다.

Node 20.19.0의 require(esm) 활성화

릴리즈 노트를 보면, Node.js 20.19.0 버전에서는 require(esm) 기능이 기본 활성화되었습니다

이전까지는 별도 플래그 없이 CommonJS의 require()로 ES 모듈을 불러오면 ERR_REQUIRE_ESM 에러가 발생했지만, 20.19.0부터는 이게 가능해진 것입니다.

아주아주 간단한 코드를 만들어 변경된 동작을 알아보겠습니다.
Assetpack을 사용할 때처럼 esm으로 config 파일을 정의하고, 이를 require로 불러오는 코드입니다.

// esm-module.js
export default {
    entry: './raw-assets',
    output: 'public/assets'
}
// cjs-require-esm.cjs
const path = './esm-module.js';
const testModule = require(path);
console.log(testModule)
Node 20.18.2Node 20.19.0
ERR_REQUIRE_ESM 에러 발생정상적으로 모듈 불러옴

여기서 하나 주의할 점은, require(esm) 기능이 활성화되면서 파일 로드는 성공했지만 바로 값을 사용할 수는 없다는 점입니다. testModule.default로 접근해야 실제로 export했던 객체를 쓸 수 있습니다.

이쯤에서 다시 assetpack 코드를 보면

  try {
    config = require(configPath); // ✅ ERR_REQUIRE_ESM 발생 X
    AssetPack = require('@assetpack/core').AssetPack;
  } catch (error) {
    if (error.code === 'ERR_REQUIRE_ESM') {
      // ...
    }
  }

Node 20.19.0 이상 환경에서는config = require(configPath); 로 모듈을 읽고 바로 사용했기 때문에, config 변수에 실제 설정값이 아닌 네임스페이스 객체가 들어갔을 것입니다.

설정된 config가 무시되는 현상이 생기는 원인까지 파악했고, 제가 assetpack 코드를 로컬에서 알아서 수정해서 쓸 순 없으므로.. 이 문제가 해결된 버전을 찾아봤습니다.

달라진 Assetpack 실행 과정

Assetpack 1.2.3 버전부터 config 파일을 읽어오는 방식을 다음과 같이 변경했습니다.

// before
config = require(configPath) as AssetPackConfig;

// after
const configModule = require(configPath);
config = (configModule.__esModule ? configModule.default : configModule);

require()로 파일을 읽었을 때, __esModule 필드를 확인해 ES 모듈이면 .default로 접근하도록 수정된 것을 알 수 있습니다.

여기까지 봤을 때 선택할 수 있는 해결 방안은 두 가지입니다.

  • Assetpack 0.8.0을 사용하려면 Node 20.19.0 미만으로 제한하기

  • Node 20.19.0 이상 환경에서 동작을 위해 Assetpack 버전 업데이트하기

해결 방안

우선 두 방법을 각각 시도해봤을 때 모두 문제가 해결되지만, 저는 두 번째 방법을 택하려고 합니다.

❌ Node 20.19.0 미만 환경으로 제한하기

  • 임시방편은 되겠지만, Node 20은 내년 4월에 maintenance 기간이 끝납니다.
  • 장기적으로 봤을 때 Assetpack 사용만을 위해 구버전에 머무르는 게 부담스러웠습니다.

✅ Assetpack 버전 업데이트하기

  • 위에서 봤듯 1.2.3 이상 버전으로 업데이트하면 문제가 해결됩니다.
  • 추가로, Assetpack v1부터는 플러그인 사용법도 훨씬 간단해졌습니다.
    • 기존에는 플러그인별로 패키지를 따로 설치하고 .assetpack.js에서 불러와야 했지만,
    • 이제는 패키지 설치 없이 바로 사용할 수 있도록 개선되었습니다.

겸사겸사 package.json을 심플하게 유지할 수 있다는 장점이 매력적이었습니다.

마무리

개발 환경의 변화가 예상치 못한 곳에서 문제를 일으키는 걸 보고,
로컬 환경 관리에 주의를 기울여야겠다는 생각을 또 한번 했습니다.

프로젝트 관리할 때 패키지 매니저나 노드 버전을 따로 적어본 적이 몇 번 없는데,
이렇게 환경 문제로 헤매고 나니 package.json에 노드 버전의 범위를 명시하는 걸 습관화해야겠단 생각이 듭니다. . .

package.json에 노드 버전 명시하기

package.json에 engine 필드를 추가하면 그 외 환경에서 실행할 때 경고 문구를 볼 수 있습니다.
저처럼 노드 버전을 바꾸는 경우 다시 프로젝트에 맞는 버전으로 변경하는 걸 까먹지 않을 수 있습니다 (っ ᵔ◡ᵔ)っ

profile
안녕하세요, 프론트엔드 개발자 이유진입니다.

0개의 댓글