
Next.js Page Rendering
대략적으로만 알고있던 Next.js 의 렌더링 방식에 대해 조금 더 제대로 알아보고 싶어서 공부를 해봤다. 어떤 시점에서 빌드를 하고, 데이터를 받아와 화면을 그리고 hydration 까지 이루어지는지에 대한 전반적인 타임라인을 그려보고 이에 대한 차이점을 명확하게 이해를 하면 이번에 새로 들어가는 프로젝트에도 도움이 많이 될 것 같아서!
지금 Next.js 13 공식 문서를 번역하고 있긴하지만, 우선 12버전을 기준으로 공부를 했다. 내 블로그를 보는 사람은 별로 없지만.. 혹시 보고있다면, 지난번에 작성했던 CSR, SSR 에 대한 내용을 먼저 이해하고 이것들을 공부하면 더 좋을 것 같습니돠.
🖐🏻 CSR, SSR 그리고 Pre-rendering

1️⃣ SSG
SSG 는 Static Site Generation 의 약자로 Next.js 에서 페이지 생성 시 기본적으로 적용되는 설정이다. SSR과의 차이점은 클라에서 요청하는 시점이 아닌 빌드 시 페이지가 미리 생성이 된다.
Example for SSG
별다른 설정 없이도 SSG 생성이 되지만 data fetching 이 필요한 경우라면 getStaticProps 함수를 export 해서 함수 내에서 데이터를 받아 리턴하면 빌드 시 getStaticProps 가 실행되고 리턴되는 값을 컴포넌트에서 받아 페이지를 미리 렌더링 한다.
단, 빌드 시점에 페이지가 미리 생성되어 fetching-data 가 변경되더라도 새로 빌드하지 않으면 반영되지 않는다.
export default function SSRPage({ pokemon }: IProps){
return (
<main>
<Pokemon pokemon={pokemon}/>
</main>
)
};
export const getStaticProps: GetStaticProps = async() => {
const response = await axios.get('<https://pokemon>...');
return {
props : { pokemon: response.data.pokemon }
}
};
2️⃣ SSR
SSR 은 CSR과는 반대 개념으로 서버에서 페이지를 렌더링하고 클라이언트 측에 보내주는 방식이다. Next.js 에서 SSG 와 ISR 과 구분을 두는 이유는 렌더링 시점 때문인데, SSR은 사용자가 요청할 때 마다 그 시점에 페이지를 새롭게 렌더링하게 된다. 그래서 fetching data 가 빈번하게 변경될 때 사용될 수 있다.
Example for SSR
해당되는 파일에서 getServerSideProps 함수를 export 하면 클라에서 페이지 요청을 보낼 때 getServerSide 함수가 호출되고 그에 대한 response 를 SSRPage 에 전달해 렌더링 시켜 클라이언트에 전달한다.
export default function SSRPage({ pokemon }: IProps){
return (
<main>
<Pokemon pokemon={pokemon}/>
</main>
)
};
export const getServerSideProps: GetServerSideProps = async() => {
const response = await axios.get('<https://pokemon>...');
return {
props : { pokemon: response.data.pokemon }
}
};
3️⃣ ISR
Incremental Static Regeneratino 의 약자로, 빌드 시점에 페이지를 렌더링한 후에 설정된 시간에 따라 페이지를 새롭게 렌더링하는 방식이다. SSG 에 포함되는 개념으로 이해하면 좋을 것 같다. SSG 는 빌드 시점에 페이지를 생성해서 fetch-data 가 변경된다면 새롭게 빌드해야 하지만 ISR 은 일정 시간마다 페이지를 업데이트하는 방식이라 보면 되겠다.
Example for ISR
SSG 방식과 동일하게 getStaticProps 함수에서 fetching data 를 props 의 key 값으로 export 하고, revalidate 값으로 숫자를 리턴하면 설정 숫자(sec) 값에 따라 페이지가 새롭게 렌더링된다.
export default function ISRPage60 : GetStaticProps = async() => {
const response = await axios.get('<https://pokemon>...');
return {
props : { pokemon : response.data.pokemon },
revalidate : 60
}
}

확실히… 어느정도 공식문서를 제대로 완독을 하고 나서, 다시 정리하니 이제야 눈에 제대로 보이는 것 같다. 뭔가 렌더링에 대한 전반적인 것들이 하나씩 하나씩 쌓여나가는 느낌..! 하지만 이 와중에 TTFB, FP, FCP 등에 대한 개념은 또 부족하다고 느껴져서 이 부분에 대해서도 추가적으로 찾아봤다.
전반적인 내용에 대해서는 간단하게만 알아보고, 이것들에 대한 최적화 방법에 대해서는 또 다시…. 딥하게 파볼 생각이다. 일단 이번엔 간단하게만!
1️⃣ Time To First Bye (TTFB)
page 를 서버에 요청했을 때, 서버에서 응답으로 보내주는 데이터의 첫 번째 바이트(First Byte)가 도착하는 시점을 의미한다. 서버로 부터 데이터를 수신하는 데 소요된 시간이므로 주로 서버 성능을 보여주는 부분이 된다.

TTFB 3단계
1️⃣ TTFB 서버 요청
클라이언트가 도메인을 입력하고 웹 사이트를 방문했을 때, 클라이언트(브라우저)에서 서버 측으로 HTTP 요청이 전송된다. DNS 조회 시간이 TTFB 에 영향을 미칠 수 있으며, DNS 및 서버가 지리적으로 멀리 있는 경우라면 데이터가 이동하는 거리에 대한 지연 시간이 생길 수 있다.
2️⃣ TTFB 서버 처리
서버 요청 후 응답을 받은 서버는 이에 대한 요청을 처리하고 응답을 해야한다. 서버 처리에는 DB 처리, 스크립트, 캐싱되지 않은 파일들, 최적화 되지 못한 코드, 디스크 I/O 및 메모리 그리고 비효율적인 서버 리소스와 같은 여러 이유로 TTFB 속도 지연이 있을 수 있다.
3️⃣ 클라이언트 응답
서버 요청 및 처리 후 클라이언트 측으로 HTTP 요청 또는 처리된 TTFB가 다시 전송된다. 클라이언트 응답은 사용하는 인터넷의 영향도 받는다. 와이파이 또는 사용중인 ISP 속도에 의해 TTFB 속도도 영향을 받게 된다.
최적화 방법
TTFB 는 최종 페이지를 불러들이는 속도와 동일하지는 않지만 TTFB 속도가 빠르다면 그만큼 페이지 로드 시간이 줄어들 수 있다.

서버 로직의 최적화
DB 쿼리 최적화
더 빠른 DB 로의 마이그레이션
서버 하드웨어 업그레이드
(더 많은 RAM 또는 CPU)
2️⃣ FP / FCP
서버에서 화면을 그리기 위한 재료들을 받아왔으니, 화면을 그려나가는 과정에 대해서도 이제 알아보자. 비어있던 화면에 첫 번째 픽셀이 찍히는 순간부터, 유저가 화면과 상호작용을 하기까지의 과정에 대한 것들이다.

💄 FP : First Paint

첫 번째 픽셀이 스크린에 페인팅 된 시간을 의미한다. 예를 들자면 background-color 가 적용된 순간이겠다.
🎨 FCP : First Contentful Paint

FCP는 First Contentful Paint 의 약자로, 페이지가 로드되기 시작하고 컨텐츠의 일부가 화면에 렌더링 되는, 즉 DOM 에 속해있는 컨텐츠 조각이 스크린에 페인팅 되는 순간을 의미한다. 예를 들어 하나 혹은 몇 개의 텍스트, 혹은 이미지 등을 이야기 한다.

FP / FCP 속도 측정
크롬의 timing API로 추적하거나 GA 같은 도구로 리포팅이 가능하다.
import { getFCP } from 'web-vitals';
getFCP(console.log); // FCP 를 이용 가능하게 되면 바로 측정 및 기록
최적화 방법
React 에서는 첫 FCP 를 빠르게 하기 위해 Next.js 처럼 서버가 첫 렌더를 먼저 그리고, hydration 으로 이벤트를 입히는 방법을 흔히 쓴다. Skeleton UI 를 이용해 FP 를 빠르게 해서 유저가 무언가 진행이 되고 있음을 인지하게할 수도 있겠다. 레이아웃 자체를 먼저 잡아주는 방식이라 로딩 스피너보다 조금 더 의미있는 렌더링이 되어 유저 경험을 높인다.
3️⃣ FMP : First Meaningful Paint
FMP 는 First Meaningful Paint 의 약자로, “Meaningful” 이라는 단어에 주목하면 그 뜻이 유추가 된다. 즉, 브라우저가 유저가 관심있어할 만한 컨텐츠를 페인트해서 나타낼 때 까지의 시간을 의미한다.

FMP 속도 측정
FMP가 의미하는 순간은 브라우저 API가 추적할 수 없다. 일반적으로 FMP가 의미하는 순간의 측정은 Hero Elements가 정의됨으로써 구분이 가능하다. 즉 사용자가 메인으로 사용하게될 컨텐츠(Hero elements)가 페인팅 되는 순간이 FMP가 의미하는 순간과 같다고 보면 되며, Lighthouse나 WebPageTest는 단지 렌더링이 발생할 때 가장 큰 레이아웃의 변화를 가지는 엘레멘트를 hero Element로 추정하고 FMP를 추정할 수 있을 뿐이다.
4️⃣ LCP: Largest Contentful Paint
LCP 는 Largest Contentful Paint 의 약자인다. 페이지에서 가장 용량이 큰 컨텐츠가 표시되는 시점을 나타내며, Core Web Vitals의 지표로 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정된다. 페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는데 사용된다.
위에서도 언급했듯이 FMP 측정은 매우 복잡하므로 가장 용량이 큰 컨텐츠가 렌더링 되는 시기인 LCP 가 주요 컨텐츠가 로드되는 시기로 판단되도록 권장이 되고 있다.

LCP 속도 저하의 일반적인 원인
서버가 느림
Client-Side Rendering
JavaScript 와 CSS 의 렌더 블로킹
브라우저에서 컨텐츠를 렌더링하려면 HTML 마크업을 DOM 트리로 구문 분석을 해야한다. HTML 파서는 외부 Style-sheet(
<link rel=”stylesheet”>) 또는 동기 JavaScript 태그 (<script src=”main.js”>) 를 만나면 일시중지 된다.스크립트와 스타일 시트는 모두 FCP 를 지연시키고 결과적으로 LCP 까지 지연시키는 렌더링 차단 리소스이다. 중요하지 않은 JavaScript 및 css 를 지연시키면 웹 페이지의 메인 컨텐츠 로드 속도를 빠르게 할 수 있다.
느린 리소스 로딩 시간
CSS 또는 JavaScript 차단 시간 증가는 성능 저하에 직접적 영향을 끼치지만, 다른 많은 유형의 리소스 로드에 걸리는 시간도 페인트 시간에 영향을 준다. LCP 에 영향을 끼치는 유형은
<img>,<svg>내부의<image>,<video>,url()함수를 통해 로드된 background-image 가 있는 요소 와 텍스트 노드 또는 기타 인라인 수준의 텍스트 요소를 포함하는 블록 수준 요소가 있다.
최적화 방법
느린 서버 응답 시간 해결
렌더링을 block 하는 JS 및 CSS 해결
✂️ CSS block 시간 단축
✂️ Javascript block 시간 단축
느린 리소스 (
img,svg,video, ...) 로드 시간 해결이미지 최적화 및 압축
중요한 리소스 preload :
rel="preload"텍스트 파일 압축
캐싱
클라이언트 사이드 렌더링의 경우
자바스크립트 최소화
서버사이드 렌더링 사용
pre-rendering 사용
5️⃣ TTI: Time to Interactive

TTI는 사용자가 사이트를 접근하고 상호 작용 할 수 있을 때 까지의 시간을 의미한다. 즉, JS와 같은 리소스들을 얼마나 빠르게 처리하는지 판단해 점수로 매긴다. TTI 점수를 높이기 위해서는 JS 파일의 코드를 개선해 파일 자체의 용량을 줄여주거나 코드 스플리팅으로 필요한 리소스들만 요청하도록 해야한다.
6️⃣ FID: First Input Delay

최초 입력 지연은 유저가 페이지와 처음 상호작용할 때부터 해당 상호작용에 대한 응답으로 브라우저가 실제로 이벤트 핸들러 처리를 시작하기까지의 시간을 측정하는 지표이다.
FCP 와 TTI 사이에 상당한 시간이 소요될 때(메인 스레드가 바쁠 때) 만약 유저가 페이지와 상호작용을 시도하면, 클릭 수신 시기와 메인스레드가 응답할 수 있는 시기 사이에 지연이 발생할 수 있다. 브라우저가 작업을 실행하는 동안 입력이 발생되므로, 입력에 응답하기 위해서는 작업이 완료될 때 까지 기다려야 하며, 이것을 우리는 FID 라고 한다.

성능 최적화
FID 는 이벤트 처리에서 “지연” 부분만 측정한다. 이벤트 처리 시간 자체나 브라우저에서 이벤트 핸들러를 실행한 이후 UI 를 업데이트하는 데 걸리는 시간은 측정하지 않는다. FID 는 클릭, 탭 및 키 누름과 같은 개별 작업의 입력 이벤트에만 초점을 맞추며, 스크롤 및 확대/축소와 같은 연속작업에 대해서는 애니메이션에 가까우므로 성능에 대해서는 개별적으로 평가된다.
FID 가 떨어지는 주요 원인으로는 과도한 JavaScript 실행이 있다. JavaScript가 웹 페이지에서 구문 분석, 컴파일 및 실행 방식을 최적화한다면 FID가 감소한다. 코드 분할 및 작업 세분화와 같은 것들을 도입한다면 FID가 눈에 띄게 개선된다.
7️⃣ TBT: Total Blocking Time
브라우저는 메인 스레드에서 JavaScript 를 실행하는 동안에는 대부분의 다른 이벤트 입력에 응답할 수 없다. 즉, 메인 스레드가 사용 중이라면 브라우저는 유저 상호작용에 응답할 수 없다는 의미이다.
총 차단 시간(TBT) 메트릭은 메인 스레드가 입력 응답을 막을 만큼 오래 차단되었을 때 FCP와 TTI 사이의 총 시간을 측정한다. 즉, TBT는 첫 컨텐츠가 눈에 보이는 시간과 컨텐츠를 상호 작용 할 수 있는 시간까지의 차이를 점수로 매기는 것이다. FCP가 빠르고 TTI가 느리다면 사용자 경험 측면에서 좋지 못하기 때문에 TBT는 중요한 점수이다.

총 차단 시간은 CPU 작업이 완료되는 데에 50ms 이상 걸리는 시간을 측정한다. FCP 이전의 긴 CPU 작업은 차단으로 간주되지 않는다. 표시되는 사용자 인터페이스가 없으면 차단되는 사용자 상호작용이 없기 때문이다. 위 그림은 기본 스레드 타임라인의 예를 보여준다.
8️⃣ CLS: Cumulative Layout Shift
누적 레이아웃 이동은 사용자가 예상하지 못한 레이아웃 이동을 경험하는 빈도를 수치화해서 시각적 안정성을 측정할 때 중요한 중심 메트릭이다. CLS 가 낮으면 우수한 유저 경험을 보장하는 데에 도움이 된다.

예상하지 못한 레이아웃 이동은 다음과 같은 상황에 발생한다.
크기가 정해지지 않은 이미지의 사용
동적으로 크기를 조정하는 광고 및 위젯
동적으로 주입된 컨텐츠
대체 크기보다 작게 렌더링 되는 폰트
애니메이션 및 전환
최적화 방법
이미지 및 비디오 요소에는 항상 width & height 속성을 포함시킨다.
유저 상호 작용에 대한 응답을 제외하고는 기존 컨텐츠 위에 컨텐츠를 삽입하지 않는다.
레이아웃 변경을 야기시키는 속성의 애니메이션보다 전환 애니메이션을 사용한다.
🚀 마무리
공부의 마지막은 늘 왜 이제껏 대충만 보고 제대로 더 들여다보지 않았는가로 끝나는 것 같다. 하지만 초벌을 하고, 한번씩 다시 돌아보면서 깊이를 늘려나가면 언젠가는 제대로 이해할 수 있는 날이 오지 않을까? 화면이 렌더링 되는 부분에 대해서는 이런식으로 계속 퍼즐들을 맞춰 나가보려한다. 예전에 보이지 않았던 것들이 하나씩 눈에 들어오고 하나씩 이해가 가기 시작한다는게 요즈음 내가 느끼는 제일 큰 즐거움인 것 같다.
조만간 웹 바이탈 관련해서, 그리고 성능 최적화와 관련해서는 더 깊게 공부를 해봐야겠다.
![[월간 이트루] 3월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1711895194426/f6a22b40-ffd3-4b11-a25b-e1b040b11853.png)
![[월간 이트루] 2월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1709129901980/5a2af3c5-8f6e-4f07-9c4f-1b43bef27ae9.png)


![[월간 이트루] 1월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1706708547437/51af577d-21f9-4256-8939-95c577a58abc.jpeg)