Server Component 알아보기 (2) - 렌더링 과정과 사용 시의 이점?
🔄 서버 컴포넌트의 렌더링 과정
지난 시간에서 서버 컴포넌트의 탄생 과정을 살펴봤다면, 이번 시간에는 본격적으로 서버 컴포넌트가 어떤 식으로 렌더링되어 사이트에 반영되는지를 살펴보는 시간을 가지도록 하겠다.
(부족하거나 틀린 내용에 대한 의견은 언제든지 감사히 받겠습니다.)
- 서버가 요청을 받고 브라우저에 보낼 응답값 만들기 시작
우선 서버에서는 브라우저에 렌더링시킬 문서를 만들 작업을 진행한다.
여기서 React의 root 컴포넌트부터 만들게 되는데, 서버에서 렌더링을 진행해야 하므로 사실상 root 컴포넌트부터가 서버 컴포넌트로 생성이 된다.
server-components-demo/server/api.server.js at main · reactjs/server-components-demo
- 서버가 루트 컴포넌트 엘리멘트를 JSON으로 직렬화
이후, root 컴포넌트는 현재 요청받은 페이지에서 HTML 태그들을 포함한 컴포넌트들을 직렬화하기 시작한다.
직렬화? (Serialization)
특정 데이터나 객체 등을 다른 컴퓨터 환경에 저장하고 이를 다시 꺼내와 재구성하거나 재활용할 수 있는 포맷 등으로 변환 혹은 복원하는 과정을 가리킨다.
이해가 잘 안 갈 수 있으니 아래의 예시 코드를 보자.
// 아래의 Test 컴포넌트를 만들었다고 하자. function Test() { return <div>배고파</div>; } // 해당 코드는 React에서 createElement를 통해 생성이 된다. React.createElement(Test, { children: "배고파" }); // 그 코드를 언제든지 재구성할 수 있도록 json의 형태로 변환한다. { $$typeof: Symbol(react.element), type: Test props: { children: "배고파" }, ... }
위와 같이 서버에서는 루트 컴포넌트부터 차례대로 해당 페이지의 컴포넌트들을 json화(JSON.stringify()
)하는, 직렬화하는 과정을 거치게 된다.
다만, 위의 과정은 서버 컴포넌트일 경우에만 해당되며 클라이언트 컴포넌트는 좀 다른 형태로서 json화되어 들어간다.
"use client" function ClientComp (result:boolean) { return (<div>하이고</div>) } { $$typeof: Symbol(react.element), type: { // 아래와 같이 'module.reference'를 쓴다. 이는 클라이언트 컴포넌트에서 쓰인다. $$typeof: Symbol(react.module.reference), // name에는 우리가 흔히 쓰는 export default를 가리킨다. name: "default", // filename에는 클라이언트 컴포넌트의 파일 경로를 나타낸다. filename: "./src/ClientComp.js" }, props: { children: "하이고" }, }
왜 이런 과정을 거치게 되는 걸까?
이전 글에서 우리는 서버 컴포넌트의 내용에 대해서 아래와 같은 내용을 이야기했다.
- 서버에서 단 한 번만 렌더링이 이루어진다.
- 클라이언트에서 사용되는 JavaScript 코드가 없다.
- 이 말은 즉, 사이드 이펙트나 동작 등이 필요한 코드를 적용할 수 없으므로 React Hook 또한 사용할 수 없다.
위와 같은 내용을 기반으로 보면 서버 컴포넌트에서는 순수하게 요소 만을 담은 컴포넌트만을 담게 될 것이다.
클라이언트 컴포넌트는 동작을 담당하는 코드인 만큼, 안에 요소 외에도 다양한 핸들링 함수들이 존재하게 될 것이며, 그 중에는 분명 외부의 요소를 가져와서 내부 함수에서 받아 동작하도록 만든 클로저로 구성된 함수들도 있을 것이다.
이럴 경우 해당 컴포넌트의 범위에서만 만들어져야 하는데, 외부 변수 등을 포함해야 하는 상황에서 범위를 벗어나게 되는 상황이 발생하다보니 직렬화하기가 어려워진다.
이런 이유로 클라이언트 컴포넌트에서는 컴포넌트의 직렬화를 하기 보단, 해당 위치에 어떠한 컴포넌트가 배치될 것인지를 알려주기 위해 ‘placeholder’같은 역할을 하는 module reference
객체가 생성된다.
이러한 과정은 ‘react-server-dom-webpack'와 같은 번들러 등을 통해서 적용된다.
서버 컴포넌트와 클라이언트 컴포넌트를 배치할 때 참고해야 할 사항이 두 가지가 있다.
하나는 서버 컴포넌트에 props로 클라이언트 컴포넌트를 담을 경우 위와 같은 이유로 직렬화 구조가 망가질 수 있다는 점이다.
그리고 다른 하나는 , 클라이언트 컴포넌트에서는 서버 컴포넌트를 직접 return
할 수 없다는 점이다.
위에서 이야기했듯 클라이언트 컴포넌트는 module reference
가 먼저 배치되는 식이다.
그런데, 이 컴포넌트에서 return
으로 서버 컴포넌트를 배치하면 어떻게 될까?
이 경우, 바운더리가 클라이언트 컴포넌트에 귀속되어 처리된다.
그래서 클라이언트 컴포넌트 속에 서버 컴포넌트를 배치해야 할 때, 클라이언트 컴포넌트에 귀속되지 않게 하려면 클라이언트 컴포넌트에서 해당 컴포넌트를 {children}
으로 받아 내려주도록 해야한다.
이렇게 root에서부터 마지막 자식 컴포넌트까지 서버 컴포넌트와 클라이언트 컴포넌트를 json으로 만드는 걸 그림으로 보면 아래와 같다.
- 브라우저에 렌더링된 내용을 전달
브라우저는 이제 서버로부터 직렬화된 JSON을 받고 이를 브라우저에 반영되도록 렌더 트리를 구축하고 배치한다.
즉, 아까 전에 직렬화한 내용을 역직렬화(JSON.parse()
)하게 된다.
이 과정 자체도 번들러를 통해서 진행하게 되는데, 구축 중에 module reference
인 객체를 발견하게 된다면 이제 여기에 배치할 클라이언트 컴포넌트가 무엇인지 확인하고 이 객체를 컴포넌트로 바꾼 후 계속해서 다음 과정을 진행하게 된다.
이러한 과정을 거치고 난 뒤, 트리 배치가 완료되면 DOM에 커밋하는 것으로 렌더링이 끝난다.
👍 서버 컴포넌트의 이점
이러한 서버 컴포넌트의 과정을 통해 어떠한 이점을 우리는 볼 수 있을까?
hydrate할 컴포넌트량의 축소
SSR 방식에서는 컴포넌트 요소들을 다 배치한 다음에 hydration을 통해 컴포넌트들에 이벤트 핸들러 등을 배치하게 된다.
하지만, 이 과정 속에서 상호작용이 필요한 컴포넌트의 children으로 받아지는 UI만 컴포넌트들도, 해당 컴포넌트와 얽혀있어 같이 hydrate되어 드러나게 된다.
서버 컴포넌트를 이용하면 위에서 이야기했듯 이런 UI만 그려지는 컴포넌트들은 json으로 담아져서 html 문서에 바로 반영되며, 핸들링 등이 동작하는 컴포넌트들과 엮어지지 않으므로 hydrate할 컴포넌트의 양이 줄어든다.
컴포넌트별로 가능한 데이터 페칭
SSR 방식에서는 Next.js 기준으로
getServerProps()
와 같은 방식은 각 페이지에서 서버 데이터를 받아서 데이터를 내려주는 식으로 적용된다.그러다보니 데이터를 리페칭할 경우, 이를 다시 반영해야 하는 만큼 페이지 전체에 렌더링이 적용되는 부하가 발생한다.
서버 컴포넌트의 방식은 이처럼 페이지가 아닌 각 서버 컴포넌트에서 데이터 페칭을 하고 이를 반영해주기 때문에 렌더링에 관한 부하를 줄일 수 있다.
줄어드는 JavaScript 번들 크기
서버 컴포넌트는 json화되어 문서에 바로 반영되어 서버가 브라우저에 보내준다. 이는 이제 클라이언트 컴포넌트에 필요한 JavaScript 데이터만을 다운받게 함으로서 웹 페이지가 받게되는 번들의 양을 줄여준다.
서버 컴포넌트는 실질적으로 사용된 지 얼마 되지 않아, 아직도 많은 논의가 오가고 있다.
하지만 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 활용하면 프로젝트를 좀 더 가볍고, 적절하게 개선하는 데에 도움이 될 수 있는 만큼 적극적으로 사용을 시도해보자.
🔖 참고 자료
Where do React Server Components fit in the history of web development?
Making Sense of React Server Components