바야흐로 목요일, 평소와 같이 배포를 했는데 갑자기 사용자들에게 앱이 흰 화면으로 보인다는 이슈가 인입이 되었다😱
S3 + CloudFront + Next.js 웹뷰 환경에서 캐싱 문제 트러블슈팅한 이야기!! 두두등장
최근 프론트엔드 배포 시스템을 AWS S3와 CloudFront 조합으로 이전한 뒤, 캐싱 문제로 사용자들에게 흰 화면이 발생하는 심각한 이슈를 겪었다.
웹뷰 환경에서는 작은 캐시 이슈도 치명적으로 작용한다는 것을 이번 경험을 통해 절실히 느꼈다.
이 글에서는 문제를 어떻게 분석하고 해결해갔는지 과정을 정리해보았다.
어느 날, 앱 고객센터로부터 "앱을 열었더니 흰 화면만 보여요"라는 제보가 들어왔다.
우리는 이전에도 배포를 여러 번 해왔고, 별다른 문제가 없었기 때문에 처음엔 뭔가 일시적인 문제겠거니 했다.
하지만 직접 웹에서 열어보니 콘솔에 아래와 같은 에러가 있었다.
_app.js?ts=1747359952985:498 Uncaught SyntaxError: Invalid or unexpected token
이후 새로고침을 하면 정상적으로 화면이 로딩되었다.
이 증상은 일부 사용자에게만 발생했고, 새로고침을 하지 않은 상태에서 내부 팝업 등을 통해 라우팅할 때 주로 발생했다.
콘솔 에러를 본 뒤, 네트워크 탭을 확인해봤다.
에러가 나는 시점에는 main-7b9a
라는 오래된 JS 파일이 로딩되고 있었고, 정상일 때는 main-f1ef
이라는 최신 JS 파일이 로딩되었다.
즉,
기존에는 프론트엔드를 다른 방식으로 배포하고 있었지만, 최근 S3 + CloudFront로 전환한 이후부터 이런 문제가 생기기 시작했다.
이 환경에서는 브라우저 캐싱이 더욱 강력하게 동작하기 때문에, 의도하지 않으면 캐시가 쉽게 무효화되지 않는다.
우리는 HTTP 캐싱을 문제의 원인으로 보고, 배포 워크플로우의 aws s3 sync
명령에 캐시 무효화 정책을 추가했다.
# _next/static/* → 캐시 X
aws s3 sync out/_next/static s3://.../_next/static \
--cache-control "no-cache, no-store, must-revalidate"
# *.html → 캐시 X
aws s3 sync out/ s3://... \
--include "*.html" \
--cache-control "no-cache, no-store, must-revalidate"
# 나머지 정적 자산 → immutable 캐시 (1년)
aws s3 sync out/ s3://... \
--exclude "_next/static/*" \
--exclude "*.html" \
--cache-control "max-age=31536000, public, immutable
CloudFront invalidation도 함께 설정하여, 모든 경로에 대해 캐시를 무효화하도록 했다.
aws cloudfront create-invalidation --paths "/*"
우리는 완벽하게 캐시 정책을 적용했다고 생각했지만, 여전히 사용자에게 흰 화면이 간헐적으로 나타났다.
원인은 생각보다 단순한 곳에 있었다.
-include
/ -exclude
옵션의 맹점S3에 파일을 업로드할 때 aws s3 sync
는 한 번의 명령에 대해 하나의 --cache-control
만 적용할 수 있다.
그런데 우리는 아래처럼 --include "*.html"
같은 옵션을 쓰고 있었다.
aws s3 sync out/ s3://... \
--include "*.html" \
--cache-control "no-cache, no-store, must-revalidate"
문제는, --include
옵션이 있다고 해서 해당 파일만 cache-control이 설정되는 게 아니라는 점이다.
정확히는:
-include
는 대상 파일을 필터링하는 역할만 하고그래서 의도치 않게 .js
나 이미지 파일에도 no-cache
가 적용되었거나, 반대로 .html
에 immutable
이 적용되어 있었을 수도 있다.
📌 참고 레퍼런스:
캐시 문제를 사용자 단에서 최대한 자동으로 해결하기 위해, 서비스워커를 직접 구현해보기로 했다. 기존에는 서비스워커가 존재하지 않았기 때문에, 이슈가 발생했을 때 사용자가 강제 새로고침을 하지 않으면 캐싱된 오래된 리소스를 계속 불러오는 문제가 있었다.
이번 이슈를 해결하기 위한 서비스워커는 다음과 같은 목적을 갖는다:
설치 시 사전 캐싱 (install
)
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_ASSETS))
);
self.skipWaiting();
});
배포 시 정의된 정적 파일들을 미리 캐싱하여, 앱 로딩 시 일부 파일들이 빠르게 불러올 수 있도록 하고 skipWaiting()
을 통해 새 버전이 즉시 활성화될 수 있도록 했다.
이전 버전 캐시 삭제 (activate
)
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) =>
Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) return caches.delete(cacheName);
})
)
)
);
self.clients.claim();
});
새로운 캐시 이름이 등록되면, 이전 버전의 캐시를 자동으로 제거함으로써 충돌을 방지했다.
네트워크-우선 or 캐시-우선 전략 (fetch
)
_next/static
, .js
등은 네트워크 우선이는 최신 JavaScript 코드가 반드시 반영되도록 하기 위해서다.
스크립트 로딩 실패 시 자동 캐시 삭제
HTML 내에 <script>
로 다음과 같은 처리를 추가했다:
window.addEventListener('error', function(event) {
if (event.target?.src?.includes('_next/static')) {
window.clearCaches();
}
}, true);
Next.js의 정적 JS 파일 로딩이 실패했을 경우 자동으로 캐시를 삭제하고 새로고침을 유도한다.
수동 캐시 정리용 메시지 이벤트 (message
)
self.addEventListener('message', (event) => {
if (event.data?.type === 'CLEAR_CACHES') {
caches.keys().then((keys) => Promise.all(keys.map(caches.delete)));
}
});
앱 내에서 serviceWorker.controller.postMessage({ type: 'CLEAR_CACHES' })
로 호출하여 명시적으로 캐시 정리를 요청할 수 있다.
URL 파라미터 or 로컬 스토리지를 이용한 캐시 무효화
앱이 열릴 때 다음과 같이 처리했다:
const forceRefresh = urlParams.get('force_refresh') || localStorage.getItem('refreshCache');
URL에 ?force_refresh=true
가 붙어있거나, 로컬 스토리지에 특정 플래그가 있을 경우 자동으로 캐시와 쿠키를 삭제하고 새로고침을 수행한다.
그래서 팀원과 논의한 결과, 서비스워커만으로는 완전한 해결이 어렵다고 판단하여 앱 단에서도 웹뷰 초기 로딩 시 캐시 삭제 처리를 추가로 구현했다.
팀원은 앱단에서 웹뷰를 초기화할 때 캐시를 삭제하는 방식으로 대응했다.
이는 대부분의 이슈를 해결할 수 있었고, 서비스 워커 방식과 병행하면서 흰 화면 문제는 거의 사라졌다. 하지만 캐싱에 대한 이점을 상쇄하는 해결방법이기 때문에 지양해야하는 해결법이다.
이번 경험을 통해,
서비스 워커나 앱단 캐시 삭제는 보완책일 뿐, 근본적으로는 HTTP 캐시 설정과 빌드 방식을 꼼꼼히 점검하는 게 정답이었다.
appDir
또는 serverActions
를 사용해 더 동적인 빌드 구조 도입이번 경험을 통해 배포 환경과 캐시 정책의 디테일이 얼마나 중요한지 깨달았다.
단순히 "잘 되겠지"라는 안일한 배포보다는, 작은 설정 하나에도 정확한 이해와 실험이 필요하다는 걸 다시금 확인했다.
누군가 비슷한 문제를 겪는다면 이 글이 도움이 되길 바란다.