Frontend
로컬에선 되는데 프로덕션에선 안 돼요 — Ant Design 5 마이그레이션에서 만난 ESM 호환성 문제
전성진··수정됨 2026.07.21

저희 팀은 시니어의 위원회 활동 참여를 쉽고 빠르게 지원하는 플랫폼인 위즈커넥트의 어드민 페이지를 운영하고 있습니다.
시니어 사용자와 위원회를 연결하는 만큼 어드민에서의 데이터 관리와 운영 효율이 서비스 품질에 직결되는 구조인데요.
최근 이 어드민 페이지의 UI 라이브러리를 Ant Design 4에서 5로 업그레이드하면서, 꽤나 당황스러운 문제를 만났습니다.
잘 되던 게 갑자기 안 되는 순간
Ant Design 5는 CSS-in-JS 전환, 더 나은 TypeScript 지원, 컴포넌트 API 개선 등 매력적인 변화를 많이 담고 있었습니다. 마이그레이션을 결정하고 코드를 수정한 뒤, 로컬에서 npm run dev를 실행했을 때 모든 것이 완벽하게 작동했습니다. DatePicker도 잘 열리고, Table도 정상이고, Form 검증도 문제없었죠.
자신감을 가지고 프로덕션에 배포했습니다. 그리고 얼마 지나지 않아 이런 오류를 마주했습니다.
TypeError: (0 , _miscUtil.tuple) is not a function
at Object.<anonymous> (/app/node_modules/rc-picker/lib/panels/DatetimePanel/index.js:23:40)DatePicker를 사용하는 페이지가 통째로 죽어버렸습니다. 로컬에서는 분명히 잘 됐는데, 프로덕션에서만 발생하는 오류. 개발자라면 누구나 한 번쯤 경험해 봤을, 가장 난감한 종류의 버그였습니다.
범인을 찾아서
첫 번째 용의자: 버전 호환성
가장 먼저 의심한 건 패키지 버전 충돌이었습니다. npm ls로 의존성 트리를 확인해 봤습니다.
bash
npm ls antd rc-picker rc-util결과는 깔끔했습니다. antd 5.28.0, rc-picker 4.11.3, rc-util 5.44.4. 버전 간 호환성에는 문제가 없었습니다. 범인은 다른 곳에 있었죠.
두 번째 용의자: 빌드 과정
오류 메시지를 다시 자세히 들여다봤습니다.
(0 , _miscUtil.tuple) is not a function이 패턴은 특이합니다. (0, _module.function)이라는 형태는 Babel이나 Webpack이 ES 모듈의 import { tuple } from './miscUtil'을 CommonJS로 변환할 때 만들어내는 코드입니다. 즉, 이 오류는 ES 모듈(ESM)과 CommonJS(CJS)의 충돌을 의미하고 있었습니다.
여기서 핵심적인 질문이 떠올랐습니다. "왜 로컬에서는 되고 프로덕션에서만 안 될까?"
개발 환경과 프로덕션 환경의 차이
Next.js의 개발 서버(next dev)와 프로덕션 빌드(next build)는 모듈을 처리하는 방식이 다릅니다.
개발 환경에서는 Webpack 개발 서버가 모든 모듈을 동적으로 처리합니다. ESM이든 CJS든 필요에 따라 자동으로 변환해 주죠. 반면 프로덕션 빌드에서는 최적화를 위해 엄격한 규칙을 적용합니다. 특히 node_modules 안의 패키지는 기본적으로 트랜스파일하지 않습니다. 이미 배포 가능한 형태로 빌드되어 있다고 가정하기 때문입니다.
문제는 이 가정이 깨졌다는 것이었습니다.
Ant Design 5의 하위 패키지들인 rc-picker, rc-util 등은 ES 모듈 형식으로 작성되어 있습니다. Next.js 13.2.4는 이들을 트랜스파일하지 않았고, 프로덕션 빌드에서 CommonJS와 ESM이 섞이면서 tuple 함수를 찾지 못하게 된 것이었죠.
한 줄의 설정이 해결한 문제
원인을 파악하고 나니 해결책은 명확했습니다. Next.js 13.1부터 제공하는 transpilePackages 옵션으로, ESM으로 작성된 패키지들을 프로덕션 빌드 시에도 트랜스파일하도록 명시하면 됩니다.
javascript
// next.config.js
const nextConfig = {
// ...기존 설정
// Ant Design 5 및 rc-* 패키지 트랜스파일 설정
transpilePackages: [
'antd',
'@ant-design/icons',
'@ant-design/cssinjs',
'rc-util',
'rc-pagination',
'rc-picker',
'rc-notification',
'rc-tooltip',
'rc-tree',
'rc-table',
],
};각 패키지를 포함한 이유가 있습니다. antd는 메인 라이브러리이고, @ant-design/cssinjs는 Ant Design 5의 핵심인 CSS-in-JS 엔진입니다. @ant-design/icons는 ESM 전용으로 배포되는 아이콘 컴포넌트이고요. rc-util은 이번 오류의 직접적 원인이었던 공통 유틸리티 함수들을 담고 있습니다. 그 외 rc-picker, rc-table 등은 Ant Design의 각 컴포넌트를 실제로 구현하는 하위 패키지들입니다.
설정을 추가하고 다시 빌드했습니다.
bash
rm -rf node_modules .next
npm install
npm run build
npm run startDatePicker가 정상적으로 동작하는 것을 확인한 순간, 정말 한숨이 나왔습니다.
이 경험에서 배운 것
로컬 테스트만으로는 부족하다
이번 이슈의 가장 큰 교훈은, npm run dev에서의 성공이 프로덕션 안정성을 보장하지 않는다는 것이었습니다. 라이브러리를 업그레이드한 뒤에는 반드시 로컬에서 프로덕션 빌드를 돌려봐야 합니다. npm run build && npm run start는 배포 전 필수 체크 항목이 되어야 합니다.
ESM 전환기의 현실
JavaScript 생태계는 지금 CommonJS에서 ES 모듈로의 전환기에 있습니다. 대부분의 모던 라이브러리가 ESM을 채택하고 있지만, 빌드 도구와 런타임 환경이 이를 완벽하게 지원하지 못하는 경우가 여전히 존재합니다. Ant Design 5처럼 대규모 라이브러리의 메이저 업그레이드에서는 이런 모듈 호환성 문제를 반드시 염두에 두어야 합니다.
오류 메시지를 읽는 법
(0 , _module.function) is not a function 같은 오류는 처음 보면 암호처럼 느껴지지만, 이것이 ESM-CJS 변환 과정에서 발생하는 전형적인 패턴이라는 것을 알고 나면 원인을 훨씬 빨리 좁힐 수 있습니다. 오류 스택에 /node_modules/ 경로가 보인다면, 해당 패키지의 모듈 형식과 빌드 설정을 점검해 보는 것이 첫 번째 단계입니다.
비슷한 문제를 만났다면
혹시 비슷한 상황에 처해 있다면, 아래 체크리스트가 도움이 될 수 있습니다.
개발 환경에서는 정상, 프로덕션에서만 오류 →
transpilePackages설정을 확인하세요.is not a function또는undefined오류 → ESM/CommonJS 혼용 문제일 가능성이 높습니다.Ant Design 컴포넌트 관련 오류 →
rc-*패키지들이transpilePackages에 포함되었는지 확인하세요.오류 스택에
/node_modules/경로 포함 → 해당 패키지를transpilePackages에 추가해 보세요.
작은 설정 하나가 프로덕션 장애를 예방할 수 있습니다. 당황하는 일 없이, 안정적인 마이그레이션 되시길 바랍니다.
환경 정보
Next.js 13.2.4
Ant Design 5.22.5 (from 4.24.8)
Node.js 18+
배포 환경: Docker (standalone output)
읽기 도구
약 8분 읽기
이 글이 도움이 되었나요?