overview
(react 에서는 2020년 12월에 React Server Components가 소개되었습니다. )
(즉, RSC은 react 팀이 설계한 새로운 애플리케이션 아키텍처이고 이것을 next.js 에서도 사용할 수있도록 업데이트 한것입니다.)
이 모든것은 필자가 Next.js 13 버전으로 새롭게 프로젝트를 시작하는데 라우팅 방식을 조금 더 향상시킬 수 있는 `app/` 이라는 디렉토리를 사용하다가 아래와 같은 에러를 만나면서 시작되었습니다.
이 에러를 해결하는 법은 정말 간단하지만 "왜? 그렇게 해야할까?"에 대해 정리가 필요하다 느꼈고 그러던 와중 `서버 컴포넌트`에 대해 자세히 몰라서 정리하게 되었습니다.
1. What are Server Components
서버 컴포넌트는 애플리케이션의 서버 부분에서 렌더링되는 컴포넌트입니다.
이는 대부분의 컴포넌트가 서버에서 렌더링되고, 작은 인터랙티브 UI 요소만 클라이언트에서 렌더링 되는 방식입니다.
그리고 이 작은 인터랙티브 UI요소들을 클라이언트 컴포넌트라고 지칭할 수 있습니다.
클라이언트 컴포넌트는 클라이언트 측에서 렌더링되는 컴포넌트입니다.
이 컴포넌트는 사용자와의 상호작용을 담당하며, 애플리케이션의 인터랙티브한 부분을 담당합니다.
클라이언트 컴포넌트는 Next.js와 같은 프레임워크에서의 서버에서 사전 렌더링되고 클라리언트에서 Hydrate되어 초기 렌더링 성능을 개선할 수 있습니다.
(Hydrate : Server Side 단에서 렌더링 된 정적 페이지와 번들링된 JS파일을 클라이언트에게 보낸 뒤, 클라이언트 단에서 HTML 코드와 React인 JS코드를 서로 매칭 시키는 과정을 말한다)
여기서 헷갈릴 수 있는 부분이 있습니다.
바로 서버 컴포넌트와 서버 사이드 렌더링(SSR)입니다.
서버 컴포넌트와 서버 사이드 렌더링은 서버에서 렌더링 되다는 유사점이 있지만 해결하고자 하는 문제점이 다릅니다.
- 서버 컴포넌트의 코드는 클라이언트로 전달되지 않습니다. 하지만 서버 사이드 렌더링(SSR)의 모든 컴포넌트의 코드는 자바스크립트 번들에 포함되어 클라이언트로 전송됩니다.
- 서버 컴포넌트는 페이지 레벨에 상관없이 모든 컴포넌트에서 서버에 접근 가능합니다. 하지만 Next.js의 경우 가장 top level의 페이지에서만 getServerProps()나 getInitialProps()로 서버에 접근 가능합니다.
- 서버 컴포넌트는 클라이언트 상태를 유지하며 refetch 될수 있습니다. 서버 컴포넌트는 HTML이 아닌 특별한 형태로 컴포넌트를 전달하기 때문에 필요한 경우 포커스, 인풋 입력값 같은 클라이언트 상태를 유지하며 여러 번 데이터를 가져오고 리렌더링하여 전달할 수 있다. 하지만 서버 사이드 렌더링(SSR)의 경우 HTML로 전달되기 때문에 새로운 refetch가 필요한 경우 HTML 전체를 리렌더링 해야 하며 이로 인해 클라리언트 상태를 유지할 수 없습니다.
서버 컴포넌트는 서버 사이드 렌더링(SSR) 대체가 아닌 보완의 수단으로 사용할 수 있습니다. 서버 사이드 렌더링(SSR)으로 초기 HTML 페이지를 빠르게 보여주고, 서버 컴포넌트로는 클라이언트로 전송되는 자바스크립트 번들 사이즈를 감소시킨다면 사용자에게 기존보다 훨씬 빠르게 인터렉팅한 페이지를 제공할 수 있을 것이다.
2. Why Server Components?
클라이언트 컴포넌트에 비해 서버 컴포넌트를 사용하면 어떠한 이점이 있을까?
1. 서버 컴포넌트를 통해 개발자는 서버 인프라를 더 잘 활용할 수 있다.
이전에는 클라이언트의 자바스크립트 번들 크기에 영향을 주었던 큰 종속성들을 완전히 서버에 유지할 수 있으므로 성능이 향상된다. 이로써 React 애플리케이션 개발은 PHP나 Ruby on Rails와 유사한 느낌을 준다. 여기에 더해 React의 강력함과 유연성, 그리고 컴포넌트 기반 UI 템플릿 모델을 함께 제공한다.
2. 초기 페이지 로드 속도 증가 및 클라이언트 측 자바스크립트 번들 크기 감소
base client-side 런타임이 cacheable해지며 크기도 예측 가능하다. 이는 애플리케이션이 규모가 커져도 증가하지 않는다.
3. Next.js로 라우트가 로드되면 초기 HTML이 서버에서 렌더링된다. 그런 다음 HTML은 브라우저에서 점진적으로 향상되어 클라이언트가 Next.js 및 React 클라이언트 측 런타임을 비동기식으로 로드하여 애플리케이션을 인계하고 상호 작용을 추가할 수 있다.
4. 서버 컴포넌트로 전환을 쉽게하기 위해 App Router 내의 모든 컴포넌트는 기본적으로 서버 컴포넌트로 지정된다.
이는 특수 파일과 함께 위치한 컴포넌트를 포함하여 자동으로 적용할 수 있게 해주며, 추가 작업 없이 탁월한 성능을 얻을 수 있게 한다.
또한 'use client' 지시문을 사용하여 필요에 따라 클라이언트 컴포넌트로 전환할 수도 있다.
3. Client Components
1) 'use client' 지시어는 무엇일까?
'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'` 지시어가 서버 전용 코드와 클라이언트 코드 사이에 있다.
`'use client'` 지시어가 파일에 정의되면 자식 구성 요소를 포함하여 파일로 가져온 다른 모든 모듈은 클라이언트 번들의 일부로 간주된다.
즉, 서버 컴포넌트가 기본값이므로 `use client` 지시문을 모듈의 시작부분에 정의하거나 가져오지 않는 한 모든 컴포넌트는 서버 컴포넌트 모듈 그래프의 일부이다.
- 서버 컴포넌트 모듈 그래프 내부에 있는 컴포넌트들은 오직 서버에서만 렌더링되도록 보장된다.
- 클라이언트 컴포넌트 모듈 그래프 내부에 있는 컴포넌트들은 대부분 클라이언트에서 렌더링 되지만 `Next.js`를 사용하면 서버에서 사전 렌더링되고 클라이언트에서 Hydrate될 수도 있다.
- `"use client"` 지시문은 다른 것들을 `import` 하기전에 반드시 파일 제일 상단에 정의되어야 한다.
- 모든 파일에서 `"use client"` 지시문을 정의할 필요는 없다. 클라이언트 모듈 경계는 "진입점"에서 한 번만 정의하면 클라이언트 컴포넌트로 간주되는 모든 모듈을 가져올 수 있다.
참고 : 원래는 `'use client'`지시어를 사용하는게 아니라 서버컴포넌트는 `파일명.server.js` 클라이언트 컴포넌트는 `파일명.client.js` 이었는데 여러 개발자가 토론하고 피드백을 줘서 'use client' 로 변경 되었다. 관련 PR
나중에 여러분들도 꼭 토론도 해보고 피드백도 주는 경험을 해보길!
4. When to use Server and Client Components?
5. keeping Server-Only Code out of Client Components
자바스크립트 모듈은 서버 컴포넌트와 클라이언트 컴포넌트 간에 공유될 수 있기 때문에, 서버에서만 실행되도록 의도된 코드가 클라이언트에 몰래 들어갈 수가 있습니다.
예를 들어 아래 코드는 `data-fetching` 함수를 사용한다.
// 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()` 함수는 서버와 클라이언트 양쪽에서 작동하는 것처럼 보입니다.
그러나 환경 변수 `API_KEY`가 `NEXT_PUBLIC`로 접두사가 붙지 않았기 때문에, 서버에서만 액세스 할 수 있는 개인 변수입니다.
`Next.js`는 클라이언트 코드에서 안전한 정보 노출을 방지하기 위해 개인 환경 변수를 빈 문자열로 대체합니다.
결과적으로 `getData()`함수는 클라이언트에서 가져와 실행할 수 있지만 예상대로 작동하지 않습니다. 또한 변수를 공개로 만들면 함수가 클라이언트에서 작동하지만 민감한 정보가 노출됩니다.
따라서 이 함수는 항상 서버에서만 실행되도록 의도하여 작성되었습니다.
1) `server only` 패키지
이러한 종류의 의도하지 않은 클라이언트 서버 코드 사용을 방지하기 위해 `server-only` 패키지를 사용하여 다른 개발자가 실수로 이러한 모듈 중 하나를 클라이언트 컴포넌트로 가져온 경우 빌드 타입 오류를 제공할 수 있다.
설정 방법
(1) 설치
npm install server-only
(2) import
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` 하는 모든 클라이언트 컴포넌트는 빌드 시간 오류를 받게 됩니다. 이 오류는 이 모듈이 서버에서만 사용될 수 있다는 것을 설명합니다.
`client-only`라는 해당 패키지는 클라이언트 전용 코드를 포함하는 모듈을 표시하는 데 사용할 수 있습니다.
클라이언트 컴포넌트에서 데이터를 가져올 수 있지만 클라이언트 데이터를 가져와야 하는 특별한 이유가 없는 한 서버 컴포넌트에서 데이터를 가져오는 것이 좋습니다. 데이터 가져오기를 서버로 이동하면 성능과 사용자 경험이 향상되기 때문입니다.
6. Third-party packages
서버 컴포넌트가 새로운 개념이기 때문에, `third-party packages`은 `useState`, `useEffect`, `createContext`와 같은 클라이언트 전용 기능을 사용하는 컴포넌트에 `"use client"`지시문을 추가하기 시작했습니다.
현재, 클라이언트 전용 기능을 사용하는 많은 `npm` 패키지의 컴포넌트는 아직 해당 지시문이 없다.
이러한 third-party 컴포넌트들은 자체적으로 `"use client"` 지시문이 있으므로 내부의 클라이언트 컴포넌트에서는 예상대로 작동하지만, 서버 컴포넌트 내에서는 작동하지 않을 것이다.
예를 들어보자
만약 클라이언트 컴포넌트 안에서 `<AcmeCarouse />`을 사용한다고 해보자
예상대로 잘 작동할 것이다.
'use client';
import { useState } from 'react';
import { AcmeCarousel } from 'acme-carousel';
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* 🟢 Works, since AcmeCarousel is used within a Client Component */}
{isOpen && <AcmeCarousel />}
</div>
);
}
그러나 만약 서버 컴포넌트 내에서 직접 사용하려고 하면 오류가 발생한다.
import { AcmeCarousel } from 'acme-carousel';
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* 🔴 Error: `useState` can not be used within Server Components */}
<AcmeCarousel />
</div>
);
}
이는 Next.js가 <AcmeCarouse/>이 클라이언트 전용 기능을 사용한다는 것을 알지 못하기 때문이다.
이를 해결하기 위해 클라이언트 전용 기능에 의존하는 third-party 컴포넌트를 자체적인 클라리언트 컴포넌트로 감싸주면 된다.
// app/carousel.tsx
'use client';
import { AcmeCarousel } from 'acme-carousel';
export default AcmeCarousel;
이제 서버 컴포넌트 내에서 직접 <Carouse />을 사용할 수 있다.
// 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>
);
}
일반적으로 대부분의 third-party 컴포넌트를 감싸주어야 할 필요는 없을 것으로 예상됩니다.
왜냐하면 대부분의 경우 이러한 컴포넌트들은 클라이언트 컴포넌트 내에서 사용될 것이기 때문입니다.
(그러나 provider 컴포넌트들은 제외됩니다. )
📁 관련 링크
1. RSC(react server components)를 지원하는 라이브러리들
2. 서버 컴포넌트의 사용 방법이 아닌 react 개발팀이 어떻게 서버컴포넌트를 처음부터 구현하였는지 깊게 공부하고 싶다면 ? : 링크
3. React Roundtable: Server Components, Suspense, and Actions
References
https://velog.io/@leehyunho2001/Hydrate
https://tech.kakaopay.com/post/react-server-components/
https://nextjs.org/docs/getting-started/react-essentials#server-components