본문으로 건너뛰기

안동민 개발노트

본문 시작

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

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

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

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

사용자 상호작용 이후 동적 갱신이 필요하거나, Geolocation API처럼 브라우저 전용 API를 써야 할 때는 클라이언트 컴포넌트(Client Components)에서 데이터 페칭이 필요합니다.

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

실습 URL은 재현성 확보를 위해 로컬 Mock 서버(json-server, http://localhost:4000) 기준으로 작성합니다.

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

클라이언트 페칭은 상호작용 이후의 변화에 쓴다

초기 SEO·비밀 데이터와 브라우저 행동 뒤 바뀌는 상태를 분리한다.

  1. 첫 화면
    Server fetch

    검색·공유에 필요한 값과 초기 HTML 준비

  2. 검색·필터
    Client fetch

    사용자 입력 뒤 필요한 데이터 재조회

  3. 반복 조회
    SWR

    stale 우선 표시와 포커스 재검증

  4. 복잡한 변경
    TanStack Query

    mutation·retry·invalidation 정책 관리


데이터 페칭의 필요성

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

클라이언트 페칭은 초기 HTML 뒤의 상태를 이어받는다

서버가 첫 화면을 만들고 사용자의 행동이 있을 때 브라우저가 로딩·오류·성공을 관리한다.

  1. Initial
    Server render

    SEO와 첫 화면에 필요한 데이터를 HTML로 제공

  2. Action
    사용자 입력

    필터·더보기·검색으로 새로운 요청 의도 발생

  3. Fetch
    Client request

    loading·error와 이전 데이터를 함께 관리

  4. Sync
    Cache update

    서버 결과를 화면과 클라이언트 캐시에 반영

클라이언트 컴포넌트는 브라우저에서 상태와 이벤트를 실행할 수 있는 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();
  }, []); // 빈 배열은 컴포넌트가 마운트될 때 한 번만 실행됨을 의미

  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>
  );
}
실습

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

그 안에 page.tsxClientDataFetcher.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 = (url: string) => fetch(url).then(res => 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은 내부적으로 캐싱과 재검증을 자동으로 처리하므로, 복잡한 로직 없이도 효율적인 데이터 관리가 가능합니다.

아래 다이어그램은 클라이언트 데이터 페칭이 마운트, 캐시 조회, 재검증, UI 갱신으로 이어지는 흐름을 정리한 것입니다.

클라이언트 데이터는 서버 shell 뒤에서 필요한 만큼만 갱신한다

마운트 이후 cache key를 확인하고 실제 요청이 필요할 때만 작은 UI 영역을 바꾼다.

  1. 1
    Server shell

    HTML과 직렬화 가능한 초기값 준비

  2. 2
    Hydration

    벤트와 브라우저 상태 연결

  3. 3
    Cache lookup

    SWR·Query key로 기존 데이터 확인

  4. 4
    API request

    없거나 stale할 때만 브라우저 fetch

  5. 5
    UI update

    성공·로딩·오류를 해당 영역에서 반영


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

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

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

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

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>
  );
}

이러한 분리된 접근 방식은 Next.js 애플리케이션의 성능을 최적화하고, 개발자가 각 컴포넌트의 역할에 집중할 수 있도록 돕습니다.

초기 로딩은 서버에서 빠르게 처리하고, 이후의 동적인 상호작용은 클라이언트에서 효율적으로 관리하여 사용자에게 최상의 경험을 제공할 수 있습니다.

초기 화면은 서버, 상호작용 이후는 클라이언트가 맡는다

데이터 필요 시점과 실행 환경을 나누면 중복 요청과 비밀 노출을 함께 막을 수 있다.

  1. Server
    SEO 콘텐츠

    게시글·상품명처럼 진입 즉시 보여야 하는 값

  2. Server
    비밀 접근

    DB·API key·내부 서비스 호출

  3. Client
    사용자 액션

    검색·필터·댓글 뒤 즉시 갱신

  4. Client
    브라우저 기능

    localStorage·geolocation·WebSocket

클라이언트 데이터 페칭은 상호작용 이후의 최신성에는 강하지만 초기 HTML과 로딩 상태를 직접 설계해야 하므로, 서버 페칭과 역할을 분리해 선택하는 것이 좋습니다.

첫 화면은 서버가 채우고 이후 최신성은 클라이언트가 잇는다

데이터를 언제 처음 보여줘야 하는지와 사용자 행동 뒤 얼마나 자주 바뀌는지를 나눠 배치한다.

  1. server first
    초기 HTML

    SEO·권한·첫 화면 핵심 데이터는 서버에서 준비

  2. handoff
    초기값 전달

    서버 결과를 직렬화해 client leaf의 시작 상태로 사용

  3. client refresh
    상호작용 이후

    검색·필터·실시간 갱신은 클라이언트가 재조회

  4. UI states
    로딩·오류·재시도

    전 데이터와 새 요청 상태를 같은 영역에서 안정적으로 표현

이 다이어그램은 클라이언트 컴포넌트에서 데이터 페칭을 Next.js 프로젝트에 넣을 때 결정해야 할 파일 위치와 런타임 경계를 정리합니다.

데이터의 첫 책임을 정한 뒤 client fetch를 붙인다

초기 화면은 서버가 준비하고, 사용자 입력·실시간 변화·브라우저 상태에 반응하는 구간만 클라이언트 요청으로 이어 간다.

  1. YES · server
    초기 목록 준비

    SEO와 첫 화면에 필요한 값을 서버에서 가져온다. 직렬화 가능한 결과를 client cache의 초기값으로 넘겨 중복 요청을 줄인다. // server: initial list

  2. NO / 이후 변화 · client
    상호작용을 요청 키로

    검색어·필터·페이지 번호처럼 브라우저에서 바뀌는 상태가 fetch key를 만든다. // client: filters, pagination

  3. 입력 변경

    검색어·필터·페이지가 새 key를 만든다.

  4. Abort 이전 요청

    늦은 응답이 최신 UI를 덮는 race condition을 막는다.

  5. SWR / Query

    stale cache를 먼저 보여주고 refetch·retry·focus revalidate를 관리한다.

  6. UI commit

    최신 결과만 data·loading·error 상태에 반영한다.

마지막으로 서버 컴포넌트 fetch와 클라이언트 데이터 라이브러리를 어떤 기준으로 선택할지 정리합니다.

데이터 페칭 도구는 시점·주체·동기화 복잡도로 고른다

API를 어디서 호출할지가 아니라 누가 최신성을 책임질지를 먼저 정한다.

  1. 진입 즉시
    Server fetch

    첫 화면·SEO·비밀 접근을 서버에서 준비

  2. 행동 이후
    Client fetch

    검색·필터·무한 스크롤처럼 입력 뒤 변화

  3. 재시도·캐시
    Query library

    동기화 규칙이 복잡할 때 전용 도구 사용

  4. 변경 작업
    Action / API

    검증·저장·무효화를 서버 경계에서 실행