카테고리 없음

[모리딥] app router, react server component

Ahyeon, Jung 2024. 3. 10. 22:00

Next.js는 파일 시스템을 기반으로 라우팅을 구현하며, 그 방법에는 app router와 page router가 존재한다. 기존 Next.js는 /page 폴더에 모든 웹 사이트의 페이지 컴포넌트를 처리했다. 파일 구조만으로 라우팅을 손쉽게 할 수 있기 때문에 편리한 개발이 가능했다. 그리고 Next.js 13부터 App router가 등장했다. App router는 상태를 유지하고 Rerendering을 방지하면서 자체 경로 안의 컴포넌트와 UI를 쉽게 공유할 수 있다.

 

App router

react server components를 기반으로 구축되어 있으며, 서버 데이터 가져오기에 맞춰져 있다. 전체 애플리케이션에 대한 전반적인 라우팅 및 탐색을 처리한다. URL을 기반으로 올바른 페이지를 렌더링하고 페이지간 전환을 관리한다. 경로 이동시 페이지를 다시 렌더링하지 않고, SPA처럼 URL만 업데이트하고 next는 변경된 세그먼트만 렌더링한다.

 

react server component를 사용하기 때문에 getServerSideProps, getStaticProps, getInitialProps와 같은 메서드는 더이상 사용하지 않으며 빌드 시에 데이터 패칭이 이루어지고 캐싱된다.

 

pages/_app.js와 pages/_document.js를 대체하여 app/layout.js를 사용하며, 루트 레이아웃에서만 <html>, <body> 태그를 포함한다. page.js에서는 params와 searchParams를 props로 전달받는다. error.js는 해당 라우팅 영역에서 사용되는 공통 에러 컴포넌트이며 특정 라우팅별로 서로 다른 에러 UI를 렌더링하는 것이 가능해진다.

 

Page router

클라이언트 중심 라우팅으로 애플리케이션의 개별 페이지 내에 라우팅을 처리한다. 이를 통해 동적 경로를 만들고, 특정 콘텐츠를 렌더링하기 위한 경로 paramater에 접근할 수 있다.

 

React server component

기존 리액트 컴포넌트는 클라이언트인 브라우저에서 sanitize-html 등의 라이브러리를 다운로드 및 실행해야 하는 등 자바스크립트 번들 크기를 차지해야했다. 해당 라이브러리를 서버에서 실행해 실행한 결과와 컴포넌트 렌더링 결과물만 클라이언트에 제공한다면 번들크기를 줄일 수 있다. 또한 클라이언트 컴포넌트에서는 백엔드 리소스에 대한 직접적인 접근이 불가능해 REST API를 사용해야했다. 그리고 lazy로 일일이 감싸지 않는 이상 항상 코드 분할을 해도 되는 컴포넌트인지 판단해야했으며, 연쇄적으로 발생하는 클라이언트와 서버의 요청을 대응하기 어려웠다.

 

서버 사이드 렌더링은 정적 콘텐츠를 빠르게 제공하고, 서버에 있는 데이터에 손쉽게 제공할 수 있는 반면 사용자의 인터렉션에 따른 다양한 사용자 경험을 제공하긴 어렵다. 클라이언트 사이드 렌더링은 사용자의 인터렉션에 따라 다양한 것을 제공할 수 있지만 서버에 비해 느리고 데이터를 가져오는 것도 어렵다. 이 두 구조의 장점을 취하고자 리액트 서버 컴포넌트가 등장하였다. 서버 컴포넌트란 하나의 언어, 하나의 프레임워크, 그리고 하나의 API와 개념을 사용하면서 서버와 클라이언트 모두에서 컴포넌트를 렌더링할 수 있는 기법을 의미한다. 일부 컴포넌트는 클라이언트에서, 일부 컴포넌트는 서버에서 렌더링된다.

서버 컴포넌트

요청이 오면 그 순간 서버에서 딱 한 번 실행될 뿐이므로 상태를 가질 수 없다. 따라서 리액트에서 상태를 가질 수 있는 useState, useReducer 등의 훅을 사용할 수 없다. 

렌더링 생명주기도 사용할 수 없다. 한번 렌더링 되면 그걸로 끝이기 때문이다. 따라서 useEffect, useLayoutEffect를 사용할 수 없다.

앞의 두 가지 제약사항으로 인해 effect나 state에 의존하는 사용자 정의 훅 또한 사용할 수 없다. 다만 effect나 state에 의존하지 않고 서버에서 제공할 수 있는 기능만 사용하는 훅이라면 충분히 사용 가능하다.

브라우저에서 실행되지 않고 서버에서만 실행되기 때문에 DOM API를 쓰거나, window, document에 접근할 수 없다.

데이터베이스, 내부 서비스, 파일 시스템 등 서버에만 있는 데이터를 async, await으로 접근할 수 있따. 컴포넌트 자체가 async한 것이 가능하다.

다른 서버 컴포넌트를 렌더링하거나 div, span, p 같은 요소를 렌더링하거나, 혹은 클라이언트 컴포넌트를 렌더링할 수 있다.

클라이언트 컴포넌트

브라우저 환경에서만 실행되므로 서버 컴포넌트를 불러오거나, 서버 전용 훅이나 유틸리티를 불러올 수 없다. 

그러나 앞의 코드에서 본 것처럼 서버 컴포넌트가 클라이언트 컴포넌트를 렌더링하는데, 그 클라이언트 컴포넌트가 자식으로 서버 컴포넌트를 갖는 구조는 가능하다. 그 이유는 클라이언트 입장에서 봤을 때 서버 컴포넌트는 이미 서버에서 만들어진 트리를 가지고 있을 것이고, 클라이언트 컴포넌트는 이미 서버에서 만들어진 그 트리를 삽입해서 보여주기만 하기 때문이다. 따라서 서버 컴포넌트와 클라이언트 컴포넌트를 중첩해서 갖는 구조로 설계하는 것이 가능하다.

이 두 가지 예외 사항을 제외하면 일반적인 리액트 컴포넌트와 같다. state와 effect를 사용할 수 있으며, 브라우저 API도 사용할 수 있다.

공용 컴포넌트

이 컴포넌트는 서버와 클라이언트 모두에서 사용할 수 있다. 공통으로 사용할 수 있는 만큼, 당연히 서버 컴포넌트와 클라이언트 컴포넌트의 모든 제약을 받는 컴포넌트가 된다.

 

기본적으로 리액트는 모든 컴포넌트를 공용 컴포넌트로 판단하며, 클라이언트 컴포넌트라는 것을 명시적으로 선언하기 위해 "use client"를 작성한다. 

 

  1. 서버가 렌더링 요청을 받는다. 서버가 렌더링 과정을 수행해야 하므로 리액트 서버 컴포넌트를 사용하는 모든 페이지는 항상 서버에서 시작된다. 즉, 루트에 있는 컴포넌트는 항상 서버 컴포넌트다.
  2. 서버는 받은 요청에 따라 컴포넌트를 JSON으로 직렬화한다. 이때 서버에서 렌더링할 수 있는 것은 직렬화해서 내보내고, 클라이언트 컴포넌트로 표시된 부분은 해당 공간을 플레이스홀더 형식으로 비워두고 나타낸다. 브라우저는 이후에 이 결과물을 받아서 다시 역직렬화한 다음 렌더링을 수행한다. 
  3. 브라우저가 리액트 컴포넌트 트리를 구성한다. 브라우저가 서버 스트리밍으로 JSON 결과물을 받았다면 이 구문을 다시 파싱한 결과물을 바탕으로 트리를 재구성해 컴포넌트를 만들어 나간다. 클라이언트 컴포넌트를 받았다면 클라이언트에서 렌더링을 진행할 것이고, 서버에서 만들어진 결과물을 받았다면 이 정보를 기반으로 리액트 트리를 그대로 만들 것이다. 그리고 최종적으로 이 트리를 렌더링해 브라우저의 DOM에 커밋한다.

서버 사이드 렌더링과 서버 컴포넌트의 차이

서버 사이드렌더링

응답받은 페이지 전체를 HTML로 렌더링하는 과정을 서버에서 수행 후 결과를 클라이언트에 보내줌 

클라이언트에서 하이드레이션 과정을 거쳐 서버의 결과물을 확인하고 이벤트를 붙이는 등 작업 수행

서버 컴포넌트

서버에서 렌더링할 수 있는 컴포넌트는 서버에서 완성해서 제공받음


새로운 fetch 도입과 getServerSideProps, getStaticProps, getInitialProps의 삭제

정적 렌더링과 동적 렌더링

캐시와 mutating, 그리고 revalidating

스트리밍을 활용한 점진적인 페이지 불러오기