-
[Docker] 라이브 챗 구현해보기4학년/Project-itda 2025. 4. 11. 14:17
라이브 챗 어떻게 구현하지 ? 생각 해봤습니다.
일단 실시간 하면 Websocket이죠. Websocket에 대해 좀 찾아보니
TCP연결위에서 작동하며 양방향 통신을 지원합니다.
이걸 이용하면 소통은 되겠는데... 저장은 어디다하죠 ?
그래서 DB를 찾아봤습니다.
우리 웹이 진짜 뭐 이것저것 많아 질 것 같아서 좀 경량화 하길 원합니다.
그래서 Postgre DB에서 모두 처리하는게 아니라 Redis를 통하여 처리 할 것입니다.
[클라이언트]
↓ 웹소켓
[FastAPI 서버]
↙️ ↘️
[Redis] [PostgreSQL]
↑ ↑
Pub/Sub 영구 저장
↓
[다른 유저에게 실시간 전달]가 구조가 되겠네요.
왜 두개를 같이 쓰냐 물어본다면 PostgreSQL이 실시간 성이 낮고 사람이 많아지면 DB부하가 커진다고 생각했습니다.
그래서 쫌 빠릿빠릿한 Redis를 같이 씁니다.
Redis가 왜 빠른가 ? 하면 redis는 디스크가 아니라 RAM에 데이터를 저장합니다. 램은 지존 빨라요 그래서 읽기 쓰기 속도가 매우 빨라요. 그리고 쿼리가 복잡하지 않아서.... JOIN WHERE ORDER BY같은 SQL연산이 없다는 점에서 빠릅니다.
그래서 채팅생각해본 채팅 구조는
[클라이언트 A → 웹소켓 전송]
↓
[FastAPI 서버]
↓
1. Redis 저장 (빠르게)
↓
2. Pub/Sub으로 클라이언트 B에게 전달
↓
3. 비동기로 PostgreSQL에도 저장 (영구 보관)가되겠습니다
아예 PostgreSQL안쓰고 최근 nnn개의 메시지만 저장할까? 생각도 해봤는데...
협업 툴에서 이전에 했던 대화 저장이 안되면 뭔 소용인가 싶더라구요.
그렇게 개발했습니다.
Server는 FastAPI이며, FrontEnd는 React를 사용했습니다.
React에서 디자인 만들고 npm start로 실행시켰습니다.
아직은 딥한 개발이 아니라서 되는지만 보려고했는데, postgreSQL이 제 컴터에 인스톨이 안됩니다 ;;
Powershell어쩌구하는데 구글링해도 똑같은 오류가 한 사람 밖에 없었고, 그마저도 러시아어라... 포기하고 도커 쓰기로 했습니다.
드디어 도커 써봐요.
먼저
도커와 레디스 인스톨해줍니다. 포스트그리는 모델이 정의됭어있어야해서, 나중에 하기로 합니다. 우선은 레디스로만 구현해보자구요
pip install
psycopg2-binaryrediswebsocketspython-dotenv이렇게 인스톨해줍니다. 솔직히 버젼같은 경우는 중요하단 건 아는데 아직 구분 잘 못하겠어서 ㅠ (공식문서 읽어보면 알겠지만 영어 아직 잘 못해서 하나하나 읽고앉아잇을 시간 x)
해주는걸로 인스톨합니다.
버젼같은 경우에서 낭패를 본 경험은... 두 세번 있습니다. react 라이브러리 사용이 react버젼과 맞지 않아서 사용 안되더라구요 ㅎ
...
암튼
인스톨하면
livechat 폴더를 따로 만들어서
파일 생성 해줍니다.
.env / docker-compose.yml / Dockerfile / main.py / requirements.txt입니다.
# Redis 연결 redis_client = redis.Redis( host=os.getenv("REDIS_HOST"), port=int(os.getenv("REDIS_PORT")), decode_responses=True ) class ConnectionManager: def __init__(self): self.active_connections: list[WebSocket] = [] self.user_counter = 0 # 익명 사용자 번호 카운터 async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) self.user_counter += 1 user_id = f"User{self.user_counter}" # Redis에서 기존 메시지 로드 messages = redis_client.lrange("chat_messages", 0, -1) for msg in reversed(messages): # 최신순으로 클라이언트에 전송 await websocket.send_text(msg) await websocket.send_text(json.dumps({"username": user_id, "message": "Connected", "timestamp": datetime.now().isoformat()})) return user_id def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket) async def broadcast(self, message: str): for connection in self.active_connections: await connection.send_text(message) manager = ConnectionManager() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/messages") def get_messages(): messages = redis_client.lrange("chat_messages", 0, -1) return [json.loads(msg) for msg in reversed(messages)] @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): user_id = await manager.connect(websocket) try: while True: data = await websocket.receive_text() message_data = json.loads(data) message = message_data["message"] timestamp = datetime.now().isoformat() message_obj = {"username": user_id, "message": message, "timestamp": timestamp} redis_client.lpush("chat_messages", json.dumps(message_obj)) redis_client.ltrim("chat_messages", 0, 99) # 최대 100개 메시지만 유지 await manager.broadcast(json.dumps(message_obj)) except WebSocketDisconnect: manager.disconnect(websocket) await manager.broadcast(json.dumps({"username": user_id, "message": "Disconnected", "timestamp": datetime.now().isoformat()}))
지피티 도움 받았읍니다. 당당하게 말합니다...
Redis 연결해주고요 포트 정해줍니다.
유저기능이 아직 구현 안되어서 익명사용자로 카운트 하기로 했습니다.
ConnectionManage는 websocket과 redis를 아주 잘 연결해 놓은 모습입니다...(찌피티가)
disconnect는 새로고집하면 되게하는 것 같군요 websocket통로도 같이 삭제하는 모습니다.
/messages 경로에 메시지를 보내면 .... 레디스 에 메시지내용을 저장하는 것 같습니다.
/ws 부분에는 메시지들을 redis에 푸시하고 연결하는 것 같습니다
docker-compose.yml에는
version: '3.8' services: app: build: . ports: - "8000:8000" depends_on: - redis volumes: - .:/app environment: - REDIS_HOST=redis - REDIS_PORT=6379 redis: image: redis:alpine ports: - "6379:6379" volumes: - redis_data:/data volumes: redis_data:
버젼과... 포트, 의존성, 환경 레디스 설정까지 하는 모습입니다.
dockerfile에는 말그대로 도커가 실행할 명령어 치면 됩니다.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]파이썬에서 실행하고 requirements 다운받고... uvicorn으로 백앤드 실행하라는 모습입니다.
프론트단에서는 채팅구현해주면 됩니다.
// WebSocket 연결 설정useEffect(() => {ws.current = new WebSocket('ws://localhost:8000/ws');
ws.current.onmessage = (event) => {const message = JSON.parse(event.data);setMessages((prev) => [...prev, message]);};
ws.current.onclose = () => {console.log('WebSocket disconnected');};
return () => {ws.current.close();};}, []);웹소켓 주소 해주구요, setMessages함수를 통해 이전 메시지들 가져옵니다.
닫게되면 disconnected되게하구요
const sendMessage = () => {if (input.trim() === '') return;const messageData = { message: input };ws.current.send(JSON.stringify(messageData));setInput('');};메시지를 작성하면, 데이터를 보냅니다. 웹소켓에
그럼 알아서 레디스에 저장하겠죠....
<div className="liveChat"><div className="title">실시간 채팅</div><div className="content box1">
<div className="chat-messages">{messages.map((msg, index) => (<div key={index} className="chat-message"><span className="username">{msg.username}</span>: {msg.message}<span className="timestamp"> ({new Date(msg.timestamp).toLocaleTimeString()})</span></div>))}<div ref={messagesEndRef} /></div><div className="chat-input"><inputtype="text"value={input}onChange={(e) => setInput(e.target.value)}onKeyPress={(e) => e.key === 'Enter' && sendMessage()}placeholder="Type a message..."className="message-input"/><button onClick={sendMessage}>Send</button></div>네 버튼까지 구현해주면 됩니다
사실 지피티가 거의해주긴했어요
ㅈㅅ ㅋㅋ
PostgreSQL,그리고 디비모델 설계할 땐 좀 집중해보도록 할게요
728x90'4학년 > Project-itda' 카테고리의 다른 글
[5] 개발 3 - 모델에 대한 고민 (0) 2025.04.15 [4] 개발 2 - Front 초안 완성 (1) 2025.04.15 내가 하는게 바이브 코딩? (0) 2025.04.10 [git]존나게 섬뜩한 순간 (1) 2025.03.26 [Front구현] 주간 달력 구하기 (0) 2025.03.20