최근 코딩테스트랑 면접준비에 진이 빠져 블로그 글을 많이 못 올렸습니다. 한 군데라도 최종합격하면 더할 나위 없이 소원이 없겠군요. ㅠ
이번 글은 Next.js의 제일 큰 특징인 사전 렌더링을 다룹니다.
참고: 인프런 - 한 입 크기로 잘라먹는 Next.js
Next.js는 React 라이브러리를 기반으로 한 프레임워크라고 생각하면 됩니다.
이때 라이브러리와 프레임워크의 차이가 헷갈리실 수 있는데, 이런 차이가 있습니다.
이를테면, React에서는 페이지 라우팅을 할 때 React Router나 Tanstack Router 등 별도 라이브러리 중 적당한 친구를 골라서 사용하면 됩니다. 대신 React 자체적으로는 라우팅 기능을 제공하지 않습니다. 구현할 방법은 다양하지만, 선택하는 건 개발자의 몫이죠.
하지만 Next.js 사용 시, 자체적으로 제공하는 Page/App Router를 통해 페이지 라우팅을 구현하는 것이 권장됩니다. 대신 별도 설정 없이 바로 사용 가능하다는 장점이 있습니다.
그 외에 Next.js에는 이미지나 폰트의 용량 최적화, TypeScript 기본 지원, API Routes를 통한 백엔드 서버의 기능 구현 등등 기능이 추가되어 있습니다. 그러므로 프로젝트를 단기간에 진행해야 할 때는, 주어진 기능이 많고 정해진 틀만 지키면 되는 Next.js가 훨씬 효율적입니다.
기존 글에 리액트는 Client Side Rendering으로 작동한다고 언급을 했었는데요,

간단히 요약하자면 클라이언트에서 직접 화면을 렌더링하는 방식이였죠.
우선 서버는 브라우저에 내용이 없는 index.html 파일을 보내고, 브라우저가 이를 렌더링합니다. 일단 빈 화면만 보이겠죠.
하지만 그 이후 서버가 브라우저에 JS 파일을 묶은 하나의 번들 파일인 React App을 보냅니다. 브라우저는 React App을 실행하여 콘텐츠를 렌더링하게 됩니다.
초기 접속만 되면, 페이지 이동이 빠르고 쾌적하다는 장점이 있었습니다. 페이지를 이동할 때마다, 서버를 거치지 않고 React App만 실행하면 되니까요.
하지만 초기 접속을 할 때, 요청 시작부터 화면 렌더링 시점까지 시간이 오래 소요된다는 단점이 있습니다. 이를 First Contentful Paint(FCP)라고도 부릅니다. 일단 빈 index.html 파일을 받고, 서버에서 React App도 받고, 이걸 뒤늦게 렌더링하는 과정에서 초기 시간이 다소 소요됩니다.
넥스트는 이러한 CSR의 장점은 유지하면서, FCP가 느린 단점을 보완하기 위해 서버 측에서 사전 렌더링을 하는 기능을 지원합니다.
이때 렌더링은, 앞선 CSR처럼 브라우저에 내용을 그린다는 의미가 아닌, 서버가 JS 코드를 HTML로 변환한다라는 뜻임에 유의합시다.

유저가 초기 접속을 하면, 빈 껍데기인 index.html만 보내주는 리액트와 다르게, 넥스트에서는 서버가 사전에 내용이 채워진 HTML 파일을 응답하도록 설정할 수 있습니다.
이 시점에서 사용자가 화면을 볼 수 있으므로, 기존 리액트에 비해 FCP가 훨씬 빨라진다는 장점이 있습니다.
물론 HTML 파일만 있으면 버튼 클릭과 같은 상호작용적인 요소가 먹히지 않을 겁니다. 이때 넥스트는 React 기반이라는 점 다시 기억해 봅시다. 그러므로 JS 번들 파일인 React App도 여전히 존재합니다.
FCP 이후 서버는 브라우저에 React App을 보내게 되고, 이후 React App이 기존 HTML 코드와 연동됩니다. 이를 황무지와 같은 HTML에 물을 준다는 느낌으로, Hydration이라고도 부릅니다. 이렇게 초기 접속으로부터 사용자가 첫 상호작용을 할 수 있기까지의 시간을 TTI(Time to Interact)로도 부릅니다.
/cv 페이지 /projects 페이지
┌──────────────┐ ┌──────────────┐
│ Header │ │ Header │ <- 유지
├──────────────┤ --> ├──────────────┤
│ │ │ │
│ CV │ │ Projects │ <- 너만 교체
│ │ │ │
├──────────────┤ ├──────────────┤
│ Footer │ │ Footer │ <- 유지
└──────────────┘ └──────────────┘
+ 이 과정에서 서버 요청은 이루어지지 않음
페이지 이동을 하는 경우 역시 기존 리액트와 마찬가지로, 서버까지 가지 않고 Hydration된 React APP을 통해 브라우저가 컴포넌트를 교체하는 CSR 방식으로 이루어집니다. 즉 페이지 이동이 빠르고 쾌적하다는 React CSR의 장점이 유지된다는 것도 중요한 특징이겠죠.
가끔가다 Next.js에선 Hydration Error라는 친구를 마주칠 수 있습니다. 이 에러는 서버에서 생성한 HTML과, 클라이언트에서 React가 렌더링한 결과가 일치하지 않을 때 발생합니다.
window, document, localStorage가 포함되어 있거나<p> 안에 <div>를 넣는 등 HTML을 잘못 중첩했거나 (서버는 수정 안하는데, 브라우저는 자동 수정해서 불일치 발생)할 때 발생합니다. 기본적으로 서버는 컴포넌트의 JSX 리턴분에 있는 HTML을 사전 렌더링하니까, 거기에 문제되는 값이 들어가면 안 됩니다.
즉 랜덤 값이나 window 같은 브라우저 전용 코드는, 서버가 사전 렌더링하지 않는 useEffect를 통해 처리하면 이런 오류를 막을 수 있습니다. 서버가 렌더링 안하니까, 불일치가 뜰 일도 없겠죠.
// 서버엔 window가 없으므로, JSX 리턴문에 넣으면 안됨
function Component() {
return <div>{window.innerWidth}</div> // 서버에는 window가 없음!
}
// useState로 분리해 놔야 함
function Component() {
const [width, setWidth] = useState(null);
useEffect(() => {
setWidth(window.innerWidth); // 브라우저에서만 실행
}, []);
return <div>{width || '로딩중...'}</div>;
}
재미있게도 Next.js에서 사전 렌더링을 할 수 있는 방법도 하나가 아닙니다. 좀 많습니다.