📝 한줄 요약
지식체계 사이트에 그래프뷰와 챗봇을 새로 붙이고 배포했는데, 실제로 접속해보니 안 됐어요. 원인을 하나씩 벗겨보니 서로 무관한 장애 3개가 겹쳐 있었어요 — ① 배포 자체가 조용히 실패한 것, ② 사이트 설정에 적힌 주소가 실제 배포 주소와 달랐던 것, ③ 새로 추가한 패키지가 기존 설정과 버전 충돌을 일으킨 것. 셋 다 로컬에서는 멀쩡한데 실제 배포에서만 드러나는 유형이라, “로컬 빌드 성공 = 다 됐다”가 아니라는 걸 제대로 겪었어요.
바쁘시면 이것만 읽어도 돼요:
- 배포 후 확인은
npm run build가 끝난 화면이 아니라, 실제 배포된 주소에 직접 접속해서 해야 해요. 둘은 다른 걸 보장해요. - “안 된다”는 말을 들으면 바로 코드부터 고치지 말고, 먼저 어떤 상태인지 로그로 확인하는 게 순서예요. 배포 플랫폼은 대부분
--logs같은 명령으로 실패 원인을 그대로 보여줘요. - 여러 도구·설정을 조합해 쓸 때는, 각각이 요구하는 버전 조건이 서로 맞는지 미리 봐야 해요. 하나를 고치려고 버전을 올렸다가 다른 하나가 깨지는 경우가 실제로 있어요.
Before — 배포는 됐다는데, 실제로는 안 됨
그래프뷰(/concepts 하단 인터랙티브 그래프)와 챗봇(사이트 전역 우측 하단 위젯)을 새로 만들어서 커밋·push했어요. 이 기능들이 서버에서 API를 호출해야 해서, 사이트를 완전 정적(static)에서 하이브리드(서버 함수 포함) 모드로 바꾸는 구조적 변경도 같이 들어갔어요. push하고 나서 “서버가 작동안하는것 같은데”라는 말을 들었어요.
과정 — 장애 3개를 순서대로 벗겨냄
1단계 — 배포 상태부터 확인
바로 코드를 뒤지지 않고, 먼저 실제로 무슨 일이 있었는지부터 봤어요.
vercel ls
최신 배포가 ● Error 상태였어요. 로그를 봤어요.
vercel inspect <배포주소> --logs
The following Serverless Functions contain an invalid "runtime":
- _render (nodejs18.x)
원인: 쓰고 있던 Vercel 어댑터(@astrojs/vercel@7.8.2)가 내부적으로 Node 18·20만 알고 있었어요. 그런데 Vercel의 클라우드 빌드 환경은 이미 Node 24를 기본으로 쓰고 있었고, 어댑터는 모르는 버전을 만나면 무조건 “nodejs18.x”로 폴백하도록 짜여 있었어요. 문제는 Vercel이 그 nodejs18.x 런타임 자체를 이미 완전히 없애버려서, 폴백한 값이 애초에 유효하지 않았던 거예요.
해결: 어댑터가 아는 버전(20.x)으로 강제 고정했어요.
// package.json
"engines": { "node": "20.x" }
다시 배포하니 ● Ready로 성공했어요.
2단계 — “논문 페이지도 안 된다”는 새 제보
배포는 성공했는데, 이번엔 “논문 페이지도 작동을 안하네”라는 말을 들었어요. 서버 쪽부터 다시 확인했어요.
curl -sL -o /dev/null -w "%{http_code}\n" https://my-learning-hub.vercel.app/papers
# → 200 (근데 최종 도착지를 보니 auth/login 페이지)
서버는 200을 주는데, 실제로 도착한 곳이 로그인 페이지였어요. astro.config.mjs에 적혀 있던 site: 'https://my-learning-hub.vercel.app'이 애초에 이 프로젝트의 실제 주소가 아니었던 거예요 — 다른 곳으로 리다이렉트되는 이름이었어요.
vercel project ls
# → learning-hub 프로젝트의 실제 주소: learning-hub-virid.vercel.app
실제 주소로 바꾸고 다시 확인하니 논문 페이지도 정상이었어요. (사실 이 시점엔 서버는 처음부터 멀쩡했고, 안내된 주소가 틀렸던 것뿐이었어요.)
3단계 — 챗봇 프로바이더를 바꾸다가 또 충돌
챗봇을 처음엔 Vercel AI Gateway로 Claude를 부르게 만들었는데, 이미 다른 프로젝트용으로 발급해둔 OpenAI 키를 재사용하기로 방향을 바꿨어요. @ai-sdk/openai 패키지를 설치했더니:
npm warn EBADENGINE Unsupported engine
required: { node: '20.x' }, current: { node: 'v22.23.1' }
@ai-sdk/openai는 Node 22 이상을 요구하는데, 1단계에서 어댑터 호환 때문에 20.x로 고정해둔 것과 정면으로 충돌했어요. 20.x를 22.x로 올리면 1단계 문제가 재발하고, 그대로 두면 이 패키지가 안 돌아가는 딜레마였어요.
해결: SDK 패키지 자체를 빼고, OpenAI Chat Completions API를 fetch로 직접 호출했어요. 추가 의존성 없이 표준 REST 호출 하나면 충분했어요.
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ model: 'gpt-4.1-mini', messages: [...] }),
});
After — 뭐가 달라졌나
| Before | After | |
|---|---|---|
| 배포 상태 | ● Error (조용히 실패) | ● Ready |
| 안내한 주소 | 남의 로그인 페이지로 리다이렉트 | 실제 프로덕션 주소로 정확히 도착 |
| 챗봇 의존성 | SDK 버전 충돌로 설치 불가 | 의존성 0개 추가, REST 직접 호출로 정상 동작 |
| 검증 방식 | npm run build 성공만 확인 | 실제 배포 주소에 curl로 직접 확인 |
배운 것
“빌드 성공”과 “실제로 된다”는 다른 말이에요. 로컬 npm run build가 끝까지 돌았다고 해서 배포 환경에서도 똑같이 동작한다는 보장은 없어요. 오늘 겪은 세 문제 모두 로컬에서는 아무 신호가 없었고, 실제 배포 주소에 접속하거나 배포 로그를 봐야만 드러났어요.
“안 된다”는 제보는 원인을 특정하지 않아요. 같은 “안 된다”는 말이 이번엔 세 번 다 다른 원인이었어요(“서버가 안 됨” → 배포 실패, “논문 페이지가 안 됨” → 주소 오안내, 이후 새로 발견 → 버전 충돌). 매번 “어디가 어떻게 안 되는지” 로그·상태부터 다시 확인하는 게, 짐작으로 코드를 고치는 것보다 빨랐어요.
여러 도구의 버전 요구사항은 서로 부딪힐 수 있어요. 어댑터 때문에 Node 20으로 고정했는데, 나중에 추가한 패키지는 Node 22를 요구했어요. 하나를 고치면 다른 하나가 깨지는 상황에서는, 둘 다 만족시키려 하기보다 아예 그 의존성을 빼는 선택지(SDK 대신 REST 직접 호출)가 더 깔끔했어요.
재사용 자산 — 배포가 “안 된다”는 말을 들었을 때 확인 순서
-
vercel ls(또는 쓰는 플랫폼의 배포 목록)로 최신 배포가 실제로 성공(Ready) 상태인지부터 본다 - 실패했다면
--logs로 원인 문구를 그대로 읽는다 — 짐작하지 않는다 - 성공했다면, 설정 파일에 적힌 주소가 아니라 플랫폼이 알려주는 실제 배포 주소로 직접 접속해서 확인한다
-
curl -sL -o /dev/null -w "%{http_code}"로 페이지가 최종적으로 어디 도착하는지(리다이렉트 포함) 확인한다 - 새 패키지를 추가하기 전에
npm install경고(EBADENGINE등)를 그냥 넘기지 않고, 기존에 고정해둔 버전 조건과 부딪히지 않는지 본다 - SDK 하나 때문에 버전 충돌이 생기면, “SDK 없이 표준 API를 직접 호출”이 더 간단한 해결일 수 있다는 걸 후보로 둔다