로컬에서는 통과한 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초가 넘게 쓰이고 있다는 사실을 뒤늦게 봤습니다. 실패 원인과는 별개였지만, 매 배포마다 반복되는 낭비였습니다.
- 태그 485개를 전부 색인시키고 있었습니다: 블로그 SEO를 다시 손본 기록블로그에는 이미 canonical, sitemap, robots.txt, 글별 Open Graph 이미지, 구조화 데이터가 들어가 있었습니다. RSS와 Atom, JSON Feed도 있었고 관련 글 링크도 붙어 있었습니다. 그래서 처음에는 큰 구멍보다는 메타 설명을 조금 다듬는 정도를 예상했습니다.
- robots.txt는 무엇이고 왜 필요한가웹사이트를 운영하다 보면 루트 주소에서 robots.txt라는 작은 텍스트 파일을 만나게 됩니다. 내용은 몇 줄뿐인데 검색엔진 최적화 점검표에는 거의 빠지지 않고 등장합니다.
- 사이트맵이 6일째 멈춘 이유: revalidateTag 가 닿지 않는 자리글을 두 편 발행하고 사이트맵을 열어 봤습니다. 둘 다 없었습니다.
- docker builder prune 상한: 왜 남의 캐시까지 깎일까?한 서비스의 개발 배포를 다른 호스트로 옮기는 중이었습니다. 러너를 새로 붙이고, 환경변수 출처를 바꾸고, 이제 워크플로를 켜기만 하면 되는 참이었습니다. 그런데 배포 스크립트 마지막에 있는 한 줄에서 멈췄습니다.