| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
Tags
- Promise
- map
- Jest
- 인터섹션
- 태그된 유니온
- CI/CD
- async/await
- MSA
- dfs
- useAppDispatch
- 인증/인가
- recoil
- RTK Query
- autosize
- ESlint
- 반공변성
- 공변성
- tailwind
- webpack
- SSR
- CORS
- 무한 스크롤
- TS
- 투포인터
- React
- 결정 알고리즘
- 호이스팅
- 리터럴 타입
- app router
- 타입 좁히기
Archives
- Today
- Total
짧은코딩
React Server Components 독후감 본문
반응형
RSC(React Server Components)
https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md
rfcs/text/0188-server-components.md at main · reactjs/rfcs
RFCs for changes to React. Contribute to reactjs/rfcs development by creating an account on GitHub.
github.com
RSC가 탄생하게 된 문서를 읽어보자.
RSC가 필요했던 이유
CSR은 UX와 성능 사이에서 트래이드오프가 항상 존재했다.
- 컴포넌트가 렌더링 되고 데이터를 요청하는 패턴(useEffect)가 반복되면, 데이터 로딩이 지연된다
- npm 라이브러리를 계속 설치하다보면 번들 사이즈가 커지게된다.
- 성능을 위해 데이터를 최상위에서 한번에 가져오면, 하위 컴포넌트의 데이터 의존성이 상위 컴포넌트에 강하게 결합되어 유지보수가 힘들어진다.
해결책
"컴포넌트 트리의 일부는 서버에서 실행하고, 일부는 브라우저에서 실행하자"가 해결책이었다.
- Server Component: 데이터 로딩, DB 접근, 무거운 연산 담당(브라우저로 코드 전송 X)
- Client Component: 클릭, 입력, 상태 관리 같은 인터랙션 담당(브라우저로 코드 전송 O)
RSC 동작 원리
- 직렬화와 제약 사항
- 직렬화는 문자열 데이터 변환이 가능한 데이터를 의미한다.
- 따라서 JSON, JSX Element는 가능하다.
- 하지만 함수, 클래스 인스턴스, DOM API는 불가능하다.
- HTML이 아닌 '데이터 스트림' 전송
- RSC는 HTML을 주고 받지 않는다. HTML을 주고 받는 것은 SSR이다
- RSC는 JSON과 유사한 데이터 스트림을 보낸다.
// 개념적 예시 (실제 프로토콜은 더 복잡함)
{
"id": "root",
"children": [
{ "type": "div", "children": "서버에서 렌더링된 텍스트" },
{ "type": "ClientComponent", "props": { "data": "..." }, "path": "chunk.js" }
]
}
3. 상태 유지
- 데이터 스트림 방식으로 인해 얻는 장점이다.
- HTML을 받아오는 것이 아니기 때문에 새로운 데이터를 서버에서 받아와도 전체를 수정하지 않고, 수정이 필요한 컴포넌트만 머지해서 클라이언트 컴포넌트가 갖고 있던(input 포커스, 스크롤 위치, 입력 중인 텍스트 등)은 유지된다.
예시
-1번 API 없이 DB 조회
// app/users/page.tsx (기본적으로 Server Component)
import { db } from '@/lib/db'; // Prisma나 ORM 가정
// 1. 컴포넌트 자체가 async 함수입니다.
export default async function UsersPage() {
// 2. 별도의 API 호출 없이 바로 DB를 찌릅니다.
// 이 코드는 서버에서만 돌기 때문에 DB URL 등이 브라우저에 노출되지 않습니다.
const users = await db.user.findMany({
where: { isActive: true },
orderBy: { createdAt: 'desc' }
});
return (
<main className="p-10">
<h1 className="text-2xl font-bold mb-4">활성 유저 목록 ({users.length}명)</h1>
<ul className="space-y-2">
{users.map((user) => (
// 3. 렌더링 된 결과(HTML)만 클라이언트로 전송됩니다.
<li key={user.id} className="border p-2 rounded">
{user.name} ({user.email})
</li>
))}
</ul>
</main>
);
}
- useState, useEffect가 아예 없다
- 따라서 데이터 로딩 상태 관리도 필요 없다
-2번 Client Component로 데이터 전달
// app/post/[id]/page.tsx
import { db } from '@/lib/db';
import LikeButton from '@/components/LikeButton';
export default async function PostDetail({ params }: { params: { id: string } }) {
// 1. 서버에서 데이터 로딩
const post = await db.post.findUnique({ where: { id: params.id } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* 2. 데이터를 Props로 넘겨줌 (직렬화 가능한 값만!) */}
{/* 함수(onClick 핸들러 등)는 여기서 만들어서 넘길 수 없음 */}
<LikeButton initialCount={post.likeCount} postId={post.id} />
</article>
);
}
- LinkButton은 브라우저로 전송
- 로직과 DB 코드는 서버에 남음
-3번 스트리밍 방식
// app/dashboard/page.tsx
import { Suspense } from 'react';
import RevenueChart from '@/components/RevenueChart'; // 서버 컴포넌트라고 가정
import UserList from '@/components/UserList'; // 서버 컴포넌트라고 가정
export default function Dashboard() {
return (
<main>
<h1>대시보드</h1>
{/* 1. 사용자 목록은 빠르니까 그냥 기다림 */}
<div className="mb-8">
<UserList />
</div>
{/* 2. 매출 차트는 3초 걸림 -> 로딩 걸어두고 페이지 먼저 보여줌 */}
<section className="h-64 bg-gray-50">
<Suspense fallback={<div className="text-gray-400">차트 분석 중... ⏳</div>}>
{/* RevenueChart 안에서 async/await로 DB 조회 중 */}
<RevenueChart />
</Suspense>
</section>
</main>
);
}
- 무거운 데이터가 전체 페이지 로딩을 막지 않게 하는 패턴
- RevenueChart가 데이터를 다 가져올 때까지 기다리지 않고, 즉시 fallback을 포함한 HTML을 먼저 내려줌
- 그리고 데이터 준비가 완료되면 해당 부분만 갈아끼우는 방식
- 따라서 Suspense, fallback도 RSC라고 볼 수 있다.
반응형
Comments
