티스토리 뷰

 

처음 서버 컴포넌트를 처음 접했을 때는 서버사이드 랜더링, 혹은 그 비슷한 것처럼 듣고 개발을 했습니다.

그러나 React Server Component(rsc)를 적극적으로 사용하다 보니 이것이 매우 큰 패러다임의 변화임을 알게 되었습니다.

리액트를 다루는 프론트엔드 개발자는 이것에 대해 더 공부하고 자세히 알아볼 필요가 있습니다.

 

React는 클라이언트 사이드 ui 라이브러리입니다. 모놀리틱 한 거대한 서비스를 작고 독립된 '컴포넌트'라는 조각으로

나눠서 조합을 통해 만들도록 합니다. 이 '컴포넌트'들을 모두 자바스크립트 함수입니다. jsx를 반환하는 함수죠.

브라우저에서 우리 js파일들을 다운로드하면 이 함수들을 실행해서 컴포넌트로 dom을 만들어 냅니다.

React는 이런 개념으로 모놀리틱 한 거대한 서비스를 효과적으로 그리고 효율적으로 만들고 유지보수 할 수 있게 만들었습니다.

하지만 React가 오픈소스로 세상에 공개되고 수많은 서비스가 만들어지면서 이에 따른 문제점도 명확해졌죠.

각자 생각하는 바가 있겠지만 RSC를 공부할 때는 RSC가 해결하려는 문제에 집중하는 게 좋을 것 같습니다.

문제는 총 3가지로 요약할 수 있습니다.

 

불편함 1. UX

 - 리액트는 클라이언트 사이드 랜더링을 사용합니다. 화면에 필요한 데이터를 가져오는 방법은 useEffect를 통해 특정 엘리먼트가 마운트 되고 나서 get요청을 보내고 응답을 가져와서 화면에 보여줍니다. 근데 리액트의 함수형 컴포넌트는 모두 동기식입니다. 그 말은 내부 로직에 의해 자식요소를 랜더링 하는데 blocking이 발생할 수도 있다는 말입니다. 예를 보겠습니다.

function Course() {
 return(
     <CourseWraper>
 		<CourseList />
 		<Testimonials />   
	 </CourseWraper>
 )
}

 

CourseList와 Testimonials컴포넌트를 감싼 CourseWraper컴포넌트를 반환하는 컴포넌트가 있습니다.

function CourseWrapper() {

    // Assume a Network Call, in real-life
    // you will handle it with useEffect
    const info = fetchWrapperInfo();
    
    return(
      <> </>
    )
}

각 컴포넌트가 랜더링 될 때 각자가 필요한 데이터만 가져와서 보여준다고 생각해 볼게요. 위의 경우엔 총 3개의 네트워크 요청이 생깁니다. CouseWrapper에서 한번, CourseList, Testimonials에서 각자 한 번씩.

이 경우 부모컴포넌트는 랜더링 된 이후에 데이터를 받아오기 시작하고 이 과정이 끝나기 전까진 자식컴포넌트의 랜더링과 api호출또한 지연됩니다. 연속해서 발생하는 api요청 때문에 하위 자식요소의 랜더링과 api요청에 waterfall이 발생합니다.

UX에 매우 안 좋은 영향이 생기겠네요. 이를 해결하기 위해 모든 네트워크 요청을 상위 부모 컴포넌트로 옮겨서 네트워크를 한 번에 처리할 수 있습니다. 

 

불편함 2. 유지보수

function Course() {
	
	// Assume a Network Call, in real-life
    // you will handle it with useEffect    
    const info = fetchAllDetails();
    
    return(
    	<CourseWrapper info={info.wrapperInfo}>
            <CourseList info={info.listInfo}/>
            <Testimonials info={info.testimonials}/>
        </CourseWrapper>
    )
 }

이렇게 해주면 client-server api call이 한번 발생하겠네요. 모든 데이터를 받아와서 props로 뿌려주기 때문에 waterfall 현상도 제거할 수 있습니다.

하지만 이 페이지가 점점 기능이 커지고 방대해진다고 생각해 보세요. 어느 순간 함수의 바디가 뚱뚱해지고

협업을 하는 다른 개발자는 어디서 어떤 네트워크가 호출되고 있는 건지 파악하기가 어려울 겁니다.

만약 Testimonials 컴포넌트가 필요 없어진다고 해볼게요. 다른 개발자는 <Testimonials /> 컴포넌트만 삭제하고 말 겁니다.

그리고 fetchAllDetails에서 가져오고 있는 info.testimonials는 잊어버리겠죠. 그때부터 우리 서비스에 over-fetching이 발생합니다.

waterfall을 해결하니 유지보수에 큰 문제가 생기는 패턴이 되었습니다.

불편함 3. 성능

세 번째는 성능문제입니다. 결국 클라이언트사이드 랜더링의 모든 코드는 자바스크립트 함수의 실행입니다.

리액트로 만든 프로젝트에 접속하면

<html>

  <body>

    <div id="root"></div>

    <script src="/static/js/bundle.js"></script>

  </ body>

</ html>

만 받아옵니다. 우리가 개발하고 화면에서 보는 모든 기능과 컴포넌트는 bundle.js에 포함되어 있습니다.

이 js번들에는 우리가 직접 작성한 코드 외에 서드파티 라이브러리와 그 의존성까지 모두 포함되어 있습니다.

그래야 우리가 라이브러리로 개발한 기능이 전부 이상 없이 돌아가겠죠.

서비스가 발전될수록 js번들도 계속 커집니다.

전통적인 클라이언트사이드 랜더링의 과정을 보겠습니다.

출처:&nbsp;https://www.joshwcomeau.com/react/server-components/

root div와 js번들만 다운받아서 js를 파싱하고 Render Shell을 해야 화면이 보입니다. 그리고 마운트 되고 나서야 api call을 하고 데이터가 주입된 콘텐츠를 보여주게 됩니다. FCP, TTI, LCP 같은 건 생각하지 말고 일단 맨 앞에 있는 Download JavaScript가 길어진다고 생각해 보세요. 유저는 Render Shell이 완료되기 전까지 흰 백지만 보고 있어야 하는데, 그 과정이 점점 늦어지겠죠?

js번들이 커지면 유저가 흰 화면을 넋 놓고 보고 있어야 되는 시간이 길어집니다.

"이거 되는 거야 마는 거야?"라고 생각하는 찰나의 순간에 이미 손가락은 뒤로 가기를 누를 겁니다. 

애써 찾아온 유저를 놓치게 되는 첫 번째 포인트입니다.

이 js번들 사이즈를 줄이고 FCP를 줄이는 것도 프론트엔드 개발자의 몫입니다. 

저는 작년에 이런 블로그를 작성했었어요.

https://pungwa.tistory.com/187

 

페이지 로딩 성능개선 gzip, lazy loading, code splitting

프론트엔드 로딩, 랜더링 성능을 전혀 생각하지 않고 개발된 프로젝트를 배포하려고 하니 이상하게 페이지가 느리고 버벅거리는 현상도 종종 나타났다. 그래서 성능에 관심을 가지게 되었고 메

pungwa.tistory.com

초기 js 번들 사이즈를 줄이는 여러 방법들이 있지만 전통적인 방식에서 결국 모든 유저는 모든 js번들을 다운로드 받아야한다.는 사실에는 변함이 없죠. 

Nextjs 같은 프레임워크를 사용하면 FCP를 앞당기기 위해 SSR(서버사이드랜더링)을 적용할 수도 있습니다.


Nextjs 서버 측에서 초기 페이지 Render Shell과정이 먼저 발생됩니다. 이때 완성된 html을 클라이언트에게 보내주면 화면에는

<div id="root"></div>가 아니라 실제 초기화면과 똑같은 html을 보여줄 수 있습니다. 

유저가 흰 화면을 보면서 "되는 거야 안되는 거야?"라고 생각하는 일을 막을 수 있겠네요. FCP가 빨리 집니다.

하지만 이와 같이 ssr을 적용한다고 해서 js번들이 줄어드는 건 아닙니다. 여전히 js번들을 다운로드하여야 하고 hydration과정을 거쳐야 이벤트 같은 상호작용이 dom에 생성되고 그제야 data fetch를 시작합니다. LCP시간에 차이는 없다고 볼 수 있습니다.

 

이와 같은 문제를 React Server Component가 해결하려 합니다.

서버컴포넌트는 서버에서 랜더링 됩니다. 그냥 받아들입시다. 원래 RSC가 존재하기 이전에도 서버에서 랜더링이 되었습니다.

서버컴포넌트 이전의 클라이언트 컴포넌트도 서버에서 랜더링이 되어왔습니다. 다만 서버에서 한 번, 클라이언트에서 또 한번 랜더링이 된 것입니다. 아무튼 서버컴포넌트는 서버에서만! 랜더링이 됩니다. 서버에서 랜더링 후 서버컴포넌트에서 fetch 함수로 네트워크 요청을 하면 해당 데이터를 가져옵니다. 이 코드는 완전히 서버에서만 동작하기 때문에 db에 연결해서 쿼리를 날릴 수도 있고, 백엔드 api 엔드포인트를 호출할 수도 있습니다. 

이런 특성이 어떻게 위 문제들을 해결하는지 천천히 볼게요.

해결책 1. UX

UX문제, 즉 네트워크 waterfall문제와는 어떤 관련이 있을까요? 클라이언트 컴포넌트는 유저의 브라우저에 마운트 된 후에야 네트워크 요청을 합니다. 즉 high latency를 가진 브라우저와 프론트엔드 서버가 html, css, js파일을 주고받은 후 마운트 후에 또다시 high latency를 가진 브라우저가 백엔드 서버와 통신을 해야 한다는 말입니다. 하지만 서버에서 랜더링 되는 서버컴포넌트가 백엔드 서버와 네트워크를 하는 경우에는 브라우저를 통하는 것보다 훨씬 적은 레이턴시로 통신이 가능하다고 합니다. 제가 이해하기로는 만약 Nextjs로 만든 프론트엔드 서버에서 서버컴포넌트를 랜더링하고 그 서버에 있는 db에 연결해서 쿼리를 날려 데이터를 가져온다면 비동기적인 네트워크 없이 데이터를 가져오지 않아도 되기때문에 waterfall이 완전히 사라진다고 이해했습니다. 하지만 저는 개인프로젝트에서 백엔드 서버를 따로 구축했습니다. 이런 경우에는 Nextjs 프론트엔드 서버와 백엔드 서버가 네트워크를 합니다. 

저는 클라이언트-서버 네트워크와 서버-서버 네트워크의 차이에 대해 잘 알지 못하지만 확실히 빠르다고 합니다. 빠르다는 말은 waterfall이 없어지는 건 아니지만 빠르기 때문에 전보다 낫다고 할 수 있습니다. 

 

해결책 2. 유지보수

위와 같은 이유로 우리는 유지보수가 용이한 각 컴포넌트에서 필요한 데이터에 책임을 지고 데이터를 가져오는 패턴을 포기할 필요가 없습니다. 각 서버 컴포넌트에서 직접 필요한 데이터를 가져오도록 코드를 작성할 수 있습니다. 결과적으로 데이터패칭은 데이터가 필요한 컴포넌트로 들어갑니다. 자연스럽게 서버컴포넌트가 트리의 상위에 위치하게 되고 데이터 패치가 필요치 않고 인터렉션이 필요한 컴포넌트는 하위로 위치하게 됩니다. 컴포넌트를 분리하는 명확한 기준이 생긴 것 같네요.

 

해결책 3. 성능

서버컴포넌트는 서버에서 랜더링 되어 데이터까지 가지게 되었습니다. React는 이 서버컴포넌트 트리를 직렬화하여 클라이언트에 보냅니다. 클라이언트의 React Renderer는 이 직렬화된 컴포넌트 트리를 재구성하여 리액트 컴포넌트 트리를 만들 수 있습니다. 즉, HTML이나 js코드 없이 직렬화된 트리만으로 화면에 컴포넌트를 그릴 수 있다는 뜻입니다. vercel에 이를 이해하기 좋은 이미지가 있습니다.

csr, ssr, server component로 구분하여 그 차이를 이해할 수 있습니다.

출처:&nbsp;https://vercel.com/blog/how-react-18-improves-application-performance
출처:&nbsp;https://vercel.com/blog/how-react-18-improves-application-performance

csr, ssr은 클라이언트에 html과 js코드를 보낸다는 것에 변함이 없습니다. html이 어느 정도 완성이 되어있느냐의 차이만 있죠.

하지만 RSC는 서버에서 랜더링 한 트리를 그대로 직렬화합니다. 그리고 보낼 준비가 완료된 컴포넌트를 즉시 클라이언트로 보냅니다. 이것을 Streaming이라고 하는데, Suspense와 함께 사용하면 멋진 서비스를 만들 수 있습니다. 아무튼, RSC는 이 직렬화된 컴포넌트 트리를 받아서 바로 화면에 트리를 만들어줍니다.

SSR과 비교해 보면 차이가 확실합니다.

프론트엔드 서버에서 Database Query과정을 거쳤기 때문에 LCP가 FCP와 동일하게 훨씬 빨라졌습니다.

즉, 유저는 처음부터 모든 데이터가 들어간 실제 화면을 볼 수 있게 됩니다. js번들도 없기 때문에 레이턴시에 영향을 줄 다른 네트워크도 없어서 빠를 겁니다. 근데 화면에 보이는 Download Javascript가 여전히 있네요? 

서버 컴포넌트는 직렬화된 트리로 랜더링 하기 때문에 자바스크립트 코드가 필요 없어서 js번들에 포함될 코드가 없다고 하지 않았나요?

맞습니다. 위 차트에 있는 Download Javascript는 서버 컴포넌트의 js코드가 아닙니다. 

현대의 웹앱은 유저 인터렉션이 필수입니다. 이벤트도 당연히 필요하고 effect도 필요합니다. 그리고 유저의 인터렉션에 반응할 state도 필수죠. 이것들은 서버컴포넌트를 사용할 수 없습니다. 당연합니다. state를 반영하려면 리랜더링이 필요합니다. 근데 서버컴포넌트는 서버에서 랜더링 되어 한번만 전송되기 때문에 리랜더링이 발생할 수 없습니다. 그래서 유저와 인터렉션이 있는 컴포넌트는 클라이언트 컴포넌트로 따로 만들어야 됩니다. 서버컴포넌트를 적용하려면 컴포넌트 구조를 섬세하게 고민할 필요가 있겠네요.

차트에 보이는 Download Javascript는 바로 이 클라이언트컴포넌트의 js코드입니다. 

 

저는 많은 부분을 서버컴포넌트로 작성하고 유저와 인터렉션이 필요한 컴포넌트(트리의 리프노드) , 즉 말단의 노드만 클라이언트로 작성하고 싶습니다. 하지만 만약 스코프가 넓은 state를 관리해야 한다면 어떨까요?

useState훅을 상위 컴포넌트에서 관리해야 하니 트리의 높은 부분에 클라이언트 컴포넌트가 생길 수도 있겠네요?

출처:&nbsp;https://www.joshwcomeau.com/react/server-components/

예를 들어 이런 트리가 있는데 Article에서 유저의 상태를 관리할 필요가 있다고 해볼게요. 그래서 Article컴포넌트가 클라이언트 컴포넌트가 되었습니다. 부모가 상태를 가지고 리랜더링 되면 자식컴포넌트도 리랜더링 됩니다. 즉 HitCounter, Discussion, Comment, Comment컴포넌트는 use client지시문을 사용하지 않았지만(서버컴포넌트로 작성하고 싶었지만) 리랜더링이 필요한 상황이 된 겁니다. 

이 경우에는 클라이언트 바운더리라는 게 생성됩니다.

결국 클라이언트 컴포넌트가 import 해서 랜더링 하는 모든 자식컴포넌트가 클라이언트컴포넌트가 되어버린 겁니다.

규칙이 생깁니다. 서버 컴포넌트는 클라이언트 컴포넌트를 포함할 수 있지만 클라이언트 컴포넌트는 서버 컴포넌트를 포함할 수 없다. 

이렇게 되면 해당 컴포넌트의 js코드가 모두 번들에 포함되어 의도한 성능개선을 제대로 할 수 없겠죠?

 

실제로 서비스를 개발하다 보면 이런 경우가 매우 많습니다. 어떻게 해야 할까요

클라이언트 컴포넌트가 상위에 와야 하는 경우에는 서버컴포넌트를 import 하지 않고 children을 랜더링 하도록 하여 RSC를 props로 넘기면 됩니다. 위 이미지를 예로 들면 Discussion, Comment, Comment컴포넌트를 

'use client'
import Hitcount from './Hitcount'

const Article = ({children}) => {
	...
reutrn 
      (<>
         <HitCount />
         {children}
      </>)
}

export default Article;
<Article>
  <Discussion />
</Article>

'use client'지시문이나 클라이언트 바운더리에 포함되지 않은 컴포넌트는 모두 서버컴포넌트가 됩니다.

이를 통해 js번들 크기를 줄이고 다양한 문제를 해결할 수 있었습니다.

 

 

참고자료

https://www.freecodecamp.org/news/how-to-use-react-server-components/

 

React Server Components – How and Why You Should Use Them in Your Code

In late 2020, the React team introduced the "Zero-Bundle-Size React Server Components" concept. Since then, the React developer community has been experimenting with and learning how to apply this forward-looking approach. React has changed how we think ab

www.freecodecamp.org

https://vercel.com/blog/how-react-18-improves-application-performance

 

How React 18 Improves Application Performance – Vercel

Learn how React 18's concurrent features like Transitions, Suspense, and React Server Components improve application performance.

vercel.com

https://www.joshwcomeau.com/react/server-components/

 

Making Sense of React Server Components

This year, the React team unveiled something they've been quietly researching for years: an official way to run React components exclusively on the server. This is a significant paradigm shift, and it's caused a whole lot of confusion in the React communit

www.joshwcomeau.com

https://tech.kakaopay.com/post/react-server-components/

 

React 18: 리액트 서버 컴포넌트 준비하기 | 카카오페이 기술 블로그

공식 릴리즈 전인 리액트 서버 컴포넌트에 대해 알아보고 준비해 봅니다.

tech.kakaopay.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2025/06   »
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
글 보관함