← 목록으로

챕터 28: 데이터 페칭 전략

서론

웹 애플리케이션의 성능과 사용자 경험은 데이터를 어떻게 가져오느냐에 크게 좌우됩니다. 너무 늦게 데이터를 가져오면 사용자는 빈 화면을 보며 기다려야 하고, 너무 많은 데이터를 미리 가져오면 초기 로딩이 느려집니다. Next.js는 이러한 문제를 해결하기 위해 다양한 데이터 페칭 전략을 제공합니다.

이번 챕터에서는 클라이언트 사이드 페칭, 서버 사이드 렌더링, 정적 생성, 그리고 ISR(Incremental Static Regeneration)까지 모든 데이터 페칭 방법을 자세히 알아보겠습니다. 각 방법의 장단점을 이해하고, 상황에 맞는 최적의 전략을 선택할 수 있게 될 것입니다.

본론

클라이언트 사이드 페칭 (CSR)

클라이언트 사이드 페칭은 브라우저에서 JavaScript를 통해 데이터를 가져오는 방식입니다. 페이지는 먼저 로드되고, 이후에 데이터를 가져와 화면을 업데이트합니다:

// app/products/client/page.js
'use client'

import { useState, useEffect } from 'react'

export default function ClientSidePage() {
  const [products, setProducts] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    fetchProducts()
  }, [])
  
  const fetchProducts = async () => {
    try {
      setLoading(true)
      const response = await fetch('https://fakestoreapi.com/products')
      
      if (!response.ok) {
        throw new Error('데이터를 가져오는데 실패했습니다')
      }
      
      const data = await response.json()
      setProducts(data)
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }
  
  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
          <p className="mt-4 text-gray-600">상품을 불러오는 중입니다...</p>
        </div>
      </div>
    )
  }
  
  if (error) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-center">
          <p className="text-red-600 mb-4">오류: {error}</p>
          <button 
            onClick={fetchProducts}
            className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
          >
            다시 시도
          </button>
        </div>
      </div>
    )
  }
  
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">상품 목록 (클라이언트 사이드)</h1>
        
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {products.slice(0, 9).map(product => (
            <div key={product.id} className="bg-white rounded-lg shadow-md p-6">
              <img 
                src={product.image} 
                alt={product.title}
                className="w-full h-48 object-contain mb-4"
              />
              <h2 className="text-lg font-semibold mb-2 line-clamp-2">
                {product.title}
              </h2>
              <p className="text-gray-600 text-sm mb-4 line-clamp-3">
                {product.description}
              </p>
              <p className="text-2xl font-bold text-blue-600">
                ${product.price}
              </p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

클라이언트 사이드 페칭의 장점:

  • 페이지 전환이 빠릅니다
  • 사용자 인터랙션에 즉시 반응합니다
  • 실시간 데이터를 쉽게 처리할 수 있습니다

단점:

  • SEO에 불리합니다 (검색 엔진이 데이터를 보지 못함)
  • 초기 로딩 시 빈 화면이 보일 수 있습니다
  • 클라이언트의 성능에 의존합니다

서버 사이드 렌더링 (SSR)

서버 사이드 렌더링은 요청이 들어올 때마다 서버에서 데이터를 가져와 HTML을 생성합니다:

// app/products/server/page.js
async function getProducts() {
  const res = await fetch('https://fakestoreapi.com/products', {
    cache: 'no-store' // 캐시 비활성화로 매번 새로운 데이터
  })
  
  if (!res.ok) {
    throw new Error('Failed to fetch products')
  }
  
  return res.json()
}

export default async function ServerSidePage() {
  const products = await getProducts()
  
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">상품 목록 (서버 사이드)</h1>
        
        <div className="bg-green-100 border border-green-400 rounded p-4 mb-6">
          <p className="text-green-700">
            이 페이지는 서버에서 렌더링되었습니다. 
            페이지 소스를 보면 모든 데이터가 HTML에 포함되어 있습니다.
          </p>
        </div>
        
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {products.slice(0, 9).map(product => (
            <div key={product.id} className="bg-white rounded-lg shadow-md p-6">
              <img 
                src={product.image} 
                alt={product.title}
                className="w-full h-48 object-contain mb-4"
              />
              <h2 className="text-lg font-semibold mb-2 line-clamp-2">
                {product.title}
              </h2>
              <p className="text-gray-600 text-sm mb-4 line-clamp-3">
                {product.description}
              </p>
              <p className="text-2xl font-bold text-blue-600">
                ${product.price}
              </p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

서버 사이드 렌더링의 장점:

  • SEO에 유리합니다
  • 초기 로딩 시 완전한 콘텐츠를 보여줍니다
  • 클라이언트 성능에 의존하지 않습니다

단점:

  • 서버 부하가 증가합니다
  • 응답 시간이 느릴 수 있습니다
  • CDN 캐싱이 어렵습니다

정적 생성 (SSG)

정적 생성은 빌드 시점에 데이터를 가져와 HTML을 미리 생성합니다:

// app/products/static/page.js
async function getProducts() {
  const res = await fetch('https://fakestoreapi.com/products', {
    next: { revalidate: false } // 빌드 시에만 실행
  })
  
  if (!res.ok) {
    throw new Error('Failed to fetch products')
  }
  
  return res.json()
}

export default async function StaticPage() {
  const products = await getProducts()
  const buildTime = new Date().toLocaleString('ko-KR')
  
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">상품 목록 (정적 생성)</h1>
        
        <div className="bg-blue-100 border border-blue-400 rounded p-4 mb-6">
          <p className="text-blue-700">
            이 페이지는 빌드 시점({buildTime})에 생성되었습니다.
            데이터가 변경되어도 다시 빌드하기 전까지는 업데이트되지 않습니다.
          </p>
        </div>
        
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {products.slice(0, 9).map(product => (
            <div key={product.id} className="bg-white rounded-lg shadow-md p-6">
              <img 
                src={product.image} 
                alt={product.title}
                className="w-full h-48 object-contain mb-4"
              />
              <h2 className="text-lg font-semibold mb-2 line-clamp-2">
                {product.title}
              </h2>
              <p className="text-gray-600 text-sm mb-4 line-clamp-3">
                {product.description}
              </p>
              <p className="text-2xl font-bold text-blue-600">
                ${product.price}
              </p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

ISR (Incremental Static Regeneration)

ISR은 정적 생성과 서버 사이드 렌더링의 장점을 결합한 방식입니다. 페이지를 정적으로 생성하되, 일정 시간마다 백그라운드에서 재생성합니다:

// app/products/isr/page.js
async function getProducts() {
  const res = await fetch('https://fakestoreapi.com/products', {
    next: { revalidate: 60 } // 60초마다 재검증
  })
  
  if (!res.ok) {
    throw new Error('Failed to fetch products')
  }
  
  return res.json()
}

export default async function ISRPage() {
  const products = await getProducts()
  const currentTime = new Date().toLocaleString('ko-KR')
  
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">상품 목록 (ISR)</h1>
        
        <div className="bg-purple-100 border border-purple-400 rounded p-4 mb-6">
          <p className="text-purple-700">
            이 페이지는 ISR을 사용합니다. 60초마다 백그라운드에서 재생성됩니다.
            현재 시간: {currentTime}
          </p>
        </div>
        
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {products.slice(0, 9).map(product => (
            <div key={product.id} className="bg-white rounded-lg shadow-md p-6">
              <img 
                src={product.image} 
                alt={product.title}
                className="w-full h-48 object-contain mb-4"
              />
              <h2 className="text-lg font-semibold mb-2 line-clamp-2">
                {product.title}
              </h2>
              <p className="text-gray-600 text-sm mb-4 line-clamp-3">
                {product.description}
              </p>
              <p className="text-2xl font-bold text-blue-600">
                ${product.price}
              </p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

데이터 페칭 전략 비교 대시보드

모든 전략을 한눈에 비교할 수 있는 대시보드를 만들어봅시다:

// app/data-fetching/page.js
'use client'

import { useState } from 'react'

export default function DataFetchingComparison() {
  const [selectedStrategy, setSelectedStrategy] = useState('csr')
  
  const strategies = {
    csr: {
      name: '클라이언트 사이드 렌더링 (CSR)',
      description: '브라우저에서 JavaScript로 데이터를 가져옵니다',
      pros: [
        '페이지 전환이 빠름',
        '실시간 데이터 처리 용이',
        '서버 부하 최소화',
        '사용자 인터랙션 즉시 반응'
      ],
      cons: [
        'SEO 최적화 어려움',
        '초기 로딩 시 빈 화면',
        '클라이언트 성능 의존',
        'API 키 노출 위험'
      ],
      useCase: '대시보드, 관리자 페이지, 인증이 필요한 페이지',
      code: `'use client'
import { useState, useEffect } from 'react'

export default function Page() {
  const [data, setData] = useState(null)
  
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(setData)
  }, [])
  
  return <div>{data}</div>
}`
    },
    ssr: {
      name: '서버 사이드 렌더링 (SSR)',
      description: '매 요청마다 서버에서 HTML을 생성합니다',
      pros: [
        'SEO 최적화 우수',
        '초기 로딩 시 완전한 콘텐츠',
        '항상 최신 데이터',
        'API 키 서버에서 안전하게 관리'
      ],
      cons: [
        '서버 부하 증가',
        '응답 시간 느림',
        'CDN 캐싱 어려움',
        '서버 비용 증가'
      ],
      useCase: '뉴스, 전자상거래 상품 페이지, 실시간 데이터가 중요한 페이지',
      code: `async function getData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store' // SSR
  })
  return res.json()
}

export default async function Page() {
  const data = await getData()
  return <div>{data}</div>
}`
    },
    ssg: {
      name: '정적 생성 (SSG)',
      description: '빌드 시점에 HTML을 미리 생성합니다',
      pros: [
        '최고의 성능',
        'SEO 최적화 우수',
        'CDN 캐싱 가능',
        '서버 부하 최소화'
      ],
      cons: [
        '빌드 시간 증가',
        '데이터 업데이트 지연',
        '동적 콘텐츠 처리 어려움',
        '빌드 후 변경 불가'
      ],
      useCase: '블로그, 문서 사이트, 마케팅 페이지, 자주 변경되지 않는 콘텐츠',
      code: `async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: false } // SSG
  })
  return res.json()
}

export default async function Page() {
  const data = await getData()
  return <div>{data}</div>
}`
    },
    isr: {
      name: 'Incremental Static Regeneration (ISR)',
      description: '정적 페이지를 점진적으로 재생성합니다',
      pros: [
        'SSG의 성능 + SSR의 유연성',
        '자동 백그라운드 업데이트',
        'CDN 캐싱 가능',
        '스케일링 용이'
      ],
      cons: [
        '설정 복잡도',
        '일시적 오래된 데이터 제공 가능',
        '재검증 시간 조정 필요',
        '디버깅 어려움'
      ],
      useCase: '전자상거래 카탈로그, 콘텐츠가 주기적으로 업데이트되는 사이트',
      code: `async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 } // ISR - 60초마다 재검증
  })
  return res.json()
}

export default async function Page() {
  const data = await getData()
  return <div>{data}</div>
}`
    }
  }
  
  const currentStrategy = strategies[selectedStrategy]
  
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">데이터 페칭 전략 가이드</h1>
        
        {/* 전략 선택 탭 */}
        <div className="flex space-x-2 mb-8 overflow-x-auto">
          {Object.entries(strategies).map(([key, strategy]) => (
            <button
              key={key}
              onClick={() => setSelectedStrategy(key)}
              className={`px-4 py-2 rounded-lg whitespace-nowrap transition-colors ${
                selectedStrategy === key
                  ? 'bg-blue-600 text-white'
                  : 'bg-white text-gray-700 hover:bg-gray-100'
              }`}
            >
              {strategy.name}
            </button>
          ))}
        </div>
        
        {/* 선택된 전략 상세 정보 */}
        <div className="bg-white rounded-lg shadow-lg p-8">
          <h2 className="text-2xl font-bold mb-4">{currentStrategy.name}</h2>
          <p className="text-gray-600 mb-6">{currentStrategy.description}</p>
          
          <div className="grid md:grid-cols-2 gap-6 mb-6">
            {/* 장점 */}
            <div className="bg-green-50 rounded-lg p-6">
              <h3 className="text-lg font-semibold text-green-800 mb-3">
                ✅ 장점
              </h3>
              <ul className="space-y-2">
                {currentStrategy.pros.map((pro, index) => (
                  <li key={index} className="flex items-start">
                    <span className="text-green-600 mr-2">•</span>
                    <span className="text-gray-700">{pro}</span>
                  </li>
                ))}
              </ul>
            </div>
            
            {/* 단점 */}
            <div className="bg-red-50 rounded-lg p-6">
              <h3 className="text-lg font-semibold text-red-800 mb-3">
                ⚠️ 단점
              </h3>
              <ul className="space-y-2">
                {currentStrategy.cons.map((con, index) => (
                  <li key={index} className="flex items-start">
                    <span className="text-red-600 mr-2">•</span>
                    <span className="text-gray-700">{con}</span>
                  </li>
                ))}
              </ul>
            </div>
          </div>
          
          {/* 사용 사례 */}
          <div className="bg-blue-50 rounded-lg p-6 mb-6">
            <h3 className="text-lg font-semibold text-blue-800 mb-2">
              💡 적합한 사용 사례
            </h3>
            <p className="text-gray-700">{currentStrategy.useCase}</p>
          </div>
          
          {/* 코드 예제 */}
          <div className="bg-gray-900 rounded-lg p-6">
            <h3 className="text-lg font-semibold text-white mb-3">
              📝 코드 예제
            </h3>
            <pre className="text-sm text-gray-300 overflow-x-auto">
              <code>{currentStrategy.code}</code>
            </pre>
          </div>
        </div>
        
        {/* 전략 선택 가이드 */}
        <div className="mt-8 bg-white rounded-lg shadow-lg p-8">
          <h2 className="text-2xl font-bold mb-6">🎯 어떤 전략을 선택해야 할까요?</h2>
          
          <div className="space-y-4">
            <div className="border-l-4 border-blue-500 pl-4">
              <p className="font-semibold">데이터가 자주 변경되고 SEO가 중요하다면?</p>
              <p className="text-gray-600">→ SSR (서버 사이드 렌더링)</p>
            </div>
            
            <div className="border-l-4 border-green-500 pl-4">
              <p className="font-semibold">성능이 최우선이고 데이터가 거의 변경되지 않는다면?</p>
              <p className="text-gray-600">→ SSG (정적 생성)</p>
            </div>
            
            <div className="border-l-4 border-purple-500 pl-4">
              <p className="font-semibold">성능도 중요하고 주기적인 업데이트도 필요하다면?</p>
              <p className="text-gray-600">→ ISR (Incremental Static Regeneration)</p>
            </div>
            
            <div className="border-l-4 border-orange-500 pl-4">
              <p className="font-semibold">사용자별 맞춤 데이터나 실시간 인터랙션이 중요하다면?</p>
              <p className="text-gray-600">→ CSR (클라이언트 사이드 렌더링)</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

결론

데이터 페칭 전략은 웹 애플리케이션의 성능과 사용자 경험을 결정하는 중요한 요소입니다. 이번 챕터에서 우리는 Next.js가 제공하는 네 가지 주요 데이터 페칭 방법을 모두 살펴보았습니다.

각 전략은 고유한 장단점을 가지고 있으며, 프로젝트의 요구사항에 따라 적절한 전략을 선택해야 합니다. 때로는 한 프로젝트 내에서도 페이지마다 다른 전략을 사용하는 것이 최선일 수 있습니다. 예를 들어, 홈페이지는 SSG로, 제품 상세 페이지는 ISR로, 사용자 대시보드는 CSR로 구현할 수 있습니다.

Next.js의 강력한 점은 이 모든 전략을 하나의 프레임워크 안에서 자유롭게 혼합하여 사용할 수 있다는 것입니다. 이제 여러분은 상황에 맞는 최적의 데이터 페칭 전략을 선택할 수 있는 지식을 갖추게 되었습니다.

다음 챕터에서는 프로덕션 배포를 위한 준비 과정을 알아보겠습니다. 성능 최적화, 환경 변수 설정, 그리고 배포 전 체크리스트까지 실제 서비스 런칭에 필요한 모든 것을 다루겠습니다!