web/snulion

실시간 채팅 구현에 대해 araboza

민사민서 2024. 11. 2. 17:13

Requirements

  • 일대일 채팅, 그룹 채팅 모두 가능해야 함.
  • 현재 열람중인 채팅방에서 발생하는 채팅 내용들을 실시간으로 확인해야 함.
  • 현재 열람 중인 채팅방이 아니더라도 좌측 목록에서 다른 채팅방으로부터의 알림을 실시간으로 받아야 함.
  • 채팅 내역은 데이터베이스에 저장되어, 브라우저를 새로 열고 로그인을 다시 해도 이전의 기록들을 보존&열람할 수 있어야 함.
  • 인증받은 사람만 채팅이 가능해야 함.

Scenario

1. A 가 채팅방을 연다.

  1. 서버로부터 기존 채팅 내역들을 불러온다 (일반적인 HTTP 요청-응답)
  2. 클라이언트와 서버가 웹소켓으로 연결된다.

 

2. A가 채팅방에 메세지를 보낸다.

  1. A의 클라이언트가 웹소켓을 통해 서버로 메세지를 전송한다.
  2. 서버는 A가 연결된 웹소켓으로부터 데이터를 받고 핸들러를 호출한다.
  3. 메세지를 DB에 저장한다.

 

3. 서버는 해당 채팅방에 참여한 다른 유저에게 웹소켓을 통해 메세지를 전송한다 (B, C 도 서버와 웹소켓으로 연결되어 있다고 가정)

  1. 해당 메세지가 전송된 채팅방의 유저 목록을 DB 에서 가져온다.
  2. 해당 유저들과 연결된 웹소켓이 있다면 메세지를 전파한다.

 

 

4. B, C 각각의 클라이언트는 웹소켓을 통해 메세지를 전달 받고 프론트 UI를 리렌더하는 등의 핸들러 동작을 수행한다.

  1. B, C의 클라이언트는 서버와 연결된 웹소켓으로부터 데이터를 받고 핸들러를 호출한다.
  2. 핸들러는 받아온 데이터를 바탕으로 필요한 작업을 수행한다.

 

 

 

채널 레이어 설계

하나의 웹소켓 채널은 하나의 서버 - 하나의 클라이언트를 연결한다.

그러나 채팅이 발생할 경우 해당 채팅방에 참여한 다수의 클라이언트 모두에게 데이터를 전송해야 한다.

위에서 보았던 Channel Layer 는 여러 클라이언트를 하나의 그룹으로 묶어 일괄적으로 데이터를 전송할 수 있게 해준다.

 

Django의 Consumer 클래스

소켓 연결 허용 + 소켓 연결 해제 + 정보 송수신 에 대한 핸들링을 쉽게 구현할 수 있도록 함

=> JsonWebsocketConsumer 클래스를 상속

 

미들웨어 장고 인증

임의의 클라이언트가 특정 채팅방에 마음대로 채팅을 보내거나 받도록 두면 안됨

- HTTP 연결 시에는 장고가 token 을 보내거나 받아 유저의 인증 정보를 체크

- 마찬가지로 웹소켓 연결 시에도 토큰을 통해 유저의 인증 정보를 체크하도록 구현

- 이 절차는 서버가 데이터를 수신한 후, 그리고 수신한 데이터를 처리하기 이전에 수행

- 웹소켓 연결 시의 헤더 교환을 통해 브라우저에 저장해두었던 토큰을 전달할 수 있음

 

=> 이 Layer를 미들웨어라고 한다

 

 

프론트 단에서 해야 할 것은?

직접적으로 서버와 연결하기

1. 최초의 웹소켓 연결 요청

2. 데이터 전송

 

통신 과정에서 클라이언트가 각종 상태 관리 하기

a. 서버와의 웹소켓 연결 후 각종 상태 초기화

b. 서버로부터 수신된 데이터 처리

c. 웹소켓 연결 종료 처리

 

=> react-use-websocket 라이브러리를 활용

=> useWebSocket은 웹소켓 연결을 쉽게 관리할 수 있도록 도와주는 React 커스텀 훅

=> 라이브러리가 웹소켓 연결과 데이터 송수신, 재연결 등 여러 복잡한 작업(1번, 2번)을 알아서 해주기 때문에, 우리는 데이터를 주고받는 로직(a, b, c)에만 집중

import React, { useState, useCallback, useEffect } from 'react';
import useWebSocket from 'react-use-websocket';

export const WebSocketDemo = () => {

  const [socketUrl, setSocketUrl] = useState('wss://echo.websocket.org');
	const options = {
	  onOpen: () => console.log('연결이 성공적으로 열렸습니다.'),
	  onClose: () => console.log('연결이 닫혔습니다.'),
	  onError: (error) => console.error('웹소켓 에러:', error),
	  onMessage: (message) => console.log('수신된 메시지:', message.data),
	};
  const { sendJsonMessage } = useWebSocket(socketUrl, options, reconnectInterval: 1000,share: true,);

 

백엔드 단에서 구현해야 할 것은?

WebSocket 등 비동기 통신 프로토콜을 지원하는 기능을 추가하는 라이브러리 = 장고의 Channel 라이브러리

Channels는 ASGI 웹서버( 예: Uvicorn 등 )에서 작동하며, Consumer라는 Django의 기존 동기식 View와 유사하지만 비동기 처리를 지원하는 구조를 차용함

 

wsgi.py 대신 asgi.py 를 작성해서 사용한다

- Uvicorn이 Django Application의 ASGI Instance 를 정의하는 부분

- asgi.py는 ASGI 사용시 Django Application 으로의 진입점

- 프로토콜을 구분해서 HTTP 요청은 get_asgi_application()으로, WebSocket 요청은 URLRouter로 전달

import os
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'likesaju.settings')

django_application = get_asgi_application() # init django application

from  . import urls 
from channels.routing import ProtocolTypeRouter, URLRouter
from webchat.middleware import JWTAuthMiddleWare

application = ProtocolTypeRouter(
    {
        "http" : django_application,
        "websocket": URLRouter(urls.websocket_urlpatterns),
    }
)

 

 

더 이상 python manage.py runserver 로 장고 내장 서버를 실행하지 말고

uvicorn likesaju.asgi:application --port 8000 --workers 4 --log-level debug --reload

uvicorn 서버 실행 커맨드를 사용하자

 

 

구현 디테일은 멋사 세미나 자료에...

(디테일 야무짐)

 

 

 

 

  1.