로컬에서는 통과한 Next.js 타입 검사가 Docker 배포에서만 실패하는 이유

Next.js 앱에서 로컬 타입 검사는 통과하는데 Docker 배포에서만 같은 오류가 다시 나오면 코드보다 빌드 이미지에 무엇이 들어갔는지 봐야 합니다. 이번 작업에서는 린트, 타입 검사, 프로덕션 빌드를 모두 통과시킨 뒤 배포했는데 원격 빌드만 실패했습니다. 같은 커밋과 같은 잠금 파일을 썼으니 처음에는 환경 차이가 크지 않을 거라고 생각했습니다.

로컬에서는 네 번 모두 통과했습니다

시작점은 사내 UI 패키지의 다형 컴포넌트와 React Three Fiber의 JSX 타입 확장이 충돌하는 문제였습니다. ElementType으로 렌더링하던 태그의 children 타입이 never로 좁혀지면서 다음 오류가 났습니다.

This JSX tag's 'children' prop expects type 'never'

렌더링할 태그를 명시적인 ComponentType으로 좁힌 패치를 만들었습니다.

const Component = Tag as ComponentType<HTMLAttributes<HTMLElement>>;

return (
<Component className={className} {...rest}>
{children}
</Component>
);

패키지 재설치 뒤에도 수정이 남도록 patch-package를 postinstall에 연결했습니다. 새로 설치한 상태를 확인하려고 의존성을 깨끗하게 다시 받은 다음 아래 검사를 차례로 실행했습니다.

npm ci
npm run lint
npm run typecheck
npm run build

네 단계가 모두 통과했고, 기존에 있던 ignoreBuildErrors 우회도 제거했습니다. 여기까지는 타입 문제가 끝난 줄 알았습니다.

새 패키지 버전이 있을 줄 알았습니다

패치를 만들기 전에는 UI 패키지의 다음 버전에 이미 수정이 들어갔을 가능성을 먼저 봤습니다. 사내 패키지 저장소에서 배포 버전을 확인했지만 프로젝트가 쓰는 버전이 최신이었습니다. 버전만 올려 해결하는 길은 없었습니다.

로컬 패치가 통과한 뒤에는 Docker도 같은 npm ci를 실행하니 같은 결과가 나올 거라고 봤습니다. 이 생각이 틀렸습니다. 명령은 같았지만 그 명령이 실행되는 시점에 보이는 파일이 달랐습니다.

Docker 로그에서 달랐던 두 줄

원격 빌드는 JavaScript 컴파일까지 마친 다음 TypeScript 단계에서 다시 실패했습니다. 그보다 앞선 설치 로그에 원인이 그대로 있었습니다.

Applying patches...
No patch files found

Compiled successfully
Running TypeScript ...
Failed to type check.

Dockerfile의 의존성 단계는 패키지 파일만 먼저 복사하고 바로 설치했습니다. 패치 디렉터리는 나중의 전체 소스 복사 단계에서 들어왔습니다.

COPY package*.json .npmrc ./
RUN npm ci

# 이 시점에야 patches/가 이미지에 들어옵니다.
COPY . .

postinstall은 이미 끝났으므로 나중에 복사된 패치를 다시 적용할 기회가 없었습니다. 로컬 npm ci와 Docker 안의 npm ci가 같은 명령이어도 입력 파일 집합은 같지 않았던 셈입니다.

postinstall보다 먼저 패치를 복사했습니다

저는 의존성 설치 전에 패치 디렉터리를 명시적으로 복사했습니다.

COPY package*.json .npmrc ./
COPY patches ./patches
RUN npm ci

패치 파일이 바뀌면 의존성 레이어의 캐시도 함께 무효화됩니다. 빌드 단계에서 patch-package를 한 번 더 실행하는 방식보다 설치 결과와 캐시 기준이 한곳에 남는 쪽을 택했습니다.

다음 배포 로그에서는 패치 적용 완료가 표시됐고 JavaScript 컴파일은 10.1초, TypeScript 검사는 15.8초에 끝났습니다. 전체 배포는 2분 15초에 성공했고 공개 헬스체크도 ok: true를 반환했습니다.

이제 저는 로컬과 Docker에서 같은 설치 명령을 썼다는 사실만으로 결과도 같다고 보지 않습니다. postinstall이 필요로 하는 파일이 Docker의 해당 레이어에 실제로 들어와 있는지, 설치 로그에 패치 적용 결과가 무엇으로 찍혔는지를 함께 확인합니다.

같은 작업에서 겪은 다른 문제:

Docker 빌드 컨텍스트가 1.06GB까지 커진 이유