실시간 채팅 앱 만들기
실시간 채팅 앱 만들기 — 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-client2. 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 응답 스트리밍용 카테고리 등
이 튜토리얼이 도움이 됐나요?