
Next.js 프로젝트에서 특정 시간 이후 점검 페이지로 리다이렉트하는 middleware를 운영 중이었다. 점검 시간 이후 수정한 내용을 배포할려고 하자 배포 스크립트가 중간에 실패하면서 새 버전이 올라가지 않는 문제가 발생했다.
배포는 AWS CodeDeploy를 통해 트리거되며, update.sh 스크립트가 블루/그린 방식으로 컨테이너를 교체한다.
CodeDeploy → appspec.yml → update.sh 실행
→ 새 컨테이너 시작 (v1 ↔ v2 교대)
→ 로컬 헬스체크 (스크립트 자체)
→ ALB 대상그룹 등록
→ AWS 대상그룹 헬스체크 대기
→ 기존 컨테이너 제거
여기서 헬스체크가 두 단계로 나뉜다.
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "[http://localhost](http://localhost/):$port/")
if [[ "$http_code" == "200" ]] || [[ "$http_code" == "404" ]]; then
log " ✓ Application is responding (HTTP $http_code)"
return 0
fi
스크립트가 localhost:포트/로 직접 curl을 날려서 컨테이너가 살아있는지 확인한다. 여기서는 200, 404을 정상으로 간주하고 있는데 여기서 10번을 시도해도 실패하면서 문제가 발생했다.
health_status=$(aws elbv2 describe-target-health --target-group-arn "$TARGET_GROUP_ARN" \
--query "TargetHealthDescriptions[?Target.Port==\`$port\\`].TargetHealth.State" --output text)
새 컨테이너를 ALB 대상그룹에 등록한 뒤, AWS가 자체적으로 헬스체크를 수행한다. 대상그룹의 헬스체크 경로가
/home으로 설정되어 있었는데, 이 요청에서도 문제가 발생했다.
AWS Code Deploy 로그를 확인해보면 응답코드 307이 찍혀 있었다. 어디서 왔을까 찾아보니 middleware의
NextResponse.redirect()는 기본적으로 307을 반환한다. 이게 실패 첫번째
Next.js redirect 공식문서
그래서 스크립트에 307 코드를 추가했다. 그랬더니 1번의 시도만에 통과가 되면서 컨테이너가 무사히 실행이되었다.
if [[ "$http_code" == "200" ]] || [[ "$http_code" == "404" ]] || [[ "$http_code" == "307" ]]; then
log " ✓ Application is responding (HTTP $http_code)"
return 0
AWS 콘솔의 대상그룹 헬스체크 상세를 보면 응답 코드가 308로 찍혀 있었다. middleware의
NextResponse.redirect()는 기본적으로 307을 반환하는데, 308은 어디서 나온 걸까?
원인은 next.config.ts에 있었다.
async redirects() {
return [
{
source: '/home',
destination: '/',
permanent: true, // 308 Permanent Redirect
},
];
},
/home → /로의 permanent redirect가 설정되어 있었다. Next.js에서 permanent: true는 308 Permanent Redirect를 반환한다.
요청 → next.config.ts redirects → middleware → 페이지 렌더링
next.config.ts의 redirects는 middleware보다 먼저 실행된다.
따라서 /home으로 들어온 헬스체크 요청은 middleware의 점검 모드 로직에 도달하기도 전에 308로 redirect되고 있었다.
const MAINTENANCE_START = new Date(
process.env.NEXT_PUBLIC_MAINTENANCE_START || '2026-03 23T23:00:00+09:00'
).getTime();
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isLive = process.env.NEXT_PUBLIC_RUNNING_MODE === 'live';
const maintenanceOff = process.env.NEXT_PUBLIC_MAINTENANCE_OFF === 'true';
const isMaintenance = isLive && !maintenanceOff && Date.now() >= MAINTENANCE_START;
if (isMaintenance && pathname !== '/maintenance') {
return NextResponse.redirect(new URL('/maintenance', request.url));
}
if (!isMaintenance && pathname === '/maintenance') {
return NextResponse.redirect(new URL('/', request.url));
}
// ...
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
middleware는 점검 시간에 /maintenance를 제외한 모든 경로를 307로 redirect한다. 하지만 /home의 경우 middleware에 도달하기 전에 next.config.ts의 redirects에서 이미 308로 처리된다.
AWS ALB 헬스체크 → GET /home
→ next.config.ts redirects: /home → / (308 Permanent Redirect)
→ middleware까지 도달하지 않음
→ ALB는 200을 기대했는데 308 수신 → unhealthy
스크립트가 MAX_ATTEMPTS(30회) 동안 healthy 대기 → 전부 실패 → 배포 롤백
이 redirect 설정은 점검 모드와 무관하게 항상 동작하므로, 대상그룹의 헬스체크 Matcher 설정에 따라 평소에도 문제가 될 수 있었던 구조였다. 😂
해결방법에는 여러개의 방법이 있었다.
// src/app/api/health/route.ts
export function GET() {
return Response.json({ status: 'ok' });
}
/api/health는 middleware matcher에서 제외되어 있고, next.config.ts의 redirects에도 해당하지 않으므로 어떤 상황에서든 항상 200을 반환한다.
실제로 적용한 방법은 AWS 콘솔에서 대상그룹의 헬스체크 Success codes에 308을 추가하는 것이었다.
AWS 콘솔 경로: EC2 → 대상 그룹 → 해당 그룹 선택 → Health checks 탭 → Edit → Success codes
기존: 200
변경: 200,308,307
/home으로 들어온 헬스체크 요청이 308로 응답되는 상황에서, AWS가 308도 정상으로 간주하도록 허용 범위를 넓혀주는 방식이다. 컨테이너가 살아있고 요청에 응답하고 있다는 사실은 변함없으므로 헬스체크 기준으로 충분하다.
헬스체크 전용 API(방법 1)와 비교하면 코드 변경 없이 콘솔 설정만으로 해결된다는 장점이 있다. 다만 헬스체크 경로나 redirect 설정이 바뀌면 응답 코드도 바뀔 수 있어 다시 확인이 필요하다는 점은 염두에 두면 좋다.
현재는 성공 코드에 대한 수정으로 충분했지만 추가 요구사항에 맞게 많은 수정이 필요하다면 다양한 변수가 생길수 있기 때문에 후에 1번의 방향으로 수정할 예정이다.
처음 진행하는 시스템 점검이었고, middleware를 활용한 구현이라 배포 환경에서 충분한 테스트를 진행하지 못한 채 로컬에서만 검증하고 올린 게 화근이었다.
이번 문제를 겪으면서 몇 가지를 새로 알게 되었다.
배포 스크립트의 로컬 헬스체크와 AWS 대상그룹 헬스체크는 완전히 별개다. 스크립트가 통과했다고 해서 대상그룹도 통과하는 게 아니다. 둘은 각각 독립적으로 동작하고, 각자의 기준으로 판단한다.
Next.js에서 next.config.ts의 redirects는 middleware보다 먼저 실행된다. 헬스체크 경로가 config의 redirect 대상에 포함되어 있으면, middleware 로직과 무관하게 redirect가 먼저 발생한다. 점검 모드를 끈 상태에서도 마찬가지다.
헬스체크는 API route로 만드는 게 가장 자연스러운 패턴이다. Next.js에서 /api 경로는 middleware matcher에서 제외하는 것이 일반적이고, config의 redirects 대상이 되는 경우도 거의 없다. 어떤 상황에서도 흔들리지 않는 헬스체크 엔드포인트가 필요하다면 /api/health가 가장 안전한 선택이다.