처음엔 느린 빌드를 고치는 일이라 생각했지만, 진짜 문제는 여러 팀이 함께 쓰는 시스템이라 누구도 쉽게 바꾸기 어려운 구조였습니다. 22개 앱·21명이 공유하던 React + Express SSR 모놀리스를 부분 빌드가 가능한 모노레포로 다시 설계한 기록입니다.
이 글은 발표자료를 보시면 더 좋습니다.
| 항목 | 내용 |
|---|---|
| 환경 | React + Express SSR, 22개 앱, 21명 공동 개발 |
| 출발 문제 | 오래 걸리는 빌드 |
| 역할 | 병목 측정, Webpack5 전환, 앱 단위 빌드 설계, Turborepo 도입, 점진적 마이그레이션 |
| 핵심 성과 | 스쿼드별 독립적 기술 의사결정 가능, 운영 빌드 65~98% 단축, Feature branch 99.5% 단축 |
| 핵심 포인트 | 빌드 속도가 아니라, 스쿼드가 스스로 기술을 결정할 수 있게 된 것 |
edu-core는 구름EDU의 핵심 레포입니다. React + Express SSR 구조 안에서 22개 앱이 함께 돌았고, 21명 이상이 동시에 수정하고 있었습니다.
원인은 전형적인 빌드 레거시였습니다.
file-loader 기반 asset 처리browserslistreact-hot-loader 기반 개발 환경이 시점의 문제 정의는 단순했습니다. 빌드 시스템이 현재 규모를 감당하지 못한다.
기존 구조를 유지한 채 빌드 체계를 현대화했습니다.
browserslist 현대화, IE 제거react-hot-loader → Fast Refreshnew-webpack-config 패키지 분리개발 환경에서는 성과가 분명했습니다.
| 항목 | Before | After | 개선율 |
|---|---|---|---|
| 2차 빌드 (캐싱) | ~115초 | ~15초 | 87% ↓ |
| dist 용량 | 529MB | 326MB | 40% ↓ |
하지만 운영 반영에 실패했습니다. 새 webpack 설정을 검증하는 동안에도 기존 config로 새 페이지가 계속 추가됐고, 21명을 같은 시점에 옮기는 일은 불가능했습니다.
교훈은 분명했습니다. 기술 선택은 맞았지만, 전환 방식이 틀렸다. 큰 공유 레포에서 중요한 것은 더 좋은 설정이 아니라 멈추지 않고 옮겨갈 수 있는 구조였습니다.
질문을 바꿔봤습니다.
답은 조직과 시스템의 불일치였습니다. edu-core는 한 팀이 빠르게 움직이던 시절의 구조였지만, 지금은 EDU B2C, EDU B2B, DEVTH로 스쿼드가 나뉘어 있었습니다. 비즈니스 책임은 분리됐는데, 시스템은 여전히 하나였습니다. 시스템 전반에 영향을 주는 변경은 어느 스쿼드도 단독으로 결정하기 어려워 늘 뒤로 밀렸습니다.
이 관점에서 기술 병목도 다시 보였습니다.
frontApp 한 줄을 고쳐도 examApp 포함 전체가 다시 빌드됨devth, channel, obserview 등이 같은 빌드 흐름에 묶임새 설계 조건은 네 가지였습니다.
필요한 것은 빠른 webpack 설정이 아니라 작은 앱 단위로 분리되고 순차 이전 가능한 구조였습니다.
빌드 시간이 아니라, 빌드 비용이 커질 수밖에 없는 구조를 해체했습니다.
SSR과 무관한 devth, obserview, channel, swcamp, captain을 별도 레포·배포 흐름으로 떼어냈습니다. 이 한 단계로 static 페이지 수정 시 운영 빌드는 1748초 → 22초.
산출물과 라우팅을 앱 기준으로 쪼갰습니다.
기존 dist/client -> 모든 앱이 함께 번들링 변경 dist/frontApp/client dist/examApp/client ...
각 앱이 독립된 package.json, webpack.config.js, client/server entry를 갖게 했습니다. 필요한 앱만 빌드하고, 나머지는 404 또는 기존 결과물을 재사용했습니다.
첫 시도의 결과물을 그대로 재활용했습니다.
@edu-core/old-webpack-configs@edu-core/new-webpack-configs앱마다 old/new를 선택하게 만들어, 전체를 한 번에 바꾸는 대신 각 스쿼드가 준비된 앱부터 순서대로 옮길 수 있게 했습니다. 첫 시도의 new-webpack-config가 여기서 핵심 자산이 됐습니다.
앱 경계가 생긴 뒤 Turborepo로 변경된 앱만 빌드하게 했습니다.
turbo:pull로 사전 결과물 활용)핵심은 캐시 도입이 아니라, 캐시가 의미 있게 동작할 수 있는 구조를 먼저 만든 것이었습니다.
SRE의 이미지 기반 배포에 맞춰 remote cache를 연결했고, 앱별로 달라진 설정·HMR 실행은 gext dev, gext build 같은 내부 CLI로 감쌌습니다. 구조는 앱 단위로 나뉘었지만 매일 쓰는 명령은 단순하게 유지했습니다.
| 시나리오 | Before | After | 개선율 |
|---|---|---|---|
| static 페이지 수정 | 1748초 | 22초 | 98% ↓ |
| 서버 코드만 수정 | 767초 | 68초 | 91% ↓ |
| React 앱 1개 수정 | 1748초 | 204초 | 88% ↓ |
| 공통 영역 수정 | 1748초 | 606초 | 65% ↓ |
| 로컬 개발환경 시작 | 약 15분 | 약 1분 | 93% ↓ |
| Feature branch 빌드 | 약 10분 | 3초 | 99.5% ↓ |
| 2차 빌드 (캐싱) | 약 115초 | 약 15초 | 87% ↓ |
| dist 사이즈 | 529MB | 326MB | 40% ↓ |
더 중요한 변화는 의사결정 범위였습니다. 이전에는 빌드 구조, 모듈 업데이트, 언어 도입 같은 변경을 어느 스쿼드도 단독으로 밀어붙이기 어려웠지만, 전환 이후에는 각 스쿼드가 담당 앱 안에서 직접 결정할 수 있게 됐습니다.
예를 들어 특정 페이지에 TypeScript를 도입할 때, 예전 같으면 다른 스쿼드와 영향도를 먼저 논의해야 했지만 이제는 담당 스쿼드가 앱 단위에서 바로 적용했습니다.
빌드 시간 단축은 눈에 보이는 성과였고, 본질적 변화는 기술 부채를 작은 단위로 나눠 실제로 해결할 수 있는 구조가 생긴 것이었습니다.
| 구분 | 첫 번째 시도 | 두 번째 시도 |
|---|---|---|
| 문제 관점 | 빌드 성능 | 조직 + 시스템 구조 |
| 해결 단위 | Webpack config | 앱 단위 경계 |
| 전환 방식 | 일괄 | 점진적 |
| DEV 결과 | 성공 | 성공 |
| OP 결과 | 실패 | 성공 |
| 남긴 것 | 교훈 + config 자산 | 기술 성과 + 조직 변화 |
핵심 세 가지를 얻었습니다.
결국 이 프로젝트는 빌드를 빠르게 만든 사례이면서, 여러 팀이 함께 쓰는 시스템을 실제로 바꿀 수 있는 구조로 다시 설계한 사례였습니다.