입력 중 표시와 온라인 상태가 있는 채팅
고객 지원 채팅, 마켓플레이스 메시지, 팀 채팅방: 바로 도착하는 메시지, 브라우저 간 “입력 중…” 표시, 항상 정확한 온라인 목록.
채팅은 사용자가 지연을 가장 크게 느끼는 곳입니다. 1초 늦게 뜨는 메시지나 끝내 나타나지 않는 “입력 중…” 표시는 제품을 방치된 것처럼 보이게 합니다. 고객 지원 채팅, 마켓플레이스의 구매자와 판매자 메시지, 팀 채팅방, 게임 내 채팅에 필요한 것은 같은 세 가지입니다. 바로 도착하는 메시지, 입력 중 표시, 그리고 정직한 온라인 목록.
사용자가 얻는 것
- 보내는 순간 방에 있는 모두에게 메시지가 보입니다. 새로고침은 필요 없습니다.
- 누군가 답장을 쓰는 동안 “Ann 님이 입력 중…”이 표시됩니다.
- 지금 대화에 있는 사람의 이름 옆에 초록색 점이 켜집니다.
사용하는 기능
- 프레즌스 채널(
presence-): 방의 멤버 목록을 모든 노드에서 정확하게 유지합니다. - 클라이언트 이벤트(
client-*, Laravel Echo의whisper): 입력 중 표시를 서버를 거치지 않고 브라우저끼리 주고받습니다. - 소켓 제외(
socket_id, Laravel의toOthers()): 보낸 사람이 자기 메시지를 두 번 받지 않습니다. - 웹훅(
member_added,member_removed): 멤버의 입장과 퇴장에 서버가 반응해야 할 때.
1단계: 방 인가
대화의 멤버만 그 채널에 들어올 수 있습니다. 인가 라우트에서 판단하고, 멤버 목록에 보여 줄 사용자 정보를 반환합니다.
app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
const { socket_id, channel_name } = req.body;
const room = channel_name.replace(/^presence-room-/, '');
if (!(await db.rooms.isMember(room, req.user.id))) return res.sendStatus(403);
res.json(pusher.authorizeChannel(socket_id, channel_name, {
user_id: String(req.user.id),
user_info: { name: req.user.name, avatar: req.user.avatarUrl },
}));
});2단계: 입장, 멤버 목록, 입력 중 표시
앱 설정에서 클라이언트 이벤트 허용을 켠 다음:
const room = pusher.subscribe('presence-room-42');
room.bind('pusher:subscription_succeeded', (members) => {
members.each((m) => online.add(m.id, m.info));
});
room.bind('pusher:member_added', (m) => online.add(m.id, m.info));
room.bind('pusher:member_removed', (m) => online.remove(m.id));
// Typing: at most one event every 2 seconds while the user writes
let last = 0;
input.addEventListener('input', () => {
if (Date.now() - last < 2000) return;
last = Date.now();
room.trigger('client-typing', { name: me.name });
});
room.bind('client-typing', ({ name }) => typing.show(name, 3000));
room.bind('message', (m) => messages.append(m));3단계: 메시지는 서버를 거쳐 보내기
메시지가 서버를 거치면 저장과 검수를 마친 뒤에 전파할 수 있습니다. 보낸 사람의 소켓 ID를 넘기면, 이미 메시지를 보여 주고 있는 보낸 사람의 탭은 건너뜁니다.
app.post('/rooms/:id/messages', requireLogin, async (req, res) => {
const msg = await db.messages.create({ room: req.params.id, user: req.user.id, text: req.body.text });
await pusher.trigger('presence-room-' + req.params.id, 'message', msg, { socket_id: req.body.socket_id });
res.json(msg);
});브라우저는 요청에 pusher.connection.socket_id를 함께 보냅니다.
Laravel에서는
// routes/channels.php
Broadcast::channel('room.{id}', function ($user, $id) {
return $user->rooms()->whereKey($id)->exists() ? ['id' => $user->id, 'name' => $user->name] : false;
});
// In the controller
broadcast(new MessageSent($message))->toOthers();Echo.join(`room.${roomId}`)
.here((users) => online.set(users))
.joining((user) => online.add(user))
.leaving((user) => online.remove(user))
.listen('MessageSent', (e) => messages.append(e.message))
.listenForWhisper('typing', (e) => typing.show(e.name, 3000));
Echo.join(`room.${roomId}`).whisper('typing', { name: me.name });팁
- 프레즌스 채널은 최대 100명까지입니다. 라이브 방송 채팅처럼 더 큰 방은 실시간 경매와 대규모 시청자를 참고하세요.
- 클라이언트 이벤트는 브라우저에서 빈도를 줄이세요. 연결마다 속도 제한이 있고, 이벤트 하나가 메시지 1건으로 계산됩니다.
- 방을 떠난 사람에게 알리려면 인앱 알림과 푸시를 함께 쓰세요.