HomeBlog
Back to Blog

Contents

    Copyright 2025. Kwangmin. All rights reserved.
    2025년 3월 10일조회수:

    Dynamic Server Usage 에러와 Async Context

    1. 개요

    Next.js에서 DynamicServerError라는 에러 메시지를 마주하였습니다.
    이 에러는 보통 Next.js의 동적 함수(예: cookies(), headers() 등)를 호출할 때, 해당 함수가 Async Context를 벗어난 실행 컨텍스트에서 호출되었을 때 발생합니다. 이 글에서는 에러가 발생하는 원인, Async Context의 개념, 그리고 올바른 사용법과 해결 방안을 살펴보겠습니다.

    Next.js 13부터 RSC가 등장하면서 API Route나 페이지에서 nextUrl.searchParams와 같은 동적 데이터를 참조할 때, 런타임에서는 문제없이 동작하지만 빌드 후(static 빌드)에서는 에러가 발생할 수 있습니다.
    이 에러는 Page couldn't be rendered statically because it used nextUrl.searchParams 라는 메시지를 throw 합니다.


    2. 에러의 원인

    Next.js는 기본적으로 페이지와 API 라우트를 정적으로 생성(static generation) 하려고 합니다. 그런데 URL의 searchParams 또는 헤더와 같이 런타임에 결정되는 값을 사용하면, 해당 페이지는 정적 생성이 불가능해집니다.


    또한 만약 page.tsx 에서 props로 동적으로 받는 url 파라미터를 주입만 하더라도 이는 SSG 에서 SSR 페이지로 빌드가 되는걸 확인할 수 있습니다. 왜냐하면 동적으로 받는 url 파라미터는 빌드 시점에 정적으로 파싱이 불가능하기 때문입니다.


    원인의 핵심은 "Next.js는 기본적으로 페이지와 API 라우트를 정적으로 생성(static generation) 하려고 합니다." 입니다.


    예를 들어 아래 코드처럼 request.nextUrl.searchParams 혹은 new URL(request.url)을 사용하면 Next.js가 내부적으로 DynamicServerError를 발생시키며 정적 렌더링을 하지않게 유도합니다.

    // 예시: API Controller
    export const getUsers = async (request) => {
      const url = new URL(request.url);
      const email = url.searchParams.get("email") || "";
      // ...
    }

    런타임 상으로는 문제없이 동작하지만 빌드 후에는 정적 렌더링이 불가능해집니다. 빌드 모드에서는 이 에러를 잡아내어 정적 페이지 생성(static generation)을 시도하는데, try/catch 블록 안에서 동적 데이터를 참조하면 Next.js가 해당 에러를 감지하지 못해 빌드 타임 에러가 발생합니다.


    3.Async Context란 무엇인가? (feat:실행 컨텍스트)

    Async Context는 동일한 실행 컨텍스트 내에서 비동기 작업이 이루어질 때, 데이터를 전달하는 방법입니다. Next.js에서는 cookies()나 headers()와 같은 함수를 호출할 때, 현재의 비동기 실행 컨텍스트에 바인딩된 데이터를 사용합니다. 하지만, 만약 이 함수들을 실행 컨텍스트를 벗어난 곳(예: setTimeout 내부)에서 호출하면, Next.js는 올바른 컨텍스트를 찾을 수 없어 에러를 발생시킵니다.


    • 실행 컨텍스트(Execution Context): 자바스크립트가 코드를 실행할 때 각 함수 호출에 대해 생성되는 환경입니다. 이 환경에는 변수, 함수 선언, 그리고 현재 실행 스택 정보 등이 포함됩니다.
    • Async Context: 비동기 함수 호출 중에도 같은 실행 컨텍스트에 묶여서 데이터를 공유할 수 있도록 하는 개념입니다. Next.js의 경우, 예를 들어 cookies() 같은 함수는 현재 비동기 실행 컨텍스트에 묶여있는 정보를 참조합니다.

    3.1 문제 상황 1: setTimeout 또는 setInterval 내부 호출

    아래 코드는 setTimeout 내부에서 cookies()를 호출하는 예제입니다. 이 경우, setTimeout이 새로운 실행 컨텍스트를 생성하기 때문에 Next.js가 필요한 Async Context(예를 들어, 클라이언트의 쿠키 정보)를 찾지 못하고 에러를 발생시킵니다.

    // 잘못된 예제: setTimeout 내부에서 cookies() 호출
    import { cookies } from 'next/headers';
     
    async function getCookieData() {
      return new Promise((resolve) => {
        setTimeout(() => {
          // setTimeout 내부는 원래의 Async Context와 분리됨
          const allCookies = cookies().getAll(); // 이 시점에 실행 컨텍스트가 달라져 에러 발생
          resolve(allCookies);
        }, 1000);
      });
    }
     
    export default async function Page() {
      const cookieData = await getCookieData();
      return <div>Cookie 데이터 확인</div>;
    }

    • 왜 에러가 발생할까?
      cookies()는 Next.js가 설정한 Async Context 내에서 실행되어야 하는 함수입니다. 그런데, setTimeout은 새로운 실행 컨텍스트를 생성하므로 원래의 Async Context에서 벗어나게 됩니다.

    • 실행 컨텍스트 손실
      setTimeout 안에서 호출된 함수는 부모 함수의 실행 컨텍스트와 다르게 동작하기 때문에, Next.js는 필요한 정보를 찾지 못해 에러를 발생시킵니다.


    3.2 문제 상황 2: Promise의 async 함수 처리 후 await 누락

    Promise를 생성하고, 이를 await 하지 않고 바로 비동기 작업이 진행되면, 동적 함수 호출이 원래의 Async Context가 아닌 새로운 컨텍스트에서 실행될 수 있습니다.

    // 잘못된 예제: await를 제대로 사용하지 않음
    import { headers } from 'next/headers';
     
    async function getHeaderData() {
      // 여기서 Promise를 반환하지만 await가 없으면 이후 호출 시점이 달라짐
      return new Promise((resolve) => {
        // 예를 들어, fetch 작업 후 then() 안에서 headers()를 호출한다고 가정
        setTimeout(() => {
          const allHeaders = headers().getAll(); // 새로운 실행 컨텍스트에서 호출됨
          resolve(allHeaders);
        }, 1000);
      });
    }
     
    export default async function Page() {
      // getHeaderData()를 제대로 await하지 않으면 컨텍스트가 바뀔 수 있음
      const headerDataPromise = getHeaderData();
      // 이 시점에서 headerDataPromise는 아직 완료되지 않았고, 이후 처리 시점에 실행 컨텍스트가 달라짐
      const headerData = await headerDataPromise;
      return <div>Header 데이터 확인</div>;
    }

    • 왜 에러가 발생할까?
      Promise를 반환하고, 이를 적절히 await 하지 않으면 해당 함수의 실행 결과가 나중에 다른 실행 컨텍스트(예: 타이머 내부나 then() 내부)에서 실행될 수 있습니다. 이렇게 되면 Next.js가 제공하는 동적 함수들이 원래의 Async Context를 벗어나게 됩니다.

    그렇다면 어떻게 해결 해야할까요?

    3.3 해결 방법 1: 동적 데이터를 미리 가져오기

    비동기 작업을 시작하기 전에 필요한 데이터를 미리 추출한 후, 이후 작업에서는 추출한 데이터를 사용합니다.

    // 올바른 예제: Async Context 내에서 미리 데이터를 추출
    import { cookies } from 'next/headers';
     
    async function getCookieData() {
      // 먼저 cookies()를 호출하여 원래의 실행 컨텍스트 내에서 데이터를 가져옴
      const allCookies = cookies().getAll();
      return new Promise((resolve) => {
        setTimeout(() => {
          // 이후 비동기 작업에서는 미리 가져온 데이터를 사용
          resolve(allCookies);
        }, 1000);
      });
    }
     
    export default async function Page() {
      const cookieData = await getCookieData();
      return <div>Cookie 데이터: {JSON.stringify(cookieData)}</div>;
    }

    3.4 해결 방법 2: async/await를 올바르게 사용하기

    // 올바른 예제: Promise를 올바르게 await
    import { headers } from 'next/headers';
     
    async function getHeaderData() {
      // headers()를 먼저 호출하여 원래 컨텍스트 내에서 데이터를 가져옴
      const allHeaders = headers().getAll();
      return new Promise((resolve) => {
        setTimeout(() => {
          // 미리 가져온 데이터를 사용
          resolve(allHeaders);
        }, 1000);
      });
    }
     
    export default async function Page() {
      // 반드시 await를 통해 올바른 실행 컨텍스트 내에서 처리
      const headerData = await getHeaderData();
      return <div>Header 데이터: {JSON.stringify(headerData)}</div>;
    }

    3.5 동적 데이터를 try/catch 블록 밖에서 사용하기

    Next.js가 동적 데이터 사용 여부를 제대로 감지할 수 있도록, URL 파싱이나 searchParams 추출 코드를 try/catch 블록 밖으로 이동합니다.

    // /app/api/armies/route.js
    import { getUsers } from "@/backend/apiControllers/getUsers";
    import { connectDB } from "@/backend/utils/dbConnect";
    import { NextResponse } from "next/server";
     
    export const GET = async (request) => {
      // try/catch 밖에서 URL 파싱
      const { searchParams } = new URL(request.url);
      
      try {
        await connectDB();
        // 필요한 경우 getUsers에 searchParams를 인자로 전달
        const users = await getUsers({ searchParams });
        return NextResponse.json(users);
      } catch (error) {
        console.error(error);
        return NextResponse.json({
          apiMessage: { errorMsg: "Internal Server Error, Please try again later" },
        });
      }
    };

    그리고 API 함수에서는 searchParams를 파라미터로 직접 받아 처리하도록 변경할 수 있습니다.

    // /app/api/armies/route.js
    // /backend/apiControllers/getUsers.js
    import prisma from "@/prisma/prismaClient";
     
    export const getUsers = async ({ searchParams }) => {
      const email = searchParams.get("email") || "";
      const phone = searchParams.get("phone") || "";
      const id = searchParams.get("id") || "";
     
      // ... rest codes ...
     

    3.6 강제로 동적 렌더링 사용하기

    Next.js에서는 특정 페이지나 API를 강제로 동적 렌더링 하도록 설정할 수 있습니다. 파일 상단에 아래와 같이 export const dynamic = 'force-dynamic';를 추가하면 정적 생성을 건너뛰고 런타임에 동적으로 렌더링합니다.

    // /app/api/armies/route.js
    export const dynamic = 'force-dynamic';
     
    import { getUsers } from "@/backend/apiControllers/getUsers";
    import { connectDB } from "@/backend/utils/dbConnect";
    import { NextResponse } from "next/server";
     
    export const GET = async (request) => {
      try {
        await connectDB();
        const users = await getUsers(request);
        return NextResponse.json(users);
      } catch (error) {
        console.error(error);
        return NextResponse.json({
          apiMessage: { errorMsg: "Internal Server Error, Please try again later" },
        });
      }
    };

    이 방법을 사용하면 Next.js가 해당 페이지나 API 라우트를 동적으로 처리하게 되어 빌드 타임 에러를 회피할 수 있습니다.

    Next.js에서는 정적 페이지 생성을 시도할 때, URL의 searchParams와 같은 동적 데이터를 사용하면 에러가 발생할 수 있습니다. 이를 해결하기 위해서는 동적 데이터를 try/catch 블록 밖으로 이동하여 Next.js가 제대로 감지하도록 하거나, export const dynamic = 'force-dynamic';를 사용해 강제로 동적 렌더링하도록 설정하는 방법이 있습니다.


    4. 결론

    • 실행 컨텍스트란 자바스크립트가 코드를 실행하는 환경을 의미하며, Async Context는 비동기 작업 중에도 이 컨텍스트를 유지하는 역할을 합니다.
    • setTimeout, setInterval과 같이 새로운 실행 컨텍스트를 생성하는 함수 내부에서 Next.js의 동적 함수(cookies(), headers() 등)를 직접 호출하면 원래의 Async Context를 잃어버려 에러가 발생합니다.
    • 이러한 문제를 해결하기 위해서는 비동기 작업 전에 필요한 데이터를 미리 추출하거나, Promise를 사용할 때 반드시 await를 사용하여 올바른 실행 컨텍스트에서 함수가 실행되도록 해야 합니다.

    Next.js는 기본적으로 페이지와 API 라우트를 정적으로 생성(static generation) 하려고 하는것을 파악하고 다이나믹한 데이터를 사용하는 경우에 동적데이터 부분을 고려를 하면서 코드를 작성하는것이 좋을 것 같습니다.

    Tags
    Next.js
    DynamicServerUsage
    AsyncContext
    React의 ErrorBoundary는 비동기 에러를 캐치를 어떻게 할까?
    AWS S3 멀티파트 업로드

    댓글 로딩 중...

    조회수:0