서버 컴포넌트는 클라이언트 쪽에서 자바스크립트를 전혀 사용하지 않아도 만들 수 있습니다. 말 그대로 전혀 필요 없습니다. 유저는 자바스크립트를 로드 할 필요도 없는 것입니다. 그렇다고 리액트 서버 컴포넌트가 서버 사이드 렌더링을 하는건 아닙니다.
리액트 서버 컴포넌트는 페이지를 렌더링하는걸 신경쓰지 않습니다. 서버에서 컴포넌트 렌더링하는걸 신경쓸 뿐입니다. 지금까지는 페이지에 집중했습니다. 이 페이지가 서버사이드 렌더링인지 static렌더링인지. 서버 컴포넌트를 사용할 때는 페이지를 신경쓰지 않습니다.
서버사이드 렌더링은 페이지에 접속하면 페이지가 서버에서 렌더링이 되는 것입니다. 페이지 전체를 로드하는데 시간이 많이 걸리게 되죠.
왜 RSC(React Server Component)일까?
기존 SSR방식은 다음과 같이 동작했습니다.
// ES modules
import ReactDOMServer from 'react-dom/server';
// CommonJS
var ReactDOMServer = require('react-dom/server');
ReactDOMServer.renderToString(element)
출처: https://programming119.tistory.com/252 [개발자 아저씨들 힘을모아:티스토리]
'renderToString'함수를 통해 초기 렌더링 결과를 HTML String으로 변환하고, 이를 바탕으로 첫 요청의 응답으로 마크업을 포함한 HTML문서를 빠르게 사용자에게 보여줍니다. 그리고 클라이언트단에서 ReactDOM.hydrate함수를 통해 바뀐부분만 수분을 공급해 줍니다.
사용 예시
일반적인 컴포넌트
- 검색창에 뭔가 입력
- onChange => 검색 Fetch => 검색결과 받아옴
- 받은 검색 결과 리액트에 넘겨서 컴포넌트 렌더링
RSC
- 검색창에 뭔가 입력
- onChange => 렌더링 서버에 키워드 Fetch =>
- 서버에서 검색 Fetch요청 보냄 => 검색결과 받아서 비 JSON/비 HTML형식으로 마크업 빌드해서 클라이언트에 보냄
- 클라이언트에서 마크업 받아서 정적 UI로 렌더링(React컴포넌트가 아니므로 컴포넌트 처리에 드는 시간 절약)
이를 도입하려는 이유?
기존 SSR방식에서는 페이지가 아닌 컴포넌트를 정적으로 export할 수가 없었습니다. 실질적으로 리액트 개발자가 조작하는 코드는 컴퍼넌트 단위로 구성이 되는데, 이는 불편한 점입니다. NextJS를 다뤄보신분이라면, pages하위에 있는 컴포넌트가 아닌 이상, SSR관련 함수들을 사용할 수 없다는 것을 알고 계실겁니다. 어쩔 수 없이 최상위가 되는 pages컴포넌트에서 서버단에서 fetch해온 데이터를 props등으로 하위 컴포넌트로 내려주거나, 그렇지 않다면 하위 컴포넌트에서 CSR방식으로 데이터를 가져오는 방법을 택했어야만 했습니다.
뿐만 아니라, UI를 렌더링하는 데에는 필요하지 않은 데이터 처리 과정에서 사용되는 모듈까지 함께 번들링되기 때문에, 큰 규모의 프로젝트의 경우 브라우저가 받아와야 하는 파일의 용량이 매우 높아지게 됩니다. 이 불필요한 청크 파일들을 받아오는 것을 막기 위해, 코드 스플리팅이나 lazy loading과 같은 기술을 이용하지만, 이 또한 결국 시간을 투자해야 되는 하나의 불편함으로 작용했습니다.
어떤 식으로 사용될까?
// Note.server.js - Server Component
import db from 'db.server';
// (A1) We import from NoteEditor.client.js - a Client Component.
import NoteEditor from 'NoteEditor.client';
function Note(props) {
const {id, isEditing} = props;
// (B) Can directly access server data sources during render, e.g. databases
const note = db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{/* (A2) Dynamically render the editor only if necessary */}
{isEditing
? <NoteEditor note={note} />
: null
}
</div>
);
}
리액트에서 제공하는 예시중 하나입니다, 위에서 보시는 바와 같이, 서버 컴포넌트는 api형식의 방시을 쓰지 않고, 직접 DB에 접근하여 note데이터를 받아오고 있습니다. 이는 RSC의 장점 중 하나입니다. 그리고 받아온 데이터를 바탕으로 NoteEditor라는 클라이언트 컴포넌트를 구성합니다. 여기서 NoteEditor는 클라이언트 컴포넌트인데, 서버 컴포넌트가 이 컴포넌트를 import할 때, 자동적으로 필요로 할 때, dynamic하게 import를 하게 됩니다. 기존 방식처럼 직접 불편한 과정들을 거칠 필요가 없게 됩니다.
export default function NoteEditor(props) {
const note = props.note;
const [title, setTitle] = useState(note.title);
const [body, setBody] = useState(note.body);
const updateTitle = event => {
setTitle(event.target.value);
};
const updateBody = event => {
setTitle(event.target.value);
};
const submit = () => {
// ...save note...
};
return (
<form action="..." method="..." onSubmit={submit}>
<input name="title" onChange={updateTitle} value={title} />
<textarea name="body" onChange={updateBody}>{body}</textarea>
</form>
);
}
요약
어떤 상황에서 어떤 컴포넌트를 써야할까?
- 서버 컴포넌트: 데이터를 받아오는 부분, 전처리 과정, 파일 시스템이 필요한 부분
- 클라이언트 컴포넌트: UI위주의 부분, 빠른 interaction이나 사용자 입력이 필요한 부분
RSC의 장점은
Zero-Bundle-Size Components
- 서버 컴포넌트는 번들에 포함되지 않기 때문에 브라우저로 가는 번들 사이즈가 현저하게 작아진다.
- 서버에서만 사용되는 패키지 모듈들은 서버에서만 유지하면 된다.
Full Access to the Backend
- API형식으로 불러올 필요 없이, 파일시스템, DB등에 편하게 접근할 수 있다. 물론 클라이언트 단에서 api를 통해 패치하는 방식도 가능합니다.
Automatic Code Splitting
// PhotoRenderer.js
// NOTE: *before* Server Components
import React from 'react';
// one of these will start loading *when rendered on the client*:
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <PhotoRenderer {...props} />;
}
}
기존의 lazy loading방식. 반드시 React.lazy를 통한 콜백으로 import를 감싸주어야 합니다.
import React from 'react';
// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <PhotoRenderer {...props} />;
}
}
client컴포넌트들은 자동적으로 코드 스플리팅이 적용되어 렌더링이 필요한 시점에 lazy하게 import합니다.
No Waterfalls
이 현상은 부모 -> 자식 계층적으로 데이터가 내려가는 컴포넌트 형태가 꾸려져 있을 떄 더 악화됩니다.
// Note.js
// NOTE: *before* Server Components
function Note(props) {
const [note, setNote] = useState(null);
useEffect(() => {
// NOTE: loads *after* rendering, triggering waterfalls in children
fetchNote(props.id).then(noteData => {
setNote(noteData);
});
}, [props.id]);
if (note == null) {
return "Loading";
} else {
return (/* render note here... */);
}
}
useEffect의 디펜던시에 props가 있기 때문에, 이 Note컴포넌트는 렌더링이 끝난 후, 부모에서 전달해주는 props가 전부 로딩이 되고 난 이후에야 fetchNote를 통해 추가적으로 로딩을 시작하게 됩니다. 그리고 그 동안 UI에서는 과도하게 길게 'Loading'을 보여줘야 하는 문제가 생기게 됩니다.
// Note.server.js - Server Component
function Note(props) {
// NOTE: loads *during* render, w low-latency data access on the server
const note = db.notes.get(props.id);
if (note == null) {
// handle missing note
}
return (/* render note here... */);
}
렌더링을 함과 동시에 data를 로드해오기 때문에, 렌더링 자체가 데이터를 가져오기를 시작하는 시간에 영향을 주지 않습니다.
'Web > NextJs' 카테고리의 다른 글
[ Next.js - DeepDive ] - REACT18 - Sever Components 2 (0) | 2022.08.13 |
---|---|
[ Next.js - DeepDive ] - REACT18 - Suspense 2 (0) | 2022.08.13 |
[ Next.js - DeepDive ] - REACT18 - Suspense (0) | 2022.08.12 |
[ Next.js - DeepDive ] - Data Fetching Reca (0) | 2022.08.12 |
[ Next.js - DeepDive ] - Fallback (0) | 2022.08.12 |