FastAPI와 React로 만드는 실시간 채팅 애플리케이션 구축 가이드

기술 스택과 아키텍처 개요

실시간 채팅 시스템은 클라이언트와 서버 간의 지속적인 양방향 통신이 필수적입니다. 기존 HTTP는 요청-응답 패턴에 의존하기 때문에 실시간성이 부족하며, WebSocket 프로토콜은 서버가 클라이언트로 데이터를 능동적으로 전송할 수 있는 전이중 통신 채널을 제공합니다.

기술 스택 비교

기술 장점 적용 분야
FastAPI 고성능, 비동기 처리, 타입 힌트 실시간 API, 마이크로서비스
React 컴포넌트 기반, 가상 DOM, 풍부한 생태계 복잡한 사용자 인터페이스
WebSocket 저지연, 전이중 통신 실시간 채팅, 게임, 알림 시스템

FastAPI WebSocket 백엔드 구현

기본 WebSocket 서비스 구축

FastAPI 애플리케이션에 WebSocket 라우트를 추가하는 방법부터 시작합니다.


from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware

application = FastAPI()

# CORS 설정: 프론트엔드 연결 허용
application.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 현재 열려있는 WebSocket 연결 목록
live_connections = []

@application.websocket("/ws")
async def ws_handler(socket: WebSocket):
    await socket.accept()
    live_connections.append(socket)
    try:
        while True:
            incoming = await socket.receive_text()
            # 연결된 모든 클라이언트에게 메시지 전송
            for conn in live_connections:
                await conn.send_text(f"Echo: {incoming}")
    except Exception as err:
        print(f"오류 발생: {err}")
    finally:
        live_connections.remove(socket)

메시지 처리 및 브로드캐스트 시스템

실제 운영 환경에서는 구조화된 메시지 처리가 필요합니다. JSON 기반 메시지를 활용한 개선된 코드 예제입니다.


from typing import List, Dict
import json
from datetime import datetime
import uuid

# 연결 정보를 저장할 클래스
class ConnectionInfo:
    def __init__(self, websocket: WebSocket):
        self.ws = websocket
        self.user_id = str(uuid.uuid4())
        self.joined_at = datetime.utcnow()

# 메시지 형식 정의
class ChatMessage:
    def __init__(self, sender_id: str, content: str, timestamp: str):
        self.sender_id = sender_id
        self.content = content
        self.timestamp = timestamp

    def to_dict(self) -> dict:
        return {
            "sender": self.sender_id,
            "message": self.content,
            "time": self.timestamp
        }

# 채팅 방 관리
chat_rooms: Dict[str, List[ConnectionInfo]] = {}

@application.websocket("/ws/chat/{room_id}")
async def chat_room_handler(socket: WebSocket, room_id: str):
    await socket.accept()
    user_info = ConnectionInfo(socket)
    
    if room_id not in chat_rooms:
        chat_rooms[room_id] = []
    chat_rooms[room_id].append(user_info)
    
    try:
        while True:
            raw_data = await socket.receive_text()
            received_msg = json.loads(raw_data)
            
            # 응답 메시지 생성
            response = ChatMessage(
                sender_id=user_info.user_id,
                content=received_msg.get("text", ""),
                timestamp=datetime.utcnow().isoformat()
            ).to_dict()
            
            # 같은 방에 있는 모든 사용자에게 전송
            for participant in chat_rooms[room_id]:
                if participant.ws != socket:  # 자신을 제외
                    await participant.ws.send_json(response)
                    
    except Exception as error:
        print(f"채팅 오류: {error}")
    finally:
        chat_rooms[room_id].remove(user_info)
        # 방이 비었으면 삭제
        if not chat_rooms[room_id]:
            del chat_rooms[room_id]

React 프론트엔드에서 WebSocket 연결

React 컴포넌트에서 WebSocket을 관리하는 방법을 살펴보겠습니다. 커스텀 훅을 사용하여 연결 상태를 관리합니다.


import { useState, useEffect, useRef, useCallback } from 'react';

function useChatWebSocket(roomId) {
  const [messages, setMessages] = useState([]);
  const [status, setStatus] = useState('disconnected');
  const socketRef = useRef(null);

  useEffect(() => {
    const wsUrl = `ws://localhost:8000/ws/chat/${roomId}`;
    
    const connect = () => {
      const ws = new WebSocket(wsUrl);
      
      ws.onopen = () => {
        setStatus('connected');
        console.log('WebSocket 연결 성공');
      };
      
      ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        setMessages(prev => [...prev, data]);
      };
      
      ws.onclose = () => {
        setStatus('disconnected');
        console.log('WebSocket 연결 종료');
        // 재연결 로직
        setTimeout(connect, 3000);
      };
      
      ws.onerror = (error) => {
        console.error('WebSocket 오류:', error);
        ws.close();
      };
      
      socketRef.current = ws;
    };
    
    connect();
    
    return () => {
      if (socketRef.current) {
        socketRef.current.close();
      }
    };
  }, [roomId]);

  const sendMessage = useCallback((text) => {
    if (socketRef.current?.readyState === WebSocket.OPEN) {
      const payload = JSON.stringify({ text });
      socketRef.current.send(payload);
    } else {
      console.warn('연결이 열려있지 않습니다.');
    }
  }, []);

  return { messages, sendMessage, status };
}

채팅 인터페이스 컴포넌트

위에서 만든 훅을 활용하여 실제 채팅 UI를 구성합니다.


function ChatRoom({ roomId }) {
  const { messages, sendMessage, status } = useChatWebSocket(roomId);
  const [inputValue, setInputValue] = useState('');
  const messagesEndRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      sendMessage(inputValue);
      setInputValue('');
    }
  };

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

  return (
    <div className="chat-container">
      <div className="status-bar">
        연결 상태: {status === 'connected' ? '✅ 연결됨' : '❌ 연결 끊김'}
      </div>
      
      <div className="messages-area">
        {messages.map((msg, index) => (
          <div key={index} className="message-item">
            {msg.sender}: 
            {msg.message}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      
      <form onSubmit={handleSubmit} className="input-form">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="메시지를 입력하세요..."
          disabled={status !== 'connected'}
        />
        <button type="submit" disabled={status !== 'connected'}>
          전송
        </button>
      </form>
    </div>
  );
}

태그: FastAPI React websocket 비동기 실시간채팅

7월 1일 21:30에 게시됨