Appearance
관리자 "온라인/오프라인" 표시 — Supabase Presence 가이드
관리자 사용자 관리 테이블에서 각 계정을 온라인 / 오프라인으로 보여주기 위한 구현과 설정을 정리한 문서입니다.
1. 목표
- 관리자 페이지 사용자 목록에서 지금 접속 중인 계정 → 온라인(녹색 뱃지)
- 접속이 끊긴 계정(로그아웃 또는 브라우저/탭 종료) → 오프라인
이걸 Supabase Realtime Presence로 구현합니다. DB 테이블에 “온라인 여부” 컬럼을 두지 않습니다.
2. 원리 (가이드 요약)
- Presence = “지금 이 채널에 누가 연결되어 있는지”를 실시간 추적하는 기능.
- 채널에 연결 → 다른 클라이언트에게 join 이벤트 → 해당 사용자를 온라인으로 표시.
- 연결이 끊기면 Realtime이 leave 이벤트를 보냄 → 해당 사용자를 오프라인으로 표시.
두 가지 끊김을 구분할 수 있습니다.
| 상황 | 동작 | 우리가 할 일 |
|---|---|---|
| 브라우저/탭 닫음, 네트워크 끊김 | WebSocket 끊김 → Realtime이 자동으로 leave 발생 → 다른 클라이언트가 leave 수신 → 오프라인 처리 | 별도 호출 없음 |
| 명시적 로그아웃 | 클라이언트가 채널에서 leave(untrack + unsubscribe) 를 호출한 뒤 signOut → leave 이벤트 전송 → 오프라인 처리 | signOut 전에 leave 호출 필수 |
따라서:
- 온라인 표시 = Presence에 track된 메타(예:
user_id)를 사용. - 브라우저 닫힘 = 연결 끊김 → 자동 leave → 오프라인.
- 로그아웃 = 먼저 채널에서 leave(untrack 후 구독 해제) 호출 → 그다음
signOut()호출.
leave를 signOut 이후에 하면 연결이 먼저 끊겨 leave가 서버에 전달되지 않을 수 있어, 반드시 signOut 전에 leave 처리해야 합니다.
(추가로 “브라우저 닫힘 vs 로그아웃”을 구분하고 싶다면, presence 메타에 session_id를 넣고 leave 수신 시 세션 유효성 검사하거나, 로그아웃 시 leave 전에 status: 'logged_out' 같은 메타를 보내는 방식이 있습니다. 단순 온/오프라인 표시만 필요하면 이 구분은 선택 사항입니다.)
3. 우리 앱 구현 요약
| 구분 | 내용 |
|---|---|
| 채널 이름 | user-presence (고정) |
| 로그인 사용자(클라이언트) | 앱 로드 시 useTrackPresence()로 위 채널에 참여, presence: { key: userId } 로 채널 생성 후 track({ user_id, online_at }) |
| 관리자 페이지 | useOnlineUserIds()로 같은 채널 구독, presenceState() / join·leave·sync 이벤트로 접속 중인 user_id Set 유지 → 테이블에 온라인 뱃지 표시 |
| 로그아웃 시 | signOut() 호출 전에 clearPresenceBeforeLogout() 호출 → 채널에서 untrack() → 잠시 대기(200ms) → unsubscribe(). 그다음 supabase.auth.signOut(), user = null |
| 브라우저/탭 닫음 | beforeunload 에서 untrack() 시도(선택). 실제 끊김은 WebSocket 종료로 Realtime이 자동 leave 처리 |
핵심: 로그아웃 시 leave를 signOut보다 먼저 호출해야, 로그아웃한 계정이 관리자 화면에서 곧바로 오프라인으로 갱신됩니다.
4. Supabase에서 할 일
이미 왼쪽 사이드바에 Realtime 메뉴가 보인다면, Realtime 기능이 켜져 있는 프로젝트입니다.
온라인/오프라인(Presence) 만 쓰는 경우 아래만 확인하면 됩니다.
4-1. Realtime → Settings
- 왼쪽에서 Realtime → Settings 로 이동합니다.
- Realtime 서비스가 활성화(Enabled) 되어 있는지 확인합니다.
(이미 Realtime 메뉴가 보이면 보통 켜져 있습니다. 토글이 있으면 On으로 두면 됩니다.) - Presence는 별도 스위치가 없어도 Realtime이 켜져 있으면 사용 가능한 경우가 많습니다.
4-2. Realtime → Policies (필수)
Presence(온라인/오프라인)를 쓰려면 Realtime → Policies 에서 정책 2개 등록이 필수입니다.
정책이 없으면 채널에 presence를 보내거나 받을 수 없어, 온라인 뱃지가 동작하지 않습니다.
오른쪽 Templates 에 있는 Presence용 템플릿을 쓰면 됩니다.
1) “누가 온라인인지 듣기”용 정책 (SELECT)
- Realtime → Policies → Create policy 클릭.
- Policy name 에 예:
Allow listening presence for authenticated입력. - Policy Command 에서 SELECT 선택.
- 오른쪽 Templates 에서 아래 항목을 클릭해 적용합니다.
"Allow listening for presences on all channels for authenticated users only"
→ 채널의 presence 상태를 조회(listen) 할 수 있게 합니다. (관리자 페이지가 “누가 온라인인지” 받을 때 필요) - 생성되는 SQL이 인증된 사용자(
auth.role() = 'authenticated'등)만 허용하는지 확인 후 Save policy 클릭.
2) “내가 온라인이라고 알리기”용 정책 (INSERT)
- 다시 Create policy 클릭.
- Policy name 에 예:
Allow broadcasting presence for authenticated입력. - Policy Command 에서 INSERT 선택.
- 오른쪽 Templates 에서 아래 항목을 클릭해 적용합니다.
"Allow broadcasting presences on all channels for authenticated users only"
→ 로그인한 사용자가 presence를 보내는(track) 할 수 있게 합니다. - Save policy 클릭.
정리하면:
- SELECT 정책 1개 → 관리자 페이지가
user-presence채널의 presence를 받을 수 있음. - INSERT 정책 1개 → 로그인 사용자가 같은 채널에 자신의 presence를 보낼 수 있음.
두 정책을 모두 저장한 뒤 앱에서 로그인 → 관리자 페이지에서 온라인 뱃지가 나오는지 확인하면 됩니다.
"messages" / API DISABLED 노란 배너가 보일 때
- 목록에 messages 옆에 "API DISABLED"가 있고, “No data will be selectable via Supabase APIs as this schema is not exposed” 라는 배너가 있을 수 있습니다.
- 그 메시지는 일부 스키마 노출 설정에 대한 안내입니다. Realtime 채널 접근 정책은 위처럼 Create policy 로 만드는 것이 맞고, 테이블은
realtime.messages(Realtime 내부용)로 두고, Templates 에서 Presence용 두 개만 적용해 저장하면 됩니다. - 정책 저장 후에도 Presence가 동작하지 않으면, Project Settings → API (또는 API settings) 에서 Realtime/해당 스키마 노출 여부를 한 번 확인해 보세요.
4-3. 하지 않아도 되는 것
- "Set up realtime for me" / "Create a trigger"
→ DB 트리거나 Broadcast 설정용입니다. Presence(온라인/오프라인)에는 필요 없습니다. 누르지 않아도 됩니다. - Database → Replication 에서 테이블 추가
→ 테이블 행 변경 구독(Postgres Changes)용입니다. Presence와 무관합니다.
4-4. 그밖에
- Project URL 과 anon key 가 웹 앱
.env(VITE_SUPABASE_URL,VITE_SUPABASE_ANON_KEY)와 일치하는지만 확인하면 됩니다.
(Project Settings → API 또는 API Keys에서 확인)
5. 브라우저 닫힘 vs 로그아웃 구분 (선택)
가이드에서 말한 대로, 구분이 필요하면 다음을 조합할 수 있습니다.
- presence 메타에
session_id,last_active포함. - 로그아웃 시
channel.leave()(또는untrack+unsubscribe)supabase.auth.signOut()- (선택) 서버에 로그아웃 이벤트 기록
- leave 이벤트 수신 시
- 해당
session_id의 세션이 아직 유효한지 검사 - 유효 → 비정상 종료(브라우저 닫힘/네트워크)
- 무효 → 정상 로그아웃
- 해당
- 세션 검사가 어렵다면: 로그아웃 시 leave 전에 메타에
status: 'logged_out'등을 넣어 두고, leave 이벤트에 그 값이 있으면 “정상 로그아웃”으로 처리.
단순 온/오프라인 표시만 필요하면 위 구분은 구현하지 않아도 됩니다. leave만 제대로 오면 오프라인으로 표시하면 됩니다.
6. 주의사항 (가이드 반영)
- 네트워크 끊김 후 재연결 지연·백그라운드 탭 정책 때문에 leave/join이 즉시 오지 않을 수 있음.
필요하면 UI에서 10~30초 정도 잠정 오프라인 타임아웃을 두는 것이 좋습니다. - Presence 메타에는 민감한 정보를 넣지 마세요.
- 인증 토큰 만료, 관리자에 의한 강제 로그아웃 등도 있으면, 그에 맞춰 세션/leave 처리만 일관되게 하면 됩니다.
7. 문제 해결
Settings에 Realtime 메뉴가 없어요 /
/settings/realtime404
→ Presence는 대시보드 설정이 필요 없습니다. 무시해도 됩니다.항상 오프라인으로만 보여요
→ Project URL·anon key가.env와 같은지 확인.
→ F12 콘솔에 Realtime/WebSocket 오류가 없는지 확인.
→ 사용자 탭에서 앱을 연 상태에서 관리자 페이지를 새로고침해 보기.로그아웃해도 계속 온라인으로 보여요
→ signOut 전에 Presence 채널에서untrack()후 잠시 대기한 뒤unsubscribe()가 호출되는지 확인 (clearPresenceBeforeLogout()).
→ 관리자 페이지 새로고침 후에도 온라인으로 남는지 확인.
→ Replication/테이블 Realtime 설정은 Presence와 무관하므로, 앱 쪽(로그아웃 시 leave, 콘솔 오류) 만 점검하면 됩니다.연결 오류
→ URL·anon key, 방화벽/네트워크(*.supabase.co, wss) 확인.
8. 관련 문서
- 01. supabase-setup.md) — 프로젝트 생성, URL/API 키
- 04. supabase-admin-users-rpc.sql) — 관리자 사용자 목록 RPC (Presence와 별개)