[Next.js + TS] 6. 데이터 가져오기
5편까지는
lib/memos.ts의 배열을 동기적으로 읽었습니다.
이번 편은 서버 컴포넌트에서 데이터를 기다리는(async) 방식으로 바꿉니다.
목표:async페이지 · 로딩 UI · 에러 UI.
이 편에서 할 일
- React의
useEffect+ fetch와 Next 서버 fetch의 차이 이해하기 getMemos/getMemoById를 async로 바꾸기- 목록·상세 페이지를
async로 고치기 loading.tsx/error.tsx맛보기
DB나 외부 API를 당장 붙이지 않아도 됩니다.
“가져오는 동안 기다린다” 흐름만 잡으면 됩니다.
왜 useEffect로 안 가져오나
Vite/CRA에서 흔했던 패턴:
"use client";
useEffect(() => {
fetch("/api/memos")
.then((res) => res.json())
.then(setMemos);
}, []);
문제는 초기 흐름이 이렇게 됩니다.
- 빈 화면(또는 로딩) 먼저 그림
- JS 실행
- 그제야 요청
- 다시 그림
App Router 서버 컴포넌트에서는:
export default async function MemosPage() {
const memos = await getMemos();
return ...
}
- 서버에서 데이터를 기다림
- 완성된 HTML을 보냄
목록처럼 첫 화면에 꼭 필요한 데이터는 서버에서 가져오는 쪽이 자연스럽습니다.
클릭 이후 추가 로드, 검색어 디바운스 같은 건 클라이언트 fetch가 맞는 경우도 많습니다.
1) 데이터 함수를 async로
lib/memos.ts를 아래처럼 바꿉니다.
import type { Memo } from "@/types/memo";
const memos: Memo[] = [
{
id: "1",
title: "Next.js 시작",
content: "App Router로 프로젝트 만들었다.",
createdAt: "2026-07-29",
},
{
id: "2",
title: "TypeScript props",
content: "MemoCard에 타입을 붙였다.",
createdAt: "2026-07-29",
},
];
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getMemos(): Promise<Memo[]> {
await delay(800); // 로딩 UI를 보기 위한 가짜 대기
return memos;
}
export async function getMemoById(id: string): Promise<Memo | undefined> {
await delay(500);
return memos.find((memo) => memo.id === id);
}
포인트:
export const memos직접 노출 대신 함수로만 가져오게 바꿈Promise<Memo[]>로 반환 타입을 명시delay는 실습용입니다. 실제 DB/API 자리라고 생각하면 됩니다
서버 컴포넌트에서는 자기 프로젝트의
/api를 다시 fetch하기보다,
이런 데이터 함수를 직접 await 하는 방식을 먼저 쓰는 게 좋습니다.
2) 목록 페이지를 async로
app/memos/page.tsx:
import MemoCard from "@/components/MemoCard";
import { getMemos } from "@/lib/memos";
export default async function MemosPage() {
const memos = await getMemos();
return (
<main>
<h1>메모 목록</h1>
<section>
{memos.map((memo) => (
<MemoCard key={memo.id} memo={memo} />
))}
</section>
</main>
);
}
바뀐 점:
async functionawait getMemos()- 더 이상
import { memos }가 아님
3) 상세도 동일하게
app/memos/[id]/page.tsx:
import { notFound } from "next/navigation";
import { getMemoById } from "@/lib/memos";
type MemoDetailPageProps = {
params: Promise<{ id: string }>;
};
export default async function MemoDetailPage({ params }: MemoDetailPageProps) {
const { id } = await params;
const memo = await getMemoById(id);
if (!memo) {
notFound();
}
return (
<main>
<h1>{memo.title}</h1>
<p>{memo.content}</p>
<time dateTime={memo.createdAt}>{memo.createdAt}</time>
</main>
);
}
getMemoById 앞에 await만 추가하면 됩니다.
(이미 async 페이지였다면 그대로 두면 됩니다.)
4) 로딩 UI: loading.tsx
데이터 기다리는 동안 보여줄 파일을 만듭니다.
경로: app/memos/loading.tsx
export default function MemosLoading() {
return (
<main>
<p>메모를 불러오는 중...</p>
</main>
);
}
/memos로 이동하면 잠깐 이 문구가 보였다가 목록이 뜹니다.
(delay(800) 덕분에 눈에 보입니다.)
loading.tsx는 같은 세그먼트(memos)의 page.tsx를 감싸는 즉시 로딩 UI입니다.
상세(memos/[id])에도 따로 두고 싶으면:
app/memos/[id]/loading.tsx
를 추가하면 됩니다.
5) 에러 UI: error.tsx
경로: app/memos/error.tsx
에러 경계는 클라이언트 컴포넌트여야 합니다.
"use client";
type MemosErrorProps = {
error: Error;
reset: () => void;
};
export default function MemosError({ error, reset }: MemosErrorProps) {
return (
<main>
<h1>메모를 불러오지 못했습니다</h1>
<p>{error.message}</p>
<button type="button" onClick={() => reset()}>
다시 시도
</button>
</main>
);
}
일부러 에러를 내려면 getMemos 안에 잠깐 넣어 볼 수 있습니다.
export async function getMemos(): Promise<Memo[]> {
await delay(800);
throw new Error("샘플 에러입니다");
// return memos;
}
확인 후 꼭 throw는 지우세요.
외부 API를 fetch할 때는
진짜 HTTP API라면 서버 컴포넌트에서 이렇게 씁니다.
export default async function MemosPage() {
const res = await fetch("https://api.example.com/memos", {
next: { revalidate: 60 }, // 60초 캐시 예시
});
if (!res.ok) {
throw new Error("메모를 가져오지 못했습니다");
}
const memos: Memo[] = await res.json();
// ...
}
지금 시리즈는 로컬 배열 + delay로 같은 async 흐름을 연습합니다.
나중에 API URL만 바꿔 끼우면 됩니다.
자주 하는 실수
1. 서버 페이지인데 useEffect로 다시 fetch
→ 초기 목록이면 async + await를 먼저
2. await 없이 Promise를 map에 넣음
→ const memos = await getMemos() 필수
3. loading.tsx에 'use client' + useState로 직접 로딩 관리
→ 세그먼트 loading.tsx만으로도 충분한 경우가 많음
4. error.tsx를 서버 컴포넌트로 작성
→ error.tsx는 "use client" 필요
이 편에서 가져갈 것
- 첫 화면 데이터는 서버 컴포넌트에서
await하는 편이 자연스럽다 - 데이터 함수는
async+Promise<타입>으로 계약한다 loading.tsx는 기다리는 동안의 UIerror.tsx는 실패한 동안의 UI (클라이언트)- 지금은 가짜 delay, 나중엔 DB/API로 교체하면 된다
다음 편 예고
[Next.js + TS] 7. 폼과 서버 액션 입문
- 메모 추가 폼 만들기
- Server Action으로 데이터 넣기
- 목록이 갱신되는 흐름 맛보기
읽기만 하던 앱에 쓰기를 붙입니다.
시리즈 목차
- 왜 Next.js + TypeScript인가
- 프로젝트 만들고 폴더 구조 이해하기
- TypeScript로 컴포넌트 타입 잡기
- 라우팅과 레이아웃
- Server Component vs Client Component
- 데이터 가져오기 ← 지금
- 폼과 서버 액션 입문
- 정리 + 다음 학습 로드맵
'frontend > NextJs' 카테고리의 다른 글
| # [Next.js + TS] 8. 정리 + 다음 학습 로드맵 (0) | 2026.07.29 |
|---|---|
| # [Next.js + TS] 7. 폼과 서버 액션 입문 (0) | 2026.07.29 |
| # [Next.js + TS] 5. Server Component vs Client Component (0) | 2026.07.29 |
| # [Next.js + TS] 4. 라우팅과 레이아웃 (0) | 2026.07.29 |
| # [Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기 (0) | 2026.07.29 |
