İçeriğe geç
Teknik YazıArchitecture

Firebase ile Realtime Chat Uygulaması

React ve Firebase Realtime Database ile anlık çalışan bir sohbet ekranı kurarken işime yarayan sade yöntemler: giriş, mesaj akışı ve performans tarafındaki kritik detaylar.

5 Aralık 202310 dk okuma

Canlı çalışan bir chat uygulamasında ilk dikkat çeken şey, mesajın anında ekrana düşmesidir. Ama işin zor kısmı orada başlamıyor. Kullanıcıyı içeri almak, mesajları düzenli biçimde toplamak ve mesaj sayısı arttıkça ekranın hantallaşmamasını sağlamak asıl mesele.

Firebase Realtime Database bu iş için hızlı bir başlangıç veriyor. WebSocket ayrıntısıyla tek tek uğraşmadan çalışan bir yapı kurabiliyorsun. Bu yazıda da React tarafında basit ama gerçek hayatta iş görecek bir chat düzenini adım adım toparlıyorum.

Proje Yapısı ve Firebase Kurulumu

bash
npm create vite@latest chat-app -- --template react-ts
cd chat-app
npm install firebase
ts
// lib/firebase.ts
import { initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getDatabase } from 'firebase/database'

const app = initializeApp({
  apiKey:            process.env.NEXT_PUBLIC_FB_API_KEY,
  authDomain:        process.env.NEXT_PUBLIC_FB_AUTH_DOMAIN,
  databaseURL:       process.env.NEXT_PUBLIC_FB_DB_URL,
  projectId:         process.env.NEXT_PUBLIC_FB_PROJECT_ID,
})

export const auth = getAuth(app)
export const db   = getDatabase(app)

Kurulumu baştan sade bırakmak gerçekten rahatlatıyor. Auth ve database erişimini tek yerde topladığında, sonradan oda mantığı, kullanıcı bilgileri ya da güvenlik kuralları eklemek çok daha derli toplu ilerliyor.

Anonim Giriş ile Hızlı Başlangıç

tsx
import { signInAnonymously, onAuthStateChanged } from 'firebase/auth'
import { auth } from '@/lib/firebase'

function useAuth() {
  const [uid, setUid] = useState<string | null>(null)

  useEffect(() => {
    const unsub = onAuthStateChanged(auth, (user) => {
      if (user) setUid(user.uid)
      else signInAnonymously(auth)
    })
    return unsub
  }, [])

  return uid
}

Anonim giriş özellikle prototipte çok iş görüyor. Kullanıcıyı ilk ekranda form doldurmaya zorlamadan uygulamayı ayağa kaldırabiliyorsun. Böylece daha en başta akışı test etmeye başlayıp gereksiz ayrıntılara takılmıyorsun.

Mesajları Dinlemek ve Göndermek

tsx
import { ref, push, onValue, query, limitToLast } from 'firebase/database'
import { db } from '@/lib/firebase'

interface Message {
  uid: string; text: string; timestamp: number
}

function useMessages(roomId: string) {
  const [messages, setMessages] = useState<Message[]>([])

  useEffect(() => {
    const q = query(ref(db, `rooms/${roomId}/messages`), limitToLast(50))
    const unsub = onValue(q, (snap) => {
      const data = snap.val() ?? {}
      setMessages(Object.values(data))
    })
    return unsub
  }, [roomId])

  return messages
}

async function sendMessage(roomId: string, uid: string, text: string) {
  await push(ref(db, `rooms/${roomId}/messages`), {
    uid, text, timestamp: Date.now(),
  })
}

Burada önemli nokta sadece yeni mesajı kaydetmek değil, geçmişi de düzgün okuyabilmek. Oda bazlı dinleyiciyi tek bir hook içine almak hem kodu toparlıyor hem de ileride sayfalama ya da farklı mesaj tipleri eklemeyi kolaylaştırıyor.

Dikkat

Mesaj sayısını sınırlamazsanız oda büyüdükçe her yeni bağlantıda bütün geçmiş yeniden iner. Başta sorun çıkarmaz ama gerçek kullanımda gereksiz veri yüküne dönüşür.

Re-render Meselesi

Canlı çalışan ekranlarda performans sorunu genelde bir anda patlamaz. Önce hafif hafif ağırlaşma hissi verir. Her yeni mesaj geldiğinde bütün listeyi yeniden çizmek küçük listelerde idare eder ama iş büyüdükçe akıcılık kaybolur. Bu yüzden mesaj öğelerini mümkün olduğunca ayrı tutmak iyi fikir.

tsx
const MessageItem = React.memo(({ msg }: { msg: Message }) => (
  <div className="flex gap-2 p-2">
    <span className="font-mono text-xs text-indigo-400">{msg.uid.slice(0,6)}</span>
    <p>{msg.text}</p>
  </div>
))
MessageItem.displayName = 'MessageItem'

<div ref={bottomRef}>
  {messages.map((m) => (
    <MessageItem key={m.timestamp} msg={m} />
  ))}
</div>

Otomatik Kaydırma

tsx
const bottomRef = useRef<HTMLDivElement>(null)

useEffect(() => {
  bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])

Chat ekranında küçük görünen ama hissi çok değiştiren şeylerden biri de kaydırma davranışı. Yeni mesaj gelince ekranın alta inmesi beklenir. Ama kullanıcı eski mesajları okuyorsa aynı hareket can sıkıcı olur. Yani burada biraz denge kurmak gerekiyor.

İpucu

Kullanıcı yukarı çıkmışsa onu zorla alta götürmeyin. Önce gerçekten listenin sonuna yakın mı diye bakın. Yoksa uygulama akıcı değil, inatçı hissettirir.