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로 동작합니다. ClientWrapper의 children으로 전달될 뿐, ClientWrapper 파일에 import된 것이 아닙니다.
다음 챕터에서는
다음 챕터에서는 클라이언트에서 서버 함수를 직접 호출하는 방법, "use server"와 Server Actions를 살펴봅니다.