From 9f827bf647981209a5392696d46b4df61e198415 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 12 Sep 2026 16:41:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(dev):=20=EB=A6=AC=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=ED=94=84=EB=A1=A0=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EB=B9=8C=EB=93=9C=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=EB=A5=BC=20=EB=81=8A=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개발 중에는 앱이 내려갈 때 Vite 를 죽이지 않음. Vite 는 한 앱 프로세스가 아니라 개발 세션의 것이고, 다음 기동이 그대로 물려씀 - Vite 가 이미 그 포트에 살아 있으면 npm run build 를 건너뜀(첫 기동에서만 빌드) 까닭 — uvicorn 리로더가 *.py 마다 앱 프로세스를 갈아 끼우는데, 옛 프로세스가 종료하며 Vite 를 죽이면 새 프로세스가 「Vite 없음」으로 보고 npm run build(최대 300초 + B07 CAD)를 처음부터 다시 돌렸음. 그 사이 /api/health 가 stale: true 인 채 옛 코드를 계속 내주어 옛 화면을 보고 헛검증하게 됨. 2026-09-12 에 두 번 멈췄고 한 번은 두 시간 반 동안 안 끝남. 배포 환경은 그대로 — 빌드도 종료 정리도 종전과 같음. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GrXDD23Dvt2sR7q3X6oekp --- main.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 30bb8b3b..c71afa88 100644 --- a/main.py +++ b/main.py @@ -169,6 +169,27 @@ def _kill_process_tree(pid: int) -> None: logger.debug("[Frontend] 프로세스 정리 중 무시된 예외: %s", exc) +def _dev_port_listening(port: int) -> bool: + """개발 서버(Vite)가 이미 그 포트를 물고 있나. + + **리로드인지 첫 기동인지를 가르는 하나뿐인 표식**이다. uvicorn 리로더는 코드가 바뀌면 + 앱 프로세스만 다시 띄우고 Vite 자식은 그대로 두므로, 포트가 살아 있으면 리로드다. + 환경변수 같은 별도 깃발을 두지 않는 까닭은 그것이 **실제와 어긋날 수 있기** 때문이다 — + 포트는 어긋나지 않는다. + """ + import socket + + probe = socket.socket() + probe.settimeout(0.5) + try: + probe.connect(("127.0.0.1", port)) + return True + except OSError: + return False + finally: + probe.close() + + def _free_dev_port(port: int) -> None: """개발 서버 포트를 물고 있는 잔여 프로세스를 정리한다. @@ -235,8 +256,22 @@ def serve_frontend_dev() -> None: def stop_frontend_dev() -> None: - """앱이 내려갈 때 개발 서버도 같이 내린다 — 유령 인스턴스를 남기지 않는다.""" + """앱이 내려갈 때 개발 서버도 같이 내린다 — 유령 인스턴스를 남기지 않는다. + + ⚠⚠ **개발 중에는 내리지 않는다** (2026-09-12 실측으로 드러난 고리). + uvicorn 리로더는 `*.py` 가 바뀔 때마다 앱 프로세스를 갈아 끼운다. 그때 여기서 + Vite 를 죽이면 **새 프로세스는 「Vite 가 없다」고 보고 `npm run build` 를 처음부터 + 다시 돌린다**(최대 300초 + B07 CAD). 그 사이 화면은 뜨지 않고 `/api/health` 는 + `stale: true` 인 채로 옛 코드를 계속 내준다 — 그날 리로드가 두 번 멈췄고 한 번은 + 두 시간 반 동안 안 끝났다. **옛 화면을 보고 헛검증하게 만드는 자리다.** + ⇒ Vite 는 **한 앱 프로세스가 아니라 개발 세션의 것**이다. 살려 두고 다음 기동이 + 그대로 물려쓴다(`_dev_port_listening`). 남은 유령은 다음 기동의 + `_free_dev_port` 가 포트로 잡아 정리하므로 쌓이지 않는다. + """ global _frontend_dev_process + if ENVIRONMENT == "development": + logger.info("[Frontend] 개발 서버는 살려 둔다 — 다음 기동이 그대로 물려쓴다") + return if _frontend_dev_process is None: return logger.info("[Frontend] 개발 서버 종료 (pid %s)", _frontend_dev_process.pid) @@ -294,12 +329,23 @@ async def lifespan(app: FastAPI): datetime.fromtimestamp(_CODE_MTIME_AT_START).isoformat(timespec="seconds"), ) - # 프론트엔드 빌드 (필수) - build_frontend() - - # 개발 환경에서 개발 서버 실행 (선택사항) - if ENVIRONMENT == "development": - serve_frontend_dev() + # ⚠⚠ **리로드마다 프론트를 다시 빌드하지 않는다** (2026-09-12 실측으로 드러난 자리). + # uvicorn 리로더는 `*.py` 가 바뀔 때마다 이 lifespan 을 다시 탄다. 그때마다 + # `npm run build`(최대 300초, B07 CAD 빌드까지 딸림)가 돌고 Vite 를 죽였다 살리느라 + # **리로드가 몇 분씩 걸리거나 아예 멈춘다** — 그날 두 번 멈췄고 한 번은 두 시간 반 동안 + # 안 끝나, `/api/health` 가 `stale: true` 인 채로 옛 코드를 계속 내주었다. + # ⇒ **옛 화면을 보고 헛검증하게 만드는 자리라 그냥 느린 것이 아니다.** + # 개발 중에는 Vite 가 HMR 로 이미 최신을 내주므로 다시 빌드할 까닭이 없다. + if ENVIRONMENT == "development" and _dev_port_listening(FRONTEND_DEV_PORT): + logger.info( + "[Frontend] 개발 서버가 %d 포트에 이미 살아 있음 — 빌드·재기동 건너뜀(리로드)", + FRONTEND_DEV_PORT, + ) + else: + # 첫 기동(또는 프로덕션) — 여기서만 빌드한다. + build_frontend() + if ENVIRONMENT == "development": + serve_frontend_dev() # DB 풀 초기화 await init_db_pool()