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

PWA의 특징과 구현 방법
REST는 왜 지금도 지배적인가, GraphQL은 어떤 문제를 해결하는가, gRPC는 언제 진짜 빛나는가. 세 프로토콜의 차이와 선택 기준을 실전 코드와 함께 정리했다.

Kubernetes는 처음엔 용어만 봐도 압도된다. Pod, ReplicaSet, Deployment, Service, Ingress가 각각 무엇이고 어떻게 연결되는지, ConfigMap과 Secret까지 실전 YAML과 함께 한 번에 정리한다.

단어와 문장을 숫자 벡터로 바꾸면 '의미'를 수학으로 계산할 수 있다. 코사인 유사도, ANN 알고리즘, OpenAI 임베딩 API까지 원리부터 실전까지 한번에 정리했다.

API는 한번 공개하면 마음대로 바꾸지 못한다. 클라이언트를 깨트리지 않으면서 API를 진화시키는 버저닝 전략 4가지를 비교하고, GitHub·Stripe·Twilio의 실제 선택을 분석한다.

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