cd /projects
$ cat deepwebide/README.md
project : DeepWebIDE - 실시간 협업 웹 IDE
period : 2025.07 - 2025.08
role : Team Lead, Fullstack Developer, Infra
stack :
ReactTypeScriptViteSpring BootJava 17WebSocketYJSRedisMySQLDockerNginxAWSGitHub ActionsSentryCoolsms
// key metrics
  • 7명 팀 프로젝트 (팀장 + 풀스택 + 인프라 담당)
  • 프론트엔드/백엔드/샌드박스/YJS 4개 서비스 풀스택 개발
  • 실시간 동시 편집 지연시간 100ms 이하 달성
  • Docker 기반 코드 실행 격리로 보안 확보
$ open demo.mp4
$ open demo.mp4 // DeepWebIDE - 실시간 협업 웹 IDE 데모 영상

문제 정의

팀 프로젝트나 페어 프로그래밍 시 동시에 코드를 편집하고 실행 결과를 공유할 수 있는 환경이 필요했습니다. 기존 솔루션들은:

  • 유료이거나 기능 제한이 있음
  • 실시간 동시 편집 품질이 낮음
  • 코드 실행 환경이 없거나 보안 취약

목표: 실시간 동시 편집 + 채팅 + 안전한 코드 실행을 지원하는 웹 기반 IDE 개발

역할과 범위

역할: 풀스택 개발자 (프론트엔드, 백엔드, 인프라 전체)
범위:

  • 프론트엔드: React + TypeScript UI/UX 전체
  • 백엔드: Spring Boot API 서버, 인증, 채팅
  • 샌드박스: Docker 기반 코드 실행 서버
  • YJS 서버: 실시간 동시 편집 WebSocket 서버
  • 인프라: AWS 배포, CI/CD 파이프라인

아키텍처

┌─────────────────────────────────────────────────────────────┐
│                        Frontend (React)                       │
│              React + TypeScript + Vite + Monaco Editor        │
└─────────────────────────────────────────────────────────────┘

        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
┌───────────────┐   ┌───────────────┐   ┌───────────────┐
│   Backend     │   │   YJS Server  │   │   Sandbox     │
│ (Spring Boot) │   │  (Node.js)    │   │(Spring Boot)  │
│               │   │               │   │               │
│ - Auth/JWT    │   │ - CRDT Sync   │   │ - Docker Exec │
│ - Chat(STOMP) │   │ - WebSocket   │   │ - File System │
│ - File Mgmt   │   │ - Y-WebSocket │   │ - Multi-lang  │
└───────┬───────┘   └───────────────┘   └───────┬───────┘
        │                                       │
        ▼                                       ▼
┌───────────────┐                       ┌───────────────┐
│    MySQL      │                       │    Docker     │
│   + Redis     │                       │  Containers   │
└───────────────┘                       └───────────────┘

레포지토리 구성

서비스기술 스택역할
deepwebide-feReact, TypeScript, Vite프론트엔드 UI
deepwebide-beSpring Boot, Java 17API 서버, 인증, 채팅
deepwebide-coderunnerSpring Boot, Docker코드 실행 샌드박스
deepwebide-yjsNode.js, TypeScript, YJS실시간 동시 편집 서버

핵심 구현

1. YJS 기반 실시간 동시 편집

**CRDT (Conflict-free Replicated Data Type)**를 활용하여 여러 사용자가 동시에 같은 코드를 편집할 때 충돌 없이 병합:

// YJS 문서 바인딩 (프론트엔드)
const yDoc = new Y.Doc();
const provider = new WebsocketProvider(
  'wss://yjs.deepwebide.com',
  roomId,
  yDoc
);

// Monaco Editor와 YJS 연동
const yText = yDoc.getText('code');
const binding = new MonacoBinding(
  yText,
  editor.getModel()!,
  new Set([editor]),
  provider.awareness
);

// 사용자 커서/선택 영역 공유
provider.awareness.setLocalStateField('user', {
  name: username,
  color: userColor,
  cursor: editor.getPosition()
});

2. Docker 샌드박스 코드 실행

안전한 코드 실행을 위해 Docker 컨테이너 격리 환경 구현:

@Service
public class CodeExecutionService {
    
    public ExecutionResult execute(CodeRequest request) {
        // 1. 임시 디렉토리에 코드 파일 생성
        Path tempDir = createTempDirectory(request.getSessionId());
        writeCodeFile(tempDir, request.getCode(), request.getLanguage());
        
        // 2. Docker 컨테이너에서 실행
        DockerClient docker = DockerClientBuilder.getInstance().build();
        
        CreateContainerResponse container = docker.createContainerCmd(getImage(request.getLanguage()))
            .withBinds(new Bind(tempDir.toString(), new Volume("/code")))
            .withNetworkDisabled(true)  // 네트워크 격리
            .withMemory(256 * 1024 * 1024L)  // 메모리 제한 256MB
            .withCpuQuota(50000L)  // CPU 제한
            .exec();
        
        // 3. 실행 및 결과 수집
        docker.startContainerCmd(container.getId()).exec();
        
        return collectOutput(container.getId(), request.getTimeout());
    }
    
    private void cleanupContainer(String containerId) {
        docker.removeContainerCmd(containerId).withForce(true).exec();
    }
}

3. WebSocket 실시간 채팅

STOMP 프로토콜과 Redis Pub/Sub을 활용한 실시간 채팅:

@Controller
public class ChatController {
    
    @MessageMapping("/chat.send/{roomId}")
    @SendTo("/topic/room/{roomId}")
    public ChatMessage sendMessage(
        @DestinationVariable String roomId,
        @Payload ChatMessage message,
        SimpMessageHeaderAccessor headerAccessor
    ) {
        message.setSender(getUserFromSession(headerAccessor));
        message.setTimestamp(Instant.now());
        
        // Redis에 메시지 저장 (히스토리)
        chatRepository.save(roomId, message);
        
        return message;
    }
}

// Redis Pub/Sub으로 서버 간 메시지 동기화
@Component
public class RedisMessageSubscriber implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
        ChatMessage chatMessage = deserialize(message);
        messagingTemplate.convertAndSend(
            "/topic/room/" + chatMessage.getRoomId(), 
            chatMessage
        );
    }
}

트러블슈팅

이슈 1: YJS 동기화 지연 문제

현상: 다수 사용자가 빠르게 입력 시 동기화가 1-2초 지연

원인: WebSocket 메시지 직렬화/역직렬화 오버헤드 + 불필요한 전체 문서 전송

해결:

  • 변경된 델타만 전송하도록 최적화
  • 메시지 배치 처리 (debounce 50ms)
  • 바이너리 프로토콜 사용
// 최적화된 업데이트 전송
const updateHandler = (update: Uint8Array, origin: unknown) => {
  if (origin !== provider) {
    // 델타만 바이너리로 전송
    provider.ws?.send(encodeUpdate(update));
  }
};
yDoc.on('update', debounce(updateHandler, 50));

결과: 동기화 지연 100ms 이하로 개선


이슈 2: 코드 실행 컨테이너 자원 누수

현상: 시간이 지나면서 서버 메모리 사용량 증가, 컨테이너 zombie 프로세스 발생

원인:

  • 타임아웃된 컨테이너가 제대로 정리되지 않음
  • 예외 발생 시 cleanup 로직 미실행

해결:

@Scheduled(fixedRate = 60000)
public void cleanupOrphanedContainers() {
    List<Container> containers = docker.listContainersCmd()
        .withLabelFilter(Map.of("app", "deepwebide"))
        .withStatusFilter(List.of("exited", "dead"))
        .exec();
    
    for (Container container : containers) {
        long age = System.currentTimeMillis() - container.getCreated() * 1000;
        if (age > 5 * 60 * 1000) {  // 5분 이상 된 컨테이너
            docker.removeContainerCmd(container.getId())
                .withForce(true)
                .withRemoveVolumes(true)
                .exec();
            log.info("Cleaned up orphaned container: {}", container.getId());
        }
    }
}

재발 방지:

  • 모든 코드 실행 로직에 try-finally로 cleanup 보장
  • 스케줄러로 주기적 정리
  • 모니터링 알림 추가

이슈 3: GitHub OAuth 콜백 처리 문제

현상: 로그인 후 프론트엔드로 리다이렉트 시 토큰 전달 실패

원인: SPA 환경에서 OAuth 콜백 URL 처리 방식 이슈

해결: Fragment 기반 토큰 전달 + 팝업 윈도우 방식

// 백엔드: 토큰을 Fragment로 전달
@GetMapping("/oauth/github/callback")
public void handleCallback(@RequestParam String code, HttpServletResponse response) {
    String accessToken = githubOAuthService.exchangeCodeForToken(code);
    User user = githubOAuthService.getUserInfo(accessToken);
    String jwt = jwtService.generateToken(user);
    
    // Fragment로 전달 (URL에 노출되지 않음)
    response.sendRedirect(frontendUrl + "/auth/callback#token=" + jwt);
}
// 프론트엔드: Fragment에서 토큰 추출
useEffect(() => {
  const hash = window.location.hash;
  const token = new URLSearchParams(hash.slice(1)).get('token');
  if (token) {
    localStorage.setItem('accessToken', token);
    window.opener?.postMessage({ type: 'AUTH_SUCCESS', token }, '*');
    window.close();
  }
}, []);

성과

지표결과
개발 기간2개월 (기획 ~ 배포)
담당 범위프론트엔드/백엔드/인프라 100%
실시간 동기화100ms 이하 지연
지원 언어Python, JavaScript, Java
동시 접속룸당 10명 이상 테스트 완료

프론트엔드 기여

  • Monaco Editor 커스터마이징: VS Code 수준의 코드 편집 경험 제공
  • 실시간 커서 공유: 다른 사용자의 커서 위치/선택 영역 시각화
  • 반응형 UI: 데스크탑/태블릿 대응
  • 다크 모드: 개발자 친화적 테마 지원

회고 및 다음 개선

잘한 점

  • MSA 구조로 각 서비스 독립 배포 가능
  • Docker 격리로 보안 확보
  • YJS 선택으로 안정적인 실시간 동기화

개선할 점

  • 파일 시스템 영구 저장 (현재는 세션 기반)
  • 더 많은 언어 지원 (Go, Rust 등)
  • 터미널 기능 추가

다음 개선 계획

  • Kubernetes로 샌드박스 확장성 개선
  • LSP(Language Server Protocol) 연동으로 자동완성/진단 기능 강화
  • 프로젝트 저장 및 공유 기능