클라이언트 컴포넌트에서 데이터 페칭
상호작용 뒤 데이터를 갱신하는 클라이언트 fetch와 SWR을 구현하고 서버 컴포넌트와 역할을 나눕니다.
지금까지는 App Router의 핵심인 서버 컴포넌트에서 데이터를 가져오는 방법(SSR, SSG, ISR)을 살펴봤습니다. 서버 컴포넌트는 초기 성능과 SEO에 장점이 있지만 모든 상황에 맞지는 않습니다. 사용자 상호작용 뒤에 화면을 동적으로 갱신해야 하거나 Geolocation API처럼 브라우저 전용 API를 써야 할 때는 클라이언트 컴포넌트(Client Components)에서 상태와 브라우저 API를 다루며, 필요한 후속 데이터는 클라이언트의 fetch나 SWR 같은 라이브러리로 요청할 수 있습니다. 서버 액션은 서버의 데이터를 바꾸는 작업을 위한 기능이라, 화면에 보여 줄 데이터를 읽는 용도로는 권장되지 않습니다.
이 글에서는 클라이언트 컴포넌트에서 데이터를 가져오는 방법과, 서버 컴포넌트와 역할을 나누어 애플리케이션의 성능과 유연성을 함께 얻는 방법을 알아봅니다. 실습 URL과 _limit 배열 응답은 6장 1절의 [email protected]와 http://localhost:4000을 기준으로 합니다. 포트는 4000번을 목 API에, 4100번을 별도의 백엔드나 실시간 서버에 나누어 두면 로컬 서비스끼리 충돌하는 일을 줄일 수 있습니다.
데이터 페칭이 필요한 경우
클라이언트 컴포넌트에서 데이터를 가져올 때는 요청을 시작하는 시점, 결과를 저장하는 위치, 다시 읽는 방식, 오류를 표시하는 기준을 먼저 나누어 생각합니다. 클라이언트 컴포넌트는 브라우저에서 상태와 이벤트를 실행할 수 있는 React 컴포넌트입니다. 첫 방문에는 서버가 초기 HTML을 만드는 데 참여할 수 있고, 브라우저가 JavaScript로 하이드레이션(hydration)한 뒤 상호작용과 이후의 렌더링을 맡습니다. useEffect, useState 같은 React 훅을 쓰거나 브라우저 전용 API에 접근해야 할 때 클라이언트 컴포넌트를 씁니다.
클라이언트 컴포넌트에서 데이터를 가져와야 하는 경우는 대략 네 가지입니다. 첫째는 버튼 클릭, 폼 제출, 검색어 입력 같은 사용자 동작에 따라 데이터를 가져와 화면을 갱신해야 할 때로, 댓글을 등록한 뒤 목록을 새로 고치거나 검색 필터를 적용하는 경우가 그렇습니다. 둘째는 localStorage, navigator.geolocation, WebSocket처럼 브라우저 환경에서만 쓸 수 있는 API로 데이터를 얻어야 할 때입니다. 셋째는 초기 로딩에는 필요하지 않고, 페이지가 열린 뒤 사용자 경험을 높이려고 비동기로 가져오는 작은 데이터입니다. 넷째는 SWR이나 TanStack Query(React Query) 같은 클라이언트 데이터 페칭 라이브러리를 쓰려는 경우입니다.
데이터 페칭하는 방법
클라이언트 컴포넌트에서 데이터를 가져오는 방법은 일반적인 React 애플리케이션과 같으며, 주로 useEffect 훅과 useState 훅을 조합합니다. 먼저 클라이언트 컴포넌트를 담을 페이지를 만듭니다. 이 페이지 자체는 서버 컴포넌트이고, 그 안에서 클라이언트 컴포넌트를 가져와 씁니다.
// src/app/client-data/page.tsx (새로 생성할 페이지)
// 이 파일은 서버 컴포넌트이지만, 그 안에서 클라이언트 컴포넌트를 임포트하여 사용합니다.
import ClientDataFetcher from './ClientDataFetcher'; // 클라이언트 컴포넌트 임포트
export default function ClientDataPage() {
return (
<div>
<h1>클라이언트 컴포넌트 데이터 페칭 예제</h1>
<p>이 페이지는 서버 컴포넌트이지만, 아래 데이터는 클라이언트 컴포넌트에서 가져옵니다.</p>
<ClientDataFetcher />
</div>
);
}다음은 마운트된 뒤 할 일 목록을 요청하는 클라이언트 컴포넌트입니다. 파일 맨 위의 "use client"가 이 파일을 클라이언트 컴포넌트로 표시하며, 로딩, 오류, 목록 세 가지 상태를 useState로 관리합니다.
// 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) 함수가 없습니다. 특히 요청 URL을 prop으로 받아 바꾸는 변형에서는 이전 요청 A가 새 요청 B보다 늦게 끝날 수 있습니다. B의 새 목록이 이미 표시된 뒤 A가 setTodos를 부르면 화면이 이전 조건의 목록으로 돌아가는데, 응답이 도착하는 순서와 사용자가 마지막으로 고른 조건의 순서가 다르기 때문입니다. 이런 경쟁 상태(race condition)를 정리 함수와 AbortController로 막는 방법은 React 교재의 useEffect와 데이터 요청에서 자세히 다룹니다. 개발 모드의 Strict Mode는 setup과 cleanup을 한 번 더 실행하며 재마운트로도 요청이 반복될 수 있으므로, 이 요청이 정확히 한 번 실행된다고 생각하지 않습니다.
URL이 바뀌는 변형을 만들 때는 다음의 별도 컴포넌트처럼 Effect를 실행할 때마다 그 결과가 아직 유효한지 검사할 수 있습니다. 이 컴포넌트는 ClientDataFetcher와 함께 쌓아 둘 필수 파일이 아니라, 늦은 응답을 처리하는 방식을 비교하려는 대안입니다. endpoint에는 같은 Todo[] 형태를 돌려주는 URL을 넣고, 부모가 그 prop을 바꾼다고 전제합니다.
"use client";
import { useEffect, useState } from 'react';
interface Todo {
userId: number;
id: number;
todo: string;
completed: boolean;
}
export default function IgnoringLateTodoFetcher({ endpoint }: { endpoint: string }) {
const [todos, setTodos] = useState<Todo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let ignore = false;
setLoading(true);
setError(null);
async function load() {
try {
const res = await fetch(endpoint);
if (!res.ok) throw new Error(`목록 조회 실패: ${res.status}`);
const data: Todo[] = await res.json();
if (!ignore) setTodos(data);
} catch (cause) {
if (!ignore) setError(cause instanceof Error ? cause.message : '목록 조회 실패');
} finally {
if (!ignore) setLoading(false);
}
}
load();
return () => { ignore = true; };
}, [endpoint]);
if (loading) return <p>목록을 불러오는 중입니다...</p>;
if (error) return <p>{error}</p>;
return <ul>{todos.map(todo => <li key={todo.id}>{todo.todo}</li>)}</ul>;
}URL이 바뀌어 A의 Effect가 정리되면 A 실행의 지역 변수 ignore가 true가 됩니다. 그래서 A가 늦게 성공해도 목록을 덮어쓰지 않고, A의 오류나 finally도 B의 error와 loading을 바꾸지 않습니다. B 실행의 ignore는 따로 있으며 아직 false이므로 B의 결과가 현재 화면에 반영됩니다. 이 방식은 늦은 결과를 무시할 뿐 네트워크 요청 자체를 취소하지는 않으며, 실제로 요청을 중단하려면 AbortController를 따로 써야 합니다. 응답 필드를 런타임에 검증하는 코드도 이 예제에는 없습니다.
src/app/client-data 폴더를 만들고 그 안에 page.tsx와 ClientDataFetcher.tsx를 위 내용으로 만듭니다. 목 API와 개발 서버(npm run dev)를 실행한 채 http://localhost:3000/client-data에 접속하면, 페이지가 열린 뒤 "할 일 목록을 불러오는 중입니다..."라는 문구가 잠시 나타났다가 클라이언트에서 가져온 할 일 목록이 표시됩니다.
데이터 페칭 라이브러리 활용
Next.js는 클라이언트 컴포넌트의 데이터 페칭을 더 효율적으로 관리할 수 있도록 SWR이나 TanStack Query 같은 라이브러리를 쓰는 방법을 안내합니다. 이런 라이브러리는 캐싱, 재검증, 오류 처리, 로딩 상태 관리 같은 데이터 페칭 로직을 감싸 주어 개발을 편하게 합니다. 여기서는 SWR로 게시물 목록을 가져와 봅니다. 먼저 SWR을 설치합니다.
npm install swr
# 또는
yarn add swr다음으로 src/app/client-data/SWRFetcher.tsx 파일을 만듭니다. useSWR의 첫 인자인 URL이 캐시의 키(key)가 되고, 두 번째 인자인 fetcher가 실제 요청을 보냅니다. fetcher가 res.ok를 검사해 던진 오류는 error로 받습니다.
// 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>
);
}useSWR이 돌려주는 isLoading은 아직 받은 데이터가 없는 첫 요청 동안 true이고, 요청이 실패하면 error에 fetcher가 던진 Error가 들어옵니다. 그래서 컴포넌트는 오류, 로딩, 목록 순서로 확인해 그중 하나만 그리며, 주소의 _limit=3 때문에 목록에는 게시물이 세 개까지만 나옵니다. 마지막으로 src/app/client-data/page.tsx에 SWRFetcher를 추가합니다. 페이지 자체는 서버 컴포넌트로 두고, 두 클라이언트 컴포넌트를 가져와 차례로 배치하기만 합니다.
// 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은 캐싱과 재검증을 내부에서 처리하므로, 직접 로직을 많이 쓰지 않아도 데이터를 효율적으로 관리할 수 있습니다.
NEXT · CH6-5
Effect 로직과 SWR 캐시의 책임을 비교합니다
본문의 Effect 예제는 todos를, SWR 예제는 posts를 요청합니다. 각 코드가 담당하는 일을 비교하며, SWR 캐시는 제공자와 key 범위에 속합니다.
| 책임 | 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 예제: 기본 포커스·재연결 재검증 등을 라이브러리가 관리합니다. 옵션으로 조정합니다.
Effect 예제에는 요청 취소나 오래된 응답을 무시하는 정리가 없습니다. 뒤의 setInterval 예제는 로컬 count만 갱신합니다. SWR 캐시와 서버 Data Cache는 별개의 저장소입니다.
위 표는 본문의 Effect 예제가 todos를, SWR 예제가 posts를 요청할 때 각 코드가 맡는 일을 비교합니다. 요청을 시작할 때 Effect 예제는 마운트된 뒤 Effect에서 요청하고, SWR 예제는 URL 키로 요청하면서 캐시의 상태를 구독합니다. HTTP 실패는 두 예제 모두 res.ok가 false이면 오류를 던지며, Effect 예제는 이를 상태에 담아 표시하고 SWR 예제는 error로 받습니다. 저장 위치는 Effect 예제가 현재 컴포넌트의 useState이고, SWR 예제는 같은 캐시 제공자(provider)와 같은 키의 결과를 여러 컴포넌트가 함께 쓸 수 있습니다.
다시 읽는 방식에서 두 예제의 차이가 가장 큽니다. Effect 예제는 자동 재검증을 구현하지 않았지만, SWR은 창에 다시 포커스가 오거나 네트워크가 다시 연결될 때 재검증하는 기본 동작을 라이브러리가 관리하며 옵션으로 조정할 수 있습니다. 표의 아래 설명처럼 Effect 예제에는 요청을 취소하거나 오래된 응답을 무시하는 정리가 없고, 뒤에 나오는 setInterval 예제는 로컬의 count만 갱신합니다. 또한 브라우저의 SWR 캐시와 서버의 Data Cache는 서로 다른 저장소입니다.
서버 컴포넌트와 역할 나누기
Next.js App Router에서는 서버 컴포넌트와 클라이언트 컴포넌트가 데이터를 가져오는 역할을 나누어 설계할 수 있습니다. 페이지의 초기 로딩에 필요한 핵심 데이터는 SEO와 빠른 첫 화면을 위해 서버 컴포넌트에서 SSG, SSR, ISR로 가져옵니다. 페이지가 열린 뒤 사용자 상호작용에 따라 바뀌거나 브라우저 전용 기능이 필요한 데이터는 클라이언트 컴포넌트에서 가져옵니다.
다음은 이 역할 분담을 보여 주는 발췌입니다. fetch('...')는 실제 URL로 바꾸어 구현해야 하며, 브라우저에 공개해도 되는 필드만 골라 props로 넘깁니다. 먼저 부모인 서버 컴포넌트가 페이지의 전체 구조와 초기 데이터를 맡습니다.
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 접근을 맡습니다. 이 예제의 타이머는 마운트된 뒤 카운트만 올리며, 주석에 적힌 실시간 API는 호출하지 않습니다. 또한 useState(initialClientData)는 처음 렌더링할 때의 값만 초기값으로 쓰므로, 이후 부모가 다른 props를 넘겨도 dynamicData가 저절로 따라 바뀌지 않습니다.
"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>
);
}버튼을 누르면 dynamicData가 현재 시각이 들어간 문자열로 바뀌지만, 이 변경은 이 컴포넌트의 상태에만 반영되고 서버의 initialData에는 전달되지 않습니다. 정리 함수의 clearInterval은 컴포넌트가 사라질 때 타이머를 멈춰, 화면에 없는 컴포넌트가 계속 상태를 바꾸지 않게 합니다.
더 깊이: 서버에서 시작한 Promise를 클라이언트에서 use로 읽기
두 방식 사이에는 세 번째 방법도 있습니다. 서버 컴포넌트가 요청을 시작만 하고 await하지 않은 채 그 Promise를 클라이언트 컴포넌트의 prop으로 넘기면, 클라이언트 컴포넌트는 React의 use API로 그 값을 읽을 수 있습니다. use를 부르는 컴포넌트를 <Suspense>로 감싸 두면 Promise가 끝날 때까지 대체 화면이 보이고, 값이 준비되면 서버가 그 결과를 스트리밍해 대체 화면을 바꿉니다. useEffect 방식과 달리 요청이 하이드레이션을 기다리지 않고 서버에서 먼저 시작된다는 점이 다릅니다(React 문서의 use).
이 방법에는 지켜야 할 조건이 있습니다. 서버에서 클라이언트로 넘기는 Promise의 결과값은 다른 props처럼 직렬화할 수 있는 값이어야 합니다. use는 try-catch 안에서 부를 수 없으므로, Promise가 거부되었을 때의 화면은 가장 가까운 에러 바운더리에서 처리합니다. 또 렌더링 중에 새로 만든 Promise는 렌더링할 때마다 다시 만들어져 대체 화면이 되풀이될 수 있으므로, Promise는 서버 컴포넌트나 이벤트 처리 함수처럼 렌더링 밖에서 만들어 넘기는 편이 좋습니다.
정리
- 사용자 동작에 따른 갱신, 브라우저 전용 API, 초기 로딩 뒤의 작은 데이터, 클라이언트 페칭 라이브러리가 필요할 때 클라이언트 컴포넌트에서 데이터를 가져옵니다.
useEffect와useState로 요청할 때는 늦게 도착한 응답이 새 결과를 덮어쓰지 않도록 정리 함수에서ignore같은 표시를 세우며, 요청 자체를 중단하려면AbortController를 씁니다.- SWR은 URL 키로 결과를 캐시하고, 포커스나 재연결 같은 시점의 재검증을 라이브러리가 관리합니다.
- 브라우저의 SWR 캐시와 서버의 Data Cache는 서로 다른 저장소입니다.
- 초기 핵심 데이터는 서버 컴포넌트에서, 상호작용 뒤의 데이터는 클라이언트 컴포넌트에서 가져오고, 서버에서 넘기는 props에는 공개해도 되는 필드만 담습니다.
연습 문제
1. 같은 요청을 다른 순서로 끝내 보기
IgnoringLateTodoFetcher를 endpoint A로 시작한 뒤 B로 바꾸고, B의 응답이 먼저, A의 응답이 나중에 도착한다고 가정합니다. 각 요청의 성공, 실패, finally 가운데 어떤 것이 현재 화면을 바꿀 수 있을까요? 정리 함수가 네트워크 요청 자체를 취소했다고 말할 수 있는지도 답해 보세요.
해설 보기
현재 화면을 바꿀 수 있는 것은 B 요청의 성공, 실패, finally뿐입니다. endpoint가 바뀔 때 A의 Effect가 정리되어 A의 ignore가 true가 되므로, A가 늦게 성공하든 실패하든 setTodos, setError, setLoading을 부르지 않습니다. B 실행의 ignore는 따로 있으며 아직 false이므로 B의 결과가 반영됩니다.
정리 함수는 요청을 취소하지 않았습니다. A의 네트워크 요청은 끝까지 진행되고 결과만 무시되며, 실제로 요청을 중단하려면 AbortController의 signal을 fetch에 넘기고 정리 함수에서 abort()를 불러야 합니다. 개발 모드의 Strict Mode가 Effect를 한 번 더 실행하는 동작도 이 코드로 요청이 하나로 줄어든다는 뜻은 아닙니다.
2. 같은 키를 쓰는 두 컴포넌트
한 페이지에 useSWR('http://localhost:4000/posts?_limit=3', fetcher)를 쓰는 컴포넌트 두 개를 나란히 두었습니다. 같은 캐시 제공자 아래에서 두 컴포넌트는 데이터를 각자 따로 저장할까요? 앞의 ClientDataFetcher를 두 개 두었을 때와 비교해 보세요.
해설 보기
같은 캐시 제공자와 같은 키를 쓰므로 두 컴포넌트는 SWR 캐시의 같은 항목을 함께 씁니다. 짧은 시간 안에 같은 키로 들어온 요청은 SWR이 중복을 줄여 처리하며, 한쪽에서 받은 결과를 다른 쪽도 함께 표시합니다.
ClientDataFetcher는 결과를 각 컴포넌트의 useState에 저장하므로, 두 개를 두면 각자 요청을 보내고 결과도 따로 보관합니다. 다만 SWR 캐시는 브라우저 안의 저장소이므로, 다른 사용자나 서버의 Data Cache와는 공유되지 않습니다.
3. 초기값을 넘긴 뒤 props가 바뀔 때
ClientInteractiveComponent에 initialClientData="A"를 넘겨 렌더링한 뒤, 부모가 다시 렌더링되면서 initialClientData="B"를 넘겼습니다. 화면의 "클라이언트에서 업데이트되는 데이터"는 무엇을 보여 줄까요?
해설 보기
여전히 A를 보여 줍니다. useState(initialClientData)의 인자는 컴포넌트가 처음 마운트될 때만 초기값으로 쓰이고, 이후의 렌더링에서는 무시되기 때문입니다. "데이터 업데이트" 버튼을 눌렀다면 그 버튼이 넣은 시각 문자열이 그대로 남습니다.
부모의 값을 항상 따라가야 한다면 상태에 복사하지 않고 props를 그대로 표시하거나, 값이 바뀔 때 컴포넌트를 새로 만들도록 key에 그 값을 넘기는 방법을 고려합니다.
클라이언트 데이터 페칭과 SWR의 자세한 사용법은 Next.js 공식 문서의 Fetching Data와 SWR 문서에서 확인할 수 있습니다.
이것으로 6장을 마칩니다. 6장에서는 SSG, SSR, ISR로 서버에서 데이터를 가져오는 방법과 클라이언트 컴포넌트에서 데이터를 가져오는 방법을 비교했습니다. 다음 장의 서버 컴포넌트부터는 서버 컴포넌트와 클라이언트 컴포넌트가 어떻게 렌더링되고 서로 어떻게 조합되는지 자세히 알아봅니다.