로컬에서는 통과한 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까지 커진 이유Docker 빌드가 느리면 패키지 설치나 애플리케이션 컴파일부터 보게 됩니다. 저도 타입 검사 실패를 고치는 데 집중하다가 그보다 앞선 load build context 단계에서 40초가 넘게 쓰이고 있다는 사실을 뒤늦게 봤습니다. 실패 원인과는 별개였지만, 매 배포마다 반복되는 낭비였습니다.
- Next.js Docker 빌드 캐시: 배포 CSS에서 패키지 스타일이 빠진 문제테마 메뉴에서 디자인 시스템을 바꾸는 기능을 배포했습니다. 메뉴에는 「Betelgeuse 라임 다크」라고 정확히 찍히는데, 화면은 기본 시스템 그대로였습니다. 바로 전 배포에서는 같은 기능이 잘 동작했습니다. 그 사이에 바뀐 것은 디자인 시스템 패키지의 버전 하나였습니다.
- Next.js에서 링크를 눌렀는데 로딩 뒤 이전 화면이 다시 나타나는 이유페이지 이동을 빠르게 보이게 만드는 장치는 대개 눈에 잘 띄지 않습니다. 미리 다음 화면을 받아 두거나, 서버 응답을 기다리는 동안 스켈레톤을 보여주는 식입니다. 이번에는 그 두 가지가 한꺼번에 작동하면서 오히려 이동을 늦추고 화면 상태까지 헷갈리게 만들었습니다.
- Next.js는 운영에서 멀쩡한데 로컬 개발 서버에서만 UI가 먹통이 되는 이유Next.js 애플리케이션은 운영 빌드가 안정적이면 개발 서버도 같은 방식으로 움직일 것 같지만, 둘은 클라이언트 코드의 수명이 다릅니다. 제가 확인한 환경은 Next.js 16.2.7과 Turbopack이었고, 운영 사이트에서는 잘 눌리던 상단 메뉴가 로컬 개발 서버에서만 간헐적으로 먹통이 됐습니다.
- Next.js 랜딩 페이지의 Lighthouse 점수가 폰트 하나로 해결되지 않는 이유성능 최적화에서 가장 당황스러운 순간은 개발 환경에서는 빠른데 배포 후 측정값만 크게 느릴 때입니다. 이번 랜딩 페이지가 그랬습니다. 개발 환경에서 관찰한 LCP는 약 1.68초였지만 PageSpeed Insights의 첫 측정은 5.5초까지 늘어났습니다. 이후 배포본을 다시 측정했을 때도 성능 점수 72점, LCP 4…