#1. 실시간 챗봇 시스템 만들기: 모바일-웹 서버까지 풀스택 연동해보기

2025. 7. 15. 16:30 · Challenge/KDT-핀테크 인턴십

1. 전체 흐름 한눈에 보기

[고객(Mobile)]   ←→   [백엔드(Node.js)]   ←→   [상담원(Web)]
      ↓ 1. 메시지 전송                             ↑ 3. 메시지 전달
      └────────→ 2. DB 저장 및 브로드캐스트 ─────────┘

 

- 모바일 앱에서 고객이 메시지를 보내면,

- 백엔드 서버가 메시지를 저장하고,

- 웹 대시보드에서 상담원이 실시간으로 응답하는 구조!

 

 

2. 실제 동작 예시

1) 고객(Mobile) → 백엔드로 메시지 전송

// socketService.sendMessage(chatRoomId, content)
socket.emit('user_message', {
  chatRoomId,
  senderType: 'USER',
  content,
  messageType: 'TEXT'
});

2) 백엔드(Node.js)에서 메시지 수신 및 DB 저장, 전체에 브로드캐스트

io.on('connection', (socket) => {
  socket.on('user_message', async (data) => {
    // 1. DB에 메시지 저장
    const messageId = await messageService.saveMessage(
      data.chatRoomId,
      data.senderType,
      data.content,
      data.messageType || 'TEXT'
    );

    // 2. 메시지 정보 구성
    const messageData = {
      id: messageId,
      chatRoomId: data.chatRoomId,
      senderType: data.senderType,
      content: data.content,
      messageType: data.messageType || 'TEXT',
      read: false,
      timestamp: new Date()
    };

    // 3. 같은 채팅방에 접속한 모든 클라이언트(고객+상담원)에게 메시지 전송
    io.emit('user_message', messageData);
    // 또는 특정 방에만: io.to(`room_${data.chatRoomId}`).emit('user_message', messageData);
  });
});

 

3) 상담원(Web)에서 메시지 수신

// socketService.onMessage((message) => { ... })
socket.on('user_message', (message) => {
  // 1. 메시지 목록에 추가
  setMessages(prev => [...prev, message]);
  // 2. UI에 실시간 표시
});

 

4) 상담원(Web) → 백엔드 → 고객(Mobile)로 응답

// socketService.sendMessage(chatRoomId, content, 'ADMIN')
socket.emit('user_message', {
  chatRoomId,
  senderType: 'ADMIN',
  content,
  messageType: 'TEXT'
});

- Backend
: 위와 동일하게 DB 저장 후 io.emit('user_message', ...)로 브로드캐스트

- Mobile

// socketService.onMessage((message) => { ... })
socket.on('user_message', (message) => {
  setMessages(prev => [...prev, message]);
});

 

 

 

3. 핵심 요약

 

- 모바일/웹 모두 `socket.emit('user_message', ...)`로 메시지 전송
- 백엔드는 메시지를 받아 DB에 저장하고, `io.emit('user_message', ...)`로 모든 클라이언트에 전달
- 모바일/웹 모두 `socket.on('user_message', ...)`로 실시간 메시지 수신

 

 

4. 실제 코드 구조 예시

(1) Mobile (React Native)

// 메시지 전송
socket.emit('user_message', { chatRoomId, senderType: 'USER', content, messageType: 'TEXT' });
// 메시지 수신
socket.on('user_message', (message) => { ... });

 

(2) Backend (Node.js)

io.on('connection', (socket) => {
  socket.on('user_message', async (data) => {
    // DB 저장
    // io.emit('user_message', messageData);
  });
});

 

(3) Web (Next.js)

// 메시지 전송
socket.emit('user_message', { chatRoomId, senderType: 'ADMIN', content, messageType: 'TEXT' });
// 메시지 수신
socket.on('user_message', (message) => { ... });

 


5. 핵심 로직

1) Socket.IO 연결 관리

// 백엔드에서 가장 중요한 부분
io.on('connection', (socket) => {
  socket.on('user_message', async (data) => {
    // 1. DB 저장
    const messageId = await messageService.saveMessage(...);
    
    // 2. 실시간 브로드캐스트
    io.emit('user_message', messageData);
  });
});

 

핵심: 메시지를 받자마자 DB에 저장하고, 즉시 모든 클라이언트에게 전달하는 것이 중요

 

2) 채팅방 기반 메시지 관리

-- messages 테이블 구조
CREATE TABLE messages (
  id INT AUTO_INCREMENT PRIMARY KEY,
  chat_room_id INT NOT NULL,  -- 어떤 채팅방의 메시지인지
  sender_type ENUM('USER', 'ADMIN', 'BOT') NOT NULL,  -- 누가 보낸 메시지인지
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

 

- 핵심: chat_room_id로 메시지를 그룹화하고, sender_type으로 사용자 구분이 핵심

 

 

3) 실시간 상태 동기화

// 프론트엔드에서 메시지 수신 시
socket.on('user_message', (message) => {
  setMessages(prev => [...prev, message]);  // 즉시 UI 업데이트
});

 

- 핵심: 소켓으로 받은 메시지를 즉시 UI에 반영해야 실시간성이 보장됩니다.

 

 

 

6. 중요 로직 및 개념 정리

 

1) Socket.IO 연결 상태 관리

// 연결 상태 확인
socket.on('connect', () => console.log('연결됨'));
socket.on('disconnect', () => console.log('연결 끊김'));
socket.on('error', (error) => console.error('소켓 오류:', error));

- 단순히 socket은 on이라는 명령어로 상태 확인이 가능

 

2) 메시지 전송 시 로딩 상태

const [isLoading, setIsLoading] = useState(false);

const sendMessage = async () => {
  setIsLoading(true);
  try {
    socket.emit('user_message', data);
  } finally {
    setIsLoading(false);
  }
};

- try-catch를 안쓴 이유? : 에러를 굳이 다루지 않아도 되는 상황이기 때문에!

  • socket.emit()은 일반적으로 비동기지만 Promise를 반환하지 않는 함수.
  • 즉, 에러가 발생할 가능성도 낮고, 설사 에러가 나더라도 그걸 굳이 처리하지 않아도 되는 단순한 로직
  • 이 경우 catch 없이 finally만 써서 로딩 상태만 정확히 정리하려는 목적
  • 에러 메세지를 보거나 로그를 남겨야 한다면 catch를 꼭 써야 한다!

 

 

3) DB 연결 풀 사용

// 백엔드에서 MySQL 연결 풀 사용
const pool = mysql.createPool({
  connectionLimit: 10,
  // ... 기타 설정
});

풀(pool) : 미리 여러 개의 DB 연결을 생성해두고, 필요할 때 꺼내서 쓰는 저장소

MySQL과 같은 데이터베이스는 연결을 맺는 과정 자체가 꽤 무겁고 시간/자원을 많이 소모하는데,
만약 매 요청마다 DB에 connect → query → disconnect 과정을 거치면 다음과 같은 문제가 발생한다:

  • 성능 저하 (매번 연결 생성/종료 비용)
  • 높은 지연 시간
  • MySQL의 동시 연결 수 제한 초과 가능

 

4) 에러 핸들링

// 프론트엔드에서 소켓 에러 처리
socket.on('error', (error) => {
  Alert.alert('연결 오류', '서버와의 연결이 끊어졌습니다.');
});

- 에러 핸들링은 필수

 

 

5) 메시지 스크롤 자동화

// 새 메시지가 오면 자동으로 맨 아래로 스크롤
useEffect(() => {
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);

=> 메시지 최신화를 위해 업데이트가 될 때마다 맨 아래로 스크롤되는 기능

 

 

6) 타입 안전성 확보

// TypeScript로 타입 정의
interface Message {
  id: number;
  chat_room_id: number;
  sender_type: 'USER' | 'ADMIN' | 'BOT';
  content: string;
  created_at: string;
}

 

 

 

7) 채팅방 입장/퇴장 관리

// 채팅방 입장
socket.emit('join_room', { chatRoomId });
// 채팅방 퇴장
socket.emit('leave_room', { chatRoomId });

 

- 주요 소켓 명령어 정리 (클라이언트 & 서버 공통)

명령어 사용 위치 설명
socket.emit(event, data) 클라이언트/서버 이벤트 이름과 데이터를 보냄
socket.on(event, callback) 클라이언트/서버 특정 이벤트를 수신하고 처리
socket.off(event) 클라이언트/서버 이벤트 리스너 제거
socket.connect() 클라이언트 소켓 서버와 수동 연결 시도
socket.disconnect() 클라이언트 수동 연결 종료
socket.id 클라이언트/서버 연결된 소켓 고유 ID

 

 

8) 메시지 읽음 표시

// 백엔드에서 메시지 읽음 처리
app.post('/api/messages/:chatRoomId/read', async (req, res) => {
  await messageService.markAllMessagesAsRead(chatRoomId);
});

 

 

 

9) 성능 최적화

// 메시지 목록 가상화 (대량 메시지 처리 시)
import { VirtualizedList } from 'react-native';

 

- 메시지가 많아질 때를 대비해 VirtualizedList를 설치해보았다.

 

VirtualizedList는?

: React Native에서 많은 양의 데이터를 효율적으로 렌더링하기 위해 사용하는 리스트 컴포넌트

화면에 보이는 항목만 실제로 렌더링하고, 나머지는 가상으로 처리하여 성능을 높이는 리스트

 

즉, 메시지가 1,000개 있어도 화면에 보이는 10개 정도만 렌더링해서 불필요한 리소스 낭비를 줄일 수 있다.

Flat한 ScrollView 모든 아이템을 한 번에 렌더링함. 메시지가 많아질수록 느려짐.
VirtualizedList 보이는 항목만 렌더링하고, 나가면 메모리에서 제거함. 매우 효율적.

 

- 특징

기능 설명
성능 향상 수천 개의 메시지를 스무스하게 처리 가능
메모리 최적화 화면 밖 아이템은 메모리에서 제거됨
고유한 렌더링 함수 사용 getItem, getItemCount 등을 통해 커스터마이징 가능
 
# 예시
import { VirtualizedList, Text } from 'react-native'; 
const messages = [...Array(1000).keys()].map(i => `Message ${i}`); 
const getItem = (data, index) => data[index]; 
const getItemCount = data => data.length; 

<VirtualizedList 
    data={messages} 
    initialNumToRender={10} 
    renderItem={({ item }) => <Text>{item}</Text>} 
    keyExtractor={(item, index) => index.toString()} 
    getItem={getItem} 
    getItemCount={getItemCount} 
/>

 

찾아보다보니 FlatList를 쓰기도 한다고 한다.

그래서 해당 항목과 어떤 게 다르고 어떤 걸 사용하면 좋은지를 알아볼까 한다.

 

 

# FlatList vs VirtualizedList

 

항목 FlatList VirtualizedList
일반적 사용 추천 직접 설정 필요
커스텀 리스트 구조 제한적 커스터마이징 가능
성능 동일하게 가상화 동일
유연성 간단하게 사용 가능 고급 제어용

📌 사실상 FlatList는 내부적으로 VirtualizedList를 래핑한 고수준 컴포넌트다.

FlatList는 들어보기만 했던 적이 있는 것 같은데, 어떤 프로젝트에서 추천을 하는지에 따라서는 예시 파일 + 최적화로 좀 더 알아봐야겠다

 

 

 

8. 마무리 및 참고자료

프로젝트 완성도

  • 실시간 채팅: Socket.IO로 실시간 메시지 전송/수신
  • 데이터 영속성: MySQL로 메시지 저장 및 조회
  • 사용자 관리: 게스트 로그인 및 채팅방 생성
  • 상담원 대시보드: 웹에서 실시간 상담 관리
  • 모바일 앱: React Native로 고객용 채팅 앱

 

향후 개선 방향 + 추가하고 싶은 프로그램

  1. AI 챗봇 연동
  2. Jwt 로그인
  3. 매크로 등록
  4. 파일 전송: 이미지, 파일 업로드 기능
  5. 푸시 알림: FCM으로 푸시 알림 구현
  6. 통계 대시보드: 상담 통계, 응답 시간 분석

 

참고 자료

  • Socket.IO 공식 문서
  • React Native 공식 문서
  • Next.js 공식 문서
  • MySQL2 공식 문서

 

유용한 라이브러리

  • 프론트엔드: lucide-react (아이콘), react-hook-form (폼 관리)
  • 백엔드: bcrypt (비밀번호 암호화), jsonwebtoken (JWT)
  • 데이터베이스: mysql2 (MySQL 클라이언트)

 

핵심 학습 포인트

  1. 실시간 통신: Socket.IO의 이벤트 기반 통신 이해
  2. 상태 관리: React/React Native의 상태 관리 패턴
  3. 데이터베이스 설계: 채팅 시스템에 최적화된 DB 스키마
  4. 에러 처리(핸들러): 네트워크 오류, DB 오류 등 예외 상황 처리
  5. 사용자 경험: 로딩 상태, 에러 메시지 등 UX 고려사항

 


 

마무리

아니 .. ㅇ니.. 아니... 아 허리아파.. 

이게 다 뭐야

 

생각해보니까 저 소켓 처음 받아오는거였네요..?

 

근데 생각보다 어렵진 않아서 다행이었다....(?) 백엔드 재밌네요 희희

백엔드 공부는 꾸준히 해볼 거 같아요..!!! 아 근데 스프링은 할 자신 없어요

우당탕탕 백엔드 적응기 도전~~~~

 

 

저작자표시 비영리 변경금지 (새창열림)

'Challenge > KDT-핀테크 인턴십' 카테고리의 다른 글

#5. 실시간 챗봇 시스템 만들기: 중간 피드백과 앞으로의 방향성  (2) 2025.07.29
#4. 실시간 챗봇 시스템 만들기: 타이핑 인디케이터 만들기  (2) 2025.07.29
#3. 실시간 챗봇 시스템 만들기: socket 실시간 연동 누락 이슈  (1) 2025.07.28
#2. 실시간 챗봇 시스템 만들기: MySql에서 NoSql로, 채팅 메시지 분리  (4) 2025.07.21
#0. 실시간 챗봇 시스템 만들기: Node.js + MySQL + Socket.IO 환경 세팅  (6) 2025.07.11
'Challenge/KDT-핀테크 인턴십' 카테고리의 다른 글
  • #4. 실시간 챗봇 시스템 만들기: 타이핑 인디케이터 만들기
  • #3. 실시간 챗봇 시스템 만들기: socket 실시간 연동 누락 이슈
  • #2. 실시간 챗봇 시스템 만들기: MySql에서 NoSql로, 채팅 메시지 분리
  • #0. 실시간 챗봇 시스템 만들기: Node.js + MySQL + Socket.IO 환경 세팅
ming0o
ming0o
Jr. 프론트엔드 개발자의 성장(개발)일지입니다. 이전 : https://velog.io/@ming0o
  • ming0o
    주토피아
    ming0o
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Goals
        • 2026 목표 및 월간 회고
      • 개발자로 살아남기
        • 트러블슈팅
        • 코드 리뷰
        • 기술 정리
      • Dev
        • Vue
        • Node & Express
        • React Native
      • Challenge
        • KDT-핀테크 인턴십
        • 한이음 드림업
      • Learning
        • JavaScript
        • Algorithm
        • CS
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Vue
    try-catch문
    Router
    Push
    타입에러
    트러블슈팅
    replace
    수동배포
    이기고만다
    지피티
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ming0o
#1. 실시간 챗봇 시스템 만들기: 모바일-웹 서버까지 풀스택 연동해보기
상단으로

티스토리툴바