이 글에서는 Next.js로 애플리케이션을 빌드하기 위해 필요한 리액트의 새로운 기능들 (Server Components)를 알아보고 언제 사용해야되는지 알아본다.
Server Components
서버와 클라이언트 컴포넌트를 사용하면 클라이언트의 상호작용과 서버 랜더링의 향상된 성능을 결합하여 서버와 클라이언트를 아우르는 앱을 개발할 수 있다.
React서버 컴포넌트는 최근 React 18 에 추가된 기능으로 서버에서 렌더링 되는 컴포넌트이다.
이제, 컴포넌트의 목적에 따라 렌더링할 위치를 유연하게 선택할 수 있다.
대부분의 컴포넌트를 서버컴포넌트로 하고, 작은 상호작용이 필요한 UI는 클라이언트 컴포넌트로 할 수 있다.
Why Server Component?
서버 컴포넌트를 사용하면
- 개발자가 서버 인프라를 더 잘활용할 수 있다.
- 클라이언트의 javascript 번들 크기가 줄어든다.
- React의 강력한 성능과 유연성, UI 템플릿을 사용할 수 있다.
- 초기 로딩이 빠르다
- 페이지를 비동기적으로 로딩이 가능하다.
Client Components
- 클라이언트 컴포넌트는 사용자와 상호작용이 가능하다.
- Next.js의 클라이언트 컴포넌트도 서버에서 미리 랜더링된다.
- 이전 page라우터 방식의 컴포넌트와 같은 작동방식이다.
- 파일 상단에 "use client"를 명시하여 클라이언트 컴포넌트로 사용할 수 있다.
언제 서버 / 클라이언트 컴포넌트를 사용해야하는가?
패턴
가능하면, 클라이언트 컴포넌트를 트리의 리프로 이동하는것이 좋다.
// SearchBar is a Client Component import SearchBar from './searchbar' // Logo is a Server Component import Logo from './logo' // Layout is a Server Component by default export default function Layout({ children }: { children: React.ReactNode }) { return ( <> <nav> <Logo /> <SearchBar /> </nav> <main>{children}</main> </> ) }
랜더링 과정
- 서버에서 React는 모든 서버 컴포넌트를 랜더링 한다. 중간에 포함된 서버 컴포넌트도 포함한다.
- 클라이언트는 클라이언트 컴포넌트를 랜더링하고 서버 컴포넌트의 랜더링 결과를 삽입하여 병합한다.
- 서버 컴포넌트가 중간에 포함된 경우에도 잘 배치된다. 클라이언트 컴포넌트도 서버에서 미리 랜더링 되긴 한다.
Client Component안에 Server Component
지원되지 않는 패턴
'use client' // This pattern will **not** work! // You cannot import a Server Component into a Client Component. import ExampleServerComponent from './example-server-component' export default function ExampleClientComponent({ children, }: { children: React.ReactNode }) { const [count, setCount] = useState(0) return ( <> <button onClick={() => setCount(count + 1)}>{count}</button> <ExampleServerComponent /> </> ) }
추천하는 패턴
서버 컴포넌트를 클라이언트 컴포넌트에 props 로 전달한다.
'use client' import { useState } from 'react' export default function ExampleClientComponent({ children, }: { children: React.ReactNode }) { const [count, setCount] = useState(0) return ( <> <button onClick={() => setCount(count + 1)}>{count}</button> {children} </> ) }
props로 전달받으면, 클라이언트 컴포넌트는 이 서버 컴포넌트가 무엇인지 사전에 알 수 없다.
// This pattern works: // You can pass a Server Component as a child or prop of a // Client Component. import ExampleClientComponent from './example-client-component' import ExampleServerComponent from './example-server-component' // Pages in Next.js are Server Components by default export default function Page() { return ( <ExampleClientComponent> <ExampleServerComponent /> </ExampleClientComponent> ) }
꼭 children으로 전달하지 않아도 된다. 모든 prop으로 가능하다.
서버 컴포넌트에서 클라이언트 컴포넌트로 prop 전달
서버 컴포넌트에서 클라이언트 컴포넌트로 전달되는 prop은 직렬화가 가능해야한다.
함수, 날짜와 같은 값은 직접 전달할 수 없다.
서버 전용 코드를 클라이언트 컴포넌트에서 제외하기
자바스크립트 모듈은 서버와 컴포넌트 모두 공유가 되기 때문에 서버에서만 실행되야하는 코드가 클라이언트에 침투할 수 도 있다.
export async function getData() { const res = await fetch('<https://external-service.com/data>', { headers: { authorization: process.env.API_KEY, }, }) return res.json() }
위 코드에서 API_KEY는 NEXT_PUBLIC이 붙지 않은 환경변수이기 때문에 클라이언트에서 작동하지 않는다.
이 함수는 서버에서만 실행되도록 의도적으로 작성되었다.
server-only 패키지
위 예시처럼 서버에서만 실행 가능하도록 하기 위해 server-only 패키지를 사용할 수 있다.
import 'server-only' export async function getData() { const res = await fetch('<https://external-service.com/data>', { headers: { authorization: process.env.API_KEY, }, }) return res.json() }
이제 getData()를 import하는 모든 클라이언트 컴포넌트는 빌드 타임 오류를 받게 된다.
Data Fetching
클라이언트 컴포넌트에서 데이터를 가져올 수 있지만, 왠만하면 서버 컴포넌트에서 데이터를 가져오는 것이 좋다. 성능과 사용자 경험이 향상된다.
Third-party packages
많은 패키지들이 "use client"를 추가하기 시작했지만, 그렇지 않은것이 많다.
'use client' import { Carousel } from 'acme-carousel' export default Carousel
이런식으로 새로운 파일을 만들어 서버컴포넌트에서 컴포넌트를 바로 import 하자
import Carousel from './carousel' export default function Page() { return ( <div> <p>View pictures</p> {/* Works, since Carousel is a Client Component */} <Carousel /> </div> ) }
Context
컨텍스트는 서버 컴포넌트에서 사용이 불가능하다.
클라이언트 컴포넌트에서는 완벽하게 지원된다.
이 또한 새로운 파일을 만들어 해결하자
'use client' import { createContext } from 'react' export const ThemeContext = createContext({}) export default function ThemeProvider({ children }) { return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider> }
그리고 왠만해서는 ThemeContext를 html 전체가 아닌 body태그 안 children만 감싸도록 하자.
Server Component 데이터 공유
db를 사용하여 공유할 수 있다.
// utils/database.ts export const db = new DatabaseConnection()
// app/users/layout.tsx import { db } from '@utils/database' export async function UsersLayout() { let users = await db.query() // ... }
// app/users/[id]/page.tsx import { db } from '@utils/database' export async function DashboardPage() { let user = await db.query() // ... }
위 예시에서는 레이아웃과 페이지 모두에서 @utils/database를 이용하여 쿼리를 수행하고, 이러한 패턴을 글로벌 싱글톤이라고 한다.
Server Component 사이의 데이터 fetching 공유
데이터를 사용하는 컴포넌트와 데이터를 fetching하는 컴포넌트를 나란히 배치하는것이 좋다.
서버컴포넌트에서 중복된 fetching은 cache를 활용하여 자동으로 제거된다.