Skip to main content

컴포넌트 Fetch 에러 (1)

작성일 : 2025.01.02

1. 기본 개념​

서버 컴포넌트​

  • Next.js 13+ 버전의 기본 컴포넌트 타입
  • 서버에서 렌더링되어 클라이언트로 전송
  • HTML과 최소한의 JavaScript만 전달
  • 별도의 지시어 없이 사용 가능
  • 데이터베이스, 파일 시스템 등에 직접 접근 가능
  • 민감한 정보(API 키, 토큰 등)를 안전하게 사용 가능

서버 컴포넌트의 데이터 페칭 특징​

  1. 자동 요청 중복 제거
async function ServerComponent() {
  // 동일한 URL의 요청은 자동으로 중복 제거됨
  const data1 = await fetch(URL); 
  const data2 = await fetch(URL); // 캐시된 결과 재사용
}
  1. 캐싱 제어
// POST 요청 캐시 방지
const data = await fetch(URL, { 
  method: 'POST',
  cache: 'no-store' 
});

// 특정 시간동안 캐시
const data = await fetch(URL, {
  next: { revalidate: 3600 } // 1시간
});
  1. 동적 함수 사용
// 동적 경로에서 데이터 가져오기
async function Product({ id }: { id: string }) {
  const data = await fetch(`${process.env.API_URL}/products/${id}`);
  const product = await data.json();
  
  return <div>{product.name}</div>;
}
  1. 병렬 데이터 페칭
async function ParallelFetch() {
  // 병렬로 데이터 요청
  const [productsPromise, categoriesPromise] = await Promise.all([
    fetch(`${API_URL}/products`),
    fetch(`${API_URL}/categories`)
  ]);

  const [products, categories] = await Promise.all([
    productsPromise.json(),
    categoriesPromise.json()
  ]);
  
  return (
    <>
      <ProductList products={products} />
      <CategoryList categories={categories} />
    </>
  );
}

클라이언트 컴포넌트​

  • 'use client' 지시어로 명시적 선언 필요
  • 브라우저에서 실행
  • 사용자 상호작용 처리 가능
  • 브라우저 API 사용 가능

2. Fetch 동작 방식 차이점​

서버 컴포넌트에서의 Fetch​

  1. 실행 환경: Next.js 서버 내부
  2. rewrites 설정 적용 안됨
  3. 전체 URL 필요
  4. 캐싱과 재검증 기능 사용 가능
// 서버 컴포넌트 예시
async function ServerComponent() {
  // ❌ 잘못된 방식
  const data = await fetch('/api/data');
  
  // ✅ 올바른 방식
  const data = await fetch('http://api.example.com/data', {
    next: { revalidate: 3600 }
  });
}

클라이언트 컴포넌트에서의 Fetch​

  1. 실행 환경: 브라우저
  2. rewrites 설정 정상 적용
  3. 상대 경로 사용 가능
  4. 브라우저의 기본 fetch 동작 따름
'use client'

function ClientComponent() {
  useEffect(() => {
    // ✅ 상대 경로 사용 가능
    fetch('/api/data')
      .then(res => res.json())
      .then(data => console.log(data));
  }, []);
}

3. 실전 해결 방안​

통합 Fetch 유틸리티 구현​

export const universalFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  // 서버 사이드 환경 체크
  if (typeof window === 'undefined') {
    if (typeof input === 'string' && !input.startsWith('http')) {
      const baseUrl = process.env.API_BASE_URL;
      return fetch(`${baseUrl}${input}`, init);
    }
  }
  return fetch(input, init);
};

환경변수 설정​

# .env.local
NEXT_PUBLIC_API_URL=http://api.example.com
API_BASE_URL=http://internal-api.example.com

rewrites 설정​

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://api.example.com/api/:path*'
      }
    ];
  }
};

4. 권장 사항​

서버 컴포넌트​

  • 전체 URL 사용하기
  • 환경변수로 베이스 URL 관리
  • next.js의 캐싱 기능 활용
  • 데이터 페칭 로직 추상화

클라이언트 컴포넌트​

  • rewrites 설정 활용하기
  • 상대 경로 사용
  • 에러 바운더리 구현
  • 로딩 상태 관리

5. 주의 사항​

  1. 서버 컴포넌트 fetch

    • rewrites 설정에 의존하지 않기
    • 항상 전체 URL 사용
    • 환경별 URL 분리
  2. 클라이언트 컴포넌트 fetch

    • CORS 이슈 고려
    • 인증 토큰 관리
    • 에러 처리 구현

6. 모범 사례​

API 클라이언트 구현​

// api-client.ts
const createApiClient = () => {
  const baseUrl = typeof window === 'undefined' 
    ? process.env.API_BASE_URL 
    : '/api';

  return {
    async get(path: string) {
      const response = await fetch(`${baseUrl}${path}`);
      return response.json();
    },
    // POST, PUT 등 추가 메서드 구현
  };
};

export const apiClient = createApiClient();

실제 사용 예시​

// 서버 컴포넌트
async function ProductList() {
  const products = await apiClient.get('/products');
  return <div>{/* 제품 목록 렌더링 */}</div>;
}

// 클라이언트 컴포넌트
'use client'
function ProductForm() {
  const handleSubmit = async () => {
    await apiClient.post('/products', formData);
  };
  return <form>{/* 폼 구현 */}</form>;
}

7. 참고자료​

공식 문서​

  • Next.js Server Components
    • 서버 컴포넌트의 기본 개념과 동작 방식
    • 데이터 페칭 패턴과 최적화 방법
  • Next.js Data Fetching
    • 새로운 App Router에서의 데이터 페칭 방법
    • 캐싱과 재검증 전략
  • Next.js Rewrites
    • URL 재작성 설정 방법
    • 프록시 설정과 미들웨어 활용

추천 블로그 포스트​