패키지 업데이트를 통해 깨달은 테스트 & 검증의 중요성

2026년 7월 8일

#infra

이전 포스팅에서 언급한 것처럼, 저는 회사에서 패키지 업데이트 Agent 를 만들어, 에이전트에게 해당 과제를 맡겼습니다.

하지만 예상하지 못한 곳에서 문제를 마주쳤어요. 코드에는 문제가 없었고, 빌드에도 문제가 없습니다. 근데 브라우저를 살펴보니 작업한 버전이 변경되어 있지 않은 것이었습니다.

문제는 배포 파이프라인 어딘가에 숨어 있는 것 같았어요.

반영이 안 된 것 같은데?

평소에 저는 직접 작업한 피처가 화면에 반영되었는지를 보고 배포 여부를 확인하고 있어요. 해당 방법이 편해서 그렇지만, 반영된 buildId 나 청크 파일의 버전 정보를 확인하는 방법도 있지요.

이번 작업은 패키지 업데이트만 진행한 작업이다 보니, 화면상의 눈에 띄는 변화가 없었습니다. 그래서 빌드 ID와 버전이 브라우저와 서버에 정상적으로 반영되었는지 확인해보았습니다. 아래 같은 과정으로 확인할 수 있었어요.

1. Network 탭에서 최초 문서 요청(`(index)`)을 선택하고 우측 Response 패널을 열어봅니다.
2. 응답 HTML 안에서 `_next/static/...` 경로로 잡히는 청크(script) URL들을 찾습니다.
3. 해당 URL 경로에 배포 버전 정보가 들어 있는지 확인합니다.

실제로 열어보니 청크 경로가 전부 ...front.net/1.77.0/_next/static/chunks/... 형태였어요.

하지만 제가 올린 배포 태그는 (1.77.1) 이었습니다. 제가 작업한 패키지 업데이트 커밋과 새로운 버전은 분명 존재하고, deploy 성공 플로우까지 탔는데도, 브라우저에는 실제로 받아오는 정적 파일들은 여전히 이전 버전 경로인 것 입니다.

chunk

GitHub Actions는 멀쩡한데, 뭐가 문제지?

다음으로는 GitHub Workflow 로그부터 확인했습니다.

역시 빌드 스텝은 에러 없이 성공했었고, 여기서 한 가지 힌트를 발견할 수 있었습니다. Next.js가 런타임에 노출하는 buildId 와 브라우저에 실제로 심어진 buildId가 서로 달랐던 것 입니다. “이미지 빌드까지는 성공했지만, 그 이후 ECS로 컨테이너를 올리는(배포) 단계에서 실패했다” 라고 정리할 수 있었습니다.

실제로 Workflow에는 Wait for new version to be live라는 스텝이 있었습니다. 이것은 배포 후 새로 올라간 BUILD_ID가 실제 서빙 트래픽에 반영되었는지 폴링하여 확인하는 것입니다.

polling

새로 빌드된 uRht2Dk-8iEy_rGbwKUI3는 준비돼 있었지만, 실제 서빙은 계속 이전 빌드(1XXeouSIs_9C41r229qnK)가 붙잡고 있는 상태였습니다. 이 로그를 통해 “빌드는 됐지만 배포 전환이 안 됐다”는 가설을 확정할 수 있었습니다.

ECS 에 안뜨는 이유 : 시나리오 정리

회사에서 저는 AWS 권한이 없어서, 인프라 담당 팀원에게 도움을 청해야했어요. 하지만 “안뜨는 것 같아요” 라고 말하는 것 보단, 시나리오를 정리하여 도움을 청하기로 했습니다. 우리에겐 클로드라는 전문가 팀원이 있으니까요.

컨테이너가 뜨자마자 죽는 경우

부팅 시점에 uncaught exception이 발생해 node server.js 실행 직후 크래시가 나고, ECS가 재시작을 반복하지만 계속 죽는 패턴. Next.js 13→16 메이저 점프로 next.config.js의 옵션(i18n 설정 등) 중 새 버전에서 지원되지 않거나 에러를 던지는 항목이 있을 가능성을 의심했습니다.

메모리 부족으로 인한 OOM Kill

Task Definition의 메모리 한도는 그대로인데, Next 16과 새 의존성들로 메모리 사용량이 늘어 시작 직후 OOM으로 종료되는 경우입니다. Stopped task 사유에 OutOfMemoryErrorEssential container exited 문구가 있는지가 판단 기준이 됩니다.

헬스체크 실패 (앱은 떠 있는데 체크만 실패)

ALB 헬스체크의 타임아웃이 새 버전의 콜드스타트 시간보다 짧게 잡혀 있어서, 실제로는 떴는데 체크 통과 전에 unhealthy로 판정되는 경우입니다.

이미지 아키텍처/pull 문제

빌드 러너와 ECS 태스크의 CPU 아키텍처(x86 vs arm64)가 맞지 않으면 exec format error로 즉시 죽습니다.

배포 회로차단기(circuit breaker)의 자동 롤백

1~4번 중 어떤 것이든 반복 실패하면 ECS가 “배포 실패”로 판단해 이전 태스크로 자동 롤백해요. 그렇기 때문에 구 버전의 작업물이 계속 보이는 것이었습니다.

요청 전에 한 번만 더… 로컬에서 재현하기

시나리오 정리 후, 요청 전에 제가 확인할 수 있는 시나리오는 지운 후 도움을 청하고 싶었습니다. 그래서 ECS와 최대한 동일한 조건으로 로컬에서 컨테이너를 띄워봤습니다.

docker build
- ECR에 푸시하지 않고 실제 stg Dockerfile 그대로 로컬에서만 이미지를 빌드

docker run
- ECS와 동일한 방식(포트 3000, Alpine + Node 20)으로 컨테이너를 실행해 실제 부팅 여부 확인

이미지 빌드는 문제없이 성공했고, 컨테이너를 띄우자마자 HTTP 200 응답을 받았어요. 또한 컨테이너는 1분 넘게 안정적으로 살아있었고, 그동안 계속 HTTP 200 정상 응답을 유지했습니다.

이 결과로 앞서 정리한 시나리오 중 컨테이너가 뜨자마자 죽음 이라는 가설은 정리할 수 있었습니다. 코드와 설정 자체는 건강했고, next.config.js의 옵션이나 Next 16 호환성 문제가 원인이 아니라는 걸 확인할 수 있었습니다.

덕분에 인프라 담당자님께 “컨테이너화가 안 되는 것 같아요” 처럼 대충(?) 요청하는 대신,

“이미지, 부팅은 정상 확인됐고 나머지 2~5번 시나리오 위주로 Stopped task 사유를 봐달라” 고 요청할 수 있었습니다.

이유

Next.js standalone 서버가 ECS/Docker가 자동 주입하는 HOSTNAME 환경변수 값으로 바인딩해버려서, 0.0.0.0이 아닌 엉뚱한 주소에서만 리스닝하고 있었던 것이 문제였습니다. 그래서 앱은 떠 있는데 ALB 헬스체크가 접근을 못 해 unhealthy로 계속 롤백됐던 거예요.

인프라 담당자 분께서 도커파일 env 에 HOSTNAME=0.0.0.0 을 명시적으로 넣어줌으로써 해결되었습니다.

회고: 테스트 & 검증의 중요성

이전까지 패키지 업데이트는 의존성 충돌만 고려하면 되는 작업이라 생각했었습니다. 실제로는 인프라와 파이프라인까지 포함한 훨씬 넓은 범위의 문제였습니다.

담당자와의 소통 전, 가능한 가설을 먼저 정리하고 검증 가능한 부분은 직접 좁혀두는 과정이 재밌었고, 협업 속도도 크게 앞당김을 느꼈습니다. (조금 부끄럽지만 지식이 0 인 것을 들키지 않기 위해, 정리한 것이기도 했어요 🙃 반성..)

그 중에서도 가장 저의 개발 가치관에 영향을 준 것은 탄탄한 테스트 & 검증 체계의 필요성이었습니다.

이번 작업에서 저는 로컬에서 직접 docker build, docker run을 돌려가며 부팅 여부와 HTTP 응답을 하나하나 수동으로 확인해야 했었습니다. 클로드의 도움을 받긴 했지만, 원인과 방향성을 잡는데 시간이 걸렸기 때문에 많은 시간이 소비되었습니다.

만약 실패 사유까지 트래킹 가능한 테스트가 있었다면?

이번 이슈 이후, 검증 단계를 더 탄탄하게 할 수 있는 아이디어들이 생각났어요.

  • 헬스체크 요청/응답의 상태 코드와 에러 메시지를 워크플로우 로그에 출력
  • 실패 시 컨테이너 내부에서 실제 리스닝 중인 포트/주소를 확인하는 진단 스텝 추가 (netstat, curl localhost:PORT 등)

특히 요즘은 이런 반복적인 업데이트 작업을 Agent가 대신 수행하다 보니, 제 역할은 점점 “작업을 하는 사람”에서 “결과가 정상적인지 검증하는 사람” 으로 변화한다고 느끼고 있습니다. 결국 사람이 매번 수동으로 확인하지 않아도 되는 배포 검증 플로우의 필요성 또한 느꼈습니다. 앞으로는 더 탄탄한 테스트 & 검증을 고민하는 개발자로 성장하고 싶습니다.


Profile picture

주희(Joy)
가치를 고민하는 과정을 함께해요