ZEP 원격제어 Extension
Chrome 확장 프로그램을 통한 ZEP.US 메타버스 원격 제어 및 모니터링 시스템
📋 프로젝트 개요
ZEP.US 메타버스 교육 환경에서 강사와 관리자가 학생들의 기기(마이크/카메라)를 원격으로 제어하고 참가자 상태를 실시간으로 모니터링하기 위해 개발한 Chrome 확장 프로그램입니다.
시스템 역할
- 원격 제어: 서버 → Extension → 학생 기기 제어 (카메라/마이크 ON/OFF)
- 실시간 모니터링: Extension → 서버 (접속자 리스트, 상태 정보 전송)
- 환경 설정: 수업 시작 시 모든 학생 환경 통일 (6x6 그리드 뷰 등)
- 상태 추적: 참가자별 마이크/카메라 상태 로그 수집
ZEP.US 위젯과의 관계
┌─────────────────────────────────────┐
│ ZEP.US 메타버스 │
├─────────────────────────────────────┤
│ │
│ [메타버스 내부] │
│ ZEP.US 위젯 (학생/관리자) │
│ └─ 문의, 소환, 타이틀 설정 등 │
│ │
│ [메타버스 외부 - 브라우저 레벨] │
│ Chrome Extension ⭐ (본 시스템) │
│ ├─ 서버 명령으로 학생 제어 │
│ │ (카메라/마이크 강제 제어) │
│ └─ 상태 정보를 서버로 전송 │
│ (접속자, 카메라/마이크 상태) │
│ │
└─────────────────────────────────────┘
핵심 차이점:
- 위젯: 메타버스 안에서 실행 (ZEP Script)
- Extension: 브라우저 레벨에서 DOM 조작
관련 프로젝트:
- 출석 모니터링 시스템 - 전체 시스템
- ZEP.US 위젯 - 메타버스 내부 도구
🎯 해결한 문제
온라인 강의의 현실적 문제
-
기기 제어 어려움
- 학생들이 마이크/카메라 켜기를 어려워함
- 강사 지시를 따르지 못하거나 무시
- 화면 공유 종료 방법을 모름
-
모니터링 한계
- 다수 학생의 참여 상태 파악 어려움
- 카메라/마이크 ON/OFF 상태 실시간 확인 불가
- 누가 화면 공유 중인지 추적 어려움
-
환경 설정 번거로움
- 수업 시작 시 통일된 환경 설정 필요
- 학생마다 개별 설정 지시 비효율적
- 6x6 그리드 뷰 설정 방법 복잡
기술적 과제
- DOM 조작의 복잡성: ZEP.US의 Radix UI 컴포넌트 제어
- 상태 동기화: 클라이언트-서버 간 실시간 상태 유지
- Manifest V3 제약: Service Worker 기반 Background Script
솔루션
- 원격 제어: WebSocket으로 서버 명령을 즉시 실행
- DOM 조작:
data-sentry-component기반 정밀 제어 - 실시간 모니터링: MutationObserver + 주기적 Heartbeat
- Optimistic Update: 명령 실행 후 즉시 상태 보고
🛠 기술 스택
Chrome Extension
- Manifest: V3 (Service Worker 기반)
- Language: JavaScript ES6+
- API: Chrome Extension API (Runtime, Storage, Tabs)
Communication
- Protocol: WebSocket (Socket.IO)
- Client: Socket.IO Client v4.x
- Server: Flask-SocketIO (Python)
DOM Manipulation
- Target: ZEP.US Radix UI 컴포넌트
- Method:
data-sentry-component선택자 기반 - Events: Click, Keyboard 이벤트 디스패치
- Observer: MutationObserver (실시간 감지)
💡 주요 구현 내용
1. 이중 버전 아키텍처
FT (Full Monitoring) 버전 - 관리자용
// extension-ft_v3
const FEATURES = {
remoteControl: true, // 원격 제어
monitoring: true, // ⭐ 실시간 모니터링
whisperMonitoring: true, // 귓속말 감지
auth: 'token' // ID/PW + Token
};특징:
- Access/Refresh Token 인증
- 참가자 상태 실시간 수집
- 귓속말 모니터링
- 자동 토큰 갱신
Instructor 버전 - 강사용
// extension-instructor-v2
const FEATURES = {
remoteControl: true, // 원격 제어
monitoring: false, // ❌ 모니터링 제거
auth: 'api_key' // API Key
};특징:
- 간단한 API Key 인증
- 수업 진행용 제어 기능만 포함
- 경량화된 구조
2. 원격 제어 시스템
마이크/카메라 토글 (존재 기반 제어)
function toggleMicrophone() {
// 상태 확인이 아닌, 현재 보이는 아이콘 클릭
const micOnIcon = document.querySelector(
'[data-sentry-component="MicFillIcon"]'
);
const micOffIcon = document.querySelector(
'[data-sentry-component="MicSlashFillIcon"]'
);
// 존재하는 아이콘을 클릭 → 토글
const iconToClick = micOnIcon || micOffIcon;
if (iconToClick) {
const button = iconToClick.closest('button');
button?.click();
// Optimistic Update
const newState = micOnIcon ? 'off' : 'on';
return newState;
}
}특징:
- 상태를 미리 확인하지 않음
- 버튼 클릭 후 상태 예측
- 즉시 서버에 보고 (응답성 향상)
화면 공유 강제 종료
function stopScreenShare() {
const screenShareIcon = document.querySelector(
'[data-sentry-component="ScreenArrowupFillIcon"]'
);
if (screenShareIcon) {
const button = screenShareIcon.closest('button');
button?.click();
return { success: true, action: 'stopped' };
}
return { success: false, reason: 'not_sharing' };
}3. 복잡한 DOM 조작: 6x6 그리드 설정
가장 복잡한 기능 - 5단계 프로세스:
async function setGridTo6x6() {
// 1단계: 레이아웃 메뉴 버튼 찾기
const layoutButton = document.querySelector(
'[data-sentry-component="LayoutIcon"]'
)?.closest('button');
if (!layoutButton) return { error: 'Button not found' };
// 2단계: 메뉴 열기
layoutButton.click();
// 3단계: MutationObserver로 메뉴 렌더링 대기
await waitForMenuRender();
// 4단계: '그리드 보기' 옵션 클릭
const gridOption = findMenuOption('그리드 보기');
gridOption?.click();
// 5단계: Radix UI 슬라이더를 키보드 이벤트로 제어
await waitForSlider();
const slider = document.querySelector('[role="slider"]');
slider.focus();
// ArrowRight 키를 6번 눌러 값 조정
for (let i = 0; i < 6; i++) {
const keyEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
code: 'ArrowRight',
keyCode: 39,
bubbles: true
});
slider.dispatchEvent(keyEvent);
}
}
function waitForMenuRender() {
return new Promise((resolve) => {
const observer = new MutationObserver((mutations) => {
// 메뉴 컨테이너 감지
const menu = document.querySelector('[role="menu"]');
if (menu) {
observer.disconnect();
resolve();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}난이도가 높은 이유:
- 단순 클릭으로 불가능
- 동적 렌더링 대기 필요
- Radix UI 슬라이더는 일반
<input type="range">가 아님 - 키보드 이벤트로만 제어 가능
4. 실시간 모니터링 (FT 버전)
ParticipantsMonitor 클래스
class ParticipantsMonitor {
constructor() {
this.observer = null;
this.debounceTimer = null;
this.lastUpdate = Date.now();
}
start() {
const videoContainer = document.querySelector(
'.PlayerVideo_video__container'
);
this.observer = new MutationObserver((mutations) => {
// Debouncing: 500ms 내 중복 업데이트 방지
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.collectParticipantsData();
}, 500);
});
this.observer.observe(videoContainer, {
childList: true,
subtree: true,
attributes: true
});
// 60초마다 정기 검증
setInterval(() => {
this.collectParticipantsData();
}, 60000);
}
collectParticipantsData() {
const participants = [];
const videoElements = document.querySelectorAll(
'.PlayerVideo_video__item'
);
videoElements.forEach(el => {
const nameEl = el.querySelector('.PlayerVideo_name');
const micIcon = el.querySelector('[data-sentry-component*="Mic"]');
const cameraOn = el.querySelector('video') !== null;
participants.push({
name: nameEl?.textContent,
micState: micIcon?.dataset?.sentryComponent?.includes('Slash')
? 'off' : 'on',
cameraState: cameraOn ? 'on' : 'off'
});
});
// 서버로 전송
chrome.runtime.sendMessage({
type: 'participants_update',
data: {
totalCount: participants.length,
participants: participants
}
});
}
}특징:
- MutationObserver: DOM 변화 실시간 감지
- Debouncing: 과도한 업데이트 방지 (500ms)
- 정기 검증: 60초마다 강제 수집
- 경량화: 필요한 정보만 추출
5. WebSocket 통신 구조
Background Service Worker
// background.js
let socket = null;
function connectToServer(config) {
socket = io(config.serverUrl, {
auth: {
token: config.accessToken,
client_id: config.clientId
},
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000
});
// 서버 명령 수신
socket.on('zep_command', (command) => {
// Content Script로 전달
chrome.tabs.query({ url: '*://zep.us/*' }, (tabs) => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, {
type: 'execute_command',
command: command
});
});
});
});
// 상태 업데이트 서버 전송
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'state_update') {
socket.emit('zep_state_update', msg.data);
}
});
}Heartbeat 시스템
// 20초마다 생존 신고
setInterval(() => {
const currentState = {
micState: getMicState(),
cameraState: getCameraState(),
screenSharing: isScreenSharing(),
mapId: getCurrentMapId(),
timestamp: Date.now()
};
socket.emit('zep_heartbeat', currentState);
}, 20000);6. 토큰 관리 (FT 버전)
자동 토큰 갱신
function setupTokenRefresh(tokenData) {
const expiresAt = new Date(tokenData.expires_at);
const refreshTime = expiresAt.getTime() - (5 * 60 * 1000); // 5분 전
setInterval(async () => {
const now = Date.now();
if (now >= refreshTime) {
try {
const newTokens = await refreshTokens(tokenData.refresh_token);
updateStoredTokens(newTokens);
console.log('✅ Token refreshed successfully');
} catch (error) {
console.error('❌ Token refresh failed:', error);
// 재로그인 필요 알림
chrome.runtime.sendMessage({
type: 'auth_required'
});
}
}
}, 60000); // 1분마다 체크
}🏗 시스템 아키텍처
전체 통신 흐름
[관리자/강사 Dashboard]
↓ (명령 전송)
[Flask Socket.IO Server]
↓ (WebSocket)
[Background Service Worker]
↓ (Chrome Runtime Message)
[Content Script]
↓ (DOM 조작)
[ZEP.US Web Page]
↓ (상태 변화)
[Content Script]
↓ (상태 수집)
[Background Service Worker]
↓ (WebSocket)
[Flask Socket.IO Server]
↓ (저장/분석)
[Database]
Extension 내부 구조
Chrome Extension
├── Background (Service Worker)
│ ├── Socket.IO 연결
│ ├── 토큰 관리
│ ├── 명령 라우팅
│ └── Heartbeat 전송
│
├── Content Script
│ ├── DOM 조작
│ ├── 상태 감지
│ ├── MutationObserver
│ └── 이벤트 디스패치
│
└── Popup
├── 설정 UI
├── 연결 상태
└── 인증 입력
📊 지원 명령어
원격 제어 명령
명령어 (action) | 설명 | 구현 |
|---|---|---|
toggle_mic | 마이크 토글 | 아이콘 클릭 |
mic_on | 마이크 강제 ON | OFF 아이콘만 클릭 |
mic_off | 마이크 강제 OFF | ON 아이콘만 클릭 |
toggle_camera | 카메라 토글 | 아이콘 클릭 |
camera_on | 카메라 강제 ON | OFF 아이콘만 클릭 |
camera_off | 카메라 강제 OFF | ON 아이콘만 클릭 |
screenoff | 화면 공유 종료 | 공유 아이콘 클릭 |
set_grid_6x6 | 6x6 그리드 설정 | 5단계 프로세스 |
Socket.IO 이벤트
Client → Server
| 이벤트 | 설명 | 주기 |
|---|---|---|
zep_connect | 초기 연결 | 1회 |
zep_state_update | 상태 변경 보고 | 즉시 |
zep_heartbeat | 생존 신고 | 20초 |
zep_participants_update | 참가자 목록 (FT) | 60초 |
Server → Client
| 이벤트 | 설명 |
|---|---|
zep_command | 원격 제어 명령 |
zep_connection_confirmed | 연결 승인 |
zep_error | 에러 발생 |
📈 성과
수업 운영 효율화
- 기기 제어 시간: 학생별 30초 → 즉시
- 환경 설정: 수동 5분 → 자동 10초
- 모니터링: 육안 확인 → 실시간 대시보드
기술적 성과
- 명령 응답 시간: 평균 200ms
- 상태 동기화: 20초 주기 자동 업데이트
- 연결 안정성: WebSocket 자동 재연결
사용자 만족도
- 강사: 수업 집중도 향상
- 관리자: 실시간 모니터링으로 즉시 대응
- 학생: 기기 조작 스트레스 감소
🔧 설치 및 사용
설치
# 1. Chrome 확장 프로그램 페이지
chrome://extensions/
# 2. 개발자 모드 활성화
# 3. 폴더 선택
- 관리자: extension-ft_v3
- 강사: extension-instructor-v2설정
FT 버전 (관리자):
Server URL: https://server.example.com
ID: admin@example.com
Password: ********
Instructor 버전 (강사):
Server URL: https://server.example.com
API Key: ********
Instructor Name: 홍길동
사용
- ZEP.US 스페이스 접속
- Extension 자동 연결
- Dashboard에서 명령 전송
- 실시간 상태 확인
🚨 기술적 도전 과제
1. Radix UI 제어의 복잡성
문제: ZEP.US는 Radix UI 사용, 일반 HTML 요소가 아님 해결:
data-sentry-component선택자 활용- 키보드 이벤트로 슬라이더 제어
- MutationObserver로 동적 렌더링 대기
2. DOM 선택자 안정성
문제: ZEP 업데이트 시 선택자 변경 가능성 대응:
- CONFIG 객체로 선택자 중앙 관리
- 다중 폴백 선택자 지원
- 에러 시 자동 로깅
const CONFIG = {
selectors: {
micOn: '[data-sentry-component="MicFillIcon"]',
micOff: '[data-sentry-component="MicSlashFillIcon"]',
// 폴백
micOnFallback: '.mic-icon.active',
micOffFallback: '.mic-icon.inactive'
}
};3. Manifest V3 제약
문제: Service Worker는 비활성화될 수 있음 해결:
- Persistent 연결 대신 재연결 로직
- Chrome Storage API로 상태 유지
- Alarms API로 주기적 작업
// Service Worker 활성 유지
chrome.alarms.create('keepalive', {
periodInMinutes: 1
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepalive') {
// Dummy 작업으로 활성 유지
console.log('Service Worker alive');
}
});4. 상태 동기화
문제: 클라이언트-서버 상태 불일치 가능성 해결:
- Optimistic Update (낙관적 업데이트)
- 20초 Heartbeat로 검증
- 서버에서 최종 상태 관리
📝 배운 점
Chrome Extension 개발
-
Manifest V3 마스터
- Service Worker 기반 Background
- Persistent 연결의 한계와 극복
- Alarms API 활용
-
Content Script 고급 기법
- MutationObserver 최적화
- 이벤트 디스패치 시뮬레이션
- DOM 조작 안정성 확보
-
WebSocket 통신
- Socket.IO 재연결 로직
- 토큰 기반 인증
- Heartbeat 패턴
DOM 조작 노하우
-
Radix UI 컨트롤
- 키보드 이벤트로 슬라이더 제어
- 동적 렌더링 대기 패턴
- 커스텀 컴포넌트 접근법
-
선택자 전략
data-*속성 활용- 폴백 선택자 준비
- XPath vs CSS Selector 선택
-
성능 최적화
- Debouncing으로 과도한 업데이트 방지
- MutationObserver 범위 제한
- 메모리 누수 방지
실시간 시스템 설계
- 상태 관리: Optimistic Update 패턴
- 에러 처리: 자동 재시도 및 알림
- 모니터링: 주기적 검증으로 일관성 보장
🔮 향후 계획
단기 (베타 테스트 중)
- FT/Instructor 이중 버전 개발
- 기본 원격 제어 기능
- 안정성 테스트 및 버그 수정
- 사용자 피드백 반영
중기
- 원격 설정 로딩 (CONFIG 서버에서 다운로드)
- 성능 최적화 (IntersectionObserver)
- 채팅 매크로 기능
- 출석 체크 자동화
장기
- 스트림덱 연동 강화
- 다중 스페이스 동시 제어
- 머신러닝 기반 이상 행동 감지
프로젝트 기간: 2025.07 - 현재 현재 상태: 베타 테스트 버전: FT v3 / Instructor v2 최종 업데이트: 2025-12-21