테크톡

Next.js가 프레임워크인 이유2: app router와 서버 컴포넌트

Ahyeon, Jung 2024. 8. 13. 02:25

확실히 프레임워크는 좋긴 좋다. 하지만 맥락을 알고 사용해야한다.

 

Next.js는 pages router를 사용하다가 리액트 13부터 app router가 등장하였다.

라우팅 방식의 변화

Pages Router의 경우 /pages/fridge.tsx와 같이 pages 디렉토리 안에 파일을 넣어두면 해당 파일명을 기반으로 라우팅이 된다. 

 

App Router의 경우 /app/my-page/page.tsx와 같이 pages 디렉토리 안에 pathname을 가진 디렉토리를 만들고, page.tsx를 만들면 폴더 구조를 기반으로 라우팅이 된다.

왜 변화하였나?

디렉토리 안에 page.tsx를 담아야했기 때문에, 오히려 pages가 직관적으로 라우팅을 명시한다고 생각했다. 하지만, 폴더 구조로 변경하면서 같은 폴더 안에 layout.tsx, loading.tsx, error.tsx 등의 컴포넌트를 담을 수 있다. 즉, 개별 페이지 그룹마다 다른 레이아웃이나 에러 컴포넌트를 보여줄 수 있다.

 

App Router가 진짜로 변화시킨 것

사실 라우팅 방식의 변화는, 그냥 파일 컨벤션을 Next.js가 정해준 것이라고 볼 수 있다. 진짜 변화는 서버 컴포넌트가 핵심이다.

Pages Router의 데이터 패칭

Next.js의 Pages Router는 데이터 패칭 메서드를 이용하여 4가지 방식의 렌더링을 제공한다.

SSG getStaticProps 빌드 시에 데이터 패칭
ISR getStaticProps + revalidate revalidate 시간에 따라 패칭한 데이터 제공
SSR getServerSideProps 요청시 서버에서 데이터 패칭
CSR useEffect 혹은 SWR, React Query 클라이언트 로드 이후 패칭

 

pages router에서의 데이터 패칭은 생각보다 간단하게 진행된다. 요청이 들어오면 메서드의 유무를 판단해서 실행해주고, 해당 데이터를 서버 컴포넌트의 props로 넣어주는 방식이다.

 

// 이거 아님 단순 구현 예시

function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps({ req, res }) {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()// Pass data to the page via props
return { props: { data } }
}
export default Page
import { useRouter } from "../libs/Router";
const About = (props: any) => {
const { push } = useRouter();
return (
<div style={{ textAlign: "center" }}>

// ...
</div>
);
};
export default About;
About.getServerSideProps = async (data: any) => {
data.hi = "ssr data";
return data;
};

 

해당 컴포넌트를 renderToString을 통해 문자열하여 응답값으로 보낸다.

Pages Router의 한계

이 과정에서 리액트 개발자들의 불만이 생겼다. 페이지를 전부 서버 사이드 렌더링으로 내려줘야하는데, 이 과정에서 데이터 패칭 API가 어떠한 문제로 블로킹되면 서버에서 페이지가 렌더링되지 않는 것이다. 각각의 getStaticProps, getServerSideProps, getInitialProps 데이터 패칭 메서드는 Suspense를 사용할 수 없었다. 사실상 데이터 패칭 중에 fallback을 보여주기 위해 Suspense를 사용하는건데, 해당 데이터 패칭은 서버에서 진행되므로 사용할 수 없는게 당연했다.

 

즉, Pages router는 데이터 패칭을 거친 이후에 전체 html을 보내버려서 서버에서 막히면 끝나버린다.

서버 컴포넌트의 도입 

이러한 이슈를 해결하기 위해 App Router가 등장하였다. 정확히 말해, 클라이언트 컴포넌트와 서버 컴포넌트가 도입되었다. 예전에 데이터 패칭 API가 없어진 건 fetch의 캐시를 사용하기 위함이다,,는 이야기를 했지만 맥락이 잘못된 이야기였다. 캐시는 다른 메서드의 역할이다. 어쨌든 다시 정정하자면 App Router는 Suspense, 즉 컴포넌트별로 fallback을 보여주기 위해 클라이언트 컴포넌트를 렌더링하고, 완성된 서버 컴포넌트를 붙이는 방식으로 구현되었다.

 

즉, App Router는 서버 컴포넌트와 클라이언트 컴포넌트를 구분하고, hydrate한다.

renderToFibeStream

renderToStream은 전체를 문자열화시키기 때문에, renderToFileStream으로 변경됨.

서버 컴포넌트와 클라이언트 컴포넌트

 

서버에서 하는 일

1. 리액트를 렌더하여 서버 컴포넌트를 React Server Component Payload(RSC Payload)로 만든다.

이 때 클라이언트 컴포넌트의 빈자리 역시 placeholder로 비워둔다.

 

2. RSC Payload와 클라이언트 컴포넌트 JavaScript instructions를 서버에서 HTML로 렌더한다.

클라이언트에서 하는 일

1. 받은 HTML을 preview로 즉각적으로 보여준다.

2. 클라이언트 컴포넌트와 서버 컴포넌트 트리를 재조정하는데 RSC Payload를 사용하고, DOM을 업데이트한다.

3. hydrate를 통해 인터랙션을 생성한다.

 

// 에러 바운더리나 로딩 상태 시각화해보고 싶다

Reference

https://youtu.be/XdiMjKSCOfc?si=g4oGZn1ZPgz22bBh

How React server components work: an in-depth guide (plasmic.app)