-
Notifications
You must be signed in to change notification settings - Fork 3
๐ WebPush ๊ตฌํ
๋ถ์ผ | ์์ฑ์ | ์์ฑ์ผ |
---|---|---|
BE | ๋ฌธ์ค๋ฏผ, ๊น์ฑํ | 24๋ 11์ 25์ผ |
์น ํธ์ ์๋ฆผ์ ์ฌ์ฉ์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฉํ์ง ์๋ ์ํ์์๋ ์ค์๊ฐ์ผ๋ก ์๋ฆผ์ ๋ฐ์ ์ ์๊ฒ ํด์ฃผ๋ ๊ธฐ์ ์ ๋๋ค. ์ด ๋ฌธ์์์๋ ์น ํธ์ ์๋ฆผ์ ๊ตฌํํ๊ธฐ ์ํ ์ ์ฒด์ ์ธ ๊ตฌ์กฐ์ ์๋ฒ ๋ฐ ํด๋ผ์ด์ธํธ ์ธก ๊ตฌํ ๋ฐฉ๋ฒ์ ์ค๋ช ํฉ๋๋ค.
- ์ฌ์ฉ์: ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ํ์ฌ ํธ์ ์๋ฆผ ์์ ์ ๋์ํฉ๋๋ค.
- ํด๋ผ์ด์ธํธ: ์๋น์ค ์์ปค๋ฅผ ๋ฑ๋กํ๊ณ ํธ์ ๊ตฌ๋ ์ ์์ฑํ์ฌ ์๋ฒ์ ์ ์กํฉ๋๋ค.
-
์๋ฒ: ๊ตฌ๋
์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ , ์๋ฆผ ์กฐ๊ฑด์ด ๋ง์กฑ๋๋ฉด
web-push
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํธ์ ๋ฉ์์ง๋ฅผ ์ ์กํฉ๋๋ค. - ํธ์ ์๋น์ค: ๋ธ๋ผ์ฐ์ ๊ณต๊ธ์๊ฐ ์ ๊ณตํ๋ ํธ์ ์๋น์ค๋ก, ์๋ฒ๋ก๋ถํฐ ๋ฐ์ ๋ฉ์์ง๋ฅผ ์ฌ์ฉ์์๊ฒ ์ ๋ฌํฉ๋๋ค.
- ์๋น์ค ์์ปค: ํด๋ผ์ด์ธํธ ์ธก์์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋์ํ๋ฉฐ, ํธ์ ๋ฉ์์ง๋ฅผ ์์ ํ๊ณ ์๋ฆผ์ ํ์ํฉ๋๋ค.
์๋ฒ ์ธก์์๋ ์น ํธ์ ์๋ฆผ์ ๊ด๋ฆฌํ๊ณ ์ ์กํ๋ ์ญํ ์ ๋ด๋นํฉ๋๋ค.
- PushSubscription ์ํฐํฐ: ์ฌ์ฉ์์ ํธ์ ๊ตฌ๋ ์ ๋ณด๋ฅผ ์ ์ฅํฉ๋๋ค.
-
PushService:
web-push
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํธ์ ์๋ฆผ์ ์ ์กํฉ๋๋ค. - PushController: ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๊ตฌ๋ ์ ๋ณด๋ฅผ ๋ฐ์ ์ ์ฅํ๊ณ ๊ด๋ฆฌํฉ๋๋ค.
- AlarmService: ์๋ฆผ ์กฐ๊ฑด์ ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฒ์ฌํ๊ณ , ์กฐ๊ฑด์ด ๋ง์กฑ๋๋ฉด ํธ์ ์๋ฆผ์ ์ ์กํฉ๋๋ค.
-
์ค์ผ์ค๋ฌ:
@nestjs/schedule
ํจํค์ง๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฆผ ์กฐ๊ฑด์ ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฒ์ฌํฉ๋๋ค.
-
VAPID ํค ์์ฑ ๋ฐ ์ค์
webPush.setVapidDetails( 'mailto:[email protected]', process.env.VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY!, );
- ์ฌ๊ธฐ์ VAPID๋ ์๋ฒ์์ ์ ์ํ๋ ํค๋ก์จ [RFC 8292](https://datatracker.ietf.org/doc/html/rfc8292) ์ ์์ ๋ง์ถฐ์ ธ ์๋ค.
- VAPID ํค์ ๊ฒฝ์ฐ
web-push
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด ์ฝ๊ฒ ์์ฑํ ์ ์๋ค.
const webPush = require('web-push'); const vapidKeys = webPush.generateVAPIDKeys(); console.log('Public Key:', vapidKeys.publicKey); console.log('Private Key:', vapidKeys.privateKey); /** Public Key: BNo.... Private Key: OqOn... */
-
PushSubscription ์ํฐํฐ ์ ์
@Entity() export class PushSubscription { @PrimaryGeneratedColumn() id: number; @ManyToOne(() => User, (user) => user.subscriptions, { onDelete: 'CASCADE' }) user: User; @Column({ type: 'text' }) endpoint: string; @Column({ type: 'text' }) p256dh: string; @Column({ type: 'text' }) auth: string; }
-
PushService ๊ตฌํ
@Injectable() export class PushService { constructor( @InjectRepository(PushSubscription) private readonly subscriptionRepository: Repository<PushSubscription>, private readonly dataSource: DataSource, ) { // VAPID ํค ์ค์ } async createSubscription(userId: number, subscriptionData: any): Promise<PushSubscription> { return await this.dataSource.transaction(async (manager) => { const user = new User(); user.id = userId; const newSubscription = manager.create(PushSubscription, { user: user, endpoint: subscriptionData.endpoint, p256dh: subscriptionData.keys.p256dh, auth: subscriptionData.keys.auth, }); return await manager.save(newSubscription); }); } async sendPushNotification(subscription: PushSubscription, payload: object): Promise<void> { // ํธ์ ์๋ฆผ ์ ์ก ๋ก์ง } }
-
PushController ๊ตฌํ
@Controller('push') export class PushController { constructor(private readonly pushService: PushService) {} @UseGuards(AuthGuard('jwt')) @Post('subscribe') async subscribe(@Body() subscriptionData: any, @Req() request: Request) { const userId = (request.user as any).id; const newSubscription = await this.pushService.createSubscription(userId, subscriptionData); return { message: 'Subscription saved.', subscriptionId: newSubscription.id }; } }
-
AlarmService์์ ์๋ฆผ ์กฐ๊ฑด ๊ฒ์ฌ ๋ฐ ์ ์ก
@Injectable() export class AlarmService { // ... @Cron('*/5 * * * * *') // ๋งค 5์ด๋ง๋ค ์คํ (ํ ์คํธ์ฉ) async checkAlarms() { const alarms = await this.alarmRepository.find({ relations: ['user', 'stock', 'user.subscriptions'], }); for (const alarm of alarms) { // ์๋ฆผ ์กฐ๊ฑด ๊ฒ์ฌ ๋ก์ง if (shouldNotify) { await this.sendPushNotification(alarm); // ์๋ ์ญ์ ๋๋ ์ํ ์ ๋ฐ์ดํธ } } } private async sendPushNotification(alarm: Alarm): Promise<void> { const payload = { title: '์ฃผ์ ์๋ฆผ', body: `${alarm.stock.name}: ์กฐ๊ฑด์ด ๋ง์กฑ๋์์ต๋๋ค.`, }; for (const subscription of alarm.user.subscriptions) { await this.pushService.sendPushNotification(subscription, payload); } } }
ํด๋ผ์ด์ธํธ ์ธก์์๋ ํธ์ ์๋ฆผ์ ์์ ํ๊ธฐ ์ํ ์ค์ ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๊ธฐ ์ํ ์ค์น ์๋ด ๋ฑ์ ๊ตฌํํฉ๋๋ค.
-
์๋น์ค ์์ปค ๋ฑ๋ก
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') .then((registration) => { console.log('Service Worker registered:', registration); }) .catch((error) => { console.error('Service Worker registration failed:', error); }); }
-
ํธ์ ๊ตฌ๋ ์์ฑ ๋ฐ ์๋ฒ ์ ์ก
async function subscribeUser() { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY') }); await fetch('/push/subscribe', { method: 'POST', body: JSON.stringify(subscription), headers: { 'Content-Type': 'application/json' } }); }
-
์๋น์ค ์์ปค์์ ํธ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ
self.addEventListener('push', function(event) { const data = event.data.json(); const options = { body: data.body, icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', }; event.waitUntil( self.registration.showNotification(data.title, options) ); });
๋ฐ์คํฌํฑ ์น ๋ธ๋ผ์ฐ์ ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์น ํธ์ ์๋ฆผ์ ์ง์ํ๋ฏ๋ก ํน๋ณํ ์ถ๊ฐ ์์ ์์ด ๊ณตํต ๊ตฌํ ์ฌํญ์ ๋ฐ๋ฅด๋ฉด ๋ฉ๋๋ค.
์๋๋ก์ด๋์ ๋ชจ๋ฐ์ผ ๋ธ๋ผ์ฐ์ ์์๋ ์น ํธ์ ์๋ฆผ๊ณผ PWA(Progressive Web App)๋ฅผ ์ง์ํฉ๋๋ค.
-
PWA ์ค์น ์๋ด
let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; document.getElementById('install-button').style.display = 'block'; }); document.getElementById('install-button').addEventListener('click', async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; deferredPrompt = null; } });
-
๋งค๋ํ์คํธ ํ์ผ ์ค์
{ "name": "Your App Name", "short_name": "App", "start_url": "/", "display": "standalone", "icons": [ { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" } ] }
iOS์์๋ ์น ํธ์ ์๋ฆผ ์ง์์ ์ ํ์ด ์์ผ๋ฏ๋ก ์ฌ์ฉ์์๊ฒ ํ ํ๋ฉด์ ์ถ๊ฐํ๋๋ก ์๋ดํด์ผ ํฉ๋๋ค.
-
iOS ์ค์น ์๋ด ๋ฐฐ๋
function isIos() { const userAgent = window.navigator.userAgent.toLowerCase(); return /iphone|ipad|ipod/.test(userAgent); } function isInStandaloneMode() { return ('standalone' in window.navigator) && (window.navigator.standalone); } if (isIos() && !isInStandaloneMode()) { document.getElementById('ios-install-banner').style.display = 'block'; }
<div id="ios-install-banner" style="display: none;"> <p>์ด ์น ์ฑ์ ์ค์นํ๋ ค๋ฉด Safari์์ ๊ณต์ ๋ฒํผ์ ๋๋ฅธ ํ "ํ ํ๋ฉด์ ์ถ๊ฐ"๋ฅผ ์ ํํ์ธ์.</p> </div>
-
ํ ํ๋ฉด์ ์ถ๊ฐ๋ ํ ํธ์ ๊ตฌ๋ ์์ฑ
if (isInStandaloneMode()) { subscribeUser(); }
- HTTPS ์ฌ์ฉ: ์น ํธ์ ์๋ฆผ๊ณผ ์๋น์ค ์์ปค๋ HTTPS ํ๊ฒฝ์์๋ง ๋์ํฉ๋๋ค.
- ๋ธ๋ผ์ฐ์ ํธํ์ฑ: ์ต์ ๋ธ๋ผ์ฐ์ ์์์ ์ง์ ์ฌ๋ถ๋ฅผ ํ์ธํด์ผ ํฉ๋๋ค.
- ์๋ฌ ์ฒ๋ฆฌ: ํธ์ ์๋ฆผ ์ ์ก ์คํจ ์ ๊ตฌ๋ ์ ๋ณด๋ฅผ ์ ๋ฐ์ดํธํ๊ฑฐ๋ ์ญ์ ํ๋ ๋ก์ง์ด ํ์ํฉ๋๋ค.
- ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์ : ์ค์น ์๋ด ๋ฐ ๊ถํ ์์ฒญ์ ์ ์ ํ ์์ ์ ์ ๊ณตํ์ฌ ์ฌ์ฉ์ ๋ง์กฑ๋๋ฅผ ๋์ ๋๋ค.
- ๋ณด์ ๊ฐํ: VAPID ํค ๊ด๋ฆฌ ๋ฐ ์ธ์ฆ ์ ์ฐจ๋ฅผ ์ฒ ์ ํ ํ์ฌ ๋ณด์์ ๊ฐํํฉ๋๋ค.
https://stackoverflow.com/questions/40392257/what-is-vapid-and-why-is-it-useful
- ๐ฉ FE ๊ธฐ์ ์ ํ์ด์
- โจ ์ฐจํธ์ ๋ฐ์ํ ๊ตฌํ๊ณผ useRef ํ์ ๋ฌธ์
- ๐ฃ ๋ถ๋ชจ ์์์ ์ํ์ ๋ฐ๋ผ ์์ ์์๋ ์คํ์ผ ๋ณํ ๋ถ์ฌํ๊ธฐ
- ๐ zod ๋์ ํ๊ธฐ
- ๐ useInfiniteQuery๋ฅผ ์ฌ์ฉํ ๊ทธ๋ํ ๋ฌดํ์คํฌ๋กค ๊ตฌํ
- ๐ซ ์ฌ์ฉ์์ ์์ ๋ณํ ์๋ ๊ทธ๋ํ ์คํฌ๋กค ๊ตฌํํ๊ธฐ
- ๐งช ์๋ง์ ๊ทธ๋ํ ๋ฐ์ดํฐ ์์ฒญ์ ์ด๋ป๊ฒ ์ค์ผ๊น
- ๐ ๋คํฌ๋ชจ๋์์ ์๋ก๊ณ ์นจ ์ ๋ผ์ดํธ๋ชจ๋๊ฐ ์ ๊น ๋ณด์ด๋ ๋ฌธ์
- ๐ ์น์์ผ์ ์ฑํ ๋ฐ์ดํฐ์ REST API์ ์ฑํ ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ๊ด๋ฆฌํ๊ธฐ
- ๐ก BE ๊ธฐ์ ์ ํ ์ด์
- โ๏ธ Node WebSocket ํ๊ณ ๋ค๊ธฐ
- โ๏ธ TypeORM Datasource mock ๋ง๋ค๊ธฐ
- โ๏ธ oauth ID range ๋ฌธ์
- ๐ custom pipe์์ Nan์ด ๋ฐ์์ง๋ ๋ฌธ์
- ๐ช nest Websocket์ ์ธ์ ์ด ์๋๋ค๊ณ ?
- ๐ด nginx websocket ์ฐ๊ฒฐ ์ ๋ฌธ์ ๋ฐ์
- ๐ WebPush ๊ตฌํ
- ๐ง ์ฐ์ ์์ ํ๋ก ์์ฒญ ์ ์ดํ๊ธฐ
- ๐ websocket์ด ๋ฆ๊ฒ ํ ๋น๋์ด ๋ฐ์๋๋ ๋ฌธ์
- ๐ฅณ typeorm์ ์ด์ฉํ FCM ์๋ฆผ ์๋น์ค
- ๐ฆ ๋ค์ค ์ ์ ๋์์ฑ ์ ์ด โ ์ฑ๊ธํค, ๋ฎคํ ์ค
- ๐ ๊ทธ๋ํ ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ์ ๊ณตํ๊ธฐ์ํ ์ ๋ต
- ๐ ๏ธ ์ธํ๋ผ ๊ธฐ์ ์คํ ์ ํ ์ด์
- ๐ Ncloud ์ค์ ๊ณผ์
- ๐ ORM ๊ธฐ์ ์คํ ๋น๊ต
- ๐ค RabbitMQ๋ก ๋ถ์ฐ ์๋ฒ์๊ฒ ๋ฉ์์ง๋ฅผ ๋ถ๋ฐฐํ๊ธฐ
- ๐ข private DB ์๋ฒ์ ์ ์ํ์ง ๋ชปํ๋ ํ์
- ๐ 1์ฃผ์ฐจ ๋ฐํ
- ๐ 2์ฃผ์ฐจ ๋ฐํ
- ๐ 3์ฃผ์ฐจ ๋ฐํ
- ๐ 4์ฃผ์ฐจ ๋ฐํ
- ๐ 5์ฃผ์ฐจ ๋ฐํ
- ๐ ์ต์ข ๋ฐํ