next.js CSR 환경에서 서버 전용 데이터 Fetch를 위한 효과적인 방법

희선·2024년 9월 26일

Trifly

목록 보기
4/7
post-thumbnail

작업을 하면서 마주했던 첫번째 난관은 react-query infinite scroll 오류 해결방법이었지만 막상 해결을 하고 나니 CSR 환경에서 fetch 하려는 함수의 동작이 서버에서만 실행될 수 있는 내용을 포함해야 한다면, 어떤 방식으로 데이터를 받아와야 하는가?
라고 다시 정리해 볼 수 있었다.



ERROR: headers was called outside a request scope

이 에러가 자꾸 발생하는것인데
두개의 컴포넌트에서 같은 데이터를 요구하는 상황으로,
동일한 fetch 함수를 사용하는 컴포넌트는 fetch가 잘 진행 되지만
무한스크롤을 사용하는 컴포넌트에서는 fetch자체가 이루어지지 않는다
며칠을 들여다 봤는지 참,, 다시보니까 좀 웃김



🔎 우선 모든 코드를 살펴보자
page.tsx
MobileReservationList.tsx
FetchOrderList.tsx
RQProvider.tsx


무한스크롤을 구현하기 위해서는 무한스크롤이 적용될 요소는QueryClientProvider로 감싸주어야 하는데 이 감싸져있는 코드를 가진 파일 또한 CSR환경이어야 한다.
우선 나는 여기부터 잘못된 길을 가고있었다

const page = async ({ searchParams }: { searchParams: any }) => {

  const keyword = searchParams.keyword || "";
  const page = searchParams.page || "";

  return (
    <div>
      <div className="reservation-header">
        <h2 className="title">예약내역</h2>
        <Search />
      </div>
      <ReservationList page={page} keyword={keyword} />
      <RQProvider>
        <MobileReservationList keyword={keyword} />
      </RQProvider>
    </div>
  );
};

export default page;

SSR환경인 page.tsx 파일에서
MobileReservationList를 감싸는 QueryClientProvider 가지고 있기 때문이다.
따라서 나는 컴포넌트 분리를 한번 더 해줬다
MobileReservationList안에서 QueryClientProvider로 감싼 무한스크롤 컴포넌트를 만들기로 한것

const MobileReservationList = ({ keyword }: { keyword: string }) => {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
    useInfiniteQuery({
      queryKey: ["reservations"],
      queryFn: ({ pageParam = 1 }) => FetchOrderList(pageParam),
      getNextPageParam: (lastPage) => {
        const { page, totalPages } = lastPage.pagination;
        return page < totalPages ? page + 1 : false;
      },
    });

  if (isLoading) {
    return <div>로딩 중...</div>;
  }

  return (
    <>
      <RQProvider>
        <OrdersList keyword={keyword} />
      </RQProvider>
    </>
  );
};

export default MobileReservationList;




이렇게 수정을 하면 하나의 문제점은 해결된것인데
오늘의 핵심 문제점은 여기서부터 시작이다!



MobileReservationList에서 import된 FetchOrderList의 코드를 살펴보자

export const FetchOrderList = async (
  page?: string,
  keyword?: string,
): Promise<MultiItem<OrderItem>> => {
  const params = new URLSearchParams();
  const session = await auth();
  const token = session?.accessToken as string;

  if (page) {
    params.set("page", page);
  }

  if (keyword) {
    params.set("keyword", keyword);
  }

  if (LIMIT) {
    params.set("limit", LIMIT);
  }

  params.toString();

  const url = `${API}/orders?${params.toString()}`;

  const res = await fetch(url, {
    method: "GET",
    headers: {
      "client-id": CLIENT_ID,
      Authorization: `Bearer ${token}`,
    },
  });
  const resJson = await res.json();

  return resJson;
};

이 코드의 문제가 되는 부분은 headers가 문제였다

동일한 데이터 fetch를 요구하는 컴포넌트가 있었는데
그 컴포넌트는 문제없이 fetch를 잘 해왔기 때문에 더 혼란스러웠다.



여기서 찾은 문제는
Next.js 13에서 headers는 요청과 응답의 스코프 내에서만 사용할 수 있다는 것이다

이로 인해 클라이언트 컴포넌트에서 headers를 호출하려고 하면 문제가 발생할 수 있는데 auth() 함수가 서버에서만 실행될 수 있는 내용을 포함하고 있다면, 클라이언트에서 이를 호출하면 문제가 발생한다는 것!
문제는 나는 이 함수를 client에서 호출하고 있었다 ㅎㅎ


아직 스타일 작업은 되지 않은 모습이지만 작업 과정을 살펴본다면
예약 내역 페이지에서 pc 환경에서는 페이지네이션을, 모바일 환경에서는 무한스크롤을 반응형으로 적용하려고 했다

따라서 두개의 컴포넌트에서 같은fetch방식을 사용했는데 여기서 문제점 발생
페이지네이션의 데이터 패치 플로우는
1. 페이지네이션의 번호를 선택하면 url 경로가 변경되고 (CSR)
2. params에 따라 데이터를 fetch해오는 방식을 가졌다 (SSR)




그래서 이렇게 크게 pagitnation.tsx파일과 reservationList.tsx파일 두개로 분리된다고 볼 수 있다
페이지네이션에서는 SSR환경에서 동일한 함수의fetch 적용 했기 때문에 문제가 없었다

같은 데이터를 받아와야 하기 때문에 불필요하게 fetch 함수를 적어야 할 필요가 없겠구나! 하고 같은 함수를 사용했는데 CSR환경에서 SSR환경이어야만 가능한 fetch함수였다

서버에서 사용자 인증도 받아야하고 데이터도 받아와야하는데 어떻게 클라이언트 컴포넌트에서 이를 해결 할 수 있을까!




📍 그래서 해결방법은?

next.js서버에서 실제 데이터를 fetch하고 client에서 next api가 fetch한 데이터에 접근해서 받아오는 방식

next.js의 api 라우트를 사용한 해결방식이다
API 라우트를 통해 외부 API에서 데이터를 가져오는 작업을 함으로써 클라이언트와 서버 간의 분리된 구조를 유지하면서도, 클라이언트 측에서 API 요청을 간편하게 처리가 가능하다.
따라서 클라이언트 측에서 직접 외부 API에 요청하는 대신, 서버 측에서 인증을 처리하고 클라이언트에 안전하게 필요한 데이터만 전달하는 방식으로, 보안성을 높일 수 있다는 장점도 가진다.


클라이언트가 Next.js의 API 라우트를 호출하면, 서버는 필요한 데이터를 처리하여 클라이언트에 응답한다. 이렇게 하면 클라이언트가 API의 내부 구조를 몰라도 되고, 서버에서 필요한 로직을 모두 처리할 수 있다!


API에 데이터를 요청하는 코드
이 코드는 src/app/api/orders/route.ts 경로에 만든 파일이다

export async function GET(request: { url: string | URL }) {
  const session = await auth(); // 서버에서 인증 처리
  const token = session?.accessToken;

  if (!token) {
    return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
  }

  const { searchParams } = new URL(request.url);
  const page = searchParams.get("page") || "1";
  const keyword = searchParams.get("keyword") || "";
  const limit = searchParams.get("limit") || "5"; // 필요 시 limit도 가져옴

  const API = process.env.NEXT_PUBLIC_MARKET_API_SERVER;
  const CLIENT_ID = process.env.NEXT_PUBLIC_MARKET_API_CLIENT_ID;

  const params = new URLSearchParams();
  params.set("page", page);
  params.set("keyword", keyword);
  params.set("limit", limit);

  const url = `${API}/orders?${params.toString()}`;
  console.log("Fetching from URL:", url);

  try {
    const response = await fetch(url, {
      method: "GET",
      headers: {
        "client-id": CLIENT_ID,
        Authorization: `Bearer ${token}`,
      },
    });

    if (!response.ok) {
      throw new Error("Failed to fetch data");
    }

    const data = await response.json();
    // console.log("패치완료", data);
    return NextResponse.json(data, { status: 200 });
  } catch (error) {
    // console.error("실패", error);
    return NextResponse.json(
      { message: "Internal Server Error" },
      { status: 500 },
    );
  }
}

그럼 이제 클라이언트에서 이 데이터에 접근해야 한다
방금 전 작성된 next api 라우트를 사용한 방식과 다른부분을 확인 할 수 있는데
fetch해오는 경로가 다르다는것이다

next api route를 사용한 fetch 경로는 실제 데이터를 받아오는 API경로
client의 fetch 경로는 next api의 경로로 접근




"use client";

export const FetchOrderListScroll = async (page: string, keyword: string) => {
  const params = new URLSearchParams();
  if (page) params.set("page", page);
  if (keyword) params.set("keyword", keyword);

  const res = await fetch(`/api/orders?${params.toString()}`, {
    method: "GET",
  });

  if (!res.ok) {
    throw new Error("데이터 패치 에러");
  }

  return res.json();
};

이렇게 fetch 경로를 api폴더의 orders폴더로 접근한다
그럼 성공적으로 데이터를 받아오게되며, 터미널에는 /api/orders?page=1이러한 경로로 성공적으로 접근 한 것 을 확인 할 수 있다



결과적으로 총 limit=5;가 설정된 데이터를 불러올 수 있고 마지막 데이터가 보여질 때 까지 더보기 버튼이 뜨도록 구현 했다!





오늘도 하나를 배웠다! 야호

profile
FE 개발자가 되기 위한 땅굴 파기! 🌱

0개의 댓글