NextJS App Route에서는 기본적으로 Server Component를 사용한단 말이지?
따라서 React Server Component가 어떻게 동작하는지 알아야 한다라고 생각을 한다.
사실 NextJS를 14버전에서 처음 접해봐서 Page Route에서의 SSR을 사용해본 경험이 없다보니 SSR, RSC 이런게 너무 혼란스러웠는데 이제야 조금식 정리가 되는기분이다.
여튼 레츠꼬
React Server Component는 Server Side Rendering(SSR)이 아니다!
Server Component와 Server Side Rendering 모두 "Server" 라는 이름이 들어가있고 또 서버에서 일을하기 때문에 헷갈리기는 한다.
하지만 이 두개는 서로 다르다.
Server Component를 사용하면 SSR을 사용할 필요가 없고, 반대로 SSR또한 Server Component를 사용할 필요가 없다.
Server Side Rendering은 React Tree를 Raw HTML로 렌더링하는 환경을 말하는데, Server Component와 Client Component를 구분하지 않고 동일한 방식으로 렌더링 한다.
따라서 SSR과 RSC를 서로 같이 사용할 수 있어서, Server Component를 Server Side Rendring으로 수행하고, 브라우저에서 Hydraion을 시킬 수 있다.
위 사진이 "어떤 페이지를 구성하는 React Tree"라고 가정하자.
그리고 이 Tree를 잘 보아하니, Server Component와 Client Component가 서로 섞여있다! (대충보면 포도맛, 오렌지맛 사탕처럼 보이기도 ㅋㅋ 왜때문에 힙쏘트가 생각날까?)
그리고 몇개의 컴포넌트는 서버에서 렌더링이 되고, 또 몇개의 컴포넌트는 클라이언트에서 렌더링이 된다고 하자.
그렇다고 할때, Server는 평소와 같이 Server Component를 렌더링해서 div
혹은 p
와 같은 Native HTML Element로 전환한다.
그러나! Browser에서 렌더링 되어야 하는 Client Component를 만나게 되면 서버 컴포넌트와 같이 HTML Element로 전환하는게 아니라, "야! 여기 클라이언트 컴포넌트다! 여기 구멍 뚫어둘 태니까 이거 설명서대로 메꿔라!" 라는 설명서를 대친 출력한다.
그런다음 사용자의 브라우저는 "Server Component는 div
나 p
처럼 HTML Element로 변해있고, Client Component는 여기 구멍 설명서보고 메꿔라!
라고 설명서로 대체된 결과물" 을 가지고 모든 구멍을 메꾼다.
그렇다면 끝난다!
물론 실제로 이렇게 동작하지는 않지만, 이런식으로 동작한다는 대략적인 설명이다.
일단 첫번째로, 뭐가 서버 컴포넌트이고 뭐가 클라이언트 컴포넌트인지 구분을 해야하지 않겠는가??
리액트 팀에서는 이를 정의해뒀는데, 파일 확장자의 이름을 기반으로 서버와 클라이언트 컴포넌트를 나눈다고 한다.
.server.jsx
라는 확장자로 끝나게 되면 이는 Server Compnent를 포함하는 파일이다. 반대로 .client.jsx
라는 확장자로 파일이 끝나게 되면, Client Component를 포함하는 파일이다.
그리고 파일의 확장자에 .server.jsx
or .client.jsx
둘 다 없는경우는, Server Component와 Client Component 모두 사용할 수 있는 컴포넌트가 들어있는 파일이다.
그리고 이전 포스팅에서 몇번 적었지만 복습 차원에서 한번 더 적어보자면, Client Component에서는 Server Component를 Import해서 사용할 수 없다.
즉 아래와 같이 작성된 코드는 정상적으로 동작하지 못한다.
// ClientComponent.client.jsx
// NOT OK:
import ServerComponent from './ServerComponent.server'
export default function ClientComponent() {
return (
<div>
<ServerComponent />
</div>
)
}
그렇다면, Client Component에서 Server Component를 사용하고 싶다면 어찌 해야할까?
이것도 이전 포스팅에서 적었지만 복습을 위해서 한번 더 적어보자면, childeren으로 받아서 사용할 수 있다.
그래서 아래 코드는 정삭적으로 동작하는 코드다.
// ClientComponent.client.jsx
export default function ClientComponent({ children }) {
return (
<div>
<h1>Hello from client land</h1>
{children}
</div>
)
}
// ServerComponent.server.jsx
export default function ServerComponent() {
return <span>Hello from server land</span>
}
// OuterServerComponent.server.jsx
// OuterServerComponent can instantiate both client and server
// components, and we are passing in a <ServerComponent/> as
// the children prop to the ClientComponent.
import ClientComponent from './ClientComponent.client'
import ServerComponent from './ServerComponent.server'
export default function OuterServerComponent() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
React Server Component를 렌더링하려고 할 때 실제로 어떤 일이 일어나는지를 살펴보자!!
서버 컴포넌트를 사용하기 위해서 모든 내용을 이해할 필요은 없지만, 작동하는 방식에 대해서 개념정도는 알아둬야 하지 않을까!?
서버가 렌더링의 일부를 수행해야 하기 때문에 Server Component를 사용하는 페이지의 수명은 항상 서버에서 부터 시작되고, 일부 API의 호출에 대한 응답으로 React 컴포넌트를 렌더링하게 된다.
이 루트 컴포넌트는 항상 Server Component로써 다른 Server Component 또는 Client Component를 렌더링할 수 있다.
여튼저튼, 서버는 서버로 들어온 요청을 바탕으로 어떤 서버 컴포넌트가 어떤 props를 사용하는지 파악한다. (그래야 html로 만들거아닌가~)
그리고 이 요청은 일반적으로 특정 URL에 대한 페이지의 요청의 형태로 이뤄진다.
즉, 사용자가 웹 애플리케이션을 딱 접속하면 서버로 요청이 들어가고, 그러면 서버는 요청이 들어온걸 바탕으로 어떤 서버 컴포넌트가 어떤 props를 사용하는지 파악한다는 이야기.
여기서 최종 목표는 초기의 Root Server Component를 기본적인 HTML 태그와 클라이언트 컴포넌트는 Placeholders로 표시한 Tree로 렌더링하것이다!
말이 좀 어려운데, 위에서 언급한거를 다시한번 기억해보자.
서버 컴포넌트는 <div>
or <p>
같은 기본적인 HTML Element로 바꾸고, 클라이언트 컴포넌트는 야 이거 클라이언트 컴포넌트니까, 여기에 시키는대로 만들어라~ 라는 설명서로 바꿔준다고 했다.
여기서 그 설명서가 바로 Placeholder라고 생각하면 이해하기 편할것 같다.
직렬화(Serializes)
프로그램의 오브젝트에 담긴 데이터를 어떤 외부 파일에 write 및 전송하는 것
역직렬화(Deserialize)
어떤 외부 파일의 데이터를 프로그램 내의 오브젝트로 read 하는것
따라서 직렬화된 트리를 사용자의 브라우저에게 보내고, 브라우저는 전달받은 직렬화된 트리를 역직렬화를 할 수 있다, 그러면 역직렬화 하는 과정에서 설명서와 Placeholder로 대체한 클라이언트 컴포넌트를, 설명서를 토대로 진짜 클라이언트 컴포넌트로 채우고 최종 결과를 렌더링할 수 있다!
자 그러면, 리액트 엘리먼트가 실제로 뭔지 한번 생각해보자.
리액트 엘리먼트는 type
필드가 문자열인 객체인데, div
와 같은 기본적인 HTML 태그일 경우는 문자열 객체, 함수의 경우는 리액트 컴포넌트 인스턴스다.
// React element for <div>oh my</div>
> React.createElement("div", { title: "oh my" })
{
$$typeof: Symbol(react.element),
type: "div",
props: { title: "oh my" },
...
}
// React element for <MyComponent>oh my</MyComponent>
> function MyComponent({children}) {
return <div>{children}</div>;
}
> React.createElement(MyComponent, { children: "oh my" });
{
$$typeof: Symbol(react.element),
type: MyComponent // reference to the MyComponent function
props: { children: "oh my" },
...
}
기본 HTML Tag 엘리먼트가 아닌 컴포넌트 엘리먼트가 있는 경우는, type
필드는 컴포넌트 함수를 참조하고, 함수는 JSON으로 직렬화 할 수 없다.
구체적으로 직렬화될 리액트 엘리먼트를 발견할때
div
와 같이 기본적인 HTML 태그 경우
=> 이미 직렬화 완료
서버 컴포넌트인 경우
=> props와 함께 서버 컴포넌트 함수를 호출하고 그 결과를 직렬화 함 (UI 렌더링하는 return 부분을 직렬화 한다는 내용인듯? 어차피 Server Component는 인터랙티브한 코드가 없으니 서버에서 Pre-Rednered가 가능하잖아?)
클라이언트 컴포넌트인 경우
=> "module reference object" 이라는 새로운 형태로 직렬화가 가능한 Reference 형태로 직렬화를 대체함
React Server Component는 컴포넌트 함수 대신 직렬화 가능한 "reference"라는 moduel reference object라고 하는 리액트 엘리먼트의 type
필드에 새로운 값을 도입했다.
예를들어서 ClientComponent
엘리먼트는 아래와 같은 모습이다.
// ClientComponent.client.jsx
export default function ClientComponent({ children }) {
return (
<div>
<h1>Hello from client land</h1>
{children}
</div>
)
}
// module reference objeft
{
$$typeof: Symbol(react.element),
// The type field now has a reference object,
// instead of the actual component function
type: {
$$typeof: Symbol(react.module.reference),
// ClientComponent is the default export...
name: "default",
// from this file!
filename: "./src/ClientComponent.client.js"
},
props: { children: "oh my" },
}
그런데! Client Component Function을 직렬화 가능한 Module Object로 바꾸는건 어디서 바꾸는걸가? 바로 번들러다!
여튼저튼 "module object"로 다시 넘어와서 아래 예시 코드를 한번 살펴보자.
// ClientComponent.client.jsx
export default function ClientComponent({ children }) {
return (
<div>
<h1>Hello from client land</h1>
{children}
</div>
)
}
// ServerComponent.server.jsx
export default function ServerComponent() {
return <span>Hello from server land</span>
}
// OuterServerComponent.server.jsx
// OuterServerComponent can instantiate both client and server
// components, and we are passing in a <ServerComponent/> as
// the children prop to the ClientComponent.
import ClientComponent from './ClientComponent.client'
import ServerComponent from './ServerComponent.server'
export default function OuterServerComponent() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
위와 같은 코드가 있다고 할때, OuterServerComponent
함수를 직렬화 하게 되면 아래와 같은 JSON Tree가 생성된다.
{
// The ClientComponent element placeholder with "module reference"
$$typeof: Symbol(react.element),
type: {
$$typeof: Symbol(react.module.reference),
name: "default",
filename: "./src/ClientComponent.client.js"
},
props: {
// children passed to ClientComponent, which was <ServerComponent />.
children: {
// ServerComponent gets directly rendered into html tags;
// notice that there's no reference at all to the
// ServerComponent - we're directly rendering the `span`.
$$typeof: Symbol(react.element),
type: "span",
props: {
children: "Hello from server land"
}
}
}
}
결과적으로 Client Component를 module reference 라는 새로운 타입을 적용해서 직렬화 작업을 하게되면, 서버에서 아래와 같이 보이는 React Tree 브라우저로 전송하게 된다.
브라우저는 서버로부터 위 사진과 같이 직렬화된 React Tree를 전달받게 되고, 이 전달받은 React Tree를 실제로 브라우저에서 렌더링하기 위해서 조율하기 시작한다.
이 과정에서 클라이언트 컴포넌트를 직렬화하기 위해서 사용한 "module referecnce"를 만날때마다, 설명서를 참조해서 실제 클라이언트 컴포넌트로 바꾸게 된다.
참고로 서버에서 클라이언트 컴포넌트를 "module reference object"로 바꿔준게 번들러였고, 이제 브라우저에서 이런 module reference를 실제 클라이언트 컴포넌트 함수로 대체하는 방법을 아는것도 번들러다.
결과적으로 브라우저에서 번들러가 서버로부터 내려받은 React Tree에서 module reference를 실제 클라이언트 컴포넌트로 바꾸게되면, 아래와 같이 HTML Native Tag와 클라이언트 컴포넌트로 리액트 트리가 재조정된다.
그런다음 평소와 같이 이 트리를 랜더링하고 DOM에 적용하면 화면이 보이게 된다.
아! 참고로 function은 직렬화가 불가능하다. 그러니 클라이언트 컴포넌트도 함수이니까 직렬화가 불가능하고, module reference를 사용한거다.