codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›Next.js 16 으로 첫 풀스택 앱 만들기›2단계

2단계

2단계 — Server vs Client 컴포넌트

0회 조회

2단계 — Server vs Client 컴포넌트

App Router 의 핵심 개념. 기본이 Server Component, 필요할 때만 'use client'.

1. 왜 분리?

  • 많은 코드는 브라우저에 내려갈 필요가 없음
  • 서버에서 DB · API 호출 후 HTML 만 전송
  • 브라우저 번들 크기 감소 · 초기 로딩 빠름
  • 민감 정보 (API 키) 서버에 머무름

2. Server Component (기본)

// src/app/posts/page.tsx
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 60 },   // 60초 캐시
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();           // 서버에서 실행
  return (
    <ul className="space-y-2 p-6">
      {posts.map((p: any) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

특징:

  • 컴포넌트가 async 가능
  • fetch 결과 자동 캐시 · 재검증 (revalidate)
  • useState · onClick 사용 불가 (서버에 이벤트 핸들러 없음)
  • document · window 접근 불가

3. Client Component

// src/app/counter.tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [n, setN] = useState(0);
  return (
    <button
      onClick={() => setN(n + 1)}
      className="rounded bg-blue-600 px-4 py-2 text-white"
    >
      {n}
    </button>
  );
}

파일 최상단 'use client' 지시어. 이후 이 파일 및 import 한 컴포넌트 모두 클라이언트 모드.

4. 언제 'use client'?

  • useState · useEffect · useRef
  • onClick · onChange 등 이벤트 핸들러
  • window · localStorage · IntersectionObserver
  • 브라우저 전용 라이브러리 (Mapbox · chart.js 등)

5. 두 컴포넌트 함께

// src/app/page.tsx  (Server)
import Counter from "./counter";

async function getUser() {
  return { name: "홍길동" };
}

export default async function Home() {
  const user = await getUser();
  return (
    <main className="p-6 space-y-4">
      <h1 className="text-2xl">안녕하세요, {user.name} 님</h1>
      <Counter />  {/* Client Component */}
    </main>
  );
}

Server 가 Client 를 import · 조합 가능. 반대 (Client → Server) 는 직접 불가.

6. 데이터 흐름 — props

// Server
<Counter initial={42} />
// Client
"use client";
export default function Counter({ initial }: { initial: number }) {
  const [n, setN] = useState(initial);
  ...
}

Server 가 가져온 데이터를 Client 에 props 로 전달. 단 직렬화 가능한 값만 (함수 · class 인스턴스 불가).

7. 경계를 얕게 유지

나쁨:

"use client";          // 루트에 선언
// 전체 앱이 클라이언트 모드

좋음:

// app/layout.tsx — Server (기본)
// app/components/ThemeToggle.tsx — Client (이 컴포넌트만)

인터랙션 부분만 Client. 나머지는 Server.

8. cookies() · headers()

Server Component · Server Action 에서만 사용 가능.

import { cookies, headers } from "next/headers";

export default async function Page() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value ?? "light";
  return <div>theme: {theme}</div>;
}

Next 15+ 부터 cookies() · headers() 가 Promise 반환 (async).

9. Server Action — 폼 서브밋

클라이언트에서 서버 함수 호출. API 라우트 작성 없이.

// src/app/new-post/page.tsx
async function createPost(formData: FormData) {
  "use server";
  const title = formData.get("title") as string;
  // DB 저장
  console.log("saving:", title);
}

export default function NewPostPage() {
  return (
    <form action={createPost} className="p-6 space-y-2">
      <input name="title" className="border p-2" />
      <button type="submit" className="bg-blue-600 text-white px-4 py-2">
        저장
      </button>
    </form>
  );
}

form 이 제출되면 createPost 가 서버에서 실행.

10. Streaming + Suspense

비동기 Server Component 를 <Suspense> 로 감싸 부분 로딩.

import { Suspense } from "react";
import PostList from "./post-list";   // async Server Component

export default function Page() {
  return (
    <main>
      <h1>게시글</h1>
      <Suspense fallback={<p>불러오는 중...</p>}>
        <PostList />
      </Suspense>
    </main>
  );
}

전체 페이지 완성을 기다리지 않고 준비된 부분부터 표시.

11. 캐시 · revalidate

// 1시간 캐시
fetch(url, { next: { revalidate: 3600 } });

// 캐시 안 함
fetch(url, { cache: "no-store" });

// 특정 태그로 무효화 가능
fetch(url, { next: { tags: ["posts"] } });

mutation 후 revalidateTag("posts") 로 즉시 무효화.

12. force-dynamic 회피

export const dynamic = "force-dynamic";

매 요청 재렌더. DB 가 자주 변하고 실시간성이 중요한 페이지에 사용. 남발하면 성능 손해.

13. 자주 걸리는 자리

  • 'use client' 없이 useState — 빌드 에러
  • Server 에서 window — 런타임 에러
  • 직렬화 불가 props — 함수 · Date 객체 전달 시 경고
  • client 에서 DB 직접 — Server Action 또는 API route 경유
  • cookies() 를 Client 에서 — Server 전용

하고픈 말

Server 가 기본 · Client 는 옵트인. 이 한 문장이 App Router 전체를 꿰뚫습니다. 처음에는 모든 걸 Client 로 할 수도 있지만 점점 Server 비중을 올리세요.

Next

  • 03-api-drizzle

Next.js — Server and Client Components · React 19 Server Components 를 참고합니다.

← 1단계

1단계 — 프로젝트 셋업

3단계 →

3단계 — API Route + Drizzle ORM