병원 사이트 PHP 레거시 개선과 Next.js·NestJS 이전
레거시 PHP 병원 사이트를 먼저 가볍게 고치고, Next.js·NestJS 모노레포로 1:1 이전해 운영 전환까지 한 작업
- 척추 전문 병원의 PHP 사이트를 먼저 개선하고, 이어서 Next.js·NestJS 모노레포로 옮겨 운영 전환까지 맡았습니다.
- 첫 이전 방식이 화면을 맞추지 못하자 폐기하고, 레거시 CSS·JS를 그대로 싣는 1:1 이전으로 다시 옮겼습니다.
- 같은 시기 안과 사이트 두 곳의 이전도 직접 시작하거나 참여했습니다.
문제
척추 전문 병원의 홈페이지는 PHP로 만들어져 있었다. 영문은 pages/, 국문은 kr/pages/에 같은 구조의 PHP 파일이 35개씩 따로 복제되어 있었고, 관리자는 adm/ 아래 PHP 파일 123개였다. 메타데이터와 메뉴는 PHP 배열 설정 파일에 들어 있었다.
없는 페이지도 200으로 응답했다. 방문 통계는 요청마다 PHP가 테이블에 직접 기록했다. 이미지는 416개에 258MB였다.
해결
1단계 — 레거시에서 먼저 고친 것 (2026.04)
이전에 들어가기 전에 레거시 PHP 사이트부터 손봤다.
- 검색 노출 — 메타 태그, Schema.org 구조화 데이터,
sitemap.xml,robots.txt,llms.txt, 404 응답을 넣었다. - 이미지 — PNG·JPG 416개를 WebP로 바꿔 258MB를 12MB로 줄였다. 이때 만든 WebP 파일은 이전 후에도 그대로 가져갔다.
- 로딩 — JS를 defer로, CSS를 비동기로 불러오고 DB 설정 파일을 캐시했다.
2단계 — Next.js·NestJS로 1:1 이전 (2026.05 – 08)
구조. pnpm + Turborepo 모노레포에 apps/web(Next.js), apps/api(NestJS), apps/e2e(Playwright)를 두었다.
화면. 첫 시도는 변환 스크립트로 PHP 본문을 HTML 문자열로 넣는 방식이었다. 레거시 JS 플러그인과 디자인이 붙지 않아 폐기했다. 두 번째는 레거시 CSS·JS·폰트·이미지를 그대로 두고 로드 순서를 레이아웃 하나로 재현한 뒤, 페이지를 로케일별 JSX로 1:1 옮겼다.
URL. /kr/*, *.php, ?seq= 상세 주소를 새 경로로 리다이렉트했다. 규칙 순서를 조정해 레거시 주소가 한 번에 최종 주소로 가게 했다.
DB. 추정으로 쓴 스텁 모델 20개를 운영 DB에서 역추출한 실제 테이블 9개로 바꾸고, 타입 에러 55개를 정리했다. 이후 전체 스키마를 0_init 베이스라인으로 만들고 그 위에 마이그레이션을 쌓았다.
관리자. 문의·공지·FAQ·영상·팝업·회원·설정·접속 통계를 NestJS 모듈과 관리자 화면으로 옮기면서 Redis 세션, 로그인 시도 제한, 변경 감사 로그, CSV 내보내기를 넣었다.
배포. 단일 Lightsail 인스턴스에 docker compose로 nginx·web·api·Redis·certbot을 올렸다. CI가 타입 검사·단위 테스트·빌드·E2E를 통과한 이미지만 레지스트리에 올리고, 그 이미지로 배포한다. DB와 업로드 파일은 매일 백업한다.
전환 후 수정. 레거시 JS가 React 하이드레이션 전에 DOM을 건드려 히어로 영역이 검게 남거나 애니메이션이 사라지는 문제가 있었다. 하이드레이션 신호를 받은 뒤 초기화하도록 바꿨다. 이전 과정에서 빠진 방문 수집은 미들웨어에서 복구했다.
| 정한 것 | 대신 버린 것 | 이유 |
|---|---|---|
| 프론트 이전 방식레거시 CSS·JS·이미지를 그대로 싣고 마크업을 로케일별로 1:1 이전 | 변환 스크립트로 PHP 본문을 HTML 문자열로 주입 (1차 시도) | 1차 시도는 html/body 래핑과 인라인 스크립트 실행 문제로 화면을 레거시와 같게 맞출 수 없었다. 사흘 만에 폐기하고 퍼블리싱 동일성부터 다시 잡았다. |
| DB 스키마운영 DB에서 스키마를 역추출하고 0_init을 '적용됨'으로 베이스라인 처리 | 추정으로 쓴 스텁 모델, 또는 0_init을 운영에 재실행 | 실제 테이블은 추정한 20개가 아니라 9개였다. 운영 DB에는 이미 모든 테이블이 있어 0_init을 다시 돌리면 충돌한다. |
| 자동 배포 대상CI를 통과한 커밋의 sha 태그 이미지 | :latest 이미지 | 동시에 머지돼도 검증되지 않은 이미지가 배포되지 않고, CI가 실패하면 배포 자체가 일어나지 않는다. |
| 문의 접수DB 저장을 기준으로 두고 메일 발송은 실패해도 진행 | 메일 발송 실패 시 요청 실패 | SMTP 장애로 환자 문의를 잃지 않기 위해서다. |
같은 시기의 다른 이전
- 안과 사이트 A — 이전을 직접 시작했다. 모노레포 초기 구조를 만들고, jQuery 인터랙션을 React로 바꿨다. 스크립트 12개의 직렬 로드를 4단계 병렬로 바꾸고, 렌더를 막는 CSS를 핵심 4개로 줄였다. 이미지는 582MB에서 255MB로 줄였고, 블루-그린 배포를 붙였다. 이후 동료가 이어받았다.
- 안과 사이트 B — 동료가 시작한 이전에 참여했다. 레거시
.php주소를 새 경로로 연결하고, 이관 과정에서 깨진 본문과 빠진 이미지를 복원했다. 운영이 두 번 멈춘 원인이던 DB 커넥션 풀 고갈도 찾아 고쳤다.
결과
- 258MB → 12MB
- 레거시 이미지 416개 WebP 전환
- 39
- 이전한 공개 페이지 라우트 (한·영)
- 31
- 이전한 관리자 화면
- 공개 라우트 39개를 한·영으로 서빙하고, 관리자 화면 31개를 옮겼다. NestJS 모듈 27개, Prisma 모델 12개다.
- 단위 테스트는 api 51개(Jest), web 28개(Vitest)다. Playwright는 비로그인 스모크와 로그인 흐름을 CI에서 돌린다. 로그인 흐름 테스트를 쓰다 운영 버그 2건을 찾아 고쳤다.
- 이미지 용량은 커밋 기록 기준으로 홈 18.1MB → 1.5MB, 하위 페이지 약 183MB → 약 15MB다. 로딩 시간은 측정하지 않았다.
- 2026년 7월 새 사이트로 운영을 전환했다.
- Claude Code로 구현 속도를 높였고, 이전 방식을 바꾸는 판단과 테스트·검증은 직접 했다.
계약상 병원 실명, 도메인, 화면 캡처는 공개하지 않는다.
이 프로젝트가 궁금하시면 편하게 물어보세요.
연락하기