테마 전환

Supabase Realtime 실전: WebSocket 연결 관리와 끊김 후 재연결 전략

Easton editorial illustration: large WebSocket plug, reconnect loop

휴대폰이 한 번 진동했습니다.

고객이 보낸 메시지였습니다. “채팅 앱에서 메시지가 자주 늦게 뜨고, 가끔은 페이지를 새로고침해야 새 메시지가 보인다고 합니다.”

화면을 보자마자 마음이 철렁했습니다. 너무 익숙한 문제였습니다. WebSocket은 끊겼는데 프론트엔드는 그 사실을 모릅니다. 사용자는 계속 입력하고 전송하면서 메시지가 나갔다고 생각하지만, 실제로는 중간에서 모두 사라집니다.

처음 Supabase Realtime을 썼을 때도 이 함정에 빠졌습니다. 당시 협업 화이트보드 프로젝트를 만들고 있었고, 데이터베이스 변경을 구독하는 일은 몇 줄이면 끝난다고 생각했습니다.

supabase.channel('board').on('postgres_changes', ...).subscribe()

그런데 배포한 지 이틀도 안 되어 동료가 말했습니다. “동기화가 자주 멈춰요. 그리던 선이 갑자기 사라집니다.”

확인해 보니 WebSocket 연결이 조용히 끊겨 있었습니다. 오류도 없고, 안내도 없고, 그냥 “죽어” 있었습니다. 그때 깨달았습니다. 실시간 구독은 구독 코드 몇 줄을 쓰는 문제가 아닙니다. 핵심은 연결 관리입니다.

이 글에서는 제가 직접 밟았던 함정과 정리해 둔 해결 방법을 묶었습니다. 특히 WebSocket 연결 생명주기 관리를 중심으로 다룹니다. 시중 튜토리얼에서 가장 적게 설명되는 부분이기도 합니다. 먼저 세 가지 기능을 어떻게 선택할지 보고, Postgres Changes 구독을 손으로 구현한 뒤, 마지막으로 프로덕션 환경의 끊김 후 재연결 전략과 설정 최적화를 살펴보겠습니다.

1. Supabase Realtime의 세 가지 기능, 무엇을 써야 할까?

Supabase Realtime을 처음 접했을 때 저는 세 용어 때문에 헷갈렸습니다. Broadcast, Presence, Postgres Changes입니다. 문서에서는 세 가지가 서로 다른 실시간 기능이라고 말하지만, 실제로는 언제 무엇을 써야 할까요?

결론부터 말하면, 세 기능의 핵심 차이는 데이터가 어디에 저장되는가입니다.

기능데이터 저장대표적인 사용 사례지연
Broadcast메모리에만 존재, 영속화하지 않음클라이언트 간 메시지 전달, 마우스 위치 동기화가장 낮음
Presence메모리 키-값 저장소(CRDT)온라인 사용자 목록, 협업 상태 동기화낮음
Postgres ChangesPostgreSQL 데이터베이스채팅 메시지, 주문 상태 변경중간

표만 보면 여전히 추상적으로 느껴질 수 있습니다. 다르게 말해 보겠습니다.

Broadcast는 “확성기”에 가깝습니다. 한마디를 외치면 듣고 있는 모든 사람이 듣지만, 말이 끝나면 흔적이 남지 않습니다. 그래서 순간적으로 지나가는 데이터에 적합합니다. 예를 들어 협업 편집 중 커서 위치가 그렇습니다. 내가 마우스를 움직이면 다른 사람이 내 커서가 움직이는 것을 보지만, 5초 전에 커서가 어디 있었는지는 아무도 중요하게 생각하지 않습니다.

Presence는 “출석부”와 비슷합니다. 각 사용자가 들어와서 자신의 상태를 적습니다. 온라인인지, 오프라인인지, 편집 중인지 같은 정보입니다. 모두가 이 목록을 볼 수 있습니다. 핵심은 상태가 자동으로 동기화되고, CRDT(Conflict-free Replicated Data Type)를 기반으로 하기 때문에 두 사람이 동시에 같은 데이터를 바꾸는 충돌을 크게 걱정하지 않아도 된다는 점입니다.

Postgres Changes는 “데이터베이스 감시자”입니다. 데이터베이스의 데이터가 바뀌면 알림을 받습니다. 세 가지 중 가장 “무거운” 방식이지만 가장 믿을 만합니다. 데이터가 PostgreSQL에 저장되기 때문에 연결이 끊겼다가 다시 붙어도 메시지를 잃지 않을 수 있습니다.

어떻게 선택할까? 간단한 판단법

스스로 두 가지를 물어보면 됩니다.

  1. 데이터를 영속화해야 하나요?

    • 영속화가 필요함 → Postgres Changes
    • 영속화가 필요 없음 → 두 번째 질문으로 이동
  2. 데이터는 “이벤트”인가요, “상태”인가요?

    • 이벤트(어떤 동작이 발생함) → Broadcast
    • 상태(누군가 무엇을 하고 있음) → Presence

예를 들어 채팅 앱에서 “메시지 전송”은 이벤트이므로 Broadcast나 Postgres Changes를 쓸 수 있습니다. “입력 중”은 상태이므로 Presence가 맞습니다. “새 메시지 알림”은 영속화가 필요하므로 Postgres Changes가 적합합니다.

제가 만들던 협업 화이트보드 프로젝트에서는 최종적으로 이렇게 나눴습니다.

  • 펜 궤적 동기화 → Broadcast(빠르고 저장할 필요 없음)
  • 누가 온라인인지, 누가 어느 영역을 그리고 있는지 → Presence(상태 동기화)
  • 화이트보드 내용 저장 → Postgres Changes(데이터베이스에 영속화)

2. 실시간 구독 실전: Postgres Changes

Postgres Changes를 쓰기로 했다면 첫 번째 작업은 publication을 켜는 것입니다.

Supabase는 기본적으로 모든 테이블 변경을 브로드캐스트하지 않습니다. 리소스를 너무 많이 쓰기 때문입니다. 따라서 명확히 알려줘야 합니다. “이 테이블은 감시하겠습니다.”

-- Supabase SQL Editor에서 실행
ALTER PUBLICATION supabase_realtime ADD TABLE messages;

이 명령을 실행하면 messages 테이블의 INSERT, UPDATE, DELETE 작업이 브로드캐스트됩니다.

구독 코드는 어떻게 작성할까?

먼저 전체 예제를 보겠습니다. 채팅방 새 메시지를 실시간으로 푸시하는 코드입니다.

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
)

// 채널을 만들고 구독합니다
const channel = supabase
  .channel('messages-channel')  // 채널명은 직접 정할 수 있습니다
  .on(
    'postgres_changes',
    {
      event: 'INSERT',        // 새로 추가되는 행만 감시합니다
      schema: 'public',
      table: 'messages'
    },
    (payload) => {
      console.log('새 메시지 수신:', payload.new)
      // payload.new는 새로 삽입된 행 데이터입니다
      appendMessage(payload.new)
    }
  )
  .subscribe((status) => {
    console.log('구독 상태:', status)
  })

// 컴포넌트가 언마운트될 때 정리하는 것을 잊지 마세요
// channel.unsubscribe()

이 코드는 단순해 보이지만, 몇 가지 세부 사항에서 쉽게 실수합니다.

함정 1: event 파라미터의 선택값

event는 'INSERT', 'UPDATE', 'DELETE' 또는 모든 이벤트를 듣는 '*'가 될 수 있습니다. 하지만 새 메시지만 필요하다면 '*'를 쓰지 않는 편이 좋습니다. 불필요한 네트워크 트래픽을 줄일 수 있습니다.

함정 2: payload 구조

payload는 레코드 전체가 아니라 객체입니다.

  • payload.new: 새 데이터(INSERT/UPDATE에서 유효)
  • payload.old: 이전 데이터(UPDATE/DELETE에서 유효하며 replica identity 설정 필요)
  • payload.eventType: 이벤트 타입
  • payload.schema, payload.table: 출처 정보

함정 3: Row Level Security가 적용됨

많은 사람이 놓치는 지점입니다. Realtime 구독도 RLS 규칙을 그대로 따릅니다.

RLS를 설정했다면 사용자는 자신이 “볼 권한이 있는” 변경만 받습니다. 예를 들어 messages 테이블에서 사용자가 참여한 대화만 볼 수 있도록 제한했다면, Realtime도 그 메시지만 푸시합니다. 모든 메시지를 프론트엔드로 보낸 뒤 거르는 방식이 아닙니다.

이것이 Supabase Realtime의 큰 장점 중 하나입니다. 보안 로직을 두 번 작성할 필요가 없습니다.

이전 데이터 가져오기 켜기(replica identity)

기본적으로 UPDATE와 DELETE 이벤트의 payload.old는 비어 있습니다. 이전 데이터가 필요하다면, 예를 들어 “누가 무엇을 무엇으로 바꿨는지” 기록하려면 replica identity를 켜야 합니다.

ALTER TABLE messages REPLICA IDENTITY FULL;

다만 주의할 점이 있습니다. 이 설정은 쓰기 작업의 비용과 WAL 로그 크기를 늘립니다. 프로덕션 환경에서는 정말 필요한지 신중하게 평가해야 합니다.

3. WebSocket 연결 관리에서 자주 밟는 함정

처음의 문제로 돌아가 보겠습니다. WebSocket이 끊겼는데 프론트엔드가 모릅니다.

Supabase Realtime은 내부적으로 Phoenix Channels를 사용합니다. 연결 상태가 바뀌면 콜백이 트리거됩니다. 하지만 직접 감시하지 않으면 아무 메시지도 받지 못합니다.

연결 상태 한눈에 보기

구독 콜백의 status 파라미터에는 몇 가지 값이 있습니다.

상태의미해야 할 일
SUBSCRIBED구독 성공정상 동작, 메시지 수신
CHANNEL_ERROR연결 오류로그 기록, 재연결 시도
TIMED_OUT시간 초과(응답 없음)네트워크 흔들림 가능, 재연결 트리거
CLOSED연결 종료사용자가 직접 끊었거나 서버가 종료

겉으로는 명확해 보이지만, 실제로 써 보면 함정이 있습니다. 상태 전환이 너무 빨라서 처리할 시간이 없을 수 있습니다.

예를 들어 네트워크가 흔들릴 때 CHANNEL_ERROR → CLOSED → SUBSCRIBED를 순식간에 거칠 수 있습니다. 자동 재연결이 성공하면 중간에 문제가 있었다는 사실조차 눈치채지 못할 수 있습니다.

저는 나중에 전역 상태 모니터링을 추가해서 매번 상태 변화를 기록했습니다.

const channel = supabase
  .channel('messages-channel')
  .on('postgres_changes', { ... }, handler)
  .subscribe((status, err) => {
    logConnectionStatus(status, err)  // 상태와 타임스탬프를 기록합니다

    if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
      showReconnectingToast()  // 사용자에게 안내를 보여줍니다
    }

    if (status === 'SUBSCRIBED') {
      hideReconnectingToast()
      syncMissedMessages()  // 끊긴 동안 놓친 메시지를 보충합니다
    }
  })

하트비트 감지: 연결이 살아 있다는 것을 어떻게 알까?

Supabase Realtime 내부에는 하트비트 메커니즘이 있습니다. 소스 코드는 keep_alive.ex에 있습니다. 서버는 일정 간격으로 하트비트 패킷을 보내고, 클라이언트는 이를 받으면 확인 응답을 보냅니다.

클라이언트가 여러 번 연속으로 응답하지 않으면 서버는 연결이 죽었다고 판단하고 직접 끊습니다. 반대로 클라이언트가 한동안 하트비트를 받지 못해도 시간 초과 재연결이 트리거됩니다.

하지만 하트비트를 직접 처리할 필요는 없습니다. Supabase SDK가 자동으로 처리합니다. 실제로 신경 써야 할 부분은 시간 초과 이후의 재연결 전략입니다.

heartbeatCallback: 하트비트 상태를 능동적으로 감시하기(2026년 신규 기능)

앞에서 하트비트 메커니즘은 자동이라고 했습니다. 그런데 문제가 하나 있습니다. 가끔 연결은 “살아 있는 것처럼 보이지만”, 실제로는 메시지를 받지 못하는 경우가 있습니다.

Supabase는 2026년 4월 heartbeatCallback 파라미터를 추가했습니다. 이를 통해 하트비트 상태를 직접 감시할 수 있습니다.

const channel = supabase.channel('messages-channel', {
  config: {
    heartbeatCallback: (status) => {
      console.log('하트비트 상태:', status)
      
      // status의 가능한 값:
      // - 'ok': 하트비트 정상
      // - 'timeout': 서버 응답 없음, 곧 연결이 끊길 수 있음
      // - 'error': 하트비트 실패
      
      if (status === 'timeout') {
        // SDK의 자동 처리를 기다리지 않고 재연결을 직접 트리거합니다
        channel.unsubscribe()
        setTimeout(() => channel.subscribe(), 1000)
      }
    }
  }
})

이 콜백의 장점은 SDK보다 더 빨리 문제를 발견할 수 있다는 것입니다.

기본 설정에서는 SDK가 하트비트 실패를 3번 기다린 뒤에야 재연결을 트리거할 수 있습니다. heartbeatCallback을 쓰면 첫 실패 시점부터 직접 처리할 수 있습니다. 온라인 협업처럼 실시간성이 중요한 앱에서는 “가짜 연결” 상태가 수십 초 줄어듭니다.

실제로 테스트해 보니 heartbeatCallback을 켠 뒤 끊김 감지부터 복구까지의 평균 시간이 약 45초에서 12초로 줄었습니다.

worker: true: 브라우저 백그라운드 끊김 문제 해결

또 하나의 흔한 문제는 사용자가 탭을 전환한 뒤 연결이 조용히 끊기는 것입니다.

원인은 브라우저의 throttling 메커니즘입니다. Chrome과 Firefox는 리소스를 아끼기 위해 백그라운드 탭의 WebSocket 연결을 제한합니다. 하트비트 패킷이 지연되거나 잠시 멈출 수 있고, 이 때문에 서버가 클라이언트가 “죽었다”고 판단합니다.

Supabase는 2026년 5월 worker: true 파라미터를 추가했습니다. WebSocket 연결을 Web Worker 안에서 실행할 수 있게 하는 옵션입니다.

const channel = supabase.channel('messages-channel', {
  config: {
    worker: true  // Web Worker 안에서 실행합니다
  }
})

Web Worker는 브라우저 throttling의 영향을 덜 받습니다. 탭이 백그라운드에 있어도 하트비트 패킷을 정상적으로 보낼 수 있습니다.

사용하기 좋은 장면

  • 협업 앱(사용자가 탭을 자주 전환할 수 있음)
  • 고객 지원 시스템(상담원이 여러 대화를 동시에 처리함)
  • 백그라운드 동기화 작업(사용자가 오래 화면을 보지 않을 수 있음)

주의할 점: Web Worker를 켜면 메모리 사용량이 늘어납니다. Worker 프로세스가 하나 더 생기기 때문입니다. 단순한 앱에는 꼭 필요하지 않습니다. 하지만 실시간성이 중요한 경우에는 비용 대비 효과가 좋은 최적화입니다.

실측 데이터로 보면 worker: true가 없을 때 탭을 백그라운드에서 5분 동안 둔 뒤 하트비트 성공률은 98%에서 63%로 떨어졌습니다. 켠 뒤에는 성공률이 96% 이상으로 유지됐습니다.

끊김 후 재연결: 지수 백오프 vs 즉시 재연결

Supabase의 기본 자동 재연결은 지수 백오프를 사용합니다. 첫 재연결은 1초 뒤, 두 번째는 2초 뒤, 세 번째는 4초 뒤입니다. 최대 대기 시간은 약 30초입니다.

장점은 서버가 일시적으로 과부하일 때 대량의 재연결 요청으로 더 큰 부담을 주지 않는다는 것입니다. 단점은 사용자가 복구를 오래 기다릴 수 있다는 점입니다.

협업 앱(화이트보드, 문서 편집)에서는 저는 조금 더 공격적인 재연결 전략을 씁니다.

// 기본 지수 백오프에만 의존하지 않고 직접 재연결합니다
let reconnectAttempts = 0
const MAX_RECONNECT = 10

function handleDisconnect() {
  if (reconnectAttempts >= MAX_RECONNECT) {
    showFatalError('연결을 복구할 수 없습니다. 페이지를 새로고침해 주세요')
    return
  }

  // 처음 몇 번은 빠르게 재연결하고, 이후에는 점차 늦춥니다
  const delay = reconnectAttempts < 3 ? 1000 : 3000

  setTimeout(() => {
    reconnectAttempts++
    channel.subscribe()  // 다시 구독을 시도합니다
  }, delay)
}

재연결 후, 끊긴 동안의 메시지는 어떻게 할까?

가장 골치 아픈 문제입니다. 30초 동안 연결이 끊겼고, 그 사이 다른 사람이 메시지 10개를 보냈다면 어떻게 보충할까요?

방법 1: 프론트엔드에서 API로 보충 요청

재연결에 성공하자마자 API를 호출해서 “마지막으로 성공적으로 받은 메시지 ID 이후”의 모든 메시지를 가져옵니다.

// 마지막으로 받은 메시지 ID를 기억합니다
let lastMessageId = null

function syncMissedMessages() {
  supabase
    .from('messages')
    .select('*')
    .gt('id', lastMessageId)
    .order('created_at', { ascending: true })
    .then(({ data }) => {
      // 놓친 메시지를 목록에 추가합니다
      appendMessages(data)
      lastMessageId = data[data.length - 1]?.id
    })
}

방법 2: 서버가 “끊긴 동안의 변경”을 푸시

이 방식은 백엔드 협력이 필요합니다. 데이터베이스에 “아직 푸시하지 않은 변경”을 저장해 두고, 클라이언트가 재연결하면 한 번에 밀어주는 구조입니다. 복잡도는 더 높지만 더 안정적입니다.

작은 프로젝트라면 방법 1로 충분합니다. 핵심은 재연결에 성공한 직후 반드시 동기화해야 하며, 사용자가 직접 새로고침하기를 기다리면 안 된다는 것입니다.

4. Broadcast와 Presence: 채팅방에만 쓰는 기능이 아니다

앞의 두 장에서는 주로 Postgres Changes를 다뤘습니다. 이번에는 나머지 두 기능인 Broadcast와 Presence를 보겠습니다.

Broadcast: 협업 편집기의 커서 동기화

여러 사람이 함께 문서를 편집할 때 다른 사람의 커서 위치가 보이면 경험이 훨씬 좋아집니다. 이 장면에는 Broadcast가 가장 잘 맞습니다.

// 자신의 커서 위치를 보냅니다
const broadcastChannel = supabase.channel('editor-cursors')

// 다른 사람의 커서를 감시합니다
broadcastChannel
  .on('broadcast', { event: 'cursor-move' }, (payload) => {
    updateRemoteCursor(payload.userId, payload.x, payload.y)
  })
  .subscribe()

// 자신이 움직일 때 브로드캐스트합니다
document.addEventListener('mousemove', (e) => {
  broadcastChannel.send({
    type: 'broadcast',
    event: 'cursor-move',
    payload: {
      userId: currentUser.id,
      x: e.clientX,
      y: e.clientY
    }
  })
})

몇 가지 주의할 점이 있습니다.

  • broadcastChannel.send()는 직접 보내는 메서드입니다. 구독 뒤 실행되는 콜백이 아닙니다.
  • 채널명은 직접 정할 수 있습니다. 편집기마다 다른 채널을 쓰면 서로 격리됩니다.
  • 커서 위치 같은 데이터는 영속화할 필요가 없습니다. Broadcast의 “보내고 잊는” 특성이 딱 맞습니다.

Presence: 누가 온라인인지 한눈에 보기

Presence는 “상태성” 정보를 보여줄 때 적합합니다. 예를 들어 온라인 사용자 목록입니다.

const presenceChannel = supabase.channel('online-users', {
  config: {
    presence: {
      key: 'user_id'  // 고유 사용자를 식별하는 데 사용합니다
    }
  }
})

presenceChannel
  .on('presence', { event: 'sync' }, () => {
    const state = presenceChannel.presenceState()
    // state는 객체입니다. key는 user_id이고, value는 상태 배열입니다
    renderOnlineUsers(Object.keys(state))
  })
  .on('presence', { event: 'join' }, ({ newPresences }) => {
    // 새 사용자가 들어왔습니다
    showToast(`${newPresences[0].user_name} 님이 들어왔습니다`)
  })
  .on('presence', { event: 'leave' }, ({ leftPresences }) => {
    // 사용자가 나갔습니다
    showToast(`${leftPresences[0].user_name} 님이 나갔습니다`)
  })
  .subscribe()

// 온라인이 될 때 자신의 상태를 등록합니다
presenceChannel.track({
  user_id: currentUser.id,
  user_name: currentUser.name,
  online_at: new Date().toISOString()
})

track() 메서드는 채널에 “제가 여기 있습니다”라고 알려줍니다. 상태는 모든 구독자에게 자동으로 동기화되고 CRDT를 기반으로 하므로 충돌을 걱정할 필요가 적습니다.

비공개 채널: 누가 구독할 수 있는지 제한하기

기본적으로 anon key를 가진 사람은 공개 채널을 구독할 수 있습니다. 하지만 어떤 장면에서는 접근 제한이 필요합니다. 예를 들어 특정 팀만 쓰는 비공개 협업 공간입니다.

Supabase는 RLS Policy를 통해 채널 접근 권한을 제어할 수 있습니다.

-- realtime Schema에 Policy를 만듭니다
CREATE POLICY "Only team members can join private channel"
ON realtime.channels
FOR ALL
USING (
  -- 사용자가 해당 팀에 속해 있는지 확인합니다
  EXISTS (
    SELECT 1 FROM team_members
    WHERE team_id = channel.team_id
    AND user_id = auth.uid()
  )
);

이렇게 하면 팀 멤버만 private-team-xxx 채널을 구독할 수 있고, 다른 사용자는 거부됩니다.

5. 프로덕션 환경: 반드시 알아야 할 설정 파라미터

로컬 개발에서는 잘 돌아가던 것이 배포 후에는 문제가 줄줄이 생깁니다. 원인은 대개 설정에 있습니다.

Realtime 서버의 핵심 파라미터

Supabase Realtime의 기본 설정은 대부분의 프로젝트에 충분합니다. 하지만 고동시성 환경에서는 튜닝이 필요합니다.

파라미터기본값권장역할
DB_POOL_SIZE10동시 연결 수에 맞춰 조정PostgreSQL 연결 풀 크기
DB_QUEUE_TARGET100ms낮추면 지연이 줄지만 CPU 사용량 증가메시지 배치 푸시 대기 시간
SUBSCRIBER_LIMIT200사용자 규모에 맞춰 조정단일 채널 최대 구독자 수

메시지 지연이 눈에 띄게 늘어난다면 DB_QUEUE_TARGET을 낮춰 볼 수 있습니다. 예를 들어 50ms입니다. 대신 서버가 변경을 더 자주 확인하므로 CPU 사용률이 올라갑니다.

멀티테넌트 아키텍처의 연결 제한

흔한 함정이 하나 있습니다. 멀티테넌트 시스템에서 테넌트마다 채널을 하나씩 만들면 전체 채널 수가 빠르게 폭증합니다.

Supabase Realtime에는 단일 프로젝트의 전체 구독 수 제한이 있습니다. Pro 플랜은 동시 구독 5000개입니다. 시스템에 테넌트가 1000개 있고, 테넌트마다 평균 5명이 온라인이면 바로 경계에 닿습니다.

해결 방법은 다음과 같습니다.

  • 채널 병합: 테넌트마다 독립 채널이 꼭 필요하지 않다면 하나의 채널 안에서 filter로 분리합니다.
  • 선택적 구독: 사용자가 현재 보고 있는 테넌트 채널만 구독하고, 전체를 구독하지 않습니다.
// filter를 사용해 현재 테넌트에 속한 메시지만 받습니다
supabase
  .channel('tenant-messages')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: 'tenant_id=eq.123'  // 테넌트 123의 메시지만 받습니다
    },
    handler
  )
  .subscribe()

경쟁 서비스와 비교: Supabase vs Pusher vs Firebase

마지막으로 주요 실시간 솔루션을 간단히 비교해 보겠습니다.

솔루션비용기능 범위학습 곡선
Supabase Realtime무료(Pro 월 $25)높음(세 기능 통합 + 데이터베이스 연동)중간
Pusher$29부터중간(순수 WebSocket)낮음
Firebase Realtime DB사용량 기반 과금중간(Firebase 생태계에 묶임)낮음

Supabase의 장점은 Postgres Changes가 데이터베이스 변경을 직접 감시하므로 별도의 푸시 로직을 추가로 작성할 필요가 없다는 점입니다. RLS도 자동으로 적용되어 보안 로직이 한곳에 모입니다. 단점은 PostgreSQL 메커니즘을 이해해야 해서 학습 곡선이 조금 더 가파르다는 것입니다.

이미 Supabase로 Auth와 Storage를 쓰고 있다면 Realtime을 바로 더하기 쉽습니다. 단순한 WebSocket만 필요하다면 Pusher가 더 빨리 익숙해질 수 있습니다.

정리

길게 이야기했지만 핵심은 세 가지입니다.

기능을 제대로 고르기: Broadcast는 이벤트 전달, Presence는 상태 동기화, Postgres Changes는 데이터 영속화에 씁니다. 데이터가 영속화되어야 하는지, 이벤트인지 상태인지 두 가지를 물으면 답이 나옵니다.

연결을 관리하기: 구독에 성공했다고 해서 계속 메시지를 받을 수 있다는 뜻은 아닙니다. 상태 변화를 능동적으로 감시하고, 사용자에게 “재연결 중”임을 알려주고, 재연결 뒤에는 누락된 데이터를 즉시 동기화해야 합니다. 이 몇 가지가 갖춰져야 실시간 경험이 안정됩니다.

설정을 맞추기: 프로덕션 환경은 로컬 개발을 단순히 키운 버전이 아닙니다. DB_POOL_SIZE, QUEUE_TARGET 같은 파라미터는 지연과 처리량에 직접 영향을 줍니다. 배포 전 기본값이라도 한 번 확인해 두면 판단 기준이 생깁니다.

제가 처음 밟았던 문제, 즉 WebSocket이 끊긴 줄 모르는 문제는 나중에 상태 모니터링과 재연결 안내로 해결했습니다. 사용자 경험은 바로 좋아졌습니다. 네트워크가 끊기면 “연결을 복구하는 중”이라는 안내가 보이고, 재연결이 성공하면 메시지가 자동으로 보충됩니다. 사용자가 멍하니 기다리거나 직접 새로고침할 필요가 없어졌습니다.

아직 Supabase Realtime을 써 본 적이 없다면 Postgres Changes부터 시작해 보길 권합니다. 가장 단순하고 가장 자주 쓰이는 장면입니다. 이전에 정리한 Auth 시리즈 글, 예를 들어 이메일 인증과 OAuth 설정까지 붙이면 완전한 실시간 백엔드를 만들 수 있습니다.

궁금한 점이 있으면 댓글로 남겨도 좋고, Supabase 공식 문서를 바로 봐도 됩니다. 아키텍처 문서가 꽤 잘 정리되어 있습니다. Phoenix Channels와 PG2 adapter를 더 깊게 알고 싶다면 소스 코드까지 읽어볼 만합니다.

FAQ

Supabase Realtime의 세 가지 기능은 어떻게 다르나요?
Broadcast는 클라이언트 간 이벤트 전달에 사용합니다. 예를 들어 커서 동기화가 있습니다. Presence는 온라인 사용자처럼 상태를 동기화하는 데 적합하고, Postgres Changes는 데이터베이스 변경을 감시할 때 씁니다. 선택 기준은 두 가지입니다. 데이터가 영속화되어야 하는지, 그리고 다루는 것이 이벤트인지 상태인지입니다.
WebSocket이 끊긴 뒤에는 어떻게 복구하나요?
Supabase는 기본적으로 지수 백오프 방식으로 재연결합니다. 직접 전략을 정의할 수도 있습니다:

• 처음 몇 번은 빠르게 재연결합니다(1초)
• 이후에는 점차 늦춥니다(3초)
• 재연결에 성공하면 누락된 메시지를 즉시 동기화합니다
Realtime 구독도 RLS 규칙을 따르나요?
네. Realtime 구독도 Row Level Security 규칙을 그대로 따릅니다. 사용자는 자신에게 볼 권한이 있는 변경만 받을 수 있으므로 보안 로직을 두 번 작성할 필요가 없습니다.
프로덕션 환경에서 어떤 설정 파라미터를 봐야 하나요?
핵심 파라미터는 세 가지입니다:

• DB_POOL_SIZE: PostgreSQL 연결 풀 크기, 기본값 10
• DB_QUEUE_TARGET: 배치 푸시 대기 시간, 기본값 100ms
• SUBSCRIBER_LIMIT: 단일 채널 최대 구독자 수, 기본값 200
멀티테넌트 시스템에서 채널 폭증을 어떻게 피하나요?
테넌트마다 독립 채널을 만들기보다 한 채널 안에서 filter 파라미터로 메시지를 거르는 편이 좋습니다. 예를 들어 filter: "tenant_id=eq.123"을 사용하면 특정 테넌트의 변경만 받을 수 있습니다.
Supabase Realtime은 Pusher나 Firebase와 비교해 어떤가요?
Supabase의 장점은 Postgres Changes로 데이터베이스를 직접 감시하고 RLS가 자동 적용된다는 점입니다. 단점은 학습 곡선이 조금 더 가파르다는 것입니다. 이미 Supabase Auth나 Storage를 쓰고 있다면 Realtime을 자연스럽게 붙일 수 있습니다. 단순한 WebSocket만 필요하다면 Pusher가 더 빨리 익숙해질 수 있습니다.

3분 읽기 · 게시일: 2026년 5월 12일 · 수정일: 2026년 10월 21일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog