CDN React → Next.js 15 App Router 마이그레이션 삽질기
CDN React → Next.js 15 마이그레이션 삽질기
프로젝트: TimeSlot Event OS 날짜: 2026-04-03 ~ 2026-04-05 배경: 5개 HTML 파일(6,135 LOC)을 Next.js 15 App Router로 전환
문제 1 — useSearchParams() 정적 빌드 오류
증상
Error: useSearchParams() should be wrapped in a suspense boundary at page "/sso"
useSearchParams
at SSOPage
npm run build 시 /sso 페이지에서 정적 빌드 실패.
원인
Next.js 15에서 useSearchParams()는 동적 기능(Dynamic Feature)으로 분류된다.
Suspense 경계 없이 사용하면 정적 생성(Static Generation) 단계에서 오류 발생.
해결
Suspense로 분리:
// Before — 빌드 실패
export default function SSOPage() {
const params = useSearchParams(); // ❌
...
}
// After — 빌드 성공
export default function SSOPage() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<SSOHandler />
</Suspense>
);
}
function SSOHandler() {
const params = useSearchParams(); // ✅ Suspense 내부
...
}
규칙: useSearchParams, useRouter, usePathname 등 동적 훅은 Suspense 경계 안에서 사용.
문제 2 — 환경변수 late-stage discovery
증상
App Hosting 배포 후 일부 기능 작동 안 함.
확인해보니 GEMINI_API_KEY, BULK_SEND_URL이 apphosting.yaml에 누락.
원인
구현 시작 전 .env.example을 작성하지 않아서, 어떤 환경변수가 필요한지 전체 파악이 늦었음.
해결
apphosting.yaml에 누락된 환경변수 추가.
그리고 앞으로는 구현 전에 .env.example을 먼저 작성:
# .env.example
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
GEMINI_API_KEY=
BULK_SEND_URL=
REFIRED_NOTIFICATION_URL=
교훈: .env.example은 구현 이전 인프라 문서다. 먼저 작성하면 배포 단계에서 놀라지 않는다.
문제 3 — AdminApp.tsx가 1,955줄 모노리스가 된 이유
상황
어드민 대시보드를 단일 AdminApp.tsx에 7개 탭(대시보드, 스케줄, 체크인, AI 운영, SOS, 부스로그, 계정)을 모두 구현.
결과: 1,955 LOC.
원인
레거시 HTML에서 탭 전환이 JavaScript display: none/block 방식이었고,
이를 그대로 React state로 옮기다 보니 파일 분리 없이 한 컴포넌트에 누적.
Next.js는 탭이 아닌 라우트(/admin/schedule, /admin/checkin)가 자연스러운 패턴인데,
마이그레이션 속도 우선으로 구조 변경 없이 진행.
현재 대응
이 파일은 별도 PDCA 사이클(admin-refactor)로 분리 예정.
당장 기능적으로는 문제 없으므로 지금은 유지.
교훈: Next.js App Router 마이그레이션 시 탭 UI는 반드시 파일 기반 라우팅으로 설계해야 한다.
탭 = /admin/[tab] 구조. 나중에 쪼개면 state 추출, URL 구조 변경이 동시에 발생해서 더 어렵다.
결과 요약
| 문제 | 교훈 |
|---|---|
| useSearchParams 빌드 오류 | 동적 훅은 Suspense 경계 필수 |
| 환경변수 late discovery | .env.example을 구현 전에 작성 |
| 모노리틱 Admin 컴포넌트 | Next.js 탭 = 파일 기반 라우팅으로 처음부터 설계 |