로컬에서는 통과한 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초가 넘게 쓰이고 있다는 사실을 뒤늦게 봤습니다. 실패 원인과는 별개였지만, 매 배포마다 반복되는 낭비였습니다.
- robots.txt는 무엇이고 왜 필요한가웹사이트를 운영하다 보면 루트 주소에서 robots.txt라는 작은 텍스트 파일을 만나게 됩니다. 내용은 몇 줄뿐인데 검색엔진 최적화 점검표에는 거의 빠지지 않고 등장합니다.
- 블로그를 서브도메인에서 하위 경로로 옮긴 이유, 그리고 CSP가 애드센스를 막고 있었습니다회사 사이트와 기술 블로그를 따로 운영하는 구성은 흔합니다. 회사는 duolabs.co.kr, 블로그는 blog.duolabs.co.kr.
- 로컬에서는 정상인데 운영에서만 깨진 AI 스트리밍문서 AI 기능을 로컬에서 완성하고 운영에 배포했을 때, 답변 자체는 정상인데 화면의 느낌이 완전히 달라졌습니다. 스트리밍 답변은 한꺼번에 나타났고, 전체 화면 패널은 작은 섹션 안에 갇혔으며, 모달이 열리자마자 배경 스크롤 잠금이 풀렸습니다.
- 온프레미스 Next.js 서비스를 Vercel과 Supabase로 옮기기온프레미스에서 서비스를 운영하면 애플리케이션, 데이터베이스, Redis, WebSocket 서버와 백그라운드 워커를 한 네트워크 안에 둘 수 있습니다. 프로세스를 계속 띄워두기도 쉽습니다. setInterval로 10초마다 데이터베이스를 확인하는 코드도 일단은 잘 동작합니다.