React Essentials
Next.js로 애플리케이션을 구축하려면 Server Components와 같은 React의 최신 기능에 익숙해지는 것이 도움이 됩니다. 이 페이지에서는 서버 컴포넌트와 클라이언트 컴포넌트의 차이점, 사용 시기 및 권장 패턴을 살펴봅니다.
서버 컴포넌트
서버 및 클라이언트 컴포넌트를 사용하면 개발자가 서버와 클라이언트에 걸쳐 있는 응용 프로그램을 구축하여 클라이언트 측 앱의 풍부한 상호 작용과 기존 서버 렌더링의 향상된 성능을 결합할 수 있습니다.
서버 컴포넌트에 대한 생각
React가 UI 구축에 대한 생각 방식을 어떻게 바꾸었는지와 유사하게 React Server Components는 서버 와 클라이언트를 활용하는 하이브리드 애플리케이션 구축을 위한 새로운 정신 모델을 도입합니다 .
React는 전체 애플리케이션 클라이언트 측(예: 단일 페이지 애플리케이션의 경우)을 렌더링하는 대신 이제 용도에 따라 컴포넌트를 렌더링할 위치를 선택할 수 있는 유연성을 제공합니다.
예를 들어 애플리케이션의 페이지를 고려하십시오.
페이지를 더 작은 컴포넌트로 분할하는 것은 일반적으로 좋은 개발 관행입니다. 이렇게 분할하면 각 컴포넌트가 재사용 가능하고 유지보수가 쉬워집니다. 또한, 컴포넌트를 서버에서 렌더링할 수 있으므로 초기 로딩 속도를 향상시킬 수 있습니다.
더 작은 컴포넌트 중 대부분이 비대화형(non-interactive)이라면, 이러한 컴포넌트는 서버 컴포넌트로 분류할 수 있습니다. 서버 컴포넌트는 서버에서 렌더링되며, 클라이언트에 전달되기 전에 초기 렌더링이 완료됩니다. 이렇게 하면 사용자는 페이지를 더 빠르게 로드할 수 있고, 서버 측에서 초기 데이터를 미리 로드하여 페이지의 사용자 경험을 개선할 수 있습니다.
더 작은 대화형 UI 컴포넌트는 클라이언트 컴포넌트로 분류될 수 있습니다. 이러한 컴포넌트는 클라이언트 측에서 동적으로 상호작용하고 사용자와의 상호작용에 응답합니다. 클라이언트 컴포넌트는 주로 JavaScript를 사용하여 렌더링되며, 사용자와의 상호작용에 따라 데이터를 업데이트하고 화면을 다시 렌더링합니다.
위의 접근 방식은 Next.js의 서버 우선 접근 방식과 일치합니다. Next.js는 서버 사이드 렌더링(SSR)을 지원하며, 페이지를 서버에서 사전 렌더링하여 초기 로딩 속도를 개선할 수 있습니다. 이렇게 함으로써 사용자는 빠르게 콘텐츠를 볼 수 있고, 클라이언트 측에서 상호작용할 수 있는 컴포넌트를 이용하여 부드러운 사용자 경험을 제공할 수 있습니다.
서버 컴포넌트를 사용하는 이유
서버 컴포넌트를 사용하면 개발자들은 서버 인프라를 더 효과적으로 활용할 수 있습니다. 예를 들어, 데이터 가져오기를 서버로 옮기고, 클라이언트 JavaScript 번들 크기에 영향을 주는 대형 종속성을 서버에 유지함으로써 성능을 개선할 수 있습니다. 서버 컴포넌트를 사용하면 React 애플리케이션 작성이 PHP나 Ruby on Rails와 유사한 느낌을 줄 수 있으며, React와 컴포넌트 모델을 활용하여 UI를 템플릿화할 수 있습니다.
서버 컴포넌트를 사용하면 초기 페이지 로드가 더 빠르고, 클라이언트 측 JavaScript 번들 크기가 줄어듭니다. 기본 클라이언트 측 런타임은 캐시 가능하며 크기가 예측 가능하며, 애플리케이션이 성장함에 따라 증가하지 않습니다. 클라이언트 컴포넌트를 통해 애플리케이션에서 클라이언트 측 상호작용이 사용될 때만 추가 JavaScript가 추가됩니다.
Next.js에서 라우트가 로드될 때 초기 HTML은 서버에서 렌더링됩니다. 그런 다음 이 HTML은 브라우저에서 점진적으로 향상되며, Next.js와 React 클라이언트 측 런타임을 비동기적으로 로드함으로써 클라이언트가 애플리케이션을 제어하고 상호작용을 추가할 수 있습니다.
서버 컴포넌트로의 전환을 쉽게하기 위해 App Router 내부의 모든 컴포넌트는 기본적으로 서버 컴포넌트로 처리됩니다. 이는 추가 작업 없이 자동으로 채택할 수 있도록 하여 탁월한 성능을 제공합니다. 또한 'use client' 지시문을 사용하여 필요에 따라 클라이언트 컴포넌트로 옵트인할 수도 있습니다.
클라이언트 컴포넌트
클라이언트 컴포넌트(Client Components)는 애플리케이션에 클라이언트 측 상호작용을 추가할 수 있게 해줍니다. Next.js에서는 서버에서 사전 렌더링되고 클라이언트에서 hydrate(복원)되는 방식으로 작동합니다. 클라이언트 컴포넌트는 페이지 라우터의 컴포넌트가 항상 작동해온 방식과 유사하다고 생각할 수 있습니다.
"use client" 지시문
"use client" 지시문은 서버와 클라이언트 컴포넌트 모듈 그래프 간 경계를 선언하는 규칙입니다.
/* app/counter.tsx */
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
"use client"는 서버 전용과 클라이언트 코드 사이의 경계에 위치합니다. 이는 파일의 맨 위에 import 위에 배치되어, 서버 전용 부분에서 클라이언트 부분으로 경계를 넘어가는 지점을 정의합니다.
서버 컴포넌트가 기본값 이므로, "use client" 지시문으로 시작하는 모듈에서 정의되거나 가져오지 않는 한, 모든 컴포넌트는 서버 컴포넌트 모듈 그래프의 일부입니다.
알아두면 좋은 사항:
- 서버 컴포넌트 모듈 그래프에 있는 컴포넌트는 오직 서버에서만 렌더링됩니다.
클라이언트 컴포넌트 모듈 그래프에 있는 컴포넌트는 주로 클라이언트에서 렌더링되지만, Next.js에서는 사전 렌더링되고 클라이언트에서 hydrate(복원)될 수도 있습니다.
- "use client" 지시문은 import문보다 앞에 파일의 맨 위에 정의되어야 합니다.
- "use client"는 모든 파일에 정의할 필요는 없습니다. 클라이언트 모듈 경계는 "진입점"에서 한 번만 정의되면, 해당 경계로 가져온 모든 모듈이 클라이언트 컴포넌트로 간주됩니다.
Server Components와 Client Components를 각 각 언제 사용해야 됩니까?
서버와 클라이언트 컴포넌트 사이의 결정을 단순화하려면 app클라이언트 컴포넌트에 대한 사용 사례가 있을 때까지 서버 컴포넌트(디렉토리의 기본값)를 사용하는 것이 좋습니다.
Server Components를 사용할 때:
- 데이터 가져오는 경우
- 백엔드 리소스에 접근 해야 되는 경우
- 서버에 민감한 정보를 보관해야 되는경우
- 대용량 종속성을 서버에 유지하고 클라이언트 측 JavaScript를 줄여야되는 경우
Client Components를 사용할 때:
- 상호작용과 이벤트 리스너를 추가하는 경우 (onClick(), onChange() 등).
- 상태와 라이프사이클 효과를 사용하는 경우 (useState(), useReducer(), useEffect() 등).
- 브라우저 전용 API를 사용하는 경우
- 상태, 효과 또는 브라우저 전용 API에 의존하는 사용자 정의 훅을 사용하는 겨우
- React 클래스 컴포넌트를 사용하는 경우
패턴
클라이언트 컴포넌트를 끝으로 이동
성능을 개선하기 위해, 가능한 경우 Client Components를 컴포넌트 트리의 말단에 위치시키는 것을 권장합니다.
예를 들어, 로고, 링크 등의 정적 요소와 상태를 사용하는 대화형 검색 바를 가진 레이아웃이 있다고 가정해봅시다.
전체 레이아웃을 Client Component로 만드는 대신, 상호작용 로직을 Client Component로 이동시키고 레이아웃을 Server Component로 유지하세요. 이렇게 하면 레이아웃의 모든 컴포넌트 JavaScript를 클라이언트에 전송할 필요가 없습니다.
예를 들어, <SearchBar />를 Client Component로 만들고 layout을 Server Component로 유지하는 것입니다. 이렇게 하면 검색 바와 관련된 JavaScript만 클라이언트로 전송하면 됩니다.
/* app/layout.tsx */
// 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>
</>
);
}
Composing Client and Server Components
React는 내부적으로 다음과 같이 렌더링을 처리합니다:
- 서버에서 React는 클라이언트로 결과를 보내기 전에 모든 서버 컴포넌트를 렌더링합니다.
- 이는 클라이언트 컴포넌트 내부에 중첩된 서버 컴포넌트를 포함합니다.
- 이 단계에서 만난 클라이언트 컴포넌트는 건너뜁니다.
- 클라이언트에서 React는 클라이언트 컴포넌트를 렌더링하고 서버 컴포넌트의 렌더링 결과를 포함시킵니다. 이렇게 함으로써 서버와 클라이언트에서 수행된 작업을 병합합니다.
- 클라이언트 컴포넌트 내에 서버 컴포넌트가 중첩되어 있다면, 해당 컴포넌트의 렌더링된 내용은 올바르게 클라이언트 컴포넌트 내에 배치됩니다.
이를 통해 서버 컴포넌트와 클라이언트 컴포넌트를 조합하여 사용할 수 있으며, 서버와 클라이언트 간의 작업을 효율적으로 결합할 수 있습니다.
알아두면 좋은 정보: Next.js에서는 초기 페이지 로드 중에 위 단계에서 서버 컴포넌트의 렌더링된 결과와 클라이언트 컴포넌트가 모두 서버에서 HTML로 미리 렌더링되어 더 빠른 초기 페이지 로드를 생성합니다.
클라이언트 컴포넌트 내에 서버 컴포넌트 중첩
위에서 설명한 렌더링 흐름을 고려할 때, 클라이언트 컴포넌트에 서버 컴포넌트를 가져오는 것은 제한이 있습니다. 이 접근 방식은 추가적인 서버 라운드 트립이 필요하기 때문에 지원되지 않는 패턴입니다.
비 권장 패턴: 서버 컴포넌트를 클라이언트 컴포넌트에 가져오기
다음과 같은 패턴은 지원되지 않습니다.클라이언트 컴포넌트에서 서버 컴포넌트를 import할 수 없습니다.
/* app/example-client-component.tsx */
'use client';
// 이 패턴은 작동하지 않습니다!!!
// 클라이언트 컴포넌트에서 서버 컴포넌트를 import할 수 없습니다.
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 />
</>
);
}
권장 패턴: Server Components를 Client Components의 Props로 전달하기
대신, 클라이언트 컴포넌트를 설계할 때 React의 props를 사용하여 Server Components에 대한 "구멍"을 표시할 수 있습니다.
서버 컴포넌트는 서버에서 렌더링되며, 클라이언트에서 클라이언트 컴포넌트가 렌더링될 때 "구멍"은 서버 컴포넌트의 렌더링 결과로 채워집니다.
일반적인 패턴은 React의 children prop을 사용하여 "구멍"을 생성하는 것입니다. 우리는 <ExampleClientComponent>를 일반적인 children prop을 받아들이도록 리팩토링할 수 있으며, <ExampleClientComponent>의 import와 명시적인 중첩을 부모 컴포넌트로 올릴 수 있습니다.
/* app/example-client-component.tsx */
'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}
</>
);
}
이제, <ExampleClientComponent>는 children이 무엇인지에 대한 정보가 없습니다. 사실, <ExampleClientComponent>의 관점에서는 children이 결국 Server Component의 결과로 채워질 것임을 알지도 못합니다.
<ExampleClientComponent>의 유일한 책임은 어디에든 children이 최종적으로 배치되어야 할지를 결정하는 것입니다.
부모 Server Component에서는 <ExampleClientComponent>와 <ExampleServerComponent>를 모두 import하고 <ExampleServerComponent>를 <ExampleClientComponent>의 자식으로 전달할 수 있습니다.
/* app/page.tsx */
// 이 패턴은 작동합니다.
// 서버 컴포넌트를 클라이언트 컴포넌트의 child 또는 prop으로 전달 할 수 있습니다.
import ExampleClientComponent from './example-client-component';
import ExampleServerComponent from './example-server-component';
// Next.js의 page.tsx는 기본적으로 서버 컴포넌트 입니다.
export default function Page() {
return (
<ExampleClientComponent>
<ExampleServerComponent />
</ExampleClientComponent>
);
}
이 방법을 사용하면 <ExampleClientComponent>와 <ExampleServerComponent>의 렌더링이 독립적으로 이루어지며 독립적으로 렌더링할 수 있습니다. 이는 Server Components가 Client Components보다 먼저 서버에서 렌더링되는 방식과 일치합니다.
알아두면 좋은 사항:
- 이 패턴은 이미 레이아웃과 페이지에서 children prop을 사용하여 적용되고 있으므로 추가적인 래퍼 컴포넌트를 생성할 필요가 없습니다.
- React 컴포지션 모델의 일부로 JSX를 다른 컴포넌트에 전달하는 것은 새로운 개념이 아니며 항상 가능했습니다.
- 이러한 구성 전략은 Server Components와 Client Components 사이에서 작동합니다. 왜냐하면 prop을 받는 컴포넌트는 prop이 무엇인지에 대한 지식이 없기 때문입니다. 해당 컴포넌트는 전달받은 것을 어디에 배치해야 할지만 책임집니다.
- 이를 통해 전달된 prop은 독립적으로 렌더링될 수 있으며, 이 경우 서버에서 먼저 렌더링되고 클라이언트에서 클라이언트 컴포넌트가 렌더링됩니다.
- 이러한 "컨텐츠를 위로 올리는(lifting content up)" 전략은 상태 변경으로 인해 부모 컴포넌트가 가져온 중첩된 자식 컴포넌트를 다시 렌더링하는 것을 피하는 데에도 사용됩니다.
- children prop에 제한되지 않습니다. JSX를 전달하기 위해 어떤 prop이든 사용할 수 있습니다.
서버에서 클라이언트 컴포넌트로 props 전달(serializable)
서버에서 클라이언트 컴포넌트로 전달되는 props는 직렬화 가능해야 합니다. 즉, 함수, 날짜 등의 값은 직접적으로 클라이언트 컴포넌트에 전달할 수 없습니다.
네트워크 경계는 어디에 있습니까?
App Router에서 네트워크 경계는 Server Components와 Client Components 사이에 위치합니다. 이는 페이지에서의 경계와는 다릅니다. 페이지에서는 경계가 getStaticProps/getServerSideProps와 페이지 컴포넌트 사이에 있습니다. Server Components 내에서 가져온 데이터는 네트워크 경계를 넘지 않는 한 직렬화할 필요가 없습니다. 다만, 클라이언트 컴포넌트에 전달되어야 할 경우에는 직렬화되어야 합니다. Server Components와 함께 데이터 가져오기에 대해 더 자세히 알아보세요.
클라이언트 컴포넌트에서 서버 전용 코드 유지(Poisoning)
JavaScript 모듈은 서버와 클라이언트 컴포넌트 간에 공유될 수 있기 때문에 서버에서만 실행되도록 의도된 코드가 클라이언트에 몰래 들어갈 수 있습니다.
예를 들어 data-fetching function을 사용할때
/* lib/data.ts */
export async function getData() {
const res = await fetch('<https://external-service.com/data>', {
headers: {
authorization: process.env.API_KEY,
},
});
return res.json();
}
처음 보면 getData 함수가 서버와 클라이언트 양쪽에서 작동하는 것처럼 보입니다. 그러나 NEXT_PUBLIC로 접두사를 붙이지 않은 API_KEY 환경 변수는 서버에서만 액세스할 수 있는 개인 변수입니다. Next.js는 클라이언트 코드에서 안전한 정보 누출을 방지하기 위해 비공개 환경 변수를 빈 문자열로 대체합니다.
결과적으로, getData() 함수를 클라이언트에서 가져와 실행할 수 있지만 예상대로 작동하지 않습니다. 또한 변수를 공개하면 함수가 클라이언트에서 작동하지만 민감한 정보가 노출될 수 있습니다.
따라서, 이 함수는 항상 서버에서만 실행될 것으로 의도되어 작성되었습니다.
"server only" 패키지
서버 코드를 클라이언트 컴포넌트에서 실수로 가져오는 것을 방지하기 위해, server-only 패키지를 사용할 수 있습니다. 이를 통해 다른 개발자들이 서버 컴포넌트를 클라이언트 컴포넌트에서 실수로 가져오는 경우 빌드 시간 오류를 발생시킬 수 있습니다.
를 사용하려면 server-only먼저 패키지를 설치하십시오.
yarn add server-only
그런 다음 서버 전용 코드가 포함된 모듈로 패키지를 가져옵니다.
/* lib/data.js */
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 패키지
Server Component는 새로운 개념이기 때문에, 생태계의 서드파티 패키지들은 useState, useEffect, createContext와 같은 클라이언트 전용 기능을 사용하는 컴포넌트에 "use client" 지시문을 막 적용하기 시작했습니다.
현재, 많은 npm 패키지의 컴포넌트들은 아직 해당 지시문이 적용되지 않았습니다. 이러한 서드파티 컴포넌트들은 당신의 클라이언트 컴포넌트 내에서는 예상대로 작동하지만, 서버 컴포넌트 내에서는 작동하지 않을 것입니다.
예를 들어, 가상의 acme-carousel 패키지를 설치했다고 가정해봅시다. 이 패키지에는 <Carousel /> 컴포넌트가 있으며, 이 컴포넌트는 useState를 사용하지만 아직 "use client" 지시문이 적용되지 않았습니다.
만약 클라이언트 컴포넌트 내에서 <Carousel />을 사용한다면 예상대로 작동할 것입니다.
/* app/gallery.tsx */
'use client';
import { useState } from 'react';
import { Carousel } from 'acme-carousel';
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
);
}
그러나 서버 컴포넌트 내에서 직접 사용하려고 하면 오류가 표시됩니다.
/* app/page.tsx */
import { Carousel } from 'acme-carousel';
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
);
}
이것은 Next.js가 <Carousel /> 컴포넌트가 클라이언트 전용 인지 모르기 때문입니다 .
이 문제를 해결하기 위해서는 클라이언트 전용 기능에 의존하는 서드파티 컴포넌트를 자체적으로 만든 클라이언트 컴포넌트로 감싸주면 됩니다.
/* app/carousel.tsx */
'use client';
import { Carousel } from 'acme-carousel';
export default Carousel;
이제 <Carousel />서버 컴포넌트 내에서 직접 사용할 수 있습니다.
/* app/page.tsx */
import Carousel from './carousel';
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
);
}
대부분의 서드파티 컴포넌트는 클라이언트 컴포넌트 내에서 사용될 것이므로 감싸주는 작업이 필요하지 않을 것으로 예상됩니다. 하지만 provider 컴포넌트는 React의 상태와 컨텍스트에 의존하며 일반적으로 애플리케이션의 루트에서 필요합니다. 아래에서 서드파티 컨텍스트 제공자에 대해 더 자세히 알아볼 수 있습니다.
Library 개발자의 역할
- 라이브러리 개발자는 비슷한 방식으로 패키지를 개발하여 다른 개발자가 사용할 수 있도록 할 수 있습니다. 패키지의 클라이언트 진입점에 "use client" 지시문을 사용하여 패키지 컴포넌트를 사용자가 래핑 경계를 만들 필요 없이 직접 서버 컴포넌트에 가져올 수 있게 합니다.
- "use client"를 더 깊은 단계에서 사용하여 패키지를 최적화할 수도 있으며, 이렇게 하면 가져온 모듈을 서버 컴포넌트 모듈 그래프의 일부로 만들 수 있습니다.
- 참고로 일부 번들러는 "use client" 지시문을 제거할 수 있습니다. React Wrap Balancer 및 Vercel Analytics 리포지토리에서 "use client" 지시문을 포함하는 방법에 대한 예제를 찾을 수 있습니다.
Context
대부분의 React 애플리케이션은 createContext를 통해 직접적으로 또는 제3자 라이브러리에서 가져온 provider 컴포넌트를 통해 컴포넌트 간 데이터를 공유하기 위해 컨텍스트를 사용합니다.
Next.js 13에서는 컨텍스트가 Client Components 내에서 완전히 지원되지만, Server Components 내에서 직접 생성하거나 사용할 수는 없습니다. 이는 Server Components가 상호작용하지 않으므로(React 상태가 없음) 컨텍스트가 주로 React 상태가 업데이트된 후에 트리 깊은 곳에 있는 상호작용 컴포넌트를 다시 렌더링하기 위해 사용되기 때문입니다.
Server Components 간에 데이터를 공유하기 위한 대안에 대해 알아보기 전에, 먼저 Client Components 내에서 컨텍스트를 사용하는 방법을 살펴보겠습니다.
클라이언트 컴포넌트에서 Context 사용
모든 컨텍스트 API는 클라이언트 컴포넌트 내에서 완벽하게 지원됩니다.
/* app/sidebar.tsx */
'use client';
import { createContext, useContext, useState } from 'react';
const SidebarContext = createContext();
export function Sidebar() {
const [isOpen, setIsOpen] = useState();
return (
<SidebarContext.Provider value={{ isOpen }}>
<SidebarNav />
</SidebarContext.Provider>
);
}
function SidebarNav() {
let { isOpen } = useContext(SidebarContext);
return (
<div>
<p>Home</p>
{isOpen && <Subnav />}
</div>
);
}
그러나 Context 프로바이더는 일반적으로 현재 테마와 같은 전역적인 관심사를 공유하기 위해 응용 프로그램의 루트 근처에서 렌더링됩니다. Server Components에서 컨텍스트를 지원하지 않기 때문에 응용 프로그램의 루트에서 컨텍스트를 생성하려고 하면 오류가 발생합니다:
/* app/layout.tsx */
import { createContext } from 'react';
// createContext Server Components에서 지원되지 않습니다.
export const ThemeContext = createContext({});
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
);
}
이 문제를 해결하려면 컨텍스트를 만들고 클라이언트 컴포넌트 내부에서 해당 공급자를 렌더링합니다.
/* app/theme-provider.tsx */
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext({});
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
이제 서버 컴포넌트는 클라이언트 컴포넌트로 표시되었으므로 공급자를 직접 렌더링할 수 있습니다.
/* app/layout.tsx */
import ThemeProvider from './theme-provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
공급자가 루트에서 렌더링되면 앱 전체의 다른 모든 클라이언트 컴포넌트가 이 컨텍스트를 사용할 수 있습니다.
참고: 프로바이더를 가능한 깊은 위치에서 렌더링하는 것이 좋습니다. ThemeProvider는 전체 <html> 문서가 아닌 {children}만을 감싸고 있음에 유의하세요. 이렇게 하면 Next.js가 Server Components의 정적인 부분을 최적화하는 작업이 더욱 쉬워집니다.
서버 컴포넌트에서 Third-party Context Provider 렌더링
다른 개발자가 만든 npm 패키지는 종종 애플리케이션의 루트 근처에서 렌더링되어야 하는 Provider를 포함하고 있습니다. 이러한 프로바이더가 "use client" 지시문을 포함하고 있다면, 해당 프로바이더를 직접 Server Component 내부에서 렌더링할 수 있습니다. 그러나 Server Component가 매우 새로운 개념이기 때문에, 많은 제 3자 프로바이더는 아직 해당 지시문을 추가하지 않았을 수 있습니다.
만약 "use client"가 없는 제 3자 프로바이더를 렌더링하려고 한다면, 에러가 발생할 것입니다:
/* app/layout.tsx */
import { ThemeProvider } from 'acme-theme';
export default function RootLayout({ children }) {
return (
<html>
<body>
{/* Error: `createContext` can't be used in Server Components */}
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
이 문제를 해결하려면 고유한 클라이언트 컴포넌트에 타사 공급자를 래핑하십시오.
/* app/providers.js */
'use client';
import { ThemeProvider } from 'acme-theme';
import { AuthProvider } from 'acme-auth';
export function Providers({ children }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
이제 <Providers />루트 레이아웃 내에서 직접 가져오고 렌더링할 수 있습니다.
/* app/layout.js */
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
루트에서 프로바이더를 렌더링하면, 해당 라이브러리의 모든 컴포넌트와 훅이 자체 Client Component 내에서 예상대로 작동합니다.
제3자 라이브러리가 "use client"를 클라이언트 코드에 추가한 경우에는 래퍼 Client Component를 제거할 수 있게 될 것입니다.
서버 컴포넌트 간 데이터 공유
Server Components는 인터랙티브하지 않고 React 상태를 읽지 않기 때문에 데이터를 공유하기 위해 React context를 사용할 필요가 없습니다. 대신, 여러 Server Component가 액세스해야하는 공통 데이터에 대해 기본적인 JavaScript 패턴을 사용할 수 있습니다. 예를 들어, 모듈을 사용하여 여러 컴포넌트 간에 데이터베이스 연결을 공유할 수 있습니다.
/* 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 모듈을 가져와 데이터베이스에 접근합니다. 이러한 JavaScript 패턴은 전역 싱글톤이라고 불립니다.
서버 컴포넌트 간에 fetch request 공유
데이터를 가져올 때, 페이지나 레이아웃과 그 하위 컴포넌트들 사이에서 가져온 데이터를 공유할 수 있습니다. 이는 컴포넌트들 사이의 불필요한 결합을 일으킬 수 있으며, 컴포넌트 간에 프롭을 계속 전달하는 상황을 초래할 수 있습니다.
대신, 데이터를 소비하는 컴포넌트와 데이터를 가져오는 코드를 함께 배치하는 것을 권장합니다. Server Components에서 fetch 요청은 자동으로 중복 제거되므로 각 라우트 세그먼트는 중복 요청에 대해 걱정할 필요없이 필요한 데이터를 정확히 요청할 수 있습니다. Next.js는 fetch 캐시에서 동일한 값을 읽어올 것입니다.
요약 & 정리
- NextJS 13은 React Server Components를 제대로 대응하고 있는 프레임워크라고 생각합니다. NextJS 13의 app 라우터 방식과 간단한 규칙 몇개만 지키면 React Server Component를 적용한 서비스 개발을 편하게 이용할 수 있을것이라 판단됩니다.
- NextJS 13에서 클라이언트 컴포넌트 안에서 서버 컴포넌트를 import 할 수 없으므로 children이나 props로 서버 컴포넌트를 전달해줘야됩니다.
- 서버 컴포넌트에서 useState, useEffect, createContext와 같은 클라이언트 전용 기능을 사용하는 컴포넌트로 작성된 제3 라이브러리를 사용하는 경우에는 ‘use client‘지시문이 작성 안된 경우가 많아 에러가 날 수 도 있습니다. 이럴때는 클라이언트 컴포넌트로 재 래핑하는 작업이 필요합니다.
- NextJS 에서는 특별한 이유가 없는 한 서버 컴포넌트에서 데이터를 가져오는 것이 좋습니다. 자동적으로 캐싱 및 중복 요청 처리를 해준다고 하니 react-query와 같은 클라이언트 단에서 실행되는 데이터 요청 형태 구조가 많이 바뀔 것으로 예측됩니다.
참조
- https://nextjs.org/docs/getting-started/react-essentials
Getting Started: React Essentials | Next.js
To build applications with Next.js, it helps to be familiar with React's newer features such as Server Components. This page will go through the differences between Server and Client Components, when to use them, and recommended patterns. If you're new to
nextjs.org
'기억보단 기록을 > Next JS (App Router)' 카테고리의 다른 글
[NextJS 13] Routing - Defining Routes (0) | 2023.05.30 |
---|---|
[NextJS 13] Routing - Routing Fundamentals (0) | 2023.05.30 |
[NextJS 13] Getting Started - app Routing Project Structure(앱 라우팅 프로젝트 구조) (0) | 2023.05.24 |
[NextJS 13] Getting Started - Installation(설치 방법) (0) | 2023.05.23 |
[NextJS 13] Getting Started - 소개 (0) | 2023.05.23 |