WithU — 커플 전용 실시간 소통 플랫폼

실시간 채팅 · 포토앨범 · 캘린더를 하나로. React + Electron + NestJS로 구현된 크로스플랫폼 데스크톱/웹 하이브리드 앱입니다.

React 19 · Vite Electron 37 NestJS 11 · Socket.IO TypeORM · MySQL
개발 기간
2024.04 – 2024.05
역할
풀스택 (개인)
핵심 임팩트
실시간 메시지 동기화로 UX 개선 · 오류율 ↓
주요 기술
React, Electron, NestJS, Socket.IO
ws ▶ 채팅 동기화

Partner: 오늘 저녁 7시 어때?  
You: 좋아! 캘린더에 일정 추가했어 📅  
System: 새로운 사진 3장이 앨범에 업로드되었습니다.  

주요 기능 (핵심만 빠르게)

💬 실시간 채팅

WebSocket Gateway로 파트너 상태/읽지 않은 메시지 카운트/알림을 실시간 처리.

  • 상태 동기화, 타이핑 인디케이터
  • 메시지 타입: 텍스트/알림
  • 룸 단위 전송 & 영속화

📸 포토앨범

Multer 기반 업로드, 앨범/사진 코멘트, 갤러리 창(Electron) 연동.

  • 다중 파일 업로드/프리뷰
  • 앨범 페이징/검색
  • 코멘트/메타정보

📅 캘린더

기념일/일정 공유, 지도(카카오/구글) 연동, 오늘 일정 빠른 보기.

  • 기간 이벤트 지원
  • 타임존 정규화
  • 알림 훅 제공

케이스 스터디 — Problem → Solution → Impact

Problem

일정·사진·채팅 앱을 번갈아 쓰며 소통 맥락이 끊기고, 실시간 동기화 지연으로 사용자 만족도가 하락.

  • 앱 전환 비용 · 데이터 불일치
  • 사진 공유/저장 흐름 분절

Solution

데스크톱 친화 UI와 WebSocket 게이트웨이로 채팅·앨범·캘린더를 하나의 상태 모델로 통합.

  • Electron + React로 동일 UX
  • Socket.IO로 실시간 상태
  • TypeORM로 일관된 영속성

Impact

메시지/사진 공유 동선 단축, 실시간 동기화 체감 향상(정량 수치로 교체 권장: 예) 평균 응답시간 120ms → 60ms).

  • 교차기능 통합으로 온보딩 단축
  • 코드 재사용으로 유지보수성↑

아키텍처

React(Vite) + Electron ↔ Socket.IO(Client) ↔ NestJS(WebSocket/REST) ↔ MySQL(TypeORM)

  • 게이트웨이: ChatGateway, Album/Calendar API
  • JWT 인증, 오류/로깅 일원화
  • 갤러리 별도 창(Electron) 핸들러
React (Vite)
 ├─ Router / Hooks
 ├─ Socket.IO Client
 └─ Electron (Main/Preload)
      │
      ▼
NestJS (REST + WS)
 ├─ Auth / Chat Gateway
 ├─ Album / Calendar
 └─ TypeORM → MySQL

기술 스택

Frontend

  • React 19.1.0, TypeScript
  • Vite 7, React Router
  • socket.io-client, framer-motion
  • Electron 37

Backend

  • NestJS 11.1.3, TypeORM 0.3.25
  • MySQL2, Multer, Bcrypt
  • Socket.IO, JWT

DevOps & Tools

  • AWS EC2(예시), Electron Builder
  • ESLint, Nodemon, Concurrently, cross-env

코드 하이라이트

📁 실제 프로젝트 구조
// WithU 디렉토리 구조(요약)
withu/
├── be/                           # NestJS 백엔드
│   ├── src/
│   │   ├── auth/                 # 인증 및 파트너 매칭
│   │   ├── chat/                 # WebSocket 게이트웨이
│   │   ├── album/                # 포토앨범
│   │   ├── calendar/             # 캘린더
│   │   └── main.ts
└── fe/                           # React + Electron
    ├── electron/ (main/preload)
    └── src/
        ├── components/pages (Home/Message/PhotoAlbum/Calendar)
        ├── common/{customhooks,layout}
        └── Socket.ts
🔧 useCalendar.tsx — 일정 훅 (발췌)
// 시간대 정규화 + 오늘 일정 필터
export const useCalendar = () => {
  const [schedules, setSchedules] = useState<Schedule[]>([]);
  const loadSchedules = async () => {
    try {
      const res = await request({ url: '/u/calendar/events', method: RequestMethod.GET });
      const normalized = (res || []).map(s => ({ ...s, startDate: s.startDate?.split('T')[0] + 'T' + s.startDate?.split('T')[1]?.split('.')[0] }));
      setSchedules(normalized);
    } catch { /* ... */ }
  };
  const getToday = () => { /* ... */ };
  return { schedules, loadSchedules, getToday };
};
⚡ chat.gateway.ts — WebSocket (발췌)
@WebSocketGateway({ cors: { origin: 'http://localhost:5173', credentials: true } })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server: Server;
  handleConnection(client: Socket){ /* ... */ }
  handleDisconnect(client: Socket){ /* ... */ }
  @SubscribeMessage('sendPartnerMessage')
  handleSendPartnerMessage(data: { roomCd:string; senderCd:number; content:string; type:'message'|'alarm' }){
    const payload = { id: uuidv4(), message: data.content, sender: String(data.senderCd), ts: new Date(), type: data.type==='alarm'?'alarm':'text' };
    this.server.to(data.roomCd).emit('newPartnerMessage', payload);
  }
}
🖥 electron/main.ts — 메인 프로세스 (발췌)
import { app, BrowserWindow, ipcMain } from 'electron';
function createWindow(){
  const win = new BrowserWindow({ width:1200, height:800, titleBarStyle: 'hidden', titleBarOverlay:{ color:'#bd2b2b', symbolColor:'#fff' } });
  process.env.NODE_ENV==='development' ? win.loadURL('http://localhost:5173') : win.loadFile('dist/index.html');
}
app.whenReady().then(createWindow);

배포 & 성과

배포
AWS EC2 (도메인/아이피는 포트폴리오 공개 시 마스킹 권장)
모듈화
커스텀 훅/컴포넌트 분리로 재사용성↑
실시간
Socket.IO로 상태/메시지 동기화
package.json (요약)
{
  "name": "withu",
  "main": "dist-electron/main.js",
  "proxy": "http://<server-ip-or-domain>:3000",
  "scripts": { "dev": "concurrently "npm:dev:react" "npm:dev:ts" "npm:dev:electron"" },
  "dependencies": { "react": "^19.1.0", "electron": "^37.2.0", "socket.io-client": "^4.8.1" }
}

Contact

협업과 코드 리뷰를 좋아합니다. 필요 시 실사용 데모·코드 워크스루 제공 가능합니다.