Next.js 프로젝트를 진행하던 중 서버 컴포넌트에서 Next.js 자체 API 라우트로 요청을 보내는 코드가 있었다. 하지만 이 코드는 실행 시 오류를 발생시키며 데이터를 가져오지 못했다.
이번 글에서는 왜 서버 컴포넌트에서 상대 경로(/api/...)로 데이터를 요청하면 에러가 발생하는지를 실제 예시 프로젝트를 통해 살펴보겠다.
예시 프로젝트 구조는 다음과 같다.
app ├─ api │ └─ delay │ └─ route.ts └─ page.tsx
예를 들어, 일정 시간 동안 대기(delay)하는 API가 필요하다고 가정해보자.
export default async function Home() {
await fetch("/api/delay?delay=1000");
...
}
이때 서버 컴포넌트에서 해당 API를 호출했을 때 아래와 같은 에러가 발생했다.

Failed to parse URL from /api/delay?delay=1000
API 요청이 실패하며 위와 같은 오류를 반환했다.
왜 이런 결과가 나왔을까?
문제의 핵심은 바로 서버 컴포넌트와 클라이언트 컴포넌트의 환경 차이이다.
브라우저 환경에서는 상대 경로 요청(/api/delay)이 가능하다. 왜냐하면 브라우저에는 항상 window.location 이 존재하고 현재 페이지의 origin(https://example.com) 을 기준으로 상대 경로가 자동으로 절대 URL로 변환되기 때문이다.
예를 들어 /api/delay 요청은 실제로 https://example.com/api/delay 로 자동 변환된다.
하지만 서버 컴포넌트 는 브라우저에서 실행되지 않는다.
Next.js App Router에서 서버 컴포넌트는 Node.js 혹은 Edge 런타임 위에서 실행되고 이 환경에는 window, document, location 같은 브라우저 객체가 존재하지 않는다.
따라서 fetch("/api/delay")처럼 상대 경로를 전달하면 Node.js의 WHATWG URL 파서가 “이 URL은 base가 없어 파싱할 수 없다”는 오류를 던진다.
WHATWG URL 파서란?
URL 문자열을 해석해서 올바른 형태(프로토콜, 호스트, 경로 등)로 분리 및 검증하는 표준 로직이다.
가장 먼저 떠올릴 수 있는 방법은 환경별로 base URL을 달리 지정하는 것이다.
예를 들어 로컬에서는 http://localhost:3000, 배포 환경에서는 https://your-app.vercel.app과 같이 설정할 수 있다.
const baseUrl =
process.env.NODE_ENV === "production"
? "https://your-app.vercel.app"
: "http://localhost:3000";
await fetch(`${baseUrl}/api/delay?delay=1000`);
하지만 이 방식은 배포 도중 문제가 발생할 수 있다.

배포 과정에서 새로운 API 라우트(/api/delay)가 추가되었지만 빌드 시점에는 해당 URL이 아직 존재하지 않기 때문에 “존재하지 않는 엔드포인트를 호출한다”는 이유로 빌드 에러가 발생할 수 있다.
가장 안전하고 권장되는 방법은 API를 호출하지 않고 서버 컴포넌트 내부에서 직접 함수를 실행하는 것이다.
function delay(duration: number = 3000) {
return new Promise((resolve) => setTimeout(resolve, duration));
}
export default async function Home() {
await delay(1000);
...
}
불필요하게 Next.js 서버를 한 번 더 거치지 않기 때문에 성능상으로도, 구조적으로도 훨씬 효율적이다.
이번 경험을 통해 서버 컴포넌트에서의 코드 작성 방식에 대해 다시 한 번 생각해볼 수 있었다.
또한, 서버 환경에서는 상대 경로가 아닌 절대 경로를 명시해야 한다는 점이라는 새로운 개념도 알게 되었다.