안동민 개발노트

안동민 개발노트

서버 컴포넌트에서 데이터 페칭정적 데이터 생성 (SSG)서버 사이드 렌더링 (SSR)증분 정적 재생성 (ISR)클라이언트 컴포넌트에서 데이터 페칭
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 6장 : 데이터 페칭
  5. 클라이언트 컴포넌트에서 데이터 페칭
  1. Next.js
  2. 클라이언트 컴포넌트에서 데이터 페칭

클라이언트 컴포넌트에서 데이터 페칭

상호작용 뒤 데이터를 갱신하는 클라이언트 fetch와 SWR을 구현하고 서버 컴포넌트와 역할을 나눕니다.

지금까지는 App Router의 핵심인 서버 컴포넌트에서 데이터를 가져오는 방법(SSR, SSG, ISR)을 살펴봤습니다.

서버 컴포넌트는 초기 성능과 SEO에 강점이 있지만, 모든 상황에 항상 맞지는 않습니다.

사용자 상호작용 이후 동적 갱신이 필요하거나, Geolocation API처럼 브라우저 전용 API를 써야 할 때는 클라이언트 컴포넌트(Client Components)에서 상태와 브라우저 API를 다룹니다. 필요한 후속 데이터는 클라이언트 fetch나 Server Action 등으로 요청할 수 있습니다.

이 절에서는 클라이언트 컴포넌트에서 데이터를 페칭하는 방법과, 서버 컴포넌트와의 역할 분담을 통해 애플리케이션의 성능과 유연성을 극대화하는 전략에 대해 알아보겠습니다.

실습 URL과 _limit 배열 응답은 6장 1절의 json-server@0.17.4·http://localhost:4000 기준입니다.

포트 정책은 4000=Mock API, 4100=별도 백엔드/실시간 서버로 분리해 두면 로컬 서비스 간 충돌을 줄일 수 있습니다.


데이터 페칭의 필요성

클라이언트 컴포넌트의 데이터 페칭에서는 마운트 시점, 캐시 위치, 재검증 방식, 오류 표시 기준을 먼저 나눠 봅니다.

클라이언트 컴포넌트는 브라우저에서 상태와 이벤트를 실행할 수 있는 React 컴포넌트입니다.

첫 방문에는 서버가 초기 HTML 생성에 참여할 수 있고, 브라우저가 JavaScript로 하이드레이션한 뒤 상호작용과 후속 렌더링을 맡습니다.

useEffect, useState와 같은 React 훅을 사용하거나, 브라우저 전용 API에 접근해야 할 때 사용됩니다.

클라이언트 컴포넌트에서 데이터 페칭이 필요한 경우
  • 사용자 상호작용에 따른 동적 데이터 업데이트: 버튼 클릭, 폼 제출, 검색어 입력 등 사용자 액션에 따라 실시간으로 데이터를 가져와 UI를 업데이트해야 할 때. (예: 댓글 등록 후 목록 갱신, 검색 필터 적용)
  • 브라우저 전용 API 의존성: localStorage, navigator.geolocation, WebSockets 등 브라우저 환경에서만 사용 가능한 API를 통해 데이터를 가져와야 할 때.
  • 작은 규모의 클라이언트 전용 데이터: 초기 로딩 시점에 필요하지 않고, 페이지 로드 후 사용자 경험을 향상시키기 위해 비동기적으로 가져오는 데이터.
  • 서드파티 클라이언트 라이브러리 사용: SWR, React Query와 같은 클라이언트 사이드 데이터 페칭 라이브러리를 사용하고자 할 때.

데이터 페칭하는 방법

클라이언트 컴포넌트에서 데이터를 페칭하는 방법은 일반적인 React 애플리케이션에서 데이터를 가져오는 방식과 동일합니다.

주로 useEffect 훅과 useState 훅을 조합하여 사용합니다.

src/app/client-data/page.tsx
// src/app/client-data/page.tsx (새로 생성할 페이지)
// 이 파일은 서버 컴포넌트이지만, 그 안에서 클라이언트 컴포넌트를 임포트하여 사용합니다.

import ClientDataFetcher from './ClientDataFetcher'; // 클라이언트 컴포넌트 임포트

export default function ClientDataPage() {
  return (
    <div>
      <h1>클라이언트 컴포넌트 데이터 페칭 예제</h1>
      <p>이 페이지는 서버 컴포넌트이지만, 아래 데이터는 클라이언트 컴포넌트에서 가져옵니다.</p>
      <ClientDataFetcher />
    </div>
  );
}
src/app/client-data/ClientDataFetcher.tsx
// src/app/client-data/ClientDataFetcher.tsx (새로 생성할 클라이언트 컴포넌트)
"use client"; // 이 파일은 클라이언트 컴포넌트임을 명시

import React, { useState, useEffect } from 'react';

interface Todo {
  userId: number;
  id: number;
  todo: string;
  completed: boolean;
}

export default function ClientDataFetcher() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchTodos() {
      try {
        setLoading(true);
        setError(null);
        // 클라이언트 측에서 fetch API를 사용하여 데이터를 가져옵니다.
        const res = await fetch('http://localhost:4000/todos?_limit=5');
        if (!res.ok) {
          throw new Error('Failed to fetch todos');
        }
        const payload = await res.json();
        const data: Todo[] = payload;
        setTodos(data);
      } catch (err: any) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchTodos();
  }, []); // 마운트 시 설정; 개발 Strict Mode에서는 추가 실행 가능

  if (loading) {
    return <p>할 일 목록을 불러오는 중입니다...</p>;
  }

  if (error) {
    return <p style={{ color: 'red' }}>에러 발생: {error}</p>;
  }

  return (
    <div style={{ border: '1px dashed #f0ad4e', padding: '15px', marginTop: '20px', borderRadius: '8px' }}>
      <h2 style={{ color: '#f0ad4e' }}>클라이언트에서 가져온 할 일 목록</h2>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.todo}
          </li>
        ))}
      </ul>
      <button onClick={() => alert('클라이언트에서만 가능한 액션!')}>클라이언트 액션</button>
    </div>
  );
}

원문의 Effect에는 요청 취소나 늦은 응답 무시를 위한 cleanup이 없습니다. 개발 Strict Mode의 추가 setup·cleanup이나 재마운트로 요청이 반복될 수 있으므로, 정확히 한 번 실행되는 요청으로 해석하지 않습니다.

실습

src/app/client-data 폴더를 만듭니다.

그 안에 page.tsx와 ClientDataFetcher.tsx 파일을 위 내용으로 생성합니다.

개발 서버(npm run dev)가 실행 중이라면, http://localhost:3000/client-data로 접속하여 페이지를 확인해 보세요.

페이지가 로드된 후 할 일 목록을 불러오는 중입니다... 메시지가 잠시 나타났다가, 클라이언트에서 데이터를 가져와 할 일 목록이 표시되는 것을 볼 수 있습니다.


데이터 페칭 라이브러리 활용

Next.js는 클라이언트 컴포넌트에서 데이터 페칭을 더 효율적으로 관리할 수 있도록 SWR, React Query(TanStack Query)와 같은 라이브러리 사용을 권장합니다.

이러한 라이브러리는 캐싱, 재검증, 에러 처리, 로딩 상태 관리 등 복잡한 데이터 페칭 로직을 추상화하여 개발 편의성을 높여줍니다.

SWR을 사용한 예시
SWR 설치
npm install swr
# 또는
yarn add swr
src/app/client-data/SWRFetcher.tsx 파일 생성
src/app/client-data/SWRFetcher.tsx
// src/app/client-data/SWRFetcher.tsx
"use client"; // 클라이언트 컴포넌트임을 명시

import useSWR from 'swr';
import React from 'react';

interface Post {
  id: number;
  title: string;
  body: string;
}

// 데이터를 가져오는 fetcher 함수 (SWR에 전달)
const fetcher = async (url: string) => {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`);
  return res.json();
};

export default function SWRFetcher() {
  // useSWR 훅을 사용하여 데이터 페칭 및 캐싱 관리
  const { data, error, isLoading } = useSWR<Post[]>('http://localhost:4000/posts?_limit=3', fetcher);

  if (error) return <p style={{ color: 'red' }}>SWR 에러: {error.message}</p>;
  if (isLoading) return <p>SWR로 게시물을 불러오는 중입니다...</p>;

  return (
    <div style={{ border: '1px dashed #6c757d', padding: '15px', marginTop: '20px', borderRadius: '8px' }}>
      <h2 style={{ color: '#6c757d' }}>SWR로 가져온 게시물 목록</h2>
      <ul>
        {data?.map((post) => (
          <li key={post.id}>
            <strong>{post.title}</strong>
          </li>
        ))}
      </ul>
      <button onClick={() => alert('SWR 캐싱된 데이터!')}>데이터 확인</button>
    </div>
  );
}
src/app/client-data/page.tsx에 SWRFetcher 추가
src/app/client-data/page.tsx
// src/app/client-data/page.tsx
import ClientDataFetcher from './ClientDataFetcher';
import SWRFetcher from './SWRFetcher'; // SWRFetcher 임포트

export default function ClientDataPage() {
  return (
    <div>
      <h1>클라이언트 컴포넌트 데이터 페칭 예제</h1>
      <p>이 페이지는 서버 컴포넌트이지만, 아래 데이터는 클라이언트 컴포넌트에서 가져옵니다.</p>
      <ClientDataFetcher />
      <SWRFetcher /> {/* SWRFetcher 컴포넌트 추가 */}
    </div>
  );
}

실습 확인: http://localhost:3000/client-data로 다시 접속하여, SWR로 가져온 게시물 목록이 추가로 표시되는 것을 확인해 보세요.

SWR은 내부적으로 캐싱과 재검증을 자동으로 처리하므로, 복잡한 로직 없이도 효율적인 데이터 관리가 가능합니다.

원문의 Effect fetch와 SWR이 맡는 일

두 예제의 실제 구현 범위를 비교합니다. SWR의 캐시는 제공자와 key 범위를 따르며 서버 Data Cache와 같은 저장소가 아닙니다.

원문의 Effect fetch와 SWR이 맡는 일
책임Effect 예제SWR 예제
시작마운트 뒤 Effect에서 요청합니다.URL key로 요청하고 캐시 상태를 구독합니다.
HTTP 실패res.ok가 false이면 오류를 던져 상태로 표시합니다.교정한 fetcher가 res.ok를 검사하고 던진 오류를 error로 받습니다.
저장 위치현재 컴포넌트의 useState에 목록을 둡니다.같은 캐시 제공자와 key의 결과를 공유할 수 있습니다.
다시 읽기자동 재검증 정책을 구현하지 않았습니다.기본 포커스·재연결 재검증 등을 라이브러리가 관리합니다. 옵션으로 조정합니다.
시작
Effect 예제: 마운트 뒤 Effect에서 요청합니다.
SWR 예제: URL key로 요청하고 캐시 상태를 구독합니다.
HTTP 실패
Effect 예제: res.ok가 false이면 오류를 던져 상태로 표시합니다.
SWR 예제: 교정한 fetcher가 res.ok를 검사하고 던진 오류를 error로 받습니다.
저장 위치
Effect 예제: 현재 컴포넌트의 useState에 목록을 둡니다.
SWR 예제: 같은 캐시 제공자와 key의 결과를 공유할 수 있습니다.
다시 읽기
Effect 예제: 자동 재검증 정책을 구현하지 않았습니다.
SWR 예제: 기본 포커스·재연결 재검증 등을 라이브러리가 관리합니다. 옵션으로 조정합니다.

서버 컴포넌트와의 데이터 페칭 시너지

Next.js App Router에서는 서버 컴포넌트와 클라이언트 컴포넌트의 데이터 페칭 역할을 나누어 설계할 수 있습니다.

  • 초기 데이터는 서버에서: 페이지의 초기 로딩에 필요한 핵심 데이터(SEO, 빠른 사용자 경험)는 서버 컴포넌트에서 SSG, SSR, ISR을 통해 가져옵니다.
  • 동적/사용자 상호작용 데이터는 클라이언트에서: 페이지 로드 후 사용자 상호작용에 따라 변경되거나, 브라우저 전용 기능이 필요한 데이터는 클라이언트 컴포넌트에서 가져옵니다.
일반적인 패턴

다음은 역할 분담을 보이는 발췌입니다. fetch('...')는 실제 URL로 구현해야 하며, 브라우저에 공개할 필드만 props로 선택합니다. 자식의 타이머는 마운트 후 카운트만 올리고 주석 속 실시간 API는 호출하지 않습니다. useState(initialClientData)는 초기값을 사용하며 이후 props 변경을 자동 동기화하지 않습니다.

서버 컴포넌트 (부모): 페이지의 전체 구조와 초기 데이터를 담당합니다.

src/app/some-page/page.tsx (서버 컴포넌트)
import ClientInteractiveComponent from './ClientInteractiveComponent';

async function getServerData() {
  // 서버에서만 접근 가능한 민감한 데이터나 초기 데이터 페칭
  const data = await fetch('...');
  return data.json();
}

export default async function SomePage() {
  const initialData = await getServerData(); // 서버에서 초기 데이터 페칭

  return (
    <div>
      <h1>서버에서 렌더링된 제목</h1>
      <p>초기 데이터: {initialData.someValue}</p>
      {/* 클라이언트 컴포넌트에 초기 데이터를 prop으로 전달할 수 있습니다. */}
      <ClientInteractiveComponent initialClientData={initialData.clientSpecificValue} />
    </div>
  );
}

클라이언트 컴포넌트 (자식): 사용자 상호작용, 동적 데이터 업데이트, 브라우저 API 접근 등을 담당합니다.

src/app/some-page/ClientInteractiveComponent.tsx (클라이언트 컴포넌트)
"use client";

import React, { useState, useEffect } from 'react';

export default function ClientInteractiveComponent({ initialClientData }: { initialClientData: string }) {
  const [dynamicData, setDynamicData] = useState(initialClientData);
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 사용자 상호작용 후 데이터 페칭 또는 브라우저 API 사용
    const interval = setInterval(() => {
      setCount(prev => prev + 1);
      // fetch('/api/realtime-update').then(...)
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <div style={{ border: '1px solid blue', padding: '10px', marginTop: '10px' }}>
      <p>클라이언트에서 업데이트되는 데이터: {dynamicData}</p>
      <p>카운트: {count}</p>
      <button onClick={() => setDynamicData('새로운 데이터: ' + new Date().toLocaleTimeString())}>데이터 업데이트</button>
    </div>
  );
}

증분 정적 재생성 (ISR)

이전 페이지

서버 컴포넌트

다음 페이지

이 페이지의 목차

데이터 페칭의 필요성데이터 페칭하는 방법데이터 페칭 라이브러리 활용서버 컴포넌트와의 데이터 페칭 시너지