Skip to content

NarciSource/Chat-GraphQL

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

408 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GraphQL 채팅 서비스

🚩 목차

🛠️ 기술 스택

GraphQL Apollo
NestJS Express NodeJS TypeScript
Redis DynamoDB
Vuejs Vite Pinia Quasar
Steiger ESLint Prettier
Vitest TypeDoc Postman Voyager
Docker Compose Docker Kong NGINX

💁 소개

GraphQL를 활용한 실시간 다대다 채팅 서비스입니다.

사용자는 채팅 방을 생성하고, 다른 사용자와 동시에 메시지를 주고받으며,
타이핑 상태 알림으로 대화 몰입도를 높일 수 있습니다.
모든 메시지와 이벤트는 Redis에 저장 후 즉시 소비되어, 초저지연 실시간 통신을 제공합니다.

또한, 처리된 메시지와 이벤트는 DynamoDB에 영구 저장되어
필요 시 애플리케이션에서 이전 대화 내역이나 상태를 조회할 수 있습니다.
이를 통해 실시간성과 데이터 영속성을 모두 만족합니다.

🎥 데모

chat-es.mp4
screen-01 screen-02

💡 주요 기능

주요 기능 내용
WebSocket을 통한 실시간 채팅 WebSocket을 사용하여 사용자 간에 실시간으로 채팅할 수 있습니다.
다대다 채팅 기능 여러 사용자가 동시에 참여하여 채팅을 진행할 수 있는 다대다 채팅 기능을 제공합니다.
채팅 방 생성 및 삭제 사용자가 새로운 채팅 방을 생성하고, 필요에 따라 방을 삭제할 수 있습니다.
채팅 방 내 사용자 추가 채팅 방 생성 후, 다른 사용자를 해당 방에 추가할 수 있는 기능을 제공합니다.
입력 타이핑 상태 표시 사용자가 타이핑 중일 때 다른 사용자에게 그 상태를 실시간으로 표시해줍니다.
채팅방 내용 키워드 검색 채팅방 내의 메시지를 키워드를 통해 쉽게 검색할 수 있는 기능을 제공합니다.
채팅 대화 디스크 저장 모든 채팅 내용을 디스크에 저장하여 나중에 다시 볼 수 있게 합니다.

📊 다이어그램

🏗️ Architecture Diagram

architecture

  • 백엔드
    • Apollo Server: Express 플러그인으로 GraphQL Query / Mutation / Subscription / Resolver 처리
    • Kong API Gateway: 클라이언트 요청 라우팅 및 로드밸런싱, WebSocket 업그레이드 지원
    • NestJS 서버: 이벤트 처리, 비즈니스 로직 실행
    • Business: 클라이언트 요청 처리, 레플리카 간 Redis Adapter를 통해 세션 동기화
    • Consumers: Redis Streams를 구독하여 이벤트를 처리하고, DynamoDB와 Elasticsearch로 데이터 동기화
    • Redis
      • Pub/Sub: 서버 레플리카 간 메시지 동기화
      • Streams: 레디스 스트림에 메시지 저장 후 소비
      • Storage: 캐싱 및 데이터 저장소 역할
    • DynamoDB: Redis Streams에서 전달된 이벤트 데이터를 영구 저장
    • Elasticsearch: Redis Streams에서 전달된 이벤트 데이터를 색인하여, 검색어 기반 대화 내역 조회 지원
  • 프론트엔드
    • Apollo Client: GraphQL Query/Mutation/Subscription 처리, 클라이언트 캐싱, 데이터 페칭
    • NGINX: 프론트엔드 애플리케이션 정적 파일 서빙
    • Vue: UI 렌더링 및 상태 관리
    • Quasar: 웹 UI 구성 및 스타일링
  • 데이터 흐름
    1. 클라이언트(Vue)에서 Apollo Client로 GraphQL Query / Mutation / Subscription 요청 전송
    2. Kong API Gateway가 WebSocket 업그레이드를 처리하고 요청을 NestJS 서버 레플리카로 전달
    3. Apollo Server + NestJS에서 요청 처리 후 비즈니스 로직 실행
      • Query / Mutation: 비즈니스 로직 실행 후 응답 반환
      • Subscription: Redis Pub/Sub를 통해 실시간 이벤트 브로드캐스트
      • Redis Storage에서 데이터 조회/저장
    4. Consumer가 이벤트를 읽어 DynamoDB에 데이터 저장
    5. Consumer가 이벤트를 읽어 Elasticsearch에 색인
    6. 클라이언트는 Subscription을 구독하고, 서버에서 발행된 메시지를 실시간 수신
    7. 화면에 실시간 업데이트 (채팅 메시지, 타이핑 상태, 방 생성)

📡 Communication Diagram

classDiagram
    direction RL

    class ApolloClient {
        +currentUser // 현재 사용자
        +currentRoom // 현재 방
        +messages // 수신한 메시지 목록
        +subscribeSystem() // 시스템 메시지 구독
        +subscribeRoomCreated() // 방 생성 구독
        +subscribeMessage() // 채팅 메시지 구독
        +subscribeTyping() // 타이핑 상태 구독
        +subscribeUserPresence() // 사용자 상태 구독
        +queryGetHistory() // 메시지 기록 요청
        +mutationCreateRoom() // 방 생성 요청
        +mutationJoinRoom() // 방 참가 요청
        +mutationLeaveRoom() // 방 퇴장 요청
        +mutationSendMessage() // 메시지 전송
        +mutationTyping() // 타이핑 상태 전송
    }

    class ApolloServer {
        +schema // Query, Mutation, Subscription 정의
        +Query.getChatHistory() : [Message!]!
        +Mutation.createRoom() : String!
        +Mutation.joinRoom() : Boolean!
        +Mutation.leaveRoom() : Boolean!
        +Mutation.message() : Boolean!
        +Mutation.typing() : Boolean!
        +Subscription.message(roomId) : Message!
        +Subscription.roomCreated(userId) : Room!
        +Subscription.system(input) : Message!
        +Subscription.typing(roomId) : Message!
        +Subscription.userPresence() : [String!]!
    }

    ApolloClient --> ApolloServer : HTTP (Query/Mutation)
    ApolloClient --|> ApolloServer : WebSocket (Subscription)
Loading

🛰️ GraphQL Schema Diagram

GraphQL Voyager는 GraphQL 스키마를 시각적으로 탐색하고 구조를 이해할 수 있도록 돕는 정적/인터랙티브 시각화 도구
타입과 타입 간 참조를 그래프 형태로 표현

voyager
GraphQL Voyager
classDiagram
  direction LR

  class Message {
    +content : String
    +roomId : String!
    +userId : String!
  }

  class Room {
    +participants : [String!]!
    +roomId : String!
  }

  class SystemInput {
    +roomId : String
    +userId : String
  }

  class Query {
    +users() : [String!]!
    +history(roomId: String!) : [Message!]!
    +search(keyword: String!, userId: String!) : [Message!]!
  }

  class Mutation {
    +createRoom(hostId: String!, participants: [String!]!) : String!
    +joinRoom(roomId: String!, userId: String!) : Boolean!
    +leaveRoom(roomId: String!, userId: String!) : Boolean!
    +message(content: String!, roomId: String!, userId: String!) : Boolean!
    +setUser(id: String!) : Boolean!
    +typing(roomId: String!, userId: String!) : Boolean!
  }

  class Subscription {
    +message(roomId: String!) : Message!
    +roomCreated(userId: String!) : Room!
    +system(input: SystemInput!) : Message!
    +typing(roomId: String!) : Message!
    +userPresence : [String!]!
  }

  %% 관계
  Mutation --> Room : create/join/leave
  Mutation --> Message : send
  Subscription --> Message : publishes
  Subscription --> Room : publishes
  Subscription --> SystemInput : uses
  Message --> Room : belongs to
  Query --> Message : get
Loading

📐 Sequence Diagram

sequenceDiagram
  participant Web1
  participant Web2
  participant APIGateway@{ "type" : "queue" }
  participant Servers@{ "type" : "collections" }
  participant RedisStore@{ "type": "database" }
  participant RedisPubSub@{ "type" : "queue" }
  participant RedisStreams@{ "type" : "queue" }
  participant DynamoDB@{ "type" : "database" }
  participant Elasticsearch@{ "type" : "database"}

  activate Servers

  %% muation %%
  rect rgba(155, 198, 142, 0.7)
    Web1 ->> APIGateway: mutation setUser(id)
    APIGateway ->> Servers: forward
    Servers ->> RedisStore: store
  end

  %% 구독 %%
  rect rgb(191, 223, 255, 0.7)
    Web1 ->> APIGateway: subscription roomCreated(userId)
    APIGateway ->> Servers:
    Servers ->> RedisPubSub: subscribe
  end

  par For each room
    rect rgba(155, 198, 142, 0.7)
      Web1 ->> APIGateway: mutation createRoom(hostId, participants)
      APIGateway ->> Servers:
      Servers ->> RedisStore: [roomId, members]
      Servers ->> RedisPubSub: publish roomCreated(room)
      RedisPubSub ->> RedisPubSub: publish
    end

    opt Room created
      rect rgba(155, 198, 142, 0.7)
        Web2 ->> APIGateway: mutation joinRoom(roomId, userId)
        APIGateway ->> Servers:
        Servers ->> RedisPubSub: publish roomCreated(room)
      end

      rect rgb(233, 191, 201, 0.7)
        Web2 ->> APIGateway: query history(roomId)
        APIGateway ->> Servers:
        Servers ->> DynamoDB: getMessageHistory()
        DynamoDB -->> Servers: MessageHistory
        Servers -->> APIGateway:
        APIGateway -->> Web2: [messages]
      end

      rect rgb(191, 223, 255, 0.7)
        Web1 ->> APIGateway: subscription message(roomId)
        Web2 ->> APIGateway:
        APIGateway ->> Servers:
        Servers ->> RedisStreams: subscribe

        Web1 ->> APIGateway: subscription typing(roomId)
        Web1 ->> APIGateway: subscription system(input)
        APIGateway ->> Servers:
        Servers ->> RedisPubSub: subscribe
      end

      rect rgba(155, 198, 142, 0.7)
        loop Multiple events
          Web1 ->> APIGateway: mutation message(roomId, userId, content)
          APIGateway ->> Servers:
          Servers ->> RedisStreams: publish message(message)
          RedisStreams ->> RedisStreams: publish

          rect rgba(255,235,200,0.7)
            loop Sync Consumer
              RedisStreams ->> DynamoDB: store
              RedisStreams ->> Elasticsearch: indexing
            end
          end

          Web1 ->> APIGateway: mutation typing(roomId, userId)
          APIGateway ->> Servers:
          Servers ->> RedisPubSub: publish typing(ping)
          RedisPubSub ->> RedisPubSub: publish
        end
      end
    end

  and Search
    rect rgb(233, 191, 201, 0.7)
      Web1 ->> APIGateway: query search(userId, keyword)
      APIGateway ->> Servers:
      Servers ->> Elasticsearch: searchByKeyword()
      Elasticsearch -->> Servers: search result
      Servers -->> APIGateway:
      APIGateway -->> Web1: [messages]
    end

  and Sync GraphQL Subscriptions
    rect rgba(255,235,200,0.7)
      RedisStreams -->> Servers: asyncIterator
      RedisPubSub -->> Servers: asyncIterator
      Servers -->> APIGateway: push
      APIGateway -->> Web1: [roomCreated| message | typing | system]
      APIGateway -->> Web2:
    end
  end
  deactivate Servers
Loading

📂 폴더 구조

열기
Chat-Service
├─ infra
│  └─ docker-compose.yml
├─ project
│  ├─ client
│  │  ├─ Dockerfile
│  │  ├─ nginx.conf
│  │  └─ codegen.yml
│  ├─ server
│  │  ├─ Dockerfile
│  │  └─ graphql
│  │     └─ schema.gql
│  └─ consumers
│     ├─ stream-dynamo-consumer
│     │  └─ Dockerfile
│     └─ docker-compose.yml
├─ .env
├─ .prettierrc
├─ docker-compose.yml
└─ README.md

🗂️ 서브 프로젝트

프로젝트 저장소 설명 브랜치/버전
Backend /Chat-Service--Backend/tree/graphql GraphQL + Redis 기반 실시간 채팅 서버 graphql / v2.3.0
Frontend /Chat-Service--Frontend/tree/graphql Vue + Vite 클라이언트 graphql / v2.2.0
Consumers /Chat-GraphQL/tree/consumers Redis-Streams에서 DynamoDB/Elasticsearch로 동기화 main / v2.0.0
Infra /Chat-GraphQL/tree/infra 인프라 정의 main / v1.3.0

🚀 실행 방법

$ git clone https://github.com/NarciSource/Chat-GraphQL.git

$ cd Chat-GraphQL

$ docker-compose up -d

🖥️ 접속 안내

환경 URL
web http://localhost:80
server healthcheck http://localhost:3000
graphql schema http://localhost:3000/voyager
graphql playground http://localhost:3000/graphql
api gateway 대시보드 http://localhost:3002
elasticsearch 대시보드 http://localhost:5601

About

GraphQL 채팅 서비스

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Contributors 2

  •  
  •