☁️ goormTIL | Next.js #67

매루·2025년 12월 15일

goormTIL

목록 보기
65/67
post-thumbnail

📅 2025-12-12

➡️ Next.js Tanstack Query, Zustand에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리


🔎 학습 리마인드

📌 Tanstack Query

💡 복습

  • 서버 데이터를 효율적으로 가져오고, 캐싱하고, 동기화하는 기능을 제공하는 서버 상태 관리 라이브러리
  • API 호출과 관련된 로직을 컴포넌트 내부에서 직관적으로 처리할 수 있으며, 로딩·에러 상태 관리까지 일관되게 제공함

  • useQuery : 데이터 가져올 때 사용

    useQuery({
      queryKey: ["books"],
      queryFn: getBooks,
      staleTime: 60_000,
      gcTime: 60_000,
    });
    • queryKey필수 인자 값

      • 배열 형태
      • 쿼리를 식별하는 고유 키
    • queryFn필수 인자 값

      • Promise를 반환하는 함수
      • 실제 API 호출 로직
    • staleTime

      • 데이터가 신선(fresh)하게 유지되는 시간
      • 이 시간 동안 재요청 없음
    • gcTime

      • 캐시 보관 시간
      • 지나면 캐시 완전 삭제

  • useMutation : 데이터 생성(Create), 수정(Update), 삭제(Delete) 등의 작업에 사용
    const { mutate, isPending, isError } = useMutation({
      mutationFn: async (payload) => {
        const res = await axios.post("/api/data", payload);
        return res.data;
      },
      onSuccess: () => {
        // 성공했을 때 후처리
      },
      onError: () => {
        // 실패했을 때 처리
      },
    });
    • onSuccessonError 메서드를 통해 성공, 실패했을 시 res 데이터를 핸들링 할 수 있음

💡 Next JS에서의 Tanstack Query

🔗 React TanStack Query Nextjs Suspense Streaming Example | TanStack Query Docs

설치

npm i @tanstack/react-query @tanstack/react-query-devtools

Provider 구성

// In Next.js, this file would be called: app/providers.tsx
"use client";

// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import {
  isServer,
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // With SSR, we usually want to set some default staleTime
        // above 0 to avoid refetching immediately on the client
        staleTime: 60 * 1000,
      },
    },
  });
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient();
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient();
    return browserQueryClient;
  }
}

export default function Providers({ children }: { children: React.ReactNode }) {
  // NOTE: Avoid useState when initializing the query client if you don't
  //       have a suspense boundary between this and the code that may
  //       suspend because React will throw away the client on the initial
  //       render if it suspends and there is no boundary
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools />
      {children}
    </QueryClientProvider>
  );
}
  • QueryClientProvider는 내부적으로 useContext 훅을 사용하기 때문에 "use client"클라이언트 컴포넌트 선언해야 함

  • makeQueryClient()
    • QueryClient 인스턴스를 생성하는 함수
    • staleTime을 1분으로 설정하여 데이터가 일정 시간 동안 신선한 상태로 유지되도록 함
    • refetch가 발생하는 문제를 줄이는 데 도움이 됨

  • layoutProvider 적용
    import Providers from "./providers";
    
    export default function RootLayout({ children }) {
      return (
        <html lang="en">
          <body>
            <Providers>{children}</Providers>
          </body>
        </html>
      );
    }

💡 Prefetch +Hydration

  1. 서버 컴포넌트
    • prefetchQuery()로 데이터 미리 요청
  2. dehydrate()
    • QueryClient 상태 직렬화
  3. HydrationBoundary
    • 클라이언트로 상태 전달
  4. 클라이언트 useQuery
    • 즉시 캐시 데이터 사용 (재요청 X)
import { QueryClient, dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getBooks } from "@/server-action";
import BookList from "@/components/BookList";

export default async function Page() {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ["books"],
    queryFn: getBooks,
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <BookList />
    </HydrationBoundary>
  );
}

💡 Client Component가 Server Component를 감싸도 서버에서 실행되는 이유

  • "use client"는 클라이언트 상호작용을 포함한다는 의미이지, 컴포넌트 전체가 CSR로 바뀐다는 뜻이 아님
  • Next.js는 여전히 서버에서 프리렌더링을 수행하고, 필요한 클라이언트 로직만 번들로 보냄
  • 클라이언트 컴포넌트 내부에 서버 컴포넌트를 children 형태로 포함하는 방식은 가능

📌 Zustand

💡 복습

  • create 하나로 스토어를 만들고, Hook처럼 불러다 쓰는 구조
  • 상태와 액션을 한 파일에서 쉽게 정의 가능
  • Zustand는 state의 특정 부분을 구독하여 사용
    • 상태가 변경되더라도 그 상태를 실제로 사용하는 컴포넌트만 리렌더링

💡 Next JS에서의 Zustand

🔗 Setup with Next.js - Zustand

설치

npm i zustand

타입 및 store 정의

import { Book } from "@/type/book"; 
import { createStore } from "zustand";

export type CartBook = Book & { quantity: number };

export interface CartProps {
  books: CartBook[];
}

export interface CartState extends CartProps {
  addBook: (book: CartBook) => void;
}

export type CartStore = ReturnType<typeof createCartStore>;

export const createCartStore = (initProps?: Partial<CartProps>) => {
  const DEFAULT_PROPS: CartProps = {
    books: [],
  };

  return createStore<CartState>()((set) => ({
    ...DEFAULT_PROPS,
    ...initProps,
    addBook: (book: CartBook) =>
      set((state) => ({
        books: [...state.books, book],
      })),
  }));
};

Provider 설정

type CartProviderProps = React.PropsWithChildren<CartProps>;

export function CartProvider({ children, ...props }: CartProviderProps) {
  const storeRef = useRef<CartStore>();

  if (!storeRef.current) {
    storeRef.current = createCartStore(props);
  }

  return (
    <CartContext.Provider value={storeRef.current}>
      {children}
    </CartContext.Provider>
  );
}

사용법

const { books: items, remove } = useCartStore((state) => state);

0개의 댓글