← cd /projects
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
문제 정의
팀 프로젝트나 페어 프로그래밍 시 동시에 코드를 편집하고 실행 결과를 공유할 수 있는 환경이 필요했습니다. 기존 솔루션들은:
- 유료이거나 기능 제한이 있음
- 실시간 동시 편집 품질이 낮음
- 코드 실행 환경이 없거나 보안 취약
목표: 실시간 동시 편집 + 채팅 + 안전한 코드 실행을 지원하는 웹 기반 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-fe | React, TypeScript, Vite | 프론트엔드 UI |
| deepwebide-be | Spring Boot, Java 17 | API 서버, 인증, 채팅 |
| deepwebide-coderunner | Spring Boot, Docker | 코드 실행 샌드박스 |
| deepwebide-yjs | Node.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) 연동으로 자동완성/진단 기능 강화
- 프로젝트 저장 및 공유 기능