포스트

[이제와서 시작하는 Next.js 마스터하기 #3] Server Components와 데이터 페칭 완벽 가이드

[이제와서 시작하는 Next.js 마스터하기 #3] Server Components와 데이터 페칭 완벽 가이드

“서버에서 실행되는 React 컴포넌트?” - 네! Next.js 16의 Server Components는 게임 체인저입니다. 더 빠르고, 더 안전하고, 더 쉽게 데이터를 가져올 수 있습니다!

🎯 이 글에서 배울 내용

  • Server Components vs Client Components (제대로 이해하기)
  • 데이터 페칭 3가지 방법
  • 병렬 vs 순차 데이터 페칭
  • 데이터베이스 직접 접근하기
  • 실전 패턴과 Best Practices

예상 소요 시간: 45분 사전 지식: #1 처음 시작하는 Next.js, #2 App Router와 라우팅


🤔 Server Components가 뭔가요? (진짜 쉽게)

🍔 레스토랑으로 이해하는 Server vs Client Components

전통적인 React (Client Component만 사용):

1
2
3
4
1. 손님(브라우저)이 레스토랑 방문
2. 빈 접시와 레시피북(JavaScript)을 받음
3. 손님이 직접 요리를 만듦 (클라이언트에서 렌더링)
4. 시간 오래 걸림 + 손님 힘듦 😓

Next.js Server Components:

1
2
3
4
1. 손님(브라우저)이 레스토랑 방문
2. 주방(서버)에서 이미 완성된 요리를 받음
3. 바로 먹음! (서버에서 렌더링된 HTML)
4. 빠르고 편함! 😊

📊 한눈에 비교하기

특징 Server Component Client Component
실행 위치 서버 브라우저
번들 크기 0 (클라이언트에 안 보냄) JavaScript 파일에 포함
데이터 접근 데이터베이스 직접 접근 가능 API 호출 필요
상호작용 불가 (onClick 등 안 됨) 가능
React Hooks 사용 불가 useState, useEffect 등 사용
사용 시점 기본값 (99% 경우) 필요할 때만

🖥️ Server Components 깊이 이해하기

1. Server Component는 기본값

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/posts/page.js
// 별도 선언 없으면 자동으로 Server Component!

async function PostsPage() {
  // 서버에서 API로 데이터 가져오기
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts = await response.json();

  return (
    <div>
      <h1>게시물 목록</h1>
      {posts.slice(0, 10).map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

export default PostsPage;

왜 좋은가요?

빠른 초기 로딩

  • HTML이 서버에서 완성되어 옴
  • 사용자가 즉시 콘텐츠 확인 가능

작은 번들 크기

  • 컴포넌트 코드가 클라이언트에 안 보내짐
  • JavaScript 파일 크기 감소

데이터베이스 직접 접근

  • API 엔드포인트 만들 필요 없음
  • 보안 키를 서버에만 보관

SEO 친화적

  • 검색 엔진이 완성된 HTML을 봄

2. 언제 Server Component를 쓰나요?

✅ 이런 경우에 사용하세요:

  • 데이터를 서버에서 가져올 때
  • 서드파티 라이브러리 사용 (번들 크기 절약)
  • 환경 변수나 API 키 사용
  • 정적인 콘텐츠 표시

실전 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ✅ Server Component (추천)
async function ProductList() {
  // API로 상품 데이터 가져오기
  const response = await fetch('https://fakestoreapi.com/products');
  const products = await response.json();

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <div key={product.id} className="border p-4 rounded">
          <h3>{product.title}</h3>
          <p className="text-2xl font-bold">${product.price}</p>
          <p className="text-gray-600">{product.description}</p>
        </div>
      ))}
    </div>
  );
}

💻 Client Components 완벽 이해하기

1. Client Component는 필요할 때만

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/components/Counter.js
'use client';  // ← 이 한 줄이 Client Component로 만듦!

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  );
}

2. 언제 Client Component를 쓰나요?

✅ 이런 경우에만 사용하세요:

  • 사용자 상호작용 (onClick, onChange 등)
  • React Hooks 사용 (useState, useEffect 등)
  • 브라우저 API 사용 (localStorage, window 등)
  • 이벤트 리스너 필요

실전 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ✅ Client Component (필요함)
'use client';

import { useState, useEffect } from 'react';

export default function SearchBar() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  // 사용자 입력에 따라 검색
  useEffect(() => {
    if (query) {
      fetch(`/api/search?q=${query}`)
        .then(res => res.json())
        .then(setResults);
    }
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="검색..."
      />
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

3. 컴포넌트 합성 패턴

Server와 Client를 조합하는 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app/blog/page.js (Server Component)
import SearchBar from '@/components/SearchBar'; // Client
import PostList from '@/components/PostList';   // Server

async function BlogPage() {
  // 서버에서 데이터 가져오기
  const posts = await getPosts();

  return (
    <div>
      <h1>블로그</h1>

      {/* Client Component */}
      <SearchBar />

      {/* Server Component */}
      <PostList posts={posts} />
    </div>
  );
}

핵심 원칙:

  • Server Component 안에 Client Component 넣기: ✅ 가능
  • Client Component 안에 Server Component 넣기: ❌ 직접은 불가
  • 대신 Client Component에 Server Component를 children으로 전달: ✅ 가능!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✅ 올바른 패턴
// app/layout.js (Server)
import ClientWrapper from './ClientWrapper';
import ServerSidebar from './ServerSidebar';

export default function Layout({ children }) {
  return (
    <ClientWrapper>
      <ServerSidebar />  {/* children으로 전달 */}
      {children}
    </ClientWrapper>
  );
}

// ClientWrapper.js
'use client';

export default function ClientWrapper({ children }) {
  return (
    <div className="interactive-wrapper">
      {children}  {/* Server Component가 여기 들어감 */}
    </div>
  );
}

📥 데이터 페칭 방법

1. fetch API 사용 (추천)

Next.js는 fetch를 확장했습니다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/posts/page.js
async function PostsPage() {
  // 기본: 캐싱됨 + 자동 재검증
  const response = await fetch('https://api.example.com/posts');
  const posts = await response.json();

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

fetch 옵션 (Next.js 16):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 캐싱 비활성화 (항상 최신 데이터)
const res = await fetch('https://api.example.com/posts', {
  cache: 'no-store'
});

// 2. 60초마다 재검증
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 }
});

// 3. 태그 기반 재검증
const res = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] }
});

2. 여러 관련 데이터 한 번에 가져오기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// app/posts/page.js
async function PostsPage() {
  // 게시물 목록 가져오기
  const postsResponse = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts = await postsResponse.json();

  // 사용자 정보 가져오기
  const usersResponse = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await usersResponse.json();

  // 사용자 ID로 매핑
  const userMap = Object.fromEntries(users.map(u => [u.id, u]));

  return (
    <div>
      {posts.slice(0, 10).map(post => {
        const author = userMap[post.userId];
        return (
          <article key={post.id} className="mb-6 p-4 border rounded">
            <h2 className="text-xl font-bold">{post.title}</h2>
            <p className="text-gray-600 text-sm">
              작성자: {author?.name || '알 수 없음'}
            </p>
            <p className="mt-2">{post.body}</p>
          </article>
        );
      })}
    </div>
  );
}

3. 쿼리 파라미터로 필터링하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// app/users/[id]/posts/page.js
async function UserPostsPage({ params }) {
  const { id } = await params;

  // 특정 사용자의 게시물만 가져오기
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?userId=${id}`
  );
  const posts = await response.json();

  // 사용자 정보도 가져오기
  const userResponse = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  const user = await userResponse.json();

  return (
    <div>
      <h1>{user.name}의 게시물</h1>
      <p className="text-gray-600">{user.email}</p>

      <div className="mt-6 space-y-4">
        {posts.map(post => (
          <article key={post.id} className="p-4 border rounded">
            <h2 className="font-bold">{post.title}</h2>
            <p>{post.body}</p>
          </article>
        ))}
      </div>
    </div>
  );
}

⚡ 병렬 vs 순차 데이터 페칭

🐢 문제: 순차 페칭 (느림)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 나쁜 예: 순차적으로 데이터 가져오기
async function SlowPage() {
  // 1번 요청 (2초)
  const user = await fetch('/api/user').then(r => r.json());

  // 2번 요청 (2초) - 1번이 끝날 때까지 대기
  const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());

  // 총 4초 걸림! 😱
  return (
    <div>
      <h1>{user.name}</h1>
      <PostList posts={posts} />
    </div>
  );
}

🚀 해결: 병렬 페칭 (빠름)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 좋은 예: 병렬로 데이터 가져오기
async function FastPage() {
  // Promise.all로 동시에 실행
  const [user, posts] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);

  // 총 2초만 걸림! (가장 느린 요청 기준) 😊
  return (
    <div>
      <h1>{user.name}</h1>
      <PostList posts={posts} />
    </div>
  );
}

🎯 순차가 필요한 경우

두 번째 요청이 첫 번째 결과에 의존할 때:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function UserPostsPage({ params }) {
  const { userId } = await params;

  // 1. 사용자 정보 먼저 가져오기
  const user = await fetch(`/api/users/${userId}`).then(r => r.json());

  // 2. 그 사용자의 포스트 가져오기 (user.id 필요)
  const posts = await fetch(`/api/posts?authorId=${user.id}`).then(r => r.json());

  return (
    <div>
      <h1>{user.name}의 포스트</h1>
      {posts.map(post => <PostCard key={post.id} post={post} />)}
    </div>
  );
}

🎨 실전 패턴

1. 데이터 페칭 + 로딩 UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/posts/page.js
import { Suspense } from 'react';
import PostList from '@/components/PostList';
import PostListSkeleton from '@/components/PostListSkeleton';

export default function PostsPage() {
  return (
    <div>
      <h1>게시물</h1>

      <Suspense fallback={<PostListSkeleton />}>
        <PostList />
      </Suspense>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// components/PostList.js (Server Component)
async function PostList() {
  // 시간이 걸리는 데이터 페칭
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  );
}

export default PostList;

2. 여러 컴포넌트에서 병렬 페칭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// app/dashboard/page.js
import { Suspense } from 'react';
import UserInfo from '@/components/UserInfo';
import RecentPosts from '@/components/RecentPosts';
import Analytics from '@/components/Analytics';

export default function Dashboard() {
  return (
    <div className="grid grid-cols-2 gap-4">
      {/* 각 컴포넌트가 독립적으로 데이터 페칭 */}
      <Suspense fallback={<Skeleton />}>
        <UserInfo />  {/* 1초 걸림 */}
      </Suspense>

      <Suspense fallback={<Skeleton />}>
        <RecentPosts />  {/* 2초 걸림 */}
      </Suspense>

      <Suspense fallback={<Skeleton />}>
        <Analytics />  {/* 3초 걸림 */}
      </Suspense>
    </div>
  );
}

// 결과: 3초 후 모두 표시 (순차면 6초!)

3. 에러 처리와 함께

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/posts/[id]/page.js
import { notFound } from 'next/navigation';

async function PostDetailPage({ params }) {
  const { id } = await params;

  try {
    const post = await fetch(`https://api.example.com/posts/${id}`)
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      });

    return (
      <article>
        <h1>{post.title}</h1>
        <p>{post.content}</p>
      </article>
    );
  } catch (error) {
    // 404 페이지로 이동
    notFound();
  }
}

🎨 실전 예제: 완전한 블로그 페이지 만들기

이번엔 실제로 동작하는 블로그 페이지를 만들어봅시다!

완성된 코드 (복사해서 바로 사용 가능!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// app/blog/page.js
async function BlogPage() {
  // 1. 게시물과 사용자 정보를 병렬로 가져오기
  const [postsRes, usersRes] = await Promise.all([
    fetch('https://jsonplaceholder.typicode.com/posts'),
    fetch('https://jsonplaceholder.typicode.com/users')
  ]);

  const posts = await postsRes.json();
  const users = await usersRes.json();

  // 2. 사용자 정보를 ID로 빠르게 찾을 수 있게 Map 생성
  const userMap = new Map(users.map(u => [u.id, u]));

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-4xl font-bold mb-8">블로그</h1>

      <div className="space-y-6">
        {posts.slice(0, 10).map(post => {
          const author = userMap.get(post.userId);

          return (
            <article
              key={post.id}
              className="p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition"
            >
              {/* 제목 */}
              <h2 className="text-2xl font-semibold mb-2">
                {post.title}
              </h2>

              {/* 작성자 정보 */}
              <div className="flex items-center gap-2 text-sm text-gray-600 mb-4">
                <span>✍️ {author?.name}</span>
                <span></span>
                <span>📧 {author?.email}</span>
              </div>

              {/* 본문 */}
              <p className="text-gray-700 leading-relaxed">
                {post.body}
              </p>

              {/* 댓글 수 표시 (다음 섹션에서 구현) */}
              <div className="mt-4 text-sm text-gray-500">
                💬 댓글 보기
              </div>
            </article>
          );
        })}
      </div>
    </div>
  );
}

export default BlogPage;

코드 설명 (초보자용)

1. 병렬 데이터 페칭

1
const [postsRes, usersRes] = await Promise.all([...]);
  • 두 API를 동시에 호출 (순차보다 2배 빠름!)
  • Promise.all()은 모든 요청이 완료될 때까지 기다림

2. Map 자료구조 사용

1
const userMap = new Map(users.map(u => [u.id, u]));
  • 사용자를 ID로 빠르게 찾기 위한 자료구조
  • userMap.get(1) → O(1) 시간 복잡도 (매우 빠름!)

3. 깔끔한 UI 구성

1
className="p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition"
  • Tailwind CSS로 카드 스타일 적용
  • hover:shadow-lg로 마우스 올리면 그림자 커짐

🚀 실습: 직접 만들어보기

  1. app/blog/page.js 파일 생성
  2. 위 코드 복사해서 붙여넣기
  3. 개발 서버 실행: npm run dev
  4. http://localhost:3000/blog 접속!

축하합니다! 🎉 실제로 동작하는 블로그 페이지를 만들었습니다!

💡 추가 도전 과제

도전 1: 각 게시물에 댓글 수 표시하기

힌트: JSONPlaceholder의 /comments?postId=1 엔드포인트 사용

1
2
3
4
5
6
7
8
9
10
11
12
// 각 게시물의 댓글 수 가져오기
const commentsRes = await fetch('https://jsonplaceholder.typicode.com/comments');
const comments = await commentsRes.json();

// 게시물 ID별로 댓글 수 계산
const commentCounts = comments.reduce((acc, comment) => {
  acc[comment.postId] = (acc[comment.postId] || 0) + 1;
  return acc;
}, {});

// 표시할 때
<div>💬 댓글 {commentCounts[post.id] || 0}</div>
도전 2: 검색 기능 추가하기

힌트: Client Component로 검색창 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app/blog/components/SearchBar.js
'use client';

import { useState } from 'react';

export default function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');

  return (
    <input
      type="text"
      value={query}
      onChange={(e) => {
        setQuery(e.target.value);
        onSearch(e.target.value);
      }}
      placeholder="게시물 검색..."
      className="w-full p-3 border rounded-lg"
    />
  );
}

📝 데이터베이스 연동은 어떻게 하나요?

실제 프로젝트에서는 외부 API 대신 자체 데이터베이스를 사용하게 됩니다.

데이터베이스가 필요한 이유

외부 API의 한계:

  • ❌ 데이터를 수정할 수 없음
  • ❌ 원하는 구조로 데이터 저장 불가
  • ❌ 속도 제한 (Rate Limit)
  • ❌ 서비스 중단 위험

자체 데이터베이스의 장점:

  • ✅ 완전한 데이터 제어
  • ✅ 빠른 응답 속도
  • ✅ 원하는 구조로 설계 가능
  • ✅ 보안 강화

다음 단계

데이터베이스 연동은 #10 데이터베이스 연동 실전 포스트에서 자세히 다룰 예정입니다!

다룰 내용:

  • PostgreSQL 설치 및 설정
  • Prisma ORM 사용법
  • 데이터 모델링
  • CRUD 작업 구현
  • 실전 예제

지금은 fetch API로 충분합니다! 개념을 이해하는 게 먼저입니다. 😊


🔍 자주 묻는 질문 (FAQ)

Q1: Server Component에서 useState를 쓰고 싶어요

: Server Component에서는 useState를 쓸 수 없습니다!

해결책 1: Client Component로 만들기

1
2
3
4
5
6
7
8
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

해결책 2: 컴포넌트 분리

1
2
3
4
5
6
7
8
9
10
11
// Server Component (부모)
async function Page() {
  const data = await fetchData();

  return (
    <div>
      <ServerContent data={data} />
      <ClientCounter />  {/* Client Component */}
    </div>
  );
}
Q2: Client Component에서 API 데이터를 가져오려면?

: useEffect와 useState를 사용하세요!

Server Component에서는 간단

1
2
3
4
5
6
// Server Component (async 가능)
async function PostList() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  return <div>{/* posts 표시 */}</div>;
}

Client Component에서는 useEffect 필요

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// components/PostList.js (Client)
'use client';

import { useEffect, useState } from 'react';

export default function PostList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 컴포넌트가 마운트될 때 데이터 가져오기
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(r => r.json())
      .then(data => {
        setPosts(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Error:', error);
        setLoading(false);
      });
  }, []); // 빈 배열: 한 번만 실행

  if (loading) return <div>로딩 중...</div>;

  return (
    <div>
      {posts.slice(0, 5).map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
        </div>
      ))}
    </div>
  );
}

💡 Tip: 가능하면 Server Component를 사용하세요! 더 간단하고 빠릅니다.

Q3: async/await를 컴포넌트에서 쓸 수 있나요?

: Server Component에서만 가능합니다!

✅ Server Component (가능)

1
2
3
4
5
// 함수를 async로 선언
async function Page() {
  const data = await fetch('...').then(r => r.json());
  return <div>{data.title}</div>;
}

❌ Client Component (불가)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use client';

// async는 안 됨!
async function ClientPage() {  // 에러!
  const data = await fetch('...');
}

// 대신 useEffect + useState 사용
function ClientPage() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('...').then(r => r.json()).then(setData);
  }, []);
}
Q4: 데이터가 캐싱되는 건 좋은 건가요?

: 상황에 따라 다릅니다!

✅ 캐싱이 좋은 경우:

  • 자주 변하지 않는 데이터 (상품 목록, 블로그 글 등)
  • 서버 부하를 줄이고 싶을 때
  • 빠른 응답이 중요할 때
1
2
// 기본값: 캐싱됨
const data = await fetch('https://api.example.com/products');

❌ 캐싱을 피해야 하는 경우:

  • 실시간 데이터 (주식 가격, 채팅 등)
  • 사용자별 데이터 (프로필, 장바구니 등)
  • 항상 최신 데이터가 필요할 때
1
2
3
4
// 캐싱 비활성화
const data = await fetch('https://api.example.com/realtime', {
  cache: 'no-store'
});
Q5: 언제 Server, 언제 Client Component를 쓰나요?

: 이 플로우차트를 따라가세요!

1
2
3
4
5
6
7
8
9
10
11
데이터를 서버에서 가져오나요?
  ├─ Yes → Server Component
  └─ No ↓

사용자와 상호작용하나요? (클릭, 입력 등)
  ├─ Yes → Client Component ('use client')
  └─ No ↓

브라우저 API를 사용하나요? (localStorage 등)
  ├─ Yes → Client Component
  └─ No → Server Component (기본값)

경험 법칙:

  • 의심스러우면 Server Component 사용
  • 필요할 때만 ‘use client’ 추가
  • 대부분의 컴포넌트는 Server여야 함!

💡 초보자를 위한 Best Practices

1. Server Component를 기본으로

1
2
3
4
5
6
7
8
// ✅ 좋음: 대부분 Server Component
app/
├── page.js           # Server (기본)
├── blog/
   ├── page.js       # Server
   └── components/
       ├── PostList.js      # Server
       └── LikeButton.js    # Client (필요시만!)

2. 작은 Client Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✅ 좋음: Client 부분만 분리
// app/blog/[id]/page.js (Server)
async function PostPage({ params }) {
  const { id } = await params;
  const post = await getPost(id);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>

      {/* 작은 Client Component */}
      <LikeButton postId={id} />
    </article>
  );
}

// components/LikeButton.js (Client)
'use client';

export default function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
  // 작은 상호작용만 담당
}

3. 병렬 데이터 페칭 활용

1
2
3
4
5
6
7
8
9
10
// ✅ 좋음: Promise.all 사용
async function DashboardPage() {
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats()
  ]);

  // 동시에 실행되어 빠름!
}

4. 에러 처리는 필수

1
2
3
4
5
6
7
8
9
// ✅ 좋음: try-catch 추가
async function Page() {
  try {
    const data = await fetchData();
    return <div>{data.title}</div>;
  } catch (error) {
    return <div>데이터를 불러올 수 없습니다</div>;
  }
}

🎯 오늘 배운 내용 정리

✅ 핵심 개념

  1. Server Components
    • 서버에서 실행
    • 번들 크기 0
    • 데이터베이스 직접 접근 가능
  2. Client Components
    • 브라우저에서 실행
    • 상호작용 가능
    • useState, useEffect 사용
  3. 데이터 페칭
    • fetch API (Next.js가 확장함)
    • ORM (Prisma 등)
    • 외부 CMS/API
  4. 성능 최적화
    • 병렬 페칭 (Promise.all)
    • Suspense로 부분 로딩
    • 적절한 캐싱 전략

🚀 다음 단계

다음 포스트에서는:

  • 캐싱 전략 심화
  • revalidate와 태그
  • Next.js 16의 “use cache” directive

를 배워보겠습니다!


📚 시리즈 네비게이션

이전 글

다음 글


🔗 참고 자료


“Server Components는 처음엔 낯설지만, 익숙해지면 돌아갈 수 없습니다!” - React의 미래입니다! 🚀

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.