머릿말
이번에는 PWA를 구현하게 되었습니다.
PWA에 대해 간단히 말해보자면, Progressive Web APP입니다.
기본적으로 PWA는 HTML, CSS, JS로 만들어졌지만 네이티브 앱과 유사한 경험을 제공하는 웹 어플리케이션입니다.
왜 React Native나 Flutter가 아닌 PWA인지?
사실 처음에는 RN과 Webview를 사용하여 App store와 구글 플레이 스토어에 앱을 출시해보려고 했습니다.
하지만 개발 전 설계를 하다보니 여러 관문에 부딪혔습니다.
1. App store에 출시하기 위해서는 최소한 소셜 로그인 또는 자체 로그인이 필요한 점
2. 내 애플 계정으로 출시할 경우 만약 내가 퇴사하는 경우 인도하는 부분이 힘든 점
3. 알림 서비스는 굳이 네이티브 앱이 아니여도 PWA도 가능한 점PWA를 구현하게 된 위 3가지 이유가 가장 큰 이유 중 하나입니다.
아래 글에서는 어떻게 PWA를 구현했고, 어떤 문제에 부딪히고 해결했는지에 대해 작성하려고 합니다.
1단계 : next-pwa 설치
pnpm add next-pwa
or
npm install next-pwa저는 NextJS를 활용해 웹페이지를 만들었는데 NextJS는 next-pwa 라이브러리를 통해 간편하게 설정이 가능합니다.
이후, next.config.ts에서 withPWA로 래핑했습니다
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* ... */
};
let config: NextConfig = nextConfig;
// 프로덕션에서만 PWA 활성화
if (process.env.NODE_ENV === "production") {
const withPWA = require("next-pwa")({
dest: "public", // sw.js 출력 경로 (public/)
register: true, // 자동 SW 등록
skipWaiting: true, // 업데이트 시 즉시 활성화
disable: false,
swSrc: "public/sw-custom.js", // 커스텀 SW 소스
// swSrc 사용 시 runtimeCaching 무시됨
});
config = withPWA(nextConfig);
}
export default config;추가 글을 작성할 예정이지만, PWA를 도입하면서 FCM 알림도 구현했습니다.
뒤에서는 왜 firebase-messaging-sw.js가 아닌 sw-custom.js를 설정했는지에 대해 작성합니다.
다시 본론으로 돌아가, 빌드 시 sw-custom.js 파일을 읽어서 Workbox(캐시 매니페스트 등) 코드를 주입합니다.
그 다음, public/sw.js를 생성합니다.
마지막으로, NextJS 앱 로드 시 sw.js를 자동으로 등록합니다.
이 때, sw.js는 자동 산출물이므로 .gitignore에 추가합니다.
2단계 : manifest.json 파일 생성
manifest.json 파일은 추후 모바일 폰 화면에 추가했을 때 보일 기본적인 정보들을 담고 있습니다.
저는 manifest.json을 public 폴더 내에 위치 시키고, app/layout.tsx에서 참조했습니다.
아이콘의 경우 구글에서 icon genertor를 검색해 아무 사이트에서 192x192, 512x512 png를 추출하여 사용했습니다.
<link rel="manifest" href='/manifest.json' />
3단계 : Service Worker (sw-custom.js) 구성
Service Worker란 브라우저가 백그라운드에서 실행하는 JS Worker입니다.
- 일반 스크립트 : 페이지와 같은 스레드에서 실행, 페이지를 닫으면 종료
- Service Worker : 페이지와 분리된 Worker Thread에서 실행, 페이지가 꺼져 있어도 동작함
간단히 말하자면, 웹페이지 뒤에서 돌아가는 보조 스크립트 라고 말할 수 있습니다.
생명 주기 (Lifecycle)
[설치] install
↓
[대기] waiting (기존 SW가 있을 때)
↓
[활성화] activate
↓
[유휴] idle ← 평소 대기 상태
↓
[이벤트 처리] fetch / push / message / ...
↓
다시 idle3-1. install
- 역할 : Service Worker 설치 시 1회 실행합니다.
- 보통 하는 일
- 캐시 생성
self.skipWaiting(): 기존 SW를 빨리 대체해도 된다고 알림
3-2. activate
- 역할 : Service Worker가 활성화될 때 1회 실행합니다.
- 보통 하는 일
clients.claim(): 즉시 모든 열린 탭을 SW가 제어- 이전 캐시 정리 등
3-3. push
- 역할 : push 메시지를 받을 때
- 보통 하는 일
event.data파싱self.registration.showNotification()으로 알림 표시
3-4. notificationclick
- 역할 : 사용자가 알림을 클릭했을 때
- 보통 하는 일
event.notification.close()clients.openWindow(url)등으로 특정 페이지 열기
정리하자면, PWA에서 Service Worker는 오프라인, 푸시, 백그라운드 등을 담당합니다.
프로젝트에서 적용한 방법
public/sw-custom.js
// 1. Workbox 매니페스트 (next-pwa가 빌드 시 주입)
const WB_MANIFEST = self.__WB_MANIFEST;
// 2. install 이벤트
self.addEventListener("install", (event) => {
self.skipWaiting(); // 즉시 활성화
});
// 3. activate 이벤트
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim()); // 모든 탭 즉시 제어
});
// 4. push 이벤트 (FCM)
self.addEventListener("push", (event) => {
// FCM 페이로드 파싱 → showNotification()
});
// 5. notificationclick 이벤트
self.addEventListener("notificationclick", (event) => {
// 알림 클릭 시 Sentry 이슈 페이지로 이동
});그리고 앞서 next.config.ts에서 설정한 swSrc 경로에 public/sw-custom.js을 기입합니다.
빌드 결과 : public/sw.js
pnpm run build 실행 시
- next-pwa가
sw-custom.js를 읽습니다. - Workbox manifest를 주입합니다. (캐시할 파일 목록)
public/sw.js을 생성합니다. (압축/최적화된 최종 파일)
기타 설정
next-pwaregister: true 설정으로 next-pwa가 자동 등록합니다.
// next-pwa가 내부적으로 실행하는 코드 (의사코드)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' });
}- 앱 로드 시 자동 실행합니다.
- 개발 모드에서는 비활성화 (프로덕션만)
skipWaiting() : 업데이트 시 즉시 새 SW 활성화clients.claim() : 기존 탭도 즉시 제어
4단계 : 빌드 및 PWA 설치 확인
이제 PWA 설정이 완료되었습니다. 기타 자세한 내용은 사내 코드라서 밝힐 수는 없으나 구글링 결과 모두 비슷한 코드를 작성하셨음을 알 수 있습니다.
pnpm run build를 실행 후 public/sw.js가 생성되었는지 확인합니다.
그 다음 모바일에서 "홈 화면에 추가" 테스트를 합니다. (참고로 저는 Safari에서 추가했습니다.)
마지막으로 홈 화면에 추가된 PWA를 켜 정상적으로 작동하는지 확인합니다.





사진은 순서대로, 아래와 같습니다.
1. 배포된 웹사이트를 Safari에서 접속합니다.
2. 공유를 클릭하고 더 보기를 누릅니다.
3. 더 보기를 클릭하면, 홈 화면에 추가 버튼이 보이는데 이걸 클릭해서 홈 화면에 추가해줍니다.
4. 홈 화면에 추가된 PWA를 클릭하여 정상적으로 동작하는지 확인합니다.
결론
여기까지 왜 PWA를 사용해야 했는지, 그리고 어떻게 구현했는지에 대해 작성해봤습니다. 사실 PWA를 알게된 건 몇 년 전이지만 직접 구현해보니 여러가지 어려움이 있었습니다. firebase-messaging-sw.js 파일과 sw.js 파일이 충돌하는 문제도 겪어보고 next-pwa의 마지막 배포가 약 4년 전이라서 다른 방안도 찾아보고 많은 고민을 했던 경험이었던 것 같습니다. 생각보다 구현에는 어려움이 없으니 저처럼 굳이 앱으로 출시해야 하지 않아도 되는 개발자분이라면 PWA를 고려해보는 것도 나쁘지 않을 것 같습니다.
다음 글에서는 FCM 알림 구현에 대해 작성하겠습니다.
읽어주셔서 감사합니다.