현장에서 예약 현황을 확인해야 하는 운영자는 대부분 모바일 브라우저를 씁니다. 그런데 네트워크가 약한 체육관이나 지하층에서는 이미지가 느리게 뜨고, 파일 업로드도 꼭 실패했습니다. 그래서 PWA를 본격적으로 적용하고, S3 업로드와 오프라인 캐싱을 같이 다듬었습니다.
@ducanh2912/next-pwa 플러그인으로 Workbox 기반 캐싱을 설정해 정적 리소스와 이미지 로딩 속도를 안정화했습니다.StaleWhileRevalidate 전략을 선택했습니다.upload.ts에 helper 함수를 모았습니다.// next.config.js
const withPWA = require('@ducanh2912/next-pwa').default({
dest: 'public',
extendDefaultRuntimeCaching: true,
disable: isDev,
workboxOptions: {
skipWaiting: true,
disableDevLogs: true,
exclude: [/api/, /_next/],
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'images',
expiration: { maxEntries: 50, maxAgeSeconds: 86400 },
},
},
],
},
});
앱 아이콘, manifest는 기본적으로 Next.js의 public 디렉터리에 두고, Workbox가 생성한 sw.js를 S3 및 CloudFront 캐시에 배포했습니다.
// src/shared/lib/upload.ts
export const uploadFile = async (file: File | null, folderName: string, fileSize: number) => {
if (!file) {
errorMessage('파일을 선택해주세요.');
return { ok: false };
}
const { url, fields } = await getSignedUrl(file, folderName, fileSize);
const uploadResponse = await uploadFileToS3(url, fields, file);
return { ok: true, url: uploadResponse.url + fields.key };
};
Blob 업로드(uploadBlob)도 같은 형태로 통일해, 캔버스에서 생성한 이미지·PDF도 같은 토스트 메시지를 재사용하도록 했습니다.
서비스 워커에서는 Firebase 메시징과 충돌하지 않도록 별도의 firebase-messaging-sw.js를 두고, PWA용 sw.js는 Workbox가 생성하는 파일을 그대로 사용했습니다. 캐시 스토리지에 남은 이미지 용량이 늘어났을 때는 maxEntries를 줄여 해결했고, 설치 이후 업데이트가 즉시 반영되도록 skipWaiting을 활성화했습니다.
EntityTooLarge가 떨어졌습니다. Presigned POST 정책에 content-length-range를 추가하고, 업로드 전 로컬에서 파일 크기를 검사했습니다.image/heic로 들어와 처리하지 못했습니다. mime 패키지에서 heic을 지원하지 않아 임시로 application/octet-stream으로 업로드하고, 서버에서 이미지 매직으로 변환했습니다. 이 과정에서 Claude에게 HEIC 변환 전략을 비교 설명해 달라고 부탁하며 대안을 검토했습니다.운영자가 캐시 데이터를 기반으로 예약 목록을 확인할 수 있게 되어, 지하층에서도 업무가 끊기지 않는다고 합니다. 이미지 캐시 덕분에 첫 로딩 시간이 평균 1.8초에서 1.1초로 줄었고, 업로드 성공률도 60%대에서 95% 이상으로 올라갔습니다. 이제는 오프라인 상태에서 작성한 데이터를 어떻게 동기화할지 고민하려고 합니다. 여러분은 PWA와 업로드 경험을 어떻게 조율하고 계신가요?