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 조작

관련 프로젝트:

🎯 해결한 문제

온라인 강의의 현실적 문제

  1. 기기 제어 어려움

    • 학생들이 마이크/카메라 켜기를 어려워함
    • 강사 지시를 따르지 못하거나 무시
    • 화면 공유 종료 방법을 모름
  2. 모니터링 한계

    • 다수 학생의 참여 상태 파악 어려움
    • 카메라/마이크 ON/OFF 상태 실시간 확인 불가
    • 누가 화면 공유 중인지 추적 어려움
  3. 환경 설정 번거로움

    • 수업 시작 시 통일된 환경 설정 필요
    • 학생마다 개별 설정 지시 비효율적
    • 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
    });
  });
}

난이도가 높은 이유:

  1. 단순 클릭으로 불가능
  2. 동적 렌더링 대기 필요
  3. Radix UI 슬라이더는 일반 <input type="range">가 아님
  4. 키보드 이벤트로만 제어 가능

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마이크 강제 ONOFF 아이콘만 클릭
mic_off마이크 강제 OFFON 아이콘만 클릭
toggle_camera카메라 토글아이콘 클릭
camera_on카메라 강제 ONOFF 아이콘만 클릭
camera_off카메라 강제 OFFON 아이콘만 클릭
screenoff화면 공유 종료공유 아이콘 클릭
set_grid_6x66x6 그리드 설정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: 홍길동

사용

  1. ZEP.US 스페이스 접속
  2. Extension 자동 연결
  3. Dashboard에서 명령 전송
  4. 실시간 상태 확인

🚨 기술적 도전 과제

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 개발

  1. Manifest V3 마스터

    • Service Worker 기반 Background
    • Persistent 연결의 한계와 극복
    • Alarms API 활용
  2. Content Script 고급 기법

    • MutationObserver 최적화
    • 이벤트 디스패치 시뮬레이션
    • DOM 조작 안정성 확보
  3. WebSocket 통신

    • Socket.IO 재연결 로직
    • 토큰 기반 인증
    • Heartbeat 패턴

DOM 조작 노하우

  1. Radix UI 컨트롤

    • 키보드 이벤트로 슬라이더 제어
    • 동적 렌더링 대기 패턴
    • 커스텀 컴포넌트 접근법
  2. 선택자 전략

    • data-* 속성 활용
    • 폴백 선택자 준비
    • XPath vs CSS Selector 선택
  3. 성능 최적화

    • Debouncing으로 과도한 업데이트 방지
    • MutationObserver 범위 제한
    • 메모리 누수 방지

실시간 시스템 설계

  • 상태 관리: Optimistic Update 패턴
  • 에러 처리: 자동 재시도 및 알림
  • 모니터링: 주기적 검증으로 일관성 보장

🔮 향후 계획

단기 (베타 테스트 중)

  • FT/Instructor 이중 버전 개발
  • 기본 원격 제어 기능
  • 안정성 테스트 및 버그 수정
  • 사용자 피드백 반영

중기

  • 원격 설정 로딩 (CONFIG 서버에서 다운로드)
  • 성능 최적화 (IntersectionObserver)
  • 채팅 매크로 기능
  • 출석 체크 자동화

장기

  • 스트림덱 연동 강화
  • 다중 스페이스 동시 제어
  • 머신러닝 기반 이상 행동 감지

프로젝트 기간: 2025.07 - 현재 현재 상태: 베타 테스트 버전: FT v3 / Instructor v2 최종 업데이트: 2025-12-21