# [Next.js + TS] 5. Server Component vs Client Component

[Next.js + TS] 5. Server Component vs Client Component

4편까지는 목록·상세·링크만 있었습니다. 전부 읽기에 가깝습니다.
이번 편은 App Router에서 제일 헷갈리는 부분,
서버 컴포넌트 / 클라이언트 컴포넌트 경계를 나눕니다.


이 편에서 할 일

  1. 기본값이 서버 컴포넌트인 이유 이해하기
  2. 'use client'가 필요한 순간 정리하기
  3. 좋아요 버튼을 작은 클라이언트 컴포넌트로 분리하기
  4. MemoCard(서버)가 그 버튼을 조립하는 패턴 익히기

목표는 “전부 'use client'로 만들기”가 아닙니다.
꼭 필요한 곳만 클라이언트로 내리는 감각을 잡는 겁니다.


한 줄 정리

  Server Component Client Component
기본? App Router 기본값 파일 맨 위에 'use client'
실행 위치 서버 브라우저(+필요 시 서버에서 사전 렌더)
hooks (useState 등) 불가 가능
클릭/입력 이벤트 불가 가능
데이터 fetch 자연스러움 가능하지만 목적이 다름

React(Vite/CRA)에 익숙하면 전부 클라이언트처럼 느껴집니다.
Next App Router는 기본을 서버에 두고, 인터랙션만 브라우저로 보냅니다.


왜 기본이 서버인가

우리가 만든 MemosPage, MemoCard에는 아직:

  • useState 없음
  • onClick 없음
  • 브라우저 API 없음

그래서 서버 컴포넌트로 충분합니다.

서버에서 HTML을 만들어 보내면:

  • 첫 화면을 빨리 그릴 수 있고
  • 불필요한 JS를 클라이언트로 덜 보내고
  • (나중에) DB/시크릿에 가까운 작업을 서버에 두기 쉽습니다

“React 컴포넌트 = 무조건 브라우저”가 아니라는 점이 핵심입니다.


언제 'use client'가 필요한가

아래가 필요하면 클라이언트 컴포넌트입니다.

  • useState, useEffect, useRef 같은 hooks
  • onClick, onChange 같은 이벤트
  • window, localStorage 같은 브라우저 API
  • 일부 브라우저 전용 라이브러리

반대로, 데이터 보여주고 링크로 이동만 하면 서버로 남겨 두세요.

잘못된 습관:

"use client"; // 습관적으로 파일 맨 위에 붙임

export default function MemosPage() {
  // 사실 hooks도 이벤트도 없음
}

페이지 전체를 클라이언트로 올리면,
서버 컴포넌트의 이점을 스스로 버리는 셈입니다.


실습 아이디어: 좋아요 버튼

메모 카드에 “좋아요”를 눌러 숫자가 바뀌게 해 봅시다.
이건 useState가 필요하므로 클라이언트가 맞습니다.

하지만 MemoCard 전체를 'use client'로 바꾸지 않습니다.

MemoCard          ← 서버 (제목, 내용, 링크)
└─ LikeButton     ← 클라이언트 (클릭, state)

큰 덩어리는 서버, 인터랙션 조각만 클라이언트.
이게 App Router에서 자주 쓰는 패턴입니다.


1) LikeButton 만들기

경로: components/LikeButton.tsx

"use client";

import { useState } from "react";

export default function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <button type="button" onClick={() => setLikes((prev) => prev + 1)}>
      좋아요 {likes}
    </button>
  );
}

반드시 파일 맨 위"use client" (또는 'use client')가 있어야 합니다.
이 한 줄이 “이 파일은 클라이언트 컴포넌트 경계”라는 표시입니다.


2) 서버 MemoCard에 조립

components/MemoCard.tsx'use client'붙이지 않습니다.

import Link from "next/link";
import type { Memo } from "@/types/memo";
import LikeButton from "@/components/LikeButton";

type MemoCardProps = {
  memo: Memo;
};

export default function MemoCard({ memo }: MemoCardProps) {
  return (
    <article>
      <h2>
        <Link href={`/memos/${memo.id}`}>{memo.title}</Link>
      </h2>
      <p>{memo.content}</p>
      <time dateTime={memo.createdAt}>{memo.createdAt}</time>
      <div>
        <LikeButton />
      </div>
    </article>
  );
}

서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 import하는 것은 OK입니다.

반대로, 클라이언트 컴포넌트가 서버 컴포넌트를 같은 방식으로 막 import해서 서버 전용 이점을 유지하긴 어렵습니다.
(필요하면 children으로 서버 결과를 넘기는 패턴을 씁니다. 지금은 몰라도 됩니다.)


동작 확인

  1. npm run dev
  2. /memos에서 좋아요 클릭
  3. 숫자만 올라가면 성공

페이지를 새로고침하면 숫자는 0으로 돌아갑니다.
지금은 메모리 state만 쓰기 때문입니다. (저장은 7편 쪽에서 다룹니다.)


경계를 가르는 기준 (실전용)

고민될 때 이 순서로 물어보세요.

  1. hooks / 이벤트가 필요한가?
    → 없으면 서버
  2. 파일 전체가 아니라 버튼/입력만 필요한가?
    → 작은 클라이언트 컴포넌트로 분리
  3. 데이터 읽기가 주목적인가?
    → 페이지/카드는 서버에 두는 쪽을 먼저 생각
좋은 방향
─────────
page (Server)
  └─ MemoCard (Server)
       └─ LikeButton (Client)

피하고 싶은 방향
─────────
page (Client)  ← 페이지 통째로
  └─ 모든 것

자주 하는 실수

1. "use client"를 함수 안에 씀
→ 반드시 파일 최상단 (import보다 위)

2. 서버 컴포넌트에서 useState 사용
→ 에러 납니다. state가 필요하면 그 부분만 클라이언트로 분리

3. Link가 있다고 무조건 클라이언트
next/link는 서버 컴포넌트에서도 사용 가능

4. 샘플이라서 전부 클라이언트로 통일
→ 나중에 습관이 됩니다. 처음부터 경계를 나누는 연습을 하세요


이 편에서 가져갈 것

  • App Router 기본은 Server Component다
  • hooks/이벤트가 필요할 때만 Client Component다
  • 페이지 전체가 아니라 인터랙션 조각'use client'를 붙인다
  • 서버 카드 + 클라이언트 버튼 조합이 기본 패턴이다

다음 편 예고

[Next.js + TS] 6. 데이터 가져오기

  • 서버 컴포넌트에서 fetch 하기
  • loading.tsx / error.tsx 맛보기
  • lib/memos.ts 하드코딩에서 한 걸음 나아가기

지금은 로컬 배열을 읽고 있습니다.
다음 편에서 데이터를 가져오는 방식을 정리합니다.


시리즈 목차

  1. 왜 Next.js + TypeScript인가
  2. 프로젝트 만들고 폴더 구조 이해하기
  3. TypeScript로 컴포넌트 타입 잡기
  4. 라우팅과 레이아웃
  5. Server Component vs Client Component ← 지금
  6. 데이터 가져오기
  7. 폼과 서버 액션 입문
  8. 정리 + 다음 학습 로드맵