icon안동민 개발노트

서버와 클라이언트 컴포넌트 조합하기


 서버와 클라이언트 컴포넌트를 효과적으로 조합하는 것은 Next.js 애플리케이션의 성능과 사용자 경험을 최적화하는 핵심입니다.

 이 절에서는 두 유형의 컴포넌트를 조합하는 전략, 데이터 흐름 관리, 그리고 성능 최적화 기법에 대해 알아보겠습니다.

컴포넌트 트리에서의 배치 전략

 1. 서버 컴포넌트를 상위에, 클라이언트 컴포넌트를 하위에 배치

  • 이 전략은 가능한 많은 로직과 데이터 페칭을 서버에서 처리하고, 필요한 경우에만 클라이언트 컴포넌트를 사용합니다.
app/page.js (Server Component)
import ClientInteractiveComponent from './ClientInteractiveComponent'
 
async function getData() {
  // 서버에서 데이터 페칭
}
 
export default async function Page() {
  const data = await getData()
  return (
    <div>
      <h1>Server Rendered Content</h1>
      <ClientInteractiveComponent initialData={data} />
    </div>
  )
}

 2. 클라이언트 컴포넌트 내에서 서버 컴포넌트 사용

  • 클라이언트 컴포넌트 내에서 서버 컴포넌트를 props로 전달받아 사용할 수 있습니다.
app/ServerContent.js (Server Component)
export default async function ServerContent() {
  const data = await fetchData()
  return <div>{data}</div>
}
app/ClientWrapper.js (Client Component)
'use client'
export default function ClientWrapper({ children }) {
  return (
    <div onClick={() => console.log('Clicked')}>
      {children}
    </div>
  )
}
app/page.js
import ClientWrapper from './ClientWrapper'
import ServerContent from './ServerContent'
 
export default function Page() {
  return (
    <ClientWrapper>
      <ServerContent />
    </ClientWrapper>
  )
}

데이터 흐름 관리

 1. Props를 통한 데이터 전달

  • 서버 컴포넌트에서 클라이언트 컴포넌트로 데이터를 전달할 때 props를 사용합니다.

 2. Context를 이용한 전역 상태 관리

  • 클라이언트 컴포넌트에서 Context를 사용하여 전역 상태를 관리할 수 있습니다.
app/ThemeProvider.js (Client Component)
'use client'
import { createContext, useContext, useState } from 'react'
 
const ThemeContext = createContext()
 
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}
 
export function useTheme() {
  return useContext(ThemeContext)
}
app/layout.js
import { ThemeProvider } from './ThemeProvider'
 
export default function Layout({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>
}

성능 최적화 기법

 1. 코드 분할

  • 클라이언트 컴포넌트를 동적으로 임포트하여 초기 번들 크기를 줄입니다.
import dynamic from 'next/dynamic'
 
const DynamicClientComponent = dynamic(() => import('./ClientComponent'), {
  loading: () => <p>Loading...</p>,
})

 2. 지연 로딩

  • 필요한 시점에 클라이언트 컴포넌트를 로드합니다.

 3. 서버에서의 데이터 프리페칭

  • 서버 컴포넌트에서 데이터를 미리 가져와 클라이언트 컴포넌트에 전달합니다.

5장 페이지 및 레이아웃 컴포넌트와 연관성

 7장의 서버와 클라이언트 컴포넌트 조합 전략은 5장에서 다룬 페이지 및 레이아웃 컴포넌트 구조와 밀접하게 연관됩니다.

 레이아웃 컴포넌트는 주로 서버 컴포넌트로 구현되어 전체적인 페이지 구조를 정의하고, 그 안에 클라이언트 컴포넌트를 배치하여 동적인 상호작용을 구현할 수 있습니다.

실습 : 하이브리드 컴포넌트 구조 구현

 다음 요구사항을 만족하는 하이브리드 컴포넌트 구조를 구현해보세요.

  1. 서버 컴포넌트에서 사용자 목록을 가져옵니다.
  2. 클라이언트 컴포넌트에서 사용자 목록을 표시하고 검색 기능을 구현합니다.
  3. 검색 결과에 따라 사용자 목록을 필터링합니다.

 구현 예시

app/users/page.js (Server Component)
import UserSearch from './UserSearch'
 
async function getUsers() {
  // 서버에서 사용자 데이터 가져오기
  const res = await fetch('https://api.example.com/users')
  return res.json()
}
 
export default async function UsersPage() {
  const users = await getUsers()
  return <UserSearch initialUsers={users} />
}
app/users/UserSearch.js (Client Component)
'use client'
import { useState } from 'react'
 
export default function UserSearch({ initialUsers }) {
  const [searchTerm, setSearchTerm] = useState('')
  const [users, setUsers] = useState(initialUsers)
 
  const handleSearch = (e) => {
    const term = e.target.value.toLowerCase()
    setSearchTerm(term)
    const filteredUsers = initialUsers.filter(user => 
      user.name.toLowerCase().includes(term)
    )
    setUsers(filteredUsers)
  }
 
  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        value={searchTerm}
        onChange={handleSearch}
      />
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

 이 실습을 통해 서버 컴포넌트에서 데이터를 가져오고, 클라이언트 컴포넌트에서 상호작용을 처리하는 하이브리드 구조를 경험할 수 있습니다.

 이 패턴은 서버의 빠른 데이터 페칭과 클라이언트의 동적 UI를 결합하여 최적의 사용자 경험을 제공합니다.

 서버와 클라이언트 컴포넌트를 효과적으로 조합하는 것은 Next.js 애플리케이션의 성능과 사용자 경험을 최적화하는 핵심입니다.

 서버에서의 빠른 초기 로드와 데이터 페칭, 클라이언트에서의 부드러운 상호작용을 적절히 조화시켜 강력하고 효율적인 웹 애플리케이션을 구축할 수 있습니다.