
PWA: 웹을 앱처럼
PWA의 특징과 구현 방법

PWA의 특징과 구현 방법
공개 API를 운영하다 보면 예상치 못한 대량 요청에 시달릴 수 있다. Rate Limiting과 API Key 관리로 API를 보호하는 방법을 정리했다.

admin/user 두 역할로 시작했는데, 요구사항이 복잡해지면서 RBAC만으로 부족해졌다. ABAC까지 고려한 권한 설계를 정리했다.

서비스가 3개로 늘어나면서 각각 로그인을 구현하는 게 지옥이었다. SSO로 한 번의 인증으로 모든 서비스에 접근하게 만든 이야기.

비밀번호 찾기가 CS의 절반을 차지했는데, Passkey를 도입하니 비밀번호 자체가 필요 없어졌다. 근데 구현이 생각보다 복잡했다.

제 서비스를 만들면서 고민이 하나 있었습니다. 사용자들이 "홈 화면에 추가하고 싶다"는 요청을 많이 했거든요. 그런데 네이티브 앱을 만들려면... 생각만 해도 머리가 아팠습니다.
"웹으로 만들었는데, 굳이 네이티브 앱을 또 만들어야 하나?" 하는 생각이 들었습니다. 그러다가 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를 만들고 실제로 서비스에 적용해보니, 장단점이 명확했습니다.
PWA는 Service Worker와 Web App Manifest를 사용해서 웹을 앱처럼 만드는 기술입니다. 오프라인 지원, 홈 화면 추가, 푸시 알림 같은 앱 기능을 웹에서 구현할 수 있고, 앱스토어 없이 배포할 수 있어서 개발과 업데이트가 쉽습니다. 네이티브 앱만큼 강력하진 않지만, 대부분의 웹 서비스에는 충분하고, 무엇보다 만들기가 훨씬 쉽습니다.