REAL-TIME WEB-SOCKETS REDIS FRONTEND BACKEND COLLABORATION USER-EXPERIENCE SYSTEM-DESIGN DISTRIBUTED-SYSTEMS DATA-SYNCHRONIZATION STATE-MANAGEMENT WEB-DEVELOPMENT PRESENCE-DETECTION INTERACTIVITY

Membangun Real-time Presence Detection di Aplikasi Web: Mengintip Siapa yang Online dan Sedang Apa

⏱️ 10 menit baca
👨‍💻

Membangun Real-time Presence Detection di Aplikasi Web: Mengintip Siapa yang Online dan Sedang Apa

1. Pendahuluan

Pernahkah Anda menggunakan Google Docs, Figma, atau aplikasi kolaborasi modern lainnya dan melihat avatar kecil teman Anda bergerak di sekitar dokumen, atau indikator “typing…” saat mereka mengetik? Itu adalah fitur Real-time Presence Detection yang sedang bekerja. Fitur ini mengubah aplikasi web yang statis menjadi ruang interaktif yang terasa lebih hidup dan terhubung.

Sebagai developer, kita sering fokus pada fungsionalitas inti. Namun, di era digital ini, pengalaman pengguna (UX) yang imersif dan kolaboratif menjadi pembeda. Presence detection bukan sekadar “gimmick”; ia meningkatkan engagement, memfasilitasi kolaborasi, dan memberikan feedback visual yang sangat berharga kepada pengguna. Bayangkan aplikasi chat tanpa indikator “online” atau “mengetik”, pasti terasa hampa, bukan?

Artikel ini akan memandu Anda membangun sistem real-time presence detection dari nol. Kita akan membahas arsiteturnya, mulai dari bagaimana browser berkomunikasi dengan server, bagaimana server mengelola status kehadiran, hingga bagaimana frontend menampilkan informasi tersebut secara efisien. Kita akan menggunakan kombinasi teknologi tangguh seperti WebSockets untuk komunikasi dua arah yang persisten dan Redis sebagai penyimpanan state yang cepat dan mekanisme Pub/Sub untuk penyebaran event.

Mari kita selami bagaimana kita bisa membuat aplikasi web kita terasa lebih “hidup”!

2. Mengapa Presence Detection Penting?

Presence detection lebih dari sekadar indikator “online”. Ia memberikan konteks sosial dan fungsional yang kaya.

📌 Manfaat Utama:

🎯 Contoh Konkret Implementasi:

3. Arsitektur Umum Sistem Presence Detection

Untuk membangun sistem presence detection yang real-time dan skalabel, kita membutuhkan tiga komponen utama yang bekerja sama:

  1. Frontend (Browser): Bertanggung jawab untuk mengirimkan status kehadiran pengguna (misalnya, “online”, “mengetik”, “melihat halaman X”) dan menampilkan informasi kehadiran pengguna lain di UI.
  2. Backend (Server Aplikasi): Menerima event kehadiran dari frontend, memprosesnya, dan menyebarkannya ke pengguna lain yang relevan. Server akan menjadi jembatan antara banyak klien.
  3. Penyimpanan State & Pub/Sub (Redis): Ini adalah jantung sistem kita. Redis akan digunakan untuk:
    • Menyimpan state kehadiran: Siapa yang online, di halaman mana, status apa (mengetik, melihat).
    • Mekanisme Pub/Sub: Menyebarkan perubahan state kehadiran secara real-time ke semua server backend yang terhubung, yang kemudian akan meneruskannya ke klien frontend yang relevan.

💡 Analogi: Bayangkan Anda berada di sebuah pesta (aplikasi web). Setiap orang (pengguna) ingin tahu siapa yang ada di ruangan mana (halaman/fitur) dan apa yang sedang mereka lakukan (online, mengetik).

Ini memastikan setiap orang di ruangan yang sama mendapatkan informasi kehadiran terbaru secara efisien, bahkan jika ada banyak ruangan dan banyak orang.

4. Backend: Jantung Sistem Real-time dengan WebSockets dan Redis

Bagian backend adalah tempat keajaiban komunikasi real-time terjadi. Kita akan menggunakan Node.js dengan library ws untuk WebSockets dan ioredis untuk berinteraksi dengan Redis.

4.1 Menyiapkan Server WebSocket

Pertama, kita butuh server WebSocket yang bisa menerima koneksi dari klien.

// server.js
const WebSocket = require('ws');
const http = require('http');
const Redis = require('ioredis');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('WebSocket server is running\n');
});

const wss = new WebSocket.Server({ server });
const redis = new Redis(); // Terhubung ke Redis di localhost:6379 secara default
const redisSubscriber = new Redis(); // Subscriber terpisah untuk Pub/Sub

// Struktur data untuk melacak koneksi WebSocket dan user ID
const clients = new Map(); // Map<WebSocket, userId>

wss.on('connection', ws => {
  console.log('Client connected');

  // Contoh: Autentikasi user (di dunia nyata, ini lebih kompleks)
  // Misal, setelah autentikasi, kita tahu userId
  const userId = `user_${Math.floor(Math.random() * 1000)}`; // Dummy userId
  clients.set(ws, userId);

  // Setiap koneksi baru akan mendengarkan channel Redis yang relevan
  // Kita akan menggunakan channel 'presence_updates' untuk semua notifikasi
  redisSubscriber.subscribe('presence_updates', (err, count) => {
    if (err) {
      console.error("Failed to subscribe: %s", err.message);
    } else {
      console.log(`Subscribed to ${count} channels.`);
    }
  });

  // Event handler saat menerima pesan dari klien
  ws.on('message', async message => {
    const data = JSON.parse(message);
    const { type, payload } = data;
    const currentUserId = clients.get(ws);

    switch (type) {
      case 'USER_ONLINE':
        // Simpan status online user di Redis
        // Misal: HASH 'presence:users' -> { 'user_1': { status: 'online', page: '/dashboard', timestamp: ... } }
        await redis.hset(`presence:users`, currentUserId, JSON.stringify({
          status: 'online',
          page: payload.page,
          timestamp: Date.now()
        }));
        // Beri tahu semua yang mendengarkan bahwa ada perubahan
        redis.publish('presence_updates', JSON.stringify({
          userId: currentUserId,
          status: 'online',
          page: payload.page
        }));
        console.log(`User ${currentUserId} is online on ${payload.page}`);
        break;
      case 'USER_TYPING':
        // Update status user ke 'typing'
        await redis.hset(`presence:users`, currentUserId, JSON.stringify({
          status: 'typing',
          page: payload.page,
          timestamp: Date.now()
        }));
        redis.publish('presence_updates', JSON.stringify({
          userId: currentUserId,
          status: 'typing',
          page: payload.page
        }));
        console.log(`User ${currentUserId} is typing on ${payload.page}`);
        break;
      case 'USER_VIEWING':
        // Update halaman yang sedang dilihat
        await redis.hset(`presence:users`, currentUserId, JSON.stringify({
          status: 'online', // Masih online, hanya halaman berubah
          page: payload.page,
          timestamp: Date.now()
        }));
        redis.publish('presence_updates', JSON.stringify({
          userId: currentUserId,
          status: 'online',
          page: payload.page
        }));
        console.log(`User ${currentUserId} is viewing ${payload.page}`);
        break;
      // ... event lainnya seperti 'USER_STOP_TYPING'
    }
  });

  // Event handler saat koneksi terputus
  ws.on('close', async () => {
    const currentUserId = clients.get(ws);
    if (currentUserId) {
      // Hapus status user dari Redis
      await redis.hdel(`presence:users`, currentUserId);
      redis.publish('presence_updates', JSON.stringify({
        userId: currentUserId,
        status: 'offline'
      }));
      console.log(`User ${currentUserId} disconnected.`);
      clients.delete(ws);
    }
    redisSubscriber.unsubscribe('presence_updates');
  });

  ws.on('error', error => {
    console.error('WebSocket error:', error);
  });
});

// Event handler untuk pesan dari Redis Pub/Sub
redisSubscriber.on('message', (channel, message) => {
  const presenceUpdate = JSON.parse(message);
  console.log(`Redis Pub/Sub received: ${presenceUpdate.userId} is ${presenceUpdate.status}`);

  // Kirim pesan ke semua klien yang terhubung (kecuali pengirim sendiri jika memungkinkan)
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      // Filter jika ingin menghindari mengirim kembali ke pengirim
      // const senderId = clients.get(client);
      // if (senderId !== presenceUpdate.userId) {
        client.send(JSON.stringify({ type: 'PRESENCE_UPDATE', payload: presenceUpdate }));
      // }
    }
  });
});

server.listen(8080, () => {
  console.log('WebSocket server started on port 8080');
});

Instalasi Dependensi: npm install ws ioredis Menjalankan Redis: Pastikan Redis server Anda berjalan (misal: docker run --name my-redis -p 6379:6379 -d redis).

4.2 Peran Redis dalam Backend

💡 Redis sebagai Penyimpanan State: Kita menggunakan redis.hset untuk menyimpan status setiap user dalam sebuah Hash (presence:users). Key-nya adalah userId dan value-nya adalah string JSON yang berisi status dan page. Ini memungkinkan kita dengan cepat mengambil semua user yang online atau status spesifik seorang user.

Contoh Data Redis:

presence:users: {
  "user_123": "{\"status\":\"online\",\"page\":\"/dashboard\",\"timestamp\":1678886400000}",
  "user_456": "{\"status\":\"typing\",\"page\":\"/document/abc\",\"timestamp\":1678886405000}"
}

🎯 Redis sebagai Pub/Sub: Ketika status seorang user berubah (online, mengetik, offline), server backend mem-publish event ke channel Redis (presence_updates). Semua server backend lain yang subscribe ke channel ini akan menerima event tersebut dan kemudian meneruskannya ke klien frontend yang relevan melalui koneksi WebSocket mereka. Ini adalah kunci skalabilitas horizontal, karena Anda bisa memiliki banyak instance server backend yang berbagi state kehadiran melalui Redis.

5. Frontend: Mengelola Koneksi dan Memperbarui UI

Di sisi frontend, kita akan menggunakan JavaScript standar untuk mengelola koneksi WebSocket dan memperbarui UI berdasarkan event kehadiran yang diterima.

// public/index.html (Contoh sederhana)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset