Next.js

Next.js에서 Webpack externals 타입 충돌로 개발 서버가 안 켜질 때

해보구 2025. 5. 31. 14:38

 

Three.js를 사용하는 Next.js 프로젝트를 개발하던 중에 갑자기 개발 서버가 켜지지 않는 문제가 발생했다. npm run dev를 실행하면 다음과 같은 Webpack ValidationError가 발생했다.

ValidationError: Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration[1].externals should be one of these:
  [RegExp | string | object { byLayer?, <key>: [non-empty string, ...] | boolean | string | object { … } } | function, ...] | RegExp | string | object { byLayer?, <key>: [non-empty string, ...] | boolean | string | object { … } } | function

처음에는 에러 메시지가 너무 길고 복잡해서 뭐가 문제인지 파악하기 어려웠다. 하지만 자세히 보니 externals 설정에서 타입이 맞지 않는다는 내용이었다.

 

문제가 된 nextConfig.mjs 파일을 확인해보니 다음과 같은 코드가 있었다.

webpack: (config, { isServer }) => {
  if (isServer) {
    config.externals = {
      ...config.externals,  // 여기가 문제!
      canvas: 'canvas',
    };
  }
  return config;
},

Next.js에서 config.externals는 기본적으로 배열(Array) 타입인데, 이를 **객체(Object)**로 덮어쓰려고 해서 타입 충돌이 발생한 것이었다. Webpack의 externals 설정은 여러 타입을 지원하지만, 기존 설정과 호환되지 않는 방식으로 변경하면 validation 에러가 발생한다.

 

해결 과정

처음에는 Webpack 문서를 찾아보면서 externals 설정 방법을 다시 공부했다. externals는 다음과 같은 형태들을 지원한다:

  • string: 단순 문자열
  • object: 키-값 매핑
  • function: 동적 처리
  • RegExp: 정규표현식 매칭
  • array: 여러 externals 조합

문제는 기존 배열에 객체를 덮어씌우려고 했던 것이었다. 배열은 배열대로 유지하면서 새로운 external을 추가해야 했다.

해결 방법

결국 다음과 같이 수정해서 문제를 해결했다.

// 수정 전 (문제가 된 코드)
if (isServer) {
  config.externals = {
    ...config.externals,
    canvas: 'canvas',
  };
}

// 수정 후 (해결된 코드)
if (isServer) {
  config.externals.push('canvas');
}

배열에서 spread operator를 사용해서 객체로 변환하려던 시도를 포기하고, 단순히 push() 메서드를 사용해서 배열에 새로운 external을 추가하는 방식으로 변경했다.

전체 수정된 코드

최종적으로 수정된 nextConfig.mjs 파일은 다음과 같다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false,
  experimental: {
    esmExternals: false,
    optimizePackageImports: ['three', '@react-three/fiber', '@react-three/drei'],
  },
  webpack: (config, { isServer }) => {
    // 서버사이드에서 canvas 관련 오류 방지
    if (isServer) {
      config.externals.push('canvas');
    }
    
    // 모바일 성능 최적화
    config.module.rules.push({
      test: /\.(glsl|vs|fs|vert|frag)$/,
      type: 'asset/source',
    });
    
    return config;
  },
  async rewrites() {
    return [];
  },
};

export default nextConfig;

추가로 알게 된 것들

이 문제를 해결하면서 Webpack externals 설정에 대해 더 깊이 이해하게 되었다.

만약 더 복잡한 조건부 처리가 필요하다면 함수형 externals도 사용할 수 있다는 것을 알았다.

config.externals.push(({ request }) => {
  if (request === 'canvas') return 'canvas';
  if (request === 'fs') return 'commonjs fs';
  // 다른 조건들...
});

또한 externals 설정이 서버사이드 렌더링과 클라이언트사이드 렌더링에서 다르게 동작할 수 있다는 점도 배웠다. 특히 Three.js 같은 WebGL 라이브러리를 사용할 때는 서버에서 실행되지 않도록 적절히 external 처리하는 것이 중요하다.

비슷한 문제 예방하기

이런 타입 충돌 문제를 예방하려면:

  1. 기존 설정의 타입을 확인하고 그에 맞게 수정하기
  2. console.log()로 config 객체 구조 먼저 파악하기
  3. TypeScript 사용시 타입 체크 활용하기
  4. Next.js 공식 문서의 webpack 설정 예제 참고하기

특히 Next.js에서 webpack 설정을 커스터마이징할 때는 기존 설정을 완전히 덮어쓰기보다는 추가하는 방식이 더 안전하다는 것을 깨달았다.

회고

이번 문제는 사실 작은 실수에서 비롯된 것이었지만, Webpack 설정에 대한 이해를 한층 더 깊게 만들어주는 계기가 되었다. 에러 메시지가 복잡해 보여도 차근차근 읽어보면 핵심 문제를 파악할 수 있다는 것을 다시 한번 느꼈다.

특히 JavaScript의 타입 유연성이 때로는 이런 혼란을 가져올 수 있다는 점도 배웠다. 배열을 객체로 변환하려던 시도가 문제였는데, 애초에 배열은 배열로 다루는 것이 가장 명확하고 안전한 방법이었다.

앞으로는 설정 파일을 수정할 때 기존 구조를 더 신중하게 파악하고, 가능하면 기존 설정을 유지하면서 필요한 부분만 추가하는 방향으로 접근해야겠다. 그리고 이런 설정 관련 이슈들을 미리 방지하기 위해 TypeScript 도입도 고려해볼 만하다고 생각한다.