지속적 연결성 구현 시 Socket.io의 대안: Pusher, Supabase Realtime, Vercel KV 비교

넥스트플랫폼 동준상 대표 (naebon@naver.com)

2026.04.26 / 동준상.넥스트플랫폼
(AWS SAA, AWS AIF, GCP GenAI Leader)

실시간 공유가 핵심 가치인 웹 서비스가 있다면, Socket.io로 지속적인 연결성을 구현하여 다수의 사용자에게 실시간성을 제공할 수 있습니다. 하지만 최근 SaaS 대부분은 서버리스 환경에서 실행되므로 Socket.io를 구현하려면 Render, Railway 등 서버리스가 아닌 Node 환경에 해당 부분을 배포해야 합니다. 이번 포스트는 서버리스 친화적인 지속적 연결성 구현 방법에 대해 함께 알아봅니다.


1. NextPads에서 실시간(Socket)이 필요한 이유

저는 제 강연 업무에서 활용하기위해 실시간 아이디어 공유 패드 NextPads를 만들고 있습니다. NextPads는 강연자와 학습자가 실시간으로 아이디어를 나누는 선반형 공유 보드로 현재는 MVP 빌드 단계이며, 핵심 제안 가치는 ‘다수의 학습자가 저장 없이 실시간으로 동기화되는 공유 보드’입니다.

강연 중 학습자 A가 카드를 올렸을 때, 강연자와 학습자 B·C의 화면에 즉각 반영되어야 합니다. Socket 없이 REST API만 쓰면 두 가지 방법밖에 없는데:

방법문제
수동 새로고침사용자가 직접 F5 눌러야 함 → 강연 흐름 끊김
폴링 (5초마다 API 호출)지연 + 서버 부하 + Vercel 함수 호출 비용 급증

강연 중 100명이 동시에 5초 폴링을 하면 분당 1,200번 API 호출이 발생합니다. 실시간 연결 하나가 훨씬 효율적입니다.


2. Vercel 등 서버리스 환경에서 Socket.io가 안 되는 이유

Vercel의 API Routes는 서버리스 함수(Serverless Function) 입니다. 요청이 들어오면 실행되고 즉시 종료됩니다. Socket.io는 서버가 클라이언트와 지속적인 연결을 유지해야 하는데, 서버리스는 구조적으로 이를 지원하지 않습니다.

Socket.io 방식:
클라이언트 ←────── 지속 연결 (수십 분) ──────→ 서버
                  연결이 살아있는 동안 이벤트 교환

Vercel 서버리스:
클라이언트 → 요청 → [함수 실행 → 응답 → 함수 종료]
                     최대 수십 초, 연결 유지 불가

3. 서버리스 환경에서 실시간성 구현 방법

옵션1. Pusher

Pusher는 실시간 메시지를 전달해주는 외부 서비스입니다. Vercel 함수에서 카드가 생성되면 Pusher에 이벤트를 보내고, Pusher가 연결된 모든 클라이언트에 브로드캐스트합니다.

카드 생성
  → POST /api/cards (Vercel 함수)
      → DB 저장
      → pusher.trigger('board-xxx', 'card:created', cardData)  ← 추가
  → 클라이언트들이 Pusher 채널 구독 중 → 즉시 수신

장점: Vercel 그대로, 별도 서버 없음, 무료 플랜(200 동시접속, 일 20만 메시지)으로 강의 충분
단점: Pusher 외부 서비스 의존

설정 방법:

npm install pusher pusher-js
# .env에 추가
PUSHER_APP_ID=...
PUSHER_KEY=...
PUSHER_SECRET=...
PUSHER_CLUSTER=ap3   # 서울 근처
VITE_PUSHER_KEY=...
VITE_PUSHER_CLUSTER=ap3

서버 측 (Vercel API Route):

// api/cards.ts
import Pusher from 'pusher';

const pusher = new Pusher({
  appId: process.env.PUSHER_APP_ID!,
  key: process.env.PUSHER_KEY!,
  secret: process.env.PUSHER_SECRET!,
  cluster: process.env.PUSHER_CLUSTER!,
});

export default async function handler(req, res) {
  // ... DB에 카드 저장 ...
  
  // 같은 보드의 모든 클라이언트에 브로드캐스트
  await pusher.trigger(`board-${boardId}`, 'card:created', newCard);
  
  res.json(newCard);
}

클라이언트 측:

// hooks/useRealtime.ts
import Pusher from 'pusher-js';

const pusher = new Pusher(import.meta.env.VITE_PUSHER_KEY, {
  cluster: import.meta.env.VITE_PUSHER_CLUSTER,
});

export function useBoardRealtime(boardId: string) {
  useEffect(() => {
    const channel = pusher.subscribe(`board-${boardId}`);
    
    channel.bind('card:created', (card) => {
      // Zustand store에 카드 추가
      useBoardStore.getState().addCard(card);
    });
    
    return () => pusher.unsubscribe(`board-${boardId}`);
  }, [boardId]);
}

옵션2. Supabase Realtime (DB도 함께 쓸 경우)

현재 사용중인 Neon(PostgreSQL) 대신, Supabase로 교체하면 DB + 실시간을 한 번에 해결할 수 있습니다. Supabase Realtime은 DB 변경사항을 자동으로 브로드캐스트합니다.

// 카드 테이블 변경을 실시간 구독
supabase
  .channel('board-cards')
  .on('postgres_changes', 
    { event: 'INSERT', schema: 'public', table: 'cards', filter: `board_id=eq.${boardId}` },
    (payload) => { addCard(payload.new); }
  )
  .subscribe();

장점: DB + 실시간 일원화, 무료 플랜 충분
단점: Neon → Supabase 마이그레이션 필요


옵션3. Vercel KV + SSE (가장 가벼운 방법)

Server-Sent Events(SSE)는 WebSocket보다 단순하고 서버리스에서도 일부 동작합니다. 단, Vercel의 Edge Runtime을 써야 하고 연결 시간 제한이 있어 불안정할 수 있습니다. 강의용으로는 비추천합니다.


결론: 서버리스 환경에서 지속적 연결성 구현을 위한 권장 플랜

지금 바로 할 것 (1~2시간):

1. pusher.com 가입 → 앱 생성 (무료)
2. npm install pusher pusher-js
3. API Route에 pusher.trigger() 3줄 추가
4. 클라이언트에 useRealtime hook 추가
5. Vercel 환경 변수 4개 추가 → 재배포

Socket.io를 Render에 올리는 것보다 훨씬 빠르고 안정적입니다. Pusher 무료 플랜의 200 동시접속은 강의 규모에 충분하고, 클러스터 ap3이 도쿄 기준이라 한국에서 응답속도도 양호합니다.


핸즈온 노트: Socket에서 Pusher로 전환 핸즈온

NextPads from Socket to Pusher Handson
NextPads: from Socket to Pusher Handson

NextPads는 항상 켜 둔 Node 프로세스 + Socket.io 대신 Vercel 서버리스 + Pusher Channels 조합으로 실시간을 구현합니다. 이 문서는 아키텍처 변화, 기대 효과, 운영·개발 시 주의점을 정리합니다.

1. 기존 구조 (Socket.io)

  • 단일 진입점: server/index.mjs에서 Socket.io가 모든 보드 이벤트를 수신·검증·브로드캐스트.
  • 상태: 메모리 Map(보드·소켓–사용자 매핑)과 socket.join('board:…') 룸으로 동기화.
  • 클라이언트: socket.emit + ACK 콜백, on('board:sync' | 'presence:update').
  • 배포: 프로세스가 24시간 떠 있어야 하므로 Vercel 기본 모델(요청 단위 함수) 과 맞지 않음.

2. 현재 구조 (Pusher)

  • 변이(쓰기): 브라우저는 HTTP POST로만 변경을 보냄 (/api/board/...). 함수가 Neon에서 보드를 읽고·쓴 뒤 성공 시 pusher.trigger로 같은 보드 구독자에게 이벤트를 밀어줌.
  • 읽기·실시간: 클라이언트는 pusher-jspresence-board-{boardId} 를 구독하고, 서버가 쏘는 board-sync 등으로 UI를 갱신합니다(변이 REST 응답으로도 같은 탭 상태를 맞출 수 있음).
  • 프레즌스: Pusher Presence 채널 + POST /api/pusher/auth 로 채널 인증. 탭 단위 ID는 sessionStorage 기반 클라이언트 ID로 join 시 전달.
  • 편집 중 표시: 브라우저 간 가벼운 신호는 client-editing 같은 클라이언트 이벤트(대시보드에서 기능 활성화 필요)로 보완할 수 있음.
  • 진실 공급원: 보드 내용은 항상 DB(Neon) 기준이며, 소켓 서버 메모리에 의존하지 않음.

3. 아키텍처 변경 요약

항목Socket.ioPusher (현재)
연결 모델양방향 TCP/WebSocket, 서버가 세션 유지서버→클라이언트 브로드캐스트 중심; 쓰기는 REST
배포 단위장기 실행 NodeVercel api/*.js + 정적 프론트
동기화 소스메모리 + DB 혼합 가능DB 저장 후 트리거로 일관된 스냅샷 전파
입장·권한소켓 핸들러 내부API 핸들러 + lib/boardMutations.mjs 등 공유 모듈
프레즌스서버 메모리 MapPusher Presence + (필요 시) 별도 이벤트

4. 전환으로 얻는 장점

  1. Vercel 네이티브: 별도 Socket 호스트·스케일링·헬스체크 없이 배포 파이프라인과 맞음.
  2. 상태 일관성: “요청 처리 → DB 반영 → 브로드캐스트” 순서가 명확해 멀티 인스턴스·콜드 스타트에도 메모리 누락 위험이 줄어듦.
  3. 운영 단순화: 실시간 인프라(연결 유지·재연결)를 Pusher가 담당.
  4. 보안 분리: PUSHER_SECRET은 서버만 보유; 브라우저에는 VITE_PUSHER_KEY 등 공개 값만.

5. 주의 사항·트레이드오프

  1. 비용·쿼터: 메시지·동시 접속 한도는 Pusher 플랜에 따름. activity·편집 이벤트는 빈도를 과하게 올리지 않는 것이 좋음.
  2. 패턴 차이: 브라우저에서 “소켓으로 서버에 명령 → 즉시 ACK” 대신 REST 응답 JSON이 ACK 역할을 함. 네트워크 실패·재시도 UX는 fetch 기준으로 설계.
  3. 빌드 타임 환경 변수: VITE_PUSHER_*빌드 시 주입됨. Vercel에 값을 넣은 뒤 재배포해야 프론트에 반영됨.
  4. Client events: client-editing 등을 쓰면 Pusher 앱 설정에서 Client events를 켜야 함.
  5. 로컬 vercel dev: 프로젝트가 Vercel에 링크되어 있고 대시보드 Development Commandnpm run dev처럼 다시 vercel dev를 부르면 재귀 오류가 날 수 있음. 이 저장소는 vercel dev --localvercel.jsondevCommand(프론트는 별도 vite)로 분리하는 방식을 사용함.
  6. 채널 보안: 공개 채널명만 쓰면 링크를 아는 누구나 구독할 수 있음. 요구 수준에 따라 private/presence 인증을 강화할 것.

6. 코드·문서 위치 (참고)

역할경로 예시
Neon 접근·스키마lib/neonBoardStore.mjs
보드 변이·권한lib/boardMutations.mjs
Pusher 서버 SDKlib/pusherServer.mjs
REST 엔드포인트api/board/**, api/pusher/auth.js
클라이언트 API·실시간src/lib/boardApi.ts, src/lib/pusherClient.ts, src/context/BoardRealtimeContext.tsx

답글 남기기