React Playground의 시스템 아키텍처: 실제로 무슨 일이 일어나는가
Source: Dev.to
고수준 개요
- 프론트엔드는 IDE처럼 동작합니다.
- 백엔드는 코디네이터 역할을 합니다.
- 워커가 실제로 제출된 코드를 실행합니다.
- Redis가 모든 것을 연결합니다.
- S3는 사용자 솔루션을 저장합니다.
프론트엔드: 에디터와 프리뷰
편집
- Monaco Editor가 사용자가 입력한 JSX를 캡처합니다.
- **Babel (standalone)**이 JSX → JavaScript 로 트랜스파일합니다.
- 결과 JavaScript가 iframe에 삽입됩니다.
왜 iframe인가?
사용자 코드가 오류가 나면 iframe이 실패를 격리시켜 나머지 앱이 살아 있도록 합니다. iframe은 다음을 제공합니다:
- 별도의 DOM
- 별도의 JavaScript 컨텍스트
React와 ReactDOM은 CDN을 통해 iframe 내부에 로드되고, 컴포넌트가 그곳에서 렌더링되어 즉시 피드백을 제공합니다.
코드 제출
사용자가 “Submit” 버튼을 클릭하면:
- 트랜스파일된 코드가 아닌 원본 JSX가 백엔드로 전송됩니다.
- 백엔드가
solutionId를 생성하고 이를 프론트엔드에 반환합니다.
이 시점에서는 아직 실행되지 않았습니다.
실시간 통신
- 프론트엔드는 WebSocket 연결을 열고
solutionId를 사용해 등록합니다. - 백엔드는 이 소켓을 통해 결과를 푸시합니다(폴링 없음).
백엔드 코디네이션
작업 큐잉
코드를 메인 서버에서 직접 실행하는 대신, 백엔드는:
- 작업을 BullMQ 큐에 푸시합니다.
- 작업 페이로드에 코드와 관련 메타데이터를 포함합니다.
큐를 사용하는 이유
- 실행이 느리고 예측 불가능하며 잠재적으로 위험합니다.
- 메인 서버에서 실행하면 다른 요청을 차단하고 성능 위험이 발생합니다.
워커 프로세스
전용 워커가 BullMQ 큐를 지속적으로 청취합니다. 작업이 도착하면:
- 워커가 작업을 가져와 Babel을 사용해 JSX를 다시 트랜스파일합니다.
- Puppeteer를 실행하고 코드를 실제 브라우저 환경에 주입한 뒤, 버튼 찾기, 클릭하기, UI 업데이트 확인 등 사용자 행동을 시뮬레이션합니다.
이 접근 방식이 제공하는 것:
- 실제 DOM 및 이벤트 루프
- Node.js만으로는 에뮬레이션할 수 없는 브라우저 수준 동작
격리와 안전성
- 각 실행은 새 Puppeteer 페이지에서 이루어지며 공유 상태가 없습니다.
- 워커는 백엔드와 별도 프로세스로 실행돼 실패가 격리됩니다.
결과 전달
- 실행이 끝나면 워커가 BullMQ에서 작업을 completed 로 표시합니다.
- 백엔드는 큐 이벤트를 청취하고
solutionId를 사용해 올바른 WebSocket을 찾아 결과를 전송합니다. - 프론트엔드는 결과를 즉시 받아 UI를 업데이트합니다.
Redis 사용
- BullMQ 큐 데이터를 저장합니다.
- 작업 이벤트(완료/실패)를 퍼블리시합니다.
- 더 빠른 읽기를 위한 캐시 레이어 역할을 합니다.
Raw Pub/Sub에서 BullMQ로 전환하면서 내장된 라이프사이클 이벤트, 재시도 처리, 더 깔끔한 코드를 제공해 아키텍처가 단순해졌습니다.
사용자 코드 저장
- 코드는 AWS S3에 저장되고, 파일 키만 데이터베이스에 보관됩니다.
- 필요할 때 백엔드가 signed URL을 생성해 프론트엔드가 코드를 가져오게 합니다.
이점
- 보안(공개 접근 차단)
- 확장성
- 메타데이터와 대용량 코드 블롭을 명확히 분리
엔드‑투‑엔드 흐름
- 사용자가 코드를 작성 → 프리뷰가 iframe에서 실행.
- 사용자가 제출 → 백엔드가
solutionId반환. - 프론트엔드가 WebSocket 열기 ID 사용.
- 백엔드가 작업을 BullMQ 큐에 푸시.
- 워커가 Puppeteer로 코드 실행.
- 워커가 작업 완료 표시; 백엔드가 이벤트 수신.
- 백엔드가 WebSocket을 통해 결과 전송.
- 프론트엔드가 UI를 업데이트 검증 결과 표시.
주요 장점
- 프론트엔드는 빠른 프리뷰를 담당하고, 백엔드는 실제 DOM을 이용한 신뢰성 있는 검증을 담당합니다.
- 워커가 독립적으로 실행돼 메인 서버가 차단되지 않습니다.
- WebSocket을 통한 실시간 업데이트로 부하가 감소하고 확장성이 향상됩니다.
- 책임이 명확히 분리돼 더 깔끔하고 유지보수하기 쉬운 아키텍처를 구현합니다.