iBetter Books
수정

Ch 03. 경계를 넘는 방법 — "use client"

주방에서 홀로 나오는 순간

모든 컴포넌트는 기본적으로 주방(서버)에 있습니다. 그런데 어떤 컴포넌트는 홀(클라이언트)에 있어야 합니다. 사용자와 직접 상호작용해야 하니까요.

"use client"는 이 컴포넌트를 홀로 보내겠다는 선언입니다. 파일 맨 위에 이 한 줄을 추가하면, 그 파일의 모든 컴포넌트가 Client Component가 됩니다.

언제 Client Component가 필요한가

세 가지 경우가 대표적입니다.

첫째, 리액트 훅을 사용할 때. useState, useEffect, useRef, useContext 등은 클라이언트에서만 사용할 수 있습니다.

둘째, 이벤트 핸들러를 연결할 때. onClick, onChange, onSubmit 등의 이벤트는 브라우저에서 발생합니다.

셋째, 브라우저 전용 API를 사용할 때. window, document, localStorage, navigator 같은 객체는 서버에 존재하지 않습니다.

// 파일: app/components/SearchBox.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function SearchBox() {
  const [query, setQuery] = useState('');
  const router = useRouter();

  function handleSearch(e: React.FormEvent) {
    e.preventDefault();
    if (query.trim()) {
      router.push(`/search?q=${encodeURIComponent(query)}`);
    }
  }

  return (
    <form onSubmit={handleSearch}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="검색어를 입력하세요"
      />
      <button type="submit">검색</button>
    </form>
  );
}
```text

### Server Component 안에 Client Component 포함하기

Server Component가 Client Component를 렌더링할 수 있습니다. 이것이 가장 일반적인 패턴입니다.

```tsx
// 파일: app/posts/page.tsx
// Server Component
import SearchBox from '@/components/SearchBox'; // Client Component

export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts').then((r) =>
    r.json()
  );

  return (
    <div>
      <SearchBox /> {/* Client Component를 포함 */}
      <ul>
        {posts.map((post: { id: number; title: string }) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}
```text

Server Component(`PostsPage`)가 데이터를 가져오고, Client Component(`SearchBox`)가 상호작용을 담당합니다. 두 가지 장점을 동시에 얻습니다.

### Client Component 안에 Server Component를 직접 import하면 안 되는 이유

반대 방향은 주의가 필요합니다. Client Component 안에서 Server Component를 직접 import하면 안 됩니다.

```tsx
// 파일: app/components/ClientWrapper.tsx
'use client';

// 이렇게 하면 안 됩니다!
import ServerComponent from './ServerComponent';

export default function ClientWrapper() {
  return (
    <div>
      <ServerComponent /> {/* ❌ Client Component 안에서 Server Component를 직접 import */}
    </div>
  );
}
```text

Client Component 파일을 import하면, 그 파일도 클라이언트 번들에 포함됩니다. Server Component를 직접 import하면, 해당 파일이 클라이언트화되어 서버 전용 기능(데이터베이스 접근 등)을 사용할 수 없게 됩니다.

대신 `children` prop을 통해 Server Component를 전달하는 패턴을 사용합니다.

```tsx
// 파일: app/components/ClientWrapper.tsx
'use client';

export default function ClientWrapper({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div className="wrapper">{children}</div>;
}
```text

```tsx
// 파일: app/page.tsx
// Server Component
import ClientWrapper from '@/components/ClientWrapper';
import ServerContent from '@/components/ServerContent'; // Server Component

export default function Page() {
  return (
    <ClientWrapper>
      <ServerContent /> {/* ✅ children으로 전달 */}
    </ClientWrapper>
  );
}

이 패턴에서 ServerContent는 여전히 Server Component로 동작합니다. ClientWrapperchildren으로 전달될 뿐, ClientWrapper 파일에 import된 것이 아닙니다.

다음 챕터에서는

다음 챕터에서는 클라이언트에서 서버 함수를 직접 호출하는 방법, "use server"와 Server Actions를 살펴봅니다.