2026년 5~6월, 아파트케어2 사용자 웹앱에서 이미지 변환 CPU가 세 번 터졌다. 매번 원인이 달랐고 처방도 달랐다. 권한 결함을 고치고, CDN을 깔고, AVIF를 걷어내면서 변환 폭주 구간의 origin 지연 최대치를 9.81초에서 1초 미만으로 끌어내렸다. 이 글은 그 한 달의 기록이자, 같은 인프라(Cloud Run + Next.js)를 쓰는 팀이 같은 함정을 피하도록 정리한 지도다.

TL;DR


무대 — 어떤 구조 위에서 벌어진 일인가

아파트케어2 사용자 앱은 플러터 네이티브 셸 안에서 Next.js 웹앱이 웹뷰로 도는 구조다. 사용자가 보는 화면 대부분은 사실상 모바일 브라우저로 서버에 접속하는 것과 같다. 서버 쪽은 단순하다. GCP asia-northeast3의 Cloud Run 위에 사용자 웹 서비스가 떠 있고(CPU 2 / 메모리 1Gi / 동시 80), 그 앞에 로드밸런서와 Cloud CDN(5/28 활성)이 있다. 핵심은 사건 당시 최소 인스턴스가 1대였다는 점이다. 트래픽이 몰리면 스케일아웃되지만 약간의 지연이 있고, 그 사이 무거운 요청이 단일 인스턴스에 쌓이면 CPU가 포화된다.

왜 Next.js 서버가 CPU를 쓰는지부터 짚어두자. CRA 같은 순수 SPA는 정적 파일만 내려주면 끝이라 서버 부담이 거의 없다. 반면 Next.js 15 App Router는 한 요청에 더 많은 일을 한다 — SSR(컴포넌트 렌더+데이터 페치), 미들웨어, 이미지 최적화(Sharp 리사이즈·포맷 변환), fetch 캐싱. 이 글이 다루는 건 이 중 이미지 변환 CPU, 즉 /_next/image 경로 하나다. 세 차례 사건과 9.81초 지연이 모두 여기 걸려 있었다.

같은 사내, 다른 선택 — v2front와의 대조

흥미로운 건, 같은 조직·비슷한 환경의 다른 프로젝트는 이 문제를 겪지 않았다는 점이다.

v2front (메인 아파트아이) webapp-aptcare2-user (본 사례)
인프라 GAE Flex Cloud Run
Next.js / 라우터 14.2 / Pages Router 15.3 / App Router
기본 렌더링 CSR 위주 SSR 기본
정적 자산 assetPrefix로 별도 CDN 자체 서버 직접 서빙
이미지 img 직접 사용 Image/_next/image Sharp 변환
CPU 이슈 없음 본 시리즈 3건

v2front는 도입 시점부터 정적 자산을 별도 CDN에 두고 이미지도 img를 직접 썼다. /_next/image 변환 경로가 아예 존재하지 않는다. 반면 본 프로젝트는 App Router의 기본 동작에 그대로 올라탔고, CDN도 5/28까지 없었다. 정적 자산·이미지·SSR HTML이 모두 단일 인스턴스로 도달하고 있었던 것이다. 둘 다 흔히 통하는 패턴이지만, 본 프로젝트가 쓰는 통합 기능은 분산 CDN이 받쳐주는 환경을 전제로 설계됐다는 점이 이번에 비용으로 드러났다.


세 번의 사건

5/15 — 컨테이너가 캐시를 못 쓰고 있었다

알림톡 발송으로 트래픽이 들어오자 인스턴스가 27개까지 폭증했다. 처음엔 단순 리소스 부족으로 보고 설정을 손봤지만, 주말 내내 증설·감소가 반복됐다. 단순 리소스 문제가 아니라는 신호였다. 로그를 다시 보니 같은 이미지 변환이 평균 45회씩 재시도되고 있었다.

⨯ Failed to write image to cache
  [Error: EACCES: permission denied, mkdir '/app/.next/cache']

Dockerfile 런타임 단계에서 .next/cache 소유권 설정이 빠져, USER node로 전환된 프로세스가 root 소유 디렉터리에 쓰려다 거부당하고 재시도하는 루프였다. 표준 standalone 패턴으로 소유권을 부여하니 일괄 해소됐다.

COPY --chown=node:node --from=builder /app/.next/standalone ./
RUN mkdir -p /app/.next/cache && chown -R node:node /app/.next