대학 정보 SSG를 멀티존으로 분리한 이유
빌드 시간을 줄이는 일처럼 보였지만, 실제 문제는 배포 책임의 분리였다
솔리드 커넥션 웹에는 대학 정보를 보여주는 catalog 페이지가 있다. 이 페이지들은 SEO와 검색 노출을 위해 정적으로 생성되고 있었다. 처음에는 이 구조가 자연스러웠다. 대학 정보는 자주 바뀌지 않고, 사용자는 검색을 통해 대학 상세 페이지로 바로 진입할 수 있어야 했기 때문이다.
문제는 서비스가 커지면서 드러났다. 대학 정보 페이지가 apps/web 빌드 안에 함께 들어가 있었고, 메인 웹을 배포할 때마다 대학 상세 페이지까지 전부 다시 정적 생성되고 있었다.
단순히 빌드 시간이 몇 초 더 걸리는 문제가 아니었다. 더 큰 문제는 작은 UI 수정이나 핫픽스에도 전체 웹 배포 흐름을 다시 타야 한다는 점이었다.
예를 들어 가벼운 UI 수정 하나를 반영하려고 해도, 기존 구조에서는 Preview 배포를 확인하고 main에 머지한 뒤 Production 배포까지 기다려야 했다. 이 과정에서 실제 반영까지 최소 10분 이상 걸리는 경우가 있었다. 수정 범위는 작았지만, 배포 단위가 하나로 묶여 있었기 때문에 항상 전체 웹과 대학 catalog SSG가 함께 움직였다.
관련 작업:
- 이슈: #542 대학 상세 페이지 정적 생성으로 인한 웹 빌드 시간 개선
- PR: #543 feat: 대학 정보 멀티존 분리
시작점: 빌드 로그에서 보인 이상한 무게감
작업 전 GitHub Actions의 Web - Build job을 보면 전체 job은 대략 40~48초 정도 걸렸다.
| CI Run | Web Build Job | Build web application |
|---|---|---|
26803027151 | 42초 | 27초 |
26802563215 | 48초 | 25초 |
26802151654 | 46초 | 25초 |
26800113782 | 40초 | 26초 |
| 평균 | 44초 | 25.75초 |
절대 시간만 보면 아주 심각한 병목처럼 보이지는 않았다. 하지만 로그에서 더 중요한 신호가 보였다.
Generating static pages using 3 workers (188/188) in 5.6s
그리고 대학 상세 경로가 메인 웹 빌드 안에서 함께 생성되고 있었다.
/university/inha/2166
/university/inha/2167
/university/inha/2168
+139 more paths
즉, 메인 웹을 빌드할 때 대학 상세 페이지 약 142개 이상이 함께 정적 생성되고 있었다.
문제는 현재 시간이 아니라 증가 방향이었다
당장 5.6초가 치명적인 문제는 아니었다. 하지만 구조적으로는 대학 데이터가 늘어날수록 빌드 타임에 생성해야 하는 페이지도 선형으로 늘어나는 상태였다.
더 큰 문제는 운영 관점이었다.
대학 정보에 버그가 생기면 메인 웹 전체를 다시 배포해야 했다. 대학 데이터만 바뀌어도 필요 없는 메인 웹 빌드를 다시 타야 했다. 반대로 메인 웹의 작은 수정에도 대학 상세 SSG가 같이 실행됐다.
즉, 문제는 단순히 “몇 초 느리다”가 아니었다.
핵심 문제는 이것이었다.
대학 정보 catalog와 main web의 배포 책임이 하나로 묶여 있었다.
이 구조에서는 변경 범위가 작아도 항상 전체 배포 흐름을 거쳐야 했다. 특히 핫픽스 상황에서는 이 점이 크게 체감됐다. 단순한 UI 수정도 Preview 확인, main 머지, Production 반영까지 이어지면서 최소 10분 이상 걸릴 수 있었고, 대학 정보와 관련 없는 수정에도 대학 catalog SSG 비용이 함께 따라왔다.
검토한 선택지
처음에는 ISR이나 온디맨드 ISR도 선택지로 검토했다. 하지만 이번 목표와는 맞지 않았다.
대학 데이터는 실시간으로 자주 바뀌는 데이터가 아니다. 약 6개월 단위로 수동 반영되는 성격의 데이터다. 그리고 SEO/GEO 관점에서 대학 상세 페이지는 정적 HTML로 안정적으로 제공되는 편이 좋았다.
그래서 목표는 “SSG를 버리자”가 아니었다.
목표는 다음에 가까웠다.
대학 정보는 계속 SSG로 만들되, main web 빌드와 배포 큐에서 분리하자.
이 결론 때문에 멀티존 방향을 선택했다.
해결 방향: university-web을 별도 Next.js zone으로 분리
PR #543에서는 대학 정보 catalog를 apps/university-web이라는 별도 Next.js 앱으로 분리했다.
구조는 이렇게 바뀌었다.
| 영역 | 변경 전 | 변경 후 |
|---|---|---|
| main web | /university catalog와 score/application 모두 포함 | score/application만 유지 |
| university catalog | apps/web 안에서 SSG | apps/university-web에서 SSG |
| 라우팅 | 하나의 Next 앱 | Vercel rewrite 기반 멀티존 |
| 배포 | web 하나에 묶임 | web / university-web 분리 가능 |
| Vercel 큐 | 하나의 프로젝트 큐를 공유 | web과 web-univ가 각각 다른 배포 큐 사용 |
apps/web에서는 catalog route를 직접 렌더링하지 않고, UNIVERSITY_WEB_DOMAIN 기반 rewrite로 university-web에 연결한다.
반대로 apps/university-web은 대학 catalog를 계속 SSG로 생성한다.
apps/web
/university/score/*
/university/application/*
catalog route는 rewrite
apps/university-web
/university
/university/[homeUniversity]
/university/[homeUniversity]/[id]
/university/[homeUniversity]/search
이렇게 분리하면서 중요한 변화가 생겼다. 이제 대학 catalog 수정은 web-univ 프로젝트의 Vercel 배포 큐를 타고, 메인 웹 수정은 web 프로젝트의 Vercel 배포 큐를 탄다.
즉, 단순히 폴더를 나눈 것이 아니라 배포 대기열 자체를 분리한 것이다.
수치로 본 결과
가장 직접적으로 개선된 지점은 main web의 정적 생성 범위다.
| 항목 | 변경 전 | 변경 후 | 효과 |
|---|---|---|---|
| main web 정적 생성 페이지 수 | 188개 | 35개 | 153개 감소, 약 81% 감소 |
| main web static generation 시간 | 5.6초 | 1.626초 | 3.974초 감소, 약 71% 감소 |
main web Build web application 단계 | 약 25~27초 | 약 23초 | 약 2~4초 감소 |
전체 Web - Build job | 약 40~48초 | 약 44초 | 고정 비용 때문에 기존 범위 내 |
여기서 중요한 점은 전체 CI job 시간이 극적으로 줄었다고 말하면 안 된다는 것이다. install, setup, cache, upload 같은 고정 비용이 있기 때문에 전체 Web - Build job은 여전히 기존 범위 안에 있다.
하지만 병목의 성격은 바뀌었다.
기존에는 main web이 대학 catalog SSG까지 책임졌다. 변경 후에는 main web의 정적 생성 대상이 188개에서 35개로 줄었다. 대학 catalog SSG는 university-web으로 이동했다.
university-web 쪽에서는 catalog SSG가 유지된다.
Generating static pages using 3 workers (158/158) in 3.3s
즉, SEO를 위해 정적 페이지를 만드는 전략은 유지하면서, 그 비용을 main web 배포에서 분리했다.
Vercel 배포 시간에서도 차이가 보였다
분리 전에는 하나의 Vercel 프로젝트에서 모든 변경이 처리됐다.
- 변경전 (web + univ)
Ready
2m 55s
분리 후에는 web과 web-univ가 각각 다른 Vercel 프로젝트와 배포 큐를 타게 됐다.
변경후 (web-univ)
Ready
1m 16s
변경후 (web)
Ready
1m 9s
Ready 기준으로 보면 기존 2분 55초에서, 분리 후 각각 1분 16초와 1분 9초 수준으로 줄었다. 배포 단위가 가벼워지면서 개별 프로젝트의 배포 준비 시간은 절반 이하 수준으로 감소했다.
더 중요한 점은 두 프로젝트가 같은 큐를 기다리지 않는다는 것이다. 기존에는 메인 웹과 대학 catalog가 하나의 배포 흐름에 묶여 있었다. 변경 후에는 대학 정보 수정은 web-univ만, 메인 웹 수정은 web만 배포할 수 있다.
이 덕분에 핫픽스 상황에서 “전체 웹을 다시 배포해야 하는가?”가 아니라 “수정된 영역만 배포할 수 있는가?”를 기준으로 대응할 수 있게 됐다.
운영 관점에서 더 큰 효과
이번 작업의 진짜 결론은 “몇 초 줄었다”보다 “어디를 다시 빌드하고 배포해야 하는가가 달라졌다”에 있다.
변경 전에는 대학 정보 버그를 고치려면 main web 전체 배포가 필요했다. 변경 후에는 university-web만 따로 배포할 수 있다.
| 상황 | 변경 전 | 변경 후 |
|---|---|---|
| 대학 상세 UI 수정 | main web 재빌드 | university-web 재빌드 |
| 대학 데이터 반영 | main web 재빌드 | university-web 재빌드 |
| 메인 웹 기능 수정 | 대학 catalog SSG도 함께 실행 | main web만 빌드 |
| catalog 장애 대응 | main web과 묶임 | university-web 단위 대응 |
| Vercel 배포 큐 | 하나의 큐에서 대기 | web / web-univ 큐 분리 |
이 구조는 대학 정보가 계속 늘어날수록 더 의미가 커진다. catalog 페이지 수가 증가해도 그 비용이 main web의 모든 배포에 따라붙지 않는다. 반대로 main web의 핫픽스도 대학 catalog SSG와 분리된 상태에서 더 빠르게 처리할 수 있다.
즉, 이번 작업은 단순한 빌드 시간 단축이 아니라 배포 blast radius를 줄인 작업이었다.
구현하면서 넣은 안전장치
멀티존은 단순히 앱을 하나 더 만드는 것으로 끝나지 않는다. 경계가 생기면 그 경계를 명확히 관리해야 한다.
특히 <Link> 대신 <a>를 강제하는 lint 규칙은 중요했다. Next.js 앱이 서로 다른 zone으로 나뉘면, 같은 도메인처럼 보여도 실제로는 다른 Next 앱일 수 있다. 이때 Next client navigation이 잘못 개입하면 존재하지 않는 route manifest를 찾는 문제가 생길 수 있다.
비용도 있었다
이 작업은 작지 않았다.
물론 대부분은 apps/university-web 분리를 위한 앱 구성, 정적 자산, 설정, 복제된 기반 코드였다. 그래도 멀티존은 공짜가 아니다.
앞으로는 다음 비용을 계속 관리해야 한다.
| 비용 | 설명 |
|---|---|
| 설정 복잡도 증가 | Vercel Project, Root Directory, env가 하나 더 생김 |
| 중복 코드 가능성 | web과 university-web 사이 공통 코드 관리 필요 |
| 배포 경계 관리 | rewrite와 직접 도메인 정책을 실수 없이 유지해야 함 |
| CI job 증가 | web과 university-web 검증/빌드가 분리되어 job 수가 늘어남 |
그래서 멀티존은 단순히 “빌드가 느리니까 앱을 쪼개자”로 결정하면 위험하다. 배포 경계가 실제 제품 경계와 맞을 때 선택해야 한다.
이번 경우에는 대학 catalog가 충분히 독립적인 제품 영역이었고, 데이터 변경 주기와 SEO 요구사항도 main web과 달랐다. 그래서 분리할 명분이 있었다.
결론
이번 작업으로 main web의 static generation 대상은 188개에서 35개로 줄었다. 정적 생성 시간은 5.6초에서 1.626초로 줄어 약 71% 개선됐다.
Vercel Ready 시간도 기존 2분 55초에서 분리 후 1분 16초, 1분 9초 수준으로 줄어 개별 배포 단위가 절반 이하로 가벼워졌다. 또한 web과 web-univ가 서로 다른 Vercel 배포 큐를 타게 되면서, 수정된 영역만 독립적으로 배포할 수 있는 구조가 됐다.
하지만 이 작업의 성과를 단순히 “전체 빌드 시간이 대폭 줄었다”라고 표현하면 부정확하다. 전체 CI job은 install, setup, cache, upload 같은 고정 비용 때문에 여전히 기존 범위 안에 있다.
더 정확한 결론은 이것이다.
대학 catalog SSG 비용을 main web 배포에서 분리했고, Vercel 배포 큐를 web / web-univ로 나누어 핫픽스의 대기 시간과 배포 blast radius를 줄였다.
빌드 최적화는 단순히 초를 줄이는 일이 아닐 때가 많다. 어떤 변경이 어떤 배포를 유발하는지, 어떤 실패가 어떤 영역을 막는지 분리하는 일도 빌드 최적화다.
이번 작업은 그 관점에서 의미가 있었다. 필요한 것만 빌드하고, 필요한 것만 배포할 수 있는 구조로 바꾸면서 운영 대응 속도와 배포 안정성을 함께 개선했다.