PWA: 웹을 앱처럼
앱스토어가 싫었습니다
제 서비스를 만들면서 고민이 하나 있었습니다. 사용자들이 "홈 화면에 추가하고 싶다"는 요청을 많이 했거든요. 그런데 네이티브 앱을 만들려면... 생각만 해도 머리가 아팠습니다.
- iOS 앱: Swift 배워야 함
- Android 앱: Kotlin 배워야 함
- 앱스토어 심사: 며칠씩 걸림
- 업데이트 배포: 또 심사 기다려야 함
- 수수료: 매출의 30%
"웹으로 만들었는데, 굳이 네이티브 앱을 또 만들어야 하나?" 하는 생각이 들었습니다. 그러다가 PWA(Progressive Web App)라는 걸 알게 됐습니다. 웹을 앱처럼 쓸 수 있게 만드는 기술이라고 하더라고요.
처음엔 반신반의했습니다. "웹이 어떻게 앱처럼 되겠어?" 하지만 직접 만들어보니... 놀라웠습니다.
첫 번째 도전 - 오프라인에서도 작동하게
PWA를 만들면서 가장 먼저 부딪힌 게 오프라인 지원이었습니다. 일반 웹사이트는 인터넷이 끊기면 그냥 "인터넷 연결 없음" 공룡 게임만 나오잖아요. 근데 PWA는 오프라인에서도 작동해야 한다고 하더라고요.
이게 어떻게 가능한가 했더니, Service Worker라는 게 핵심이었습니다. 쉽게 말하면, 브라우저와 서버 사이에서 중간 다리 역할을 하는 JavaScript 파일입니다. 이게 네트워크 요청을 가로채서, 캐시된 데이터를 먼저 확인하고, 없으면 서버에 요청하는 식으로 작동합니다.
처음 Service Worker를 등록할 때는 이렇게 했습니다:
// main.js - Service Worker 등록
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker 등록 성공:', registration);
})
.catch(error => {
console.log('Service Worker 등록 실패:', error);
});
});
}
그리고 실제 Service Worker 파일은 이렇게 만들었습니다:
// sw.js - Service Worker 구현
const CACHE_NAME = 'my-pwa-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.png'
];
// 설치 단계: 필요한 파일들을 캐시에 저장
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('캐시 열림');
return cache.addAll(urlsToCache);
})
);
});
// 네트워크 요청 가로채기
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// 캐시에 있으면 캐시 데이터 반환
if (response) {
return response;
}
// 없으면 네트워크에서 가져오기
return fetch(event.request);
})
);
});
이렇게 만들고 나니, 인터넷을 끊어도 제 웹사이트가 작동하더라고요! 처음 봤을 때 진짜 신기했습니다. "와, 이게 되네?"
홈 화면에 추가 - 진짜 앱처럼
Service Worker로 오프라인 지원을 만들고 나니, 다음 단계는 홈 화면에 추가였습니다. 사용자가 브라우저 메뉴에서 "홈 화면에 추가"를 누르면, 진짜 앱처럼 아이콘이 생기는 거죠.
이걸 위해서는 Web App Manifest 파일이 필요했습니다. JSON 파일 하나로 앱의 이름, 아이콘, 색상 등을 정의하는 겁니다:
{
"name": "내 서비스",
"short_name": "서비스",
"description": "PWA로 만든 내 서비스입니다",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
그리고 HTML에서 이 manifest 파일을 연결합니다:
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2196F3">
이렇게 하고 나니, 모바일에서 제 사이트를 열면 "홈 화면에 추가" 팝업이 뜨더라고요. 추가하면 진짜 앱처럼 아이콘이 생기고, 클릭하면 브라우저 주소창 없이 전체 화면으로 열립니다.
"display": "standalone" 이 부분이 핵심이었습니다. 이게 브라우저 UI를 숨기고 앱처럼 보이게 만드는 거였어요.
푸시 알림 - 사용자와 소통하기
PWA를 만들면서 가장 놀라웠던 건 푸시 알림이었습니다. 웹사이트에서 푸시 알림을 보낼 수 있다니! 이것도 Service Worker 덕분이었습니다.
먼저 사용자에게 알림 권한을 요청합니다:
// 알림 권한 요청
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
console.log('알림 권한 허용됨');
// 푸시 구독 등록
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'YOUR_PUBLIC_KEY'
});
// 서버에 구독 정보 전송
await sendSubscriptionToServer(subscription);
}
}
그리고 Service Worker에서 푸시 메시지를 받아서 알림을 표시합니다:
// sw.js - 푸시 알림 처리
self.addEventListener('push', (event) => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
vibrate: [200, 100, 200],
data: {
url: data.url
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// 알림 클릭 시 처리
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
이렇게 만들고 나니, 사용자가 제 사이트를 안 보고 있을 때도 알림을 보낼 수 있게 됐습니다. 진짜 네이티브 앱처럼요!
캐싱 전략 - 빠르게 로딩하기
Service Worker를 쓰면서 배운 중요한 개념이 캐싱 전략이었습니다. 모든 걸 캐시하면 오프라인에서는 좋지만, 업데이트가 안 되는 문제가 있고, 아무것도 캐시 안 하면 오프라인에서 못 쓰니까요.
제가 쓴 전략은 이렇습니다:
1. Cache First (캐시 우선): 이미지, CSS, JS 같은 정적 파일
self.addEventListener('fetch', (event) => {
if (event.request.url.match(/\.(jpg|png|css|js)$/)) {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
}
});
2. Network First (네트워크 우선): API 데이터 같은 동적 콘텐츠
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
// 네트워크 응답을 캐시에 저장
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// 네트워크 실패 시 캐시 사용
return caches.match(event.request);
})
);
}
});
이렇게 하니까, 정적 파일은 빠르게 로드되고, API 데이터는 최신 상태를 유지하면서도 오프라인에서 마지막 데이터를 볼 수 있게 됐습니다.
실제로 써보니 느낀 점
PWA를 만들고 실제로 서비스에 적용해보니, 장단점이 명확했습니다.
장점
1. 배포가 엄청 쉽습니다
- 웹 서버에 올리면 끝
- 앱스토어 심사 필요 없음
- 업데이트도 즉시 반영
2. 개발 비용이 낮습니다
- 웹 기술만 알면 됨
- iOS/Android 따로 안 만들어도 됨
- 하나의 코드베이스로 모든 플랫폼 지원
3. 사용자 경험이 좋습니다
- 오프라인에서도 작동
- 빠른 로딩 속도
- 앱처럼 홈 화면에 추가 가능
단점
1. 네이티브 기능 제한
- 카메라, 블루투스 같은 하드웨어 접근이 제한적
- 네이티브 앱만큼 자유롭지 않음
2. iOS 지원이 아쉽습니다
- Safari의 PWA 지원이 Android보다 부족
- 푸시 알림이 iOS에서 제대로 안 됨 (최근에야 지원 시작)
3. 앱스토어 노출 안 됨
- 사용자가 앱스토어에서 검색해서 찾을 수 없음
- 웹사이트를 먼저 방문해야 설치 가능
한 줄 요약
PWA는 Service Worker와 Web App Manifest를 사용해서 웹을 앱처럼 만드는 기술입니다. 오프라인 지원, 홈 화면 추가, 푸시 알림 같은 앱 기능을 웹에서 구현할 수 있고, 앱스토어 없이 배포할 수 있어서 개발과 업데이트가 쉽습니다. 네이티브 앱만큼 강력하진 않지만, 대부분의 웹 서비스에는 충분하고, 무엇보다 만들기가 훨씬 쉽습니다.