실시간 인앱 알림
답글, 멘션, 승인을 발생 즉시 열려 있는 모든 탭에 표시하고, 사용자가 오프라인이면 푸시 알림으로 대신합니다.
무언가 일어나는 순간 제품이 반응하면 사용자는 그것을 알아챕니다. 내 게시물에 달린 댓글, 승인된 요청, 팀원의 멘션. 이것을 30초마다 폴링으로 확인하면 느리고 요청도 낭비됩니다. Hibira를 쓰면 서버가 알림을 한 번만 보내도, 사용자가 열어 둔 모든 탭과 기기에 몇 밀리초 안에 표시됩니다. 오프라인일 때는 푸시 알림이 이어받습니다.
사용자가 얻는 것
- 무언가 일어나는 순간, 열려 있는 모든 탭에서 종 모양 배지와 토스트가 갱신됩니다.
- 앱이 닫혀 있을 때는 같은 알림이 휴대폰으로 옵니다.
- 새로고침하지 않아도 새 활동이 보이고, 카운터가 낡지 않습니다.
사용하는 기능
- 사용자 인증(
pusher.signin()): 사용자마다 본인만 읽을 수 있는 채널을 가집니다. - 공식 서버 SDK의
sendToUser: 그 사용자의 모든 소켓에, 어느 노드에 있든 전달됩니다. - 연결되어 있지 않은 사용자에게는 Beams 호환 API의 푸시 알림.
- 종단 간 암호화가 필요한 데이터가 담긴 알림에는 암호화 프라이빗 채널(
private-encrypted-).
1단계: 소켓에서 사용자 로그인
채널 인가 라우트 옆에 사용자 인증 라우트를 추가합니다. 서버에서 실행되며 앱 시크릿을 사용합니다.
// Express
app.post('/pusher/user-auth', requireLogin, express.urlencoded({ extended: false }), (req, res) => {
const auth = pusher.authenticateUser(req.body.socket_id, {
id: String(req.user.id),
user_info: { name: req.user.name },
});
res.json(auth);
});브라우저에서는 JavaScript 가이드대로 연결하고 로그인합니다.
const pusher = new Pusher(import.meta.env.VITE_PUSHER_APP_KEY, {
cluster: 'ap',
wsHost: 'ws.hibira.io',
wsPort: 443,
wssPort: 443,
forceTLS: true,
enabledTransports: ['ws', 'wss'],
userAuthentication: { endpoint: '/pusher/user-auth', transport: 'ajax' },
});
pusher.signin();
pusher.user.bind('notification', (n) => {
bell.increment();
toast.show(n.title, { href: n.url });
});2단계: 서버에서 보내기
새로고침 후에도 목록이 남도록 먼저 알림을 데이터베이스에 저장한 다음 보냅니다.
const n = await db.notifications.create({ userId, title: 'Ann replied to your comment', url: '/posts/42#c7' });
await pusher.sendToUser(String(userId), 'notification', { id: n.id, title: n.title, url: n.url });sendToUser 한 번은 사용자의 모든 탭과 기기에 전달되며, 받는 소켓이 몇 개든 메시지 1건으로 계산됩니다.
3단계: 오프라인이면 푸시로 대체
로그인한 페이지마다 private-user-42 같은 프라이빗 채널을 구독하게 하고, 웹훅 엔드포인트에서 channel_occupied와 channel_vacated를 받습니다. 그러면 서버는 묻지 않고도 누가 온라인인지 압니다.
app.post('/pusher/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const webhook = pusher.webhook({ headers: req.headers, rawBody: req.body.toString() });
if (!webhook.isValid()) return res.sendStatus(401);
for (const e of webhook.getEvents()) {
const id = e.channel.match(/^private-user-(.+)$/)?.[1];
if (id) await db.users.update(id, { online: e.name === 'channel_occupied' });
}
res.sendStatus(200);
});사용자가 오프라인이면 푸시 알림 가이드대로 기기에 보냅니다.
if (!(await db.users.isOnline(userId))) {
await beams.publishToUsers([String(userId)], {
web: { notification: { title: n.title, deep_link: 'https://your-site.example' + n.url } },
fcm: { notification: { title: n.title } },
apns: { aps: { alert: { title: n.title } } },
});
}팁
- 페이로드는 작게(ID, 제목, 링크) 유지하고, 사용자가 열 때 자세한 내용을 가져오세요. 이벤트 크기 제한은 10 KB(Free 플랜은 4 KB)입니다.
- 읽음 처리는 서버에서 하고
notifications-read이벤트를 보내면, 모든 탭의 배지가 한 번에 사라집니다. - 의료, 금융, 인사 데이터는
private-encrypted-user-42채널로 보내세요. Hibira는 암호문만 중계합니다. - Free 플랜에서는 앱 설정에서 사이트 도메인을 허용된 출처로 등록하세요.