4 min read
빌드 타임에는 Next.js 서버가 작동하지 않는다는 사실 (Feat. GraphQL, ISR)

tl;dr

  • Next.js의 API Route를 통해 별도의 서버를 구축하고 싶은 경우, 이에 대한 요청은 반드시 ‘use client’를 통해 클라이언트 컴포넌트로 구현해야 한다.
  • ‘use client’를 명시하지 않으면 기본적으로 ‘use server’로 동작하는데, 빌드 타임에는 Next.js 서버가 켜지지 않은 상태이므로 fetch error가 발생한다.

GraphQL 서버 구현

GraphQL이 해결하고자 하는 문제(Over fetching, Under Fetching)를 이해하게 되면서 이를 써보고도 싶었고, 이번에 사이드로 만드는 프로젝트가 사실상 무수히 많은 route를 정의해야 할 것 같아 프로젝트에 적용해보기로 했다.

Next.js가 Sever Side Action이 가능한 것을 알고 있었던터라, API Route를 통해 GraphQL 서버를 동작시키고자 했다. 이에 여러 라이브러리를 비교하던 중 러닝 커브가 작고 유연한 Apollo Server와 Apollo Client를 통해 GraphQL 통신을 개발하기 시작했다.

ISR (Incremental Static Regeneration)

또한 정적 사이트처럼 데이터를 관리하여 네트워크 요청을 최소화하고자 했다. React-Query등의 서버 상태 관리 라이브러리가 아닌 단순 정적 사이트처럼 만들고자 한 이유는 다음과 같다.

  1. 사용자 개개인의 데이터가 보여지는 부분이 없다.

애초에 정적 사이트는 빌드 타임에 모든 데이터가 정의되어 있는 상태이므로, 별도의 클라이언트 컴포넌트가 정의되어 있지 않은 이상 모두가 같은 화면을 공유하게 된다.

내가 만드는 프로젝트 또한 회원과 같은 기능이 없고 모두가 같은 화면을 보면 되므로 이를 정적으로 구현하면 된다.

  1. on-demand revalidation

정적 사이트의 치명적인 단점 중 하나는 데이터가 변경되었을 때 이를 곧바로 적용하기가 힘들다는 점이다.

Next.js에서는 revalidate 시간을 정의하여, 특정 시간마다 폴링하여 데이터를 최신화하는 방법을 소개하고 있다. 하지만 이 또한 아주 최적화된 방법은 아닌 것이, 특정 시간마다 데이터가 변경될지 아무도 모르는데도 정의한 시간마다 계속해서 데이터 최신화를 위해 네트워크 요청을 하므로 비효율적인 부분이 있다.

이를 해결하기 위해 나온 것이 바로 on-demand revalidation이다. 즉, 데이터가 변경되면 마치 webhook처럼 Next.js에 미리 정의한 API Route로 변경되었음을 요청하고, 그 요청을 받은 Next.js는 해당 데이터를 최신화하는 방식이다.

이를 통해 데이터가 변경되면 즉시 최신화되어 사용자에게 전달될 수 있도록 할 수 있다. 또한 정말 데이터가 변경된 상태에서만 데이터를 최신화하므로 비효율적인 부분이 없다.

문제 발생

GrpahQL을 on-demand revalidation을 통해 구현하고자 했고, 개발 서버까지는 문제 없이 돌아갔다. 다음이 내가 구현한 코드 중 일부이다.

export default async function MTGame() {
  const data = await apolloClient.query({
    query: GET_ALL_GENRES,
    fetchPolicy: "cache-first",
  });
  return <main></main>;
}

Next.js에서는 fecth 메서드를 통해 HTTP Request를 수행하는 것을 권장하는데, 나의 경우엔 어쩔 수 없이 Apollo 인스턴스를 사용해야 하므로 캐시된 데이터를 우선적으로 사용하는 정책을 별도로 설정했다.

그리고 빌드를 했을 때 계속해서 오류가 발생했다. 그냥 아주 단순하게 다음과 같은 에러를 뱉어냈다.

ApolloError: fetch failed

이 문제를 구글을 여러번 뒤졌으나 똑같이 따라해도 해결이 되지 않았다. 하지만 현재 Next.js의 API Route에 서버 사이드 로직을 작성한 상태인데, 서버가 켜지지 않은 빌드 타임에 데이터를 받아올 수 있을지 의문이 들었고 결국 이것이 문제임을 알아내게 되었다.

해결 방안

이를 해결하는 방법에는 2가지가 있다.

  1. GraphQL 서버를 별도의 서버로 구현하기

  2. SSR/SSG/ISR와 같은 서버 사이드 렌더링이 아닌 클라이언트 사이드 렌더링으로 정의하기

일단 1번의 경우엔 백엔드 프레임워크를 또 다시 파서 배포까지 해야 했던지라, 눈물을 머금고 2번 방법을 일단 선택했다.

"use client";
import { GET_ALL_GENRES } from "./page.query";
import { useQuery } from "@apollo/client";

export default function MTGame() {
  const { data, loading, error } = useQuery(GET_ALL_GENRES);

  return <main></main>;
}

이제 빌드를 찍어보면 아주 잘 결과물이 산출됨을 알 수 있다.

하지만 이렇게 작성하면 매 요청마다 계속 요청을 하므로, 최소한 캐싱 정책을 적용하는 것이 좋을 것이다. 하단이 이를 적용한 결과이다.

// ...existing code
export default function MTGame() {
  const { data, loading, error } = useQuery(GET_ALL_GENRES, {
    fetchPolicy: "cache-first",
    pollInterval: 1000 * 60,
    nextFetchPolicy: "cache-first",
  });

  return <main></main>;
}

이를 통해 첫 요청과 후속 요청 모두 캐시된 값이 있는지를 우선적으로 확인하고, 1분마다 폴링하여 데이터를 최신화하게 된다.

결론

만약 좀 더 정적 사이트와 관련된 내용을 더 잘 숙지했더라면 Next.js의 API Route를 통해 GraphQL 서버를 구현하지는 않았을 것인데 후회가 된다.

그래도 어디에서도 찾을 수 없는 에러 내용을 나름 혼자 문제를 정의하고 해결한 부분이라 뿌듯하다.

다음에는 절대 이런 실수를 하지 않아야지…