노트

[ 노트 ] NextJS - TanStack Query 적용 이해

hminor 2025. 12. 8. 16:06
반응형

Q1. 기존 service에서 domainService 메서드를 추가한 이유는 해당 key값인  api가 없다면 별도의 error 문구를 던지기 위해서 작성했는데 이렇게 하지 않으면 어떤식으로 controller에서 error를 던질 수 있어?

A1. controller 단에서 개별 함수(훅)으로 만들어서 각각 export하면 내보낼 당시 없는 메서드의 경우 에러가 발생하게 되니 해결되며, react/nextjs 생태계의 표준 모범에 부합.

Q2. 서버 컴포넌트용 API라는 건 별도의 유저 조작없이 특정 페이지 접속시 보이는 데이터로 예를 들자면 쇼핑몰 페이지에서 추천 상품 목록이 아닌 일반적인 것으로 의류 중 상의 탭을 클릭하면 누구에게나 공통으로 보일 데이터의 경우엔 서버 컴포넌트용 APi를 제작해서 fetch하여 페이지 로드 시 서버에서 데이터를 가져와서 바로 보일 수 있게 하면 좋겠다는거야?

A2. ㅇㅇ 맞음, 누구에게나 공통으로 보일 데이터인 "페이지의 핵심이 되는 초기 콘텐츠"는 서버 컴포넌트에서 fetch로 처리, 초기 콘텐츠 위에서 일어나는 사용자 동적인 상호작용은 클라이언트 컴포넌트와 TanStack Query가 담당.

Q2-1. fetch와 TanStack Query 모두 사용하는 즉, 서버 컴포넌트와 클라이언트 컴포넌트 둘다 처리가 필요하다면 같은 api 작성을 2번해야하는데 이는 어떤식으로 관리해야 효율적일까?

A2-1. 실제 API 요청 로직은 한 번만 작성하고, 해당 로직을 서버 컴포넌트와 TanStack Query에서 가져다 사용하기.

Step 1: 순수 API 함수 계층 만들기

services/domains.ts (새로운 단일 API 계층)

// 이 파일은 React와 무관한 순수 TypeScript/JavaScript 파일입니다.

// 1. 파라미터를 받아 URL을 만들고, fetch를 호출하고, 결과를 반환하는 핵심 로직
async function fetchDomainList(params) {
  const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
  const queryString = new URLSearchParams(params).toString();
  const url = `${API_BASE_URL}/api/domain/get/list?${queryString}`;

  // Next.js의 확장된 fetch를 사용합니다.
  const response = await fetch(url, {
    // 서버 컴포넌트에서 이 함수를 호출할 때 캐시 전략을 정할 수 있습니다.
    // 클라이언트에서는 이 옵션이 무시됩니다.
    cache: 'no-store', 
  });

  if (!response.ok) {
    // 에러 처리는 여기서 일관되게 할 수 있습니다.
    throw new Error('도메인 목록을 가져오는데 실패했습니다.');
  }

  return response.json(); // Promise<Data>를 반환합니다.
}

// 필요하다면 다른 API 함수들도 이런 식으로 만듭니다.
async function fetchDomainDetail(id) {
  // ...
}

// 2. 작성한 함수들을 export 합니다.
export const domainAPI = {
  getList: fetchDomainList,
  getDetail: fetchDomainDetail,
};

 

Step 2: 서버 컴포넌트에서 이 함수를 직접 사용하기

app/domains/page.tsx (서버 컴포넌트)

import { domainAPI } from '@/services/domains'; // Step 1에서 만든 함수를 import

export default async function DomainsPage() {
  // 서버에서 페이지를 렌더링하기 전에 데이터를 직접 호출하고 기다립니다.
  const initialDomains = await domainAPI.getList({ page: 1, limit: 10 });

  return (
    <main>
      <h1>도메인 목록 (초기 데이터는 서버 렌더링)</h1>
      {/* 
        이 초기 데이터를 클라이언트 컴포넌트에 넘겨주어
        클라이언트에서의 추가적인 상호작용을 처리하게 합니다.
      */}
      <DomainListClient initialData={initialDomains} />
    </main>
  );
}

 

Step 3: TanStack Query 훅(Controller)에서 이 함수를 재사용하기

controllers/domainController.ts (기존 Controller 수정)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { domainAPI } from '@/services/domains'; // Step 1에서 만든 함수를 import

export const useFetchDomain = (params) => {
  return useQuery({
    queryKey: ['domainList', params],
    // ✅ 핵심: 실제 API 호출 로직을 재사용합니다.
    queryFn: () => domainAPI.getList(params),
    enabled: !!params,
    staleTime: 1000 * 60 * 5, // 5분
  });
};

// Mutation을 위한 래퍼 훅도 동일한 원리로 만들 수 있습니다.
// export const useUpdateDomain = ...```
이제 `useFetchDomain` 훅은 TanStack Query의 캐싱, 상태관리 등의 기능만 담당하는 **"래퍼(Wrapper)"**가 되고, 핵심 API 로직은 `services/domains.ts`에 위임됩니다.

---

#### Step 4: 클라이언트 컴포넌트에서 완성하기 (하이브리드 패턴)

서버 컴포넌트로부터 초기 데이터를 받고, 이후의 상호작용은 TanStack Query 훅을 사용하는 클라이언트 컴포넌트를 만듭니다.

**`components/DomainListClient.tsx` (클라이언트 컴포넌트)**
```tsx
'use client';

import { useState } from 'react';
import { useFetchDomain } from '@/controllers/domainController';

export default function DomainListClient({ initialData }) {
  const [page, setPage] = useState(1);
  const [filters, setFilters] = useState({});

  // TanStack Query 훅을 사용합니다.
  const { data, isLoading } = useFetchDomain(
    // 현재 페이지와 필터 상태를 파라미터로 전달
    { page, ...filters },
    {
      // ✅ 최적화: 서버에서 받아온 초기 데이터가 있으므로, 첫 렌더링 시에는 API를 다시 호출하지 않습니다.
      initialData: initialData,
    }
  );
  
  // ... 필터링, 페이지네이션 등 UI 로직 ...
  const handleFilterChange = () => { /* setFilters(...) */ };
  const handleNextPage = () => setPage(p => p + 1);

  return (
    <div>
      {/* 필터 UI */}
      {/* ... */}
      
      {isLoading && <p>데이터 갱신 중...</p>}
      
      <ul>
        {data?.items.map((domain) => (
          <li key={domain.id}>{domain.name}</li>
        ))}
      </ul>

      <button onClick={handleNextPage}>다음 페이지</button>
    </div>
  );
}