본문으로 건너뛰기

실시간 채팅 앱 만들기

실시간 채팅 앱 만들기WebSocket 기반 채팅을 몇 줄의 코드로 구현합니다.

입문실시간18초읽기 약 2분업데이트 2026-04-14

WebSocket 기반 채팅을 몇 줄의 코드로 구현합니다.

이 튜토리얼에서 배울 내용
WebSocket 실시간 연결
카테고리 구독 및 메시지 전송
메시지 히스토리 저장
채팅 UI 구현

실시간 채팅 앱 만들기

Connect Base WebSocket을 사용하여 실시간 채팅 앱을 만들어봅니다.

완성된 기능

  • 실시간 메시지 전송/수신
  • 메시지 히스토리 (persist=true 인 카테고리)
  • 게스트 멤버 자동 가입

📌 카테고리는 콘솔의 실시간 → 카테고리 만들기 에서 미리 만들고 persist=true 를 설정하면 메시지가 저장되어 getHistory() 로 불러올 수 있습니다.

1. 프로젝트 설정

bash
npm create vite@latest chat-app -- --template react-ts
cd chat-app
npm install connectbase-client

2. SDK 설정

src/lib/connectbase.ts:

typescript
import ConnectBase from 'connectbase-client'

export const cb = new ConnectBase({
  publicKey: import.meta.env.VITE_CONNECT_BASE_PUBLIC_KEY
})

3. 채팅 훅 만들기

src/hooks/useChat.ts:

typescript
import { useState, useEffect, useCallback, useRef } from 'react'
import { cb } from '../lib/connectbase'
import type { Subscription } from 'connectbase-client'

interface ChatPayload {
  text: string
  userId: string
  userName: string
}

interface ChatMessage extends ChatPayload {
  id: string
  sentAt: number
}

export function useChat(category: string, currentUser: { id: string; name: string }) {
  const [messages, setMessages] = useState<ChatMessage[]>([])
  const [ready, setReady] = useState(false)
  const subRef = useRef<Subscription | null>(null)

  useEffect(() => {
    let cancelled = false

    async function start() {
      // 1) 게스트 멤버로 토큰 발급 후 WebSocket 연결
      const guest = await cb.auth.signInAsGuestMember()
      await cb.realtime.connect({ accessToken: guest.access_token })
      if (cancelled) return

      // 2) 카테고리 구독
      const sub = await cb.realtime.subscribe(category)
      subRef.current = sub

      // 3) 히스토리 로드 (persist=true 인 경우만)
      if (sub.info.persist) {
        const history = await sub.getHistory<ChatPayload>(50)
        if (cancelled) return
        setMessages(
          history.messages.map((m) => ({
            id: m.id ?? crypto.randomUUID(),
            text: m.data.text,
            userId: m.data.userId,
            userName: m.data.userName,
            sentAt: m.sentAt,
          }))
        )
      }

      // 4) 새 메시지 핸들러 등록
      sub.onMessage<ChatPayload>((msg) => {
        setMessages((prev) => [
          ...prev,
          {
            id: msg.id ?? crypto.randomUUID(),
            text: msg.data.text,
            userId: msg.data.userId,
            userName: msg.data.userName,
            sentAt: msg.sentAt,
          },
        ])
      })

      setReady(true)
    }

    start().catch(console.error)

    return () => {
      cancelled = true
      subRef.current?.unsubscribe().catch(() => {})
      subRef.current = null
      cb.realtime.disconnect()
      setReady(false)
    }
  }, [category])

  const sendMessage = useCallback(
    async (text: string) => {
      if (!subRef.current) return
      await subRef.current.send<ChatPayload>({
        text,
        userId: currentUser.id,
        userName: currentUser.name,
      })
    },
    [currentUser.id, currentUser.name]
  )

  return { messages, sendMessage, ready }
}

4. 채팅 컴포넌트

src/components/Chat.tsx:

tsx
import { useState, useRef, useEffect } from 'react'
import { useChat } from '../hooks/useChat'

interface ChatProps {
  category: string
  currentUser: { id: string; name: string }
}

export function Chat({ category, currentUser }: ChatProps) {
  const [input, setInput] = useState('')
  const messagesEndRef = useRef<HTMLDivElement>(null)

  const { messages, sendMessage, ready } = useChat(category, currentUser)

  // 새 메시지 시 스크롤
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!input.trim() || !ready) return

    await sendMessage(input.trim())
    setInput('')
  }

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {!ready && <p className="text-center text-gray-500">연결 중...</p>}
        {messages.map((msg) => (
          <div
            key={msg.id}
            className={`p-3 rounded-lg max-w-[70%] ${
              msg.userId === currentUser.id
                ? 'ml-auto bg-blue-500 text-white'
                : 'bg-gray-200'
            }`}
          >
            <div className="text-xs opacity-70 mb-1">{msg.userName}</div>
            <div>{msg.text}</div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      <form onSubmit={handleSubmit} className="p-4 border-t">
        <div className="flex gap-2">
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="메시지를 입력하세요..."
            disabled={!ready}
            className="flex-1 px-4 py-2 border rounded-lg"
          />
          <button
            type="submit"
            disabled={!ready}
            className="px-6 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
          >
            전송
          </button>
        </div>
      </form>
    </div>
  )
}

5. 완성!

bash
npm run dev

브라우저에서 http://localhost:5173 을 열면 채팅 앱이 실행됩니다.

다음 단계

  • 메시지 영구 저장 — 카테고리에 persist=true 설정
  • 이미지 첨부 — cb.storage.uploadFile 로 업로드한 URL을 메시지 payload에 포함
  • 회원 인증 — signInAsGuestMember 대신 signInMember 사용
  • 다른 카테고리 활용 — 알림용 카테고리, AI 응답 스트리밍용 카테고리 등

이 튜토리얼이 도움이 됐나요?