μ¬μ©μμ μμΉλ₯Ό κΈ°λ° μΌλ‘ μ£Όλ³ μ¬μ©μμ μν΅ ν μ μλ€λ νΉμ§μ κ°μ§ μΊ ννμ λ§λ€λ©΄μ,
μ£Όλ³ μ¬μ©μμ μν΅μ μν΄μλ μ¬μ©μκ° μΉ μ±μ μ μν΄ μμ§ μλλΌλ
λ©μμ§μ λν μλ¦Όμ λ°μ μ μμ΄μΌ μνν μν΅μ΄ κ°λ₯νλ€κ³ μκ°μ΄ λ€μλ€.
κ·Έλμ μ€νλΌμΈ μμλ νΈμ μλ¦Όμ λ°μ μ μμΌλ©΄ μ’κ² λ€κ³ μκ°νλ€.
λΏλ§ μλλΌ, νΈμ μλ¦Ό κΈ°λ₯μ νμ©νκ² λλ©΄ μΆν μ
λ°μ΄νΈλ μ΄λ²€νΈ λ±μ μμμ μ¦μ μ λ¬νμ¬
μ¬μ©μμκ² μ¬μ©μ μ λνκ³ νΈμμ±μ ν₯μν μ μμ κ±°λΌκ³ μκ°λμλ€.
PWA μμ νΈμ μλ¦Όμ μ¬μ©νλ €λ©΄ μλΉμ€ μ컀( Service Worker )λ₯Ό λ±λ‘ν΄μΌ νλ€. μλΉμ€ μ컀λ₯Ό λ±λ‘νλ©΄ λ°±κ·ΈλΌμ΄λμμ μ€νλλ©° νΈμ λ©μμ§λ₯Ό μ²λ¦¬ν μ μλ€. μ€νλΌμΈ νκ²½μμλ μ±μ΄ λμν μ μλλ‘ μ€μ νκΈ° μν΄μ next-pwa λΌμ΄λΈλ¬λ¦¬
λ₯Ό νμ©νλ€.
const registerServiceWorker = async () => {
try {
const registration =
await navigator.serviceWorker.register('/service-worker.js');
console.log('μλΉμ€ μ컀 λ±λ‘ μ±κ³΅:', registration);
} catch (error) {
console.error('μλΉμ€ μ컀 λ±λ‘ μ€ν¨:', error);
}
};
μλΉμ€ μ컀λ λ°±κ·ΈλΌμ΄λμμ μ€νλλ©° νΈμ λ©μμ§λ₯Ό μ²λ¦¬νλ€. μλΉμ€ μ컀λ₯Ό λ±λ‘ν΄μ νΈμ μλ¦Όμ λ°μ μ€λΉλ₯Ό νλ€.
μ¬μ©μμκ² νΈμ μλ¦Ό κΆνμ μμ²νλ€.
// app/layout
const requestPushPermission = async () => {
askPushNotification();
};
// hooks/usePushNotification
import { usePwaStore } from '@/stores/pwaState';
import { userStore } from '@/stores/userState';
import { isPwa } from '@/utils/isPwa';
import { useCallback } from 'react';
export const usePushNotification = () => {
// 첫 λ°©λ¬ΈμΈμ§λ₯Ό νμΈνλ state
const { isVisited } = userStore();
const { setIsPwaOpen, setClicked } = usePwaStore();
const denyPermission = () => {
setIsPwaOpen(false);
};
const askPushNotification = useCallback(async () => {
// λͺ¨λ¬ νμ
μ notiλ‘ μ€μ
setClicked('noti');
// PWA νκ²½μ΄κ³ 첫 λ°©λ¬Έμ΄λΌλ©΄
if (isPwa() && !isVisited) {
// 'noti-default': μλ¦Ό κΆν μ€μ μ 묻λ λͺ¨λ¬μ λμ΄λ€.
setIsPwaOpen(true, 'noti-default');
}
}, [isVisited]);
// μλ¦Ό κΆν μ€μ λͺ¨λ¬μμ μΉμΈμ λλ₯Ό κ²½μ° μ€νλ ν¨μ
const checkNotificationPermission = useCallback(async () => {
setClicked('noti');
// κΆνμ΄ 'default' λ‘ μ€μ λμ΄ μμ κ²½μ°
if (Notification.permission === 'default') {
// κΆν μΉμΈ μμ²μ νλ€.
const permission = await Notification.requestPermission();
// μΉμΈλ κ²½μ° λͺ¨λ¬ λ«κΈ°
if (permission === 'granted') {
setIsPwaOpen(false);
}
} else {
// κΆν μ€μ μ΄ λΆκ°λ₯ ν κ²½μ° 'μ§μ μ€μ 'μ λν μλ΄ λͺ¨λ¬ λμ°κΈ°
setIsPwaOpen(false);
setIsPwaOpen(true, 'noti-unsupported');
}
}, [isVisited]);
return {
denyPermission,
askPushNotification,
checkNotificationPermission,
};
};
νΈμ μλ¦Όμ λ°κΈ° μν΄μλ λΈλΌμ°μ μ pushManager
λ₯Ό μ¬μ©νμ¬ μλ²μ νΈμ ꡬλ
μ 보λ₯Ό μ μ₯ν΄μΌνλ€.
// utils/registerPushNotification.ts
import { api } from '@utils/axios';
import { toast } from 'react-toastify';
const registerPushNotification = async () => {
if (!('Notification' in window) || !('serviceWorker' in navigator)) {
toast.warn('π νΈμ μλ¦Όμ΄ μ§μλμ§ μλ νκ²½μ
λλ€');
return;
}
if ('Notification' in window && 'serviceWorker' in navigator) {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const registration = await navigator.serviceWorker.ready;
const pushSubscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlB64ToUint8Array(
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY
),
});
try {
const res = await api.post('/user/subscribe', {
endpoint: pushSubscription.endpoint,
expirationTime: pushSubscription.expirationTime,
keys: {
p256dh: arrayBufferToBase64(pushSubscription.getKey('p256dh')),
auth: arrayBufferToBase64(pushSubscription.getKey('auth')),
},
});
} catch (error) {
console.error(error);
}
}
}
};
const arrayBufferToBase64 = (buffer: ArrayBuffer | null): string => {
if (!buffer) {
throw new Error('ArrayBuffer is null or undefined');
}
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
};
const urlB64ToUint8Array = (base64String: string | undefined): Uint8Array => {
if (!base64String) {
throw new Error('Base64 string is required');
}
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; i++) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
export default registerPushNotification;
μ΄μ service-worker.js
μμ νΈμ μ΄λ²€νΈλ₯Ό μμ νμ¬ μλ¦Όμ νμν΄λ³΄μ !
// 'push' μ΄λ²€νΈ μ²λ¦¬νκΈ°
self.addEventListener('push', (event) => {
// data JSON ν λ³ν
const data = JSON.parse(event.data.text());
// νΈμ μλ¦Ό νμ΄ν λ½κΈ°
const title = data.title;
// νΈμ μλ¦Ό λ΄μ©, μμ΄μ½, λ±μ§ λ± μ€μ
const options = {
body: data.body,
icon: './images/maskable_icon_x192.png',
badge: 'images/maskable_icon_x128.png',
data: data.roomId,
};
});
// 'push μλ¦Ό ν΄λ¦' μ μ΄λ²€νΈ μ²λ¦¬νκΈ°
self.addEventListener('notificationclick', (event) => {
// νΈμ μλ¦Ό λ«κΈ°
event.notification.close();
const roomId = event.notification.data;
event.waitUntil(
// ν΄λΉνλ μ°½μ΄ μλμ§ νμΈ
self.clients.matchAll({ type: 'window' }).then((clients) => {
const client = clients.find((client) =>
client.url.includes('campingping.com/list')
);
// μλ€λ©΄ ν΄λΉ μ°½μΌλ‘ ν¬μ»€μ€ μ΄λνκ³ μ±ν
μ°½ μ΄μ΄μ£ΌκΈ°
if (client) {
client.postMessage({ type: 'OPEN_CHAT_MODAL', roomId });
client.focus();
// μλ€λ©΄ μλ‘ λμμ£ΌκΈ°
} else {
self.clients.openWindow(`/list`).then((newClient) => {
if (newClient)
newClient.postMessage({ type: 'OPEN_CHAT_MODAL', roomId });
});
}
})
);
});
μ΄λ―Έ PWAκ° λ°±κ·ΈλΌμ΄λμ μ΄λ €μμμ κ²½μ°μλ ν΄λΉ μ±ν
λ°©μΌλ‘ μ΄λκΉμ§ μ λλλ°,
μμ ν μ±μ΄ κΊΌμ Έμμλ κ²½μ°μλ μ±μ΄ μΌμ§λ λ° κΉμ§λ§ μ§νμ΄ λλ€... π
μ±ν
λ°©μ μ΄μ΄μ£Όκ³ μΆμλλ° ν΄λΉ λΆλΆμ΄ ꡬνμ΄ μ μλμ΄μ... κ°μ ν΄λκ° μμ μ΄λ€.
νΈμ μλ¦Όμ ꡬννλ©΄μ μ μΌ νλ€μλ λΆλΆμ λ°λ‘... iOS
μ Android
μλ€.........!
λλ κ°μΈμ μΌλ‘ λͺ¨λ κΈ°κΈ°λ₯Ό λ€ iOS
λ₯Ό μ¬μ©μ€μ΄λΌμ.. μ¬μ€ PWA μ€μΉλΆν° λκ΄μ΄ μμλ€.
λ§₯λΆμμλ μ€μΉκ° μ λλλ° μμ΄ν°μμλ μ€μΉκ° μλμλ κ²!
κ·Έλμ μλμ° ν
μ€νΈλ ν΄λ΄€λλ° μλμ°λ μ€μΉκ° μλλ κ²!!!
ꡬκΈλ§μ ν΅ν΄ iOS
λ μ΄λ²€νΈ λ―Έμ§μμΌλ‘ 'ν νλ©΄μ μΆκ°' λ²νΌμ ν΅ν΄ κ°λ₯νλ€λ κ²μ νμΈνκ³ ...
κ·Έκ²λ 'safari'λ₯Ό ν΅ν΄μλ§...
μ.... μ μ§ κ³ λμ΄ μμ κ±° κ°μ...! λΌλ μκ°μ΄ λ€μλ€.
κ·Έλ κ² νΈμ μλ¦Όμ ꡬννλλ° μ무리ν΄λ μ±ν
λ μ΄νμ μ μ λ΄λ €λμλ€κ° μΌλ©΄ μ μμ€λ κ±° κ°κ³ ,
νΈμ μλ¦Όλ μ μμ€λ κ±° κ°κ³ , κ·Έλ°λ° λ§₯λΆμμλ λ μ±ν
μ μ λκ³ νΈμ μλ¦Όμ μ§μμνλ€κ³ νκ³ ...
μ§μλ μ λ§€νκ³ μ΄μ λ₯Ό μ μ μλ 'λ΄ μμ΄ν° μ΄μν΄ π' μν©μ΄ κ³μλμλ€.
μ¬μ§μ΄ μΉκ΅¬λ λλν μμμ ν
μ€νΈ νλλ° μΉκ΅¬ μμ΄ν°μ λκ³ λ΄ μμ΄ν°μ μλλ κ²½μ°λ μμλ€...
κ·Έλ κ² μ€νΈλ μ€ λ°λ€κ° μ€λ§... νλ λ§μμΌλ‘ μλμ° ν
μ€νΈλ₯Ό νλλ° μλμ°λ νΈμμλ¦Όλ μ±ν
λ μλλ€...
κ·Έλμ μ€λ§... νλ λ§μμΌλ‘ κ°μ‘±λ€νν
μμ°λ κ°€λμκ° μλ μ§ λ¬Όμ΄μ λ°μμλλ° νΈμ μλ¦Όμ΄ λ무 μ μ€λκ±° π¦
κ·Έλμ μ°Ύμ보λ...
π iOS νΈμ μλ¦Ό ꡬν λ¬Έμ μν©
- λ°±κ·ΈλΌμ΄λμμ WebSocket μ μ§κ° μλ¨: μΌμ μκ° ν μ°κ²° ν΄μ
- Wi-Fi -> LTE μ ν μ WebSocket λκΉ
- νΈμ μλ¦Όμ΄ 'μ¦μ' λμ°© νμ§ μμ μ μμ: μλ²μμ μΆκ° μ€μ νμ
- νΈμ ꡬλ μλ λ§λ£ κ°λ₯μ±μ΄ μμ΄μ 'μ¬λ±λ‘' νμ
- λ°°ν°λ¦¬ μ μ½ λͺ¨λμμ νΈμ μλ¦Ό μ°¨λ¨ κ°λ₯μ± λλ¬Έμ μλ²μμ μΆκ° μ€μ νμ
??????????????????????????????????????????????????????????????
κ΅μ₯ν κΉλ€λ‘μ΄ μΉκ΅¬μλ κ²μ΄λ€ !
iOS
μμλ μ λμνκΈΈ λ°λΌλ©° λͺκ°μ§ μ°Ύμμ λ μΆκ°ν 보μ μ½λλ€...
export const socket = io(CHAT_URL, {
withCredentials: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 2000,
timeout: 5000,
transports: ['websocket', 'polling'],
});
const initPushNotification = async () => {
while (attempt < MAX_RETRIES) {
try {
await registerPushNotification();
console.log(`Push Notification λ±λ‘ μ±κ³΅ (μλ ${attempt + 1})`);
return;
} catch (error) {
attempt += 1;
console.error(
`Push Notification λ±λ‘ μ€ν¨ (μλ ${attempt}):`,
error
);
if (attempt >= MAX_RETRIES) {
toast.error('νΈμ μλ¦Ό λ±λ‘μ μ€ν¨νμ΅λλ€.');
} else {
await new Promise((resolve) => setTimeout(resolve, 3000 * attempt));
}
}
}
};
initPushNotification();
}, [userState]);
κ·Έλμ μΌλ¨ μ¬νκ³ νλ€μλ λ§μκ³Ό ν¨κ» μ 리νλ
iOS
μ Android
μ μ£Όμ μ°¨μ΄μ κΈ°λ₯ | Android( Chrome, Edge λ± ) | iOS( Safari ) |
---|---|---|
PWA μ€μΉ | μλ μ€μΉ κ°λ₯( beforeinstallprompt μ§μ) | μ§μ ν νλ©΄ μΆκ° νμ( μλ μ λ X ) |
λ°±κ·ΈλΌμ΄λ λμ | Background Sync API μ§μ | λ°±κ·ΈλΌμ΄λμμ μμ μ ν |
λ‘컬 μ μ₯μ | Persistent Storage API μ§μ | μ μ₯μ μλ μμ κ°λ₯ |
μ€νλΌμΈ λͺ¨λ | Cache API νμ© κ°λ₯ | μ€νλΌμΈ μΊμ± λΆμμ |
λΈλΌμ°μ λ³λ‘ μμ€ν
λ³λ‘ λμνλ λ°©μλ μ§μνλ μ΄λ²€νΈλ λ°©μλ λ€λ₯Έ μ μ΄ μκ°λ³΄λ€ λ§μλ€.
μλ‘μ΄ κ²λ€μ μλνλ€ λ³΄λ μ ν μμνμ§ λͺ»ν κ³³μμ λ¬Έμ λ€μ΄ λ°μνλλ°,
λ¬Όλ‘ μ λλ‘ κ΅¬νμ νμ§ λͺ»ν λΆλΆλ€μ΄λ μ΄ν΄λ₯Ό λͺ»ν λΆλΆλ μμμ§λ§
ꡬνμ ν΄λκ³ λ μ§μνμ§ μλλ€λ κ²μ λͺ°λΌμ νμ°Έ μλ±ν κ²λ§ ꡬκΈλ§νλ©΄μ
μ κ°μ μ½λ κ°μλ° λλ§ μλλκ±°μ§.. νλ μκ°λ 보λλ€.
λΈλΌμ°μ κ°μ μ°¨μ΄λ μμ€ν κ°μ μ£Όμ μ°¨μ΄μ κ°μ κ²λ€λ κΌ κ³΅λΆν΄λ΄μΌκ² λ€.
κ·Έλ¦¬κ³ iOS
μμ μΆκ°μ μΌλ‘ ν΄κ²°ν μ μλ λ°©λ²μ΄ μλ μ§ μ’ λ μ°Ύμλ³Ό μμ μ΄λ€ !