THE DEVLOG

scribbly.

Next JS 치트 시트

2025.05.10 00:41:21

Next js를 위한 기초 개념들

브라우저의 렌더링 원리

브라우저가 화면을 렌더링하며 HTML 문서, CSS 스타일 시트, JavaScript 스크립트의 세 가지 형태의 파일을 사용하며, 아래와 같은 렌더링 순서(렌더링 파이프라인)을 거친다.

여기서 DOM은 화면의 요소를 의미하며, 화면의 요소를 메모리상에서 자바스크립트 객체 형태로 표현하고 자바스크립트가 이를 조작할 수 있는 API를 제공하기에 DOM이라 부른다.

HTML → DOM
CSS → CSSOM
DOM + CSSOM → Render Tree
→ Layout → Paint → Compositing → 화면에 출력
  • HTML 파싱 → DOM 트리 생성

    • 브라우저는 HTML 문서를 한 줄씩 파싱하면서 **DOM(Document Object Model)**이라는 트리를 만든다.
    • <div>, <p> 같은 요소가 노드가 되어 계층적 구조를 이룬다.
  • CSS 파싱 → CSSOM 생성

    • <style> 태그나 외부 CSS 파일을 읽고 CSSOM(CSS Object Model) 트리를 생성한다.
    • 이 CSSOM은 스타일 규칙들을 트리 구조로 파싱한 것이다.
  • DOM + CSSOM → Render Tree 생성

    • DOM은 콘텐츠 구조, CSSOM은 스타일 정보.
    • 둘을 결합해서 Render Tree(화면에 실제로 보일 요소들만 포함됨)를 만든다.
    • 예: display: none 요소는 Render Tree에 포함되지 않음.
  • Layout (Reflow)

    • 각 요소의 크기와 위치를 계산함.
    • 뷰포트 기준으로 "어디에 무엇을 어떻게 배치할지" 결정됨.
  • Painting

    • Render Tree의 각 노드를 실제 픽셀로 변환하는 과정.
    • 색, 그림자, 이미지 등을 그려냄.
  • Compositing (합성)

    • 복잡한 레이어(예: z-index, transform 등)는 별도로 처리됨.
    • 여러 레이어를 GPU가 조합해 최종 화면을 만든다.

CSS 등을 통해 사이즈 등이 변경되면 Layout → Paint → Compositing의 리플로우 단계가 발생하고, 색상 등이 변경되면 Paint → Compositing의 리페인트 단계가 발생한다.

리액트와 Virtual DOM

리액트는 DOM을 직접 조작하지 않고, Virtual DOM이라는 중간 레이어를 통해 화면을 갱신한다.

웹 애플리케이션에서 상태(state)나 props가 변경되면, 그에 따라 UI도 바뀌어야 한다.
일반적으로는 이러한 변경을 DOM에 직접 반영해야 하지만, DOM 조작은 느리고 복잡하다.

리액트는 이를 해결하기 위해, 렌더링 직전에 Virtual DOM이라는 불변(immutable) 객체 트리를 새롭게 생성한다. 이후 이전 Virtual DOM과 새로 생성된 Virtual DOM을 비교(diffing)하여 변경된 부분만 추려낸다. 변경점만을 실제 DOM에 최소 단위로 반영함으로써, 불필요한 DOM 조작을 줄이고 효율적인 업데이트를 수행한다.

  1. 상태/props가 바뀜

  2. 새로운 Virtual DOM 트리를 만듦 (JS 객체 형태)

  3. 이전 Virtual DOM과 diffing (JS에서 비교 연산)

  4. 바뀐 부분만 실제 DOM에 반영

  5. 이후부터는 브라우저 렌더링 파이프라인

    • DOM 변경 → Render Tree 재계산 → Layout → Paint → Composite

Virtual DOM은 아래와 같은 방식으로 만들어진다.

  1. JSX 문법을 통해 작성

    const element = ```Hello````;`

  2. Babel이 JSX문법을 React 앱에 맞는 형태로 변경

    const element = React.createElement("h1", null, "Hello");

  3. React.createElement함수가 객체 트리 형태의 Virtual DOM을 생성

{
  type: 'h1',
  props: {
    children: 'Hello'
  }
}

 

CSR

React는 CSR(Client Side Rendering) 방식으로 작동한다. CSR 방식은 HTML을 서버에서 굳이 만들지 않는다. '비어있는 HTML' 파일과 이를 조작하는 자바스크립트 파일을 클라이언트에 전달하며, 이를 이용해 동적으로 화면을 렌더링한다.

CSR 방식은 사용자와 적극적으로 인터랙션하며, 새로고침이 극단으로 줄어드는 장점이 있다. 하지만 CSR 방식은 아래와 같은 문제가 있다.

  • SEO 한계: 비어있는 초기화면을 이용하기에 크롤링 봇 등은 비어있는 화면을 크롤링하고, 이로 인해 검색엔진에 노출되지 않는 등의 문제가 발생할 수 있다.
  • 긴 초기 로딩시간: CSR 앱을 초기 로딩하기 위한 JS 번들의 크기는 기본적으로 정적 화면에 비해 크다. 번들 크기가 커질수록 네트워크 지연의 영향을 크게 받으며, 더 나아가 기능이 복잡할수록 JS 번들을 파싱하고, 실행하고, 화면을 그리는 시간이 추가적으로 발생한다.
  • 높은 성능최적화 난이도: 긴 초기 로딩시간 문제와 더불어 JS 번들링, 파싱 및 실행 등 많은 영역에서 개발자의 최적화를 필요로 한다. 모듈 번들링, 플러그인 패턴, 비동기 처리 등. 성능 최적화가 이루어지지 않으면 CSR은 본질적으로 SSR, SSG보다 성능 문제에서 자유로울 수 없다.

SSR, SSG, ISG

CSR이 클라이언트에 빈 화면과 큰 JS 번들을 전달하는 방식이라면, SSR(Server Side Rendering)은 서버에서 페이지를 렌더링하여 한 후, 렌더링된 화면과 작은 JS 번들을 전달하는 방식이다. 아래와 같은 이점이 있다.

  • 빠른 최초 콘텐츠 표시(First Contentful Paint): 서버에서 완성된 HTML을 전송하기 때문에 브라우저는 자바스크립트가 모두 로드되기 전에 사용자에게 시각적인 콘텐츠를 먼저 보여줄 수 있다. 이는 TTI(Time to Interactive)는 느릴 수 있어도, 유저의 체감 성능은 크게 향상된다.
  • SEO 최적화에 유리함: SSR은 서버에서 HTML이 완성된 상태로 전송되기 때문에, 검색 엔진 크롤러가 페이지의 전체 콘텐츠를 직접 읽을 수 있다. 이는 CSR 방식에서 발생하는 '빈 화면' 문제를 근본적으로 해결하며, 검색 결과 노출과 페이지 인덱싱 측면에서 큰 이점을 제공한다.
  • 페이지 단위 초기 로딩 성능 우수: CSR에 비해 초기 번들 사이즈가 작고, 필요한 데이터만 서버에서 가져와 렌더링하기 때문에 네트워크 환경이 불안정한 상황에서도 비교적 안정적인 초기 화면 표시가 가능하다. 사용자가 빠르게 페이지 내용을 확인할 수 있으므로, 이탈률을 줄이는 데 효과적이다.
  • 복잡한 페이지일수록 효율적: 특히 데이터 의존성이 높은 페이지, 예를 들어 사용자 인증에 따라 내용이 달라지는 대시보드나, 초기 데이터가 많아야 의미가 있는 리스트 페이지 등에서 SSR은 데이터가 완전히 준비된 상태로 화면을 전달할 수 있으므로 사용자 경험을 크게 향상시킬 수 있다.
  • JS에 의존하지 않는 접근성: 브라우저가 JS 실행을 차단하거나 실패한 상황에서도 SSR 방식은 최소한의 콘텐츠 전달이 가능하므로 접근성이 개선된다. 이는 노후 장비, 저사양 기기 사용자 또는 JS 비활성화 환경에서도 유용하다.

특히 SSR 방식은 초기 렌더링 속도에서도 CSR보다 우위를 보여주는 경우가 많은데, CSR에서는 초기 렌더링 이전에 네트워크를 통해 자바스크립트 번들을 불러오고, 이를 파싱하고 계산하는 시간이 길어지는 반면, SSR 방식은 이러한 시간이 줄어들기 때문이다. SSR 방식에서는 렌더링을 수행하는 위치와 JS 파일을 저장하는 위치가 물리적으로 가까우며, 이는 네트워크 성능에 직접적인 영향을 미친다.

**SSG(Static Site Generation)**는 빌드 시점에 모든 페이지를 HTML로 미리 렌더링하여, 정적 파일로 만들어두는 방식이다. 전통적인 방식의 웹 브라우저가 사용하는 방식이다. 사용자는 CDN에 저장된 HTML을 즉시 받아본다.

  • 가장 빠른 최초 콘텐츠 표시(First Contentful Paint): SSG는 빌드 시점에 HTML을 완성해놓고, 사용자가 접속하면 CDN에서 해당 정적 파일을 바로 전송한다. 자바스크립트 번들보다 훨씬 빠르게 HTML이 도달하므로, FCP 성능이 다른 방식보다 우수하다.
  • 트래픽 폭주에도 안정적인 응답: 모든 페이지가 미리 생성된 정적 파일이기 때문에, 서버가 실시간으로 렌더링할 필요가 없다. 정적 파일은 CDN에 캐시되어 전 세계 어디서나 빠르게 제공될 수 있으며, 이는 트래픽이 몰려도 서버 부하 없이 대응할 수 있다는 장점을 제공한다.
  • SEO 최적화에 유리함: SSR과 마찬가지로, SSG도 완성된 HTML을 반환하기 때문에 검색 엔진 크롤러는 페이지 콘텐츠를 완전히 파악할 수 있다. JavaScript 실행을 기다릴 필요 없이 콘텐츠를 바로 인식할 수 있어, 인덱싱과 검색 노출에 효과적이다.
  • 서버리스 구조로 운영 가능: SSG는 서버에서 동적으로 페이지를 생성할 필요가 없기 때문에, 서버 인프라 없이 CDN 기반의 정적 호스팅만으로도 전체 사이트를 운영할 수 있다. 이는 유지보수 비용을 줄이고 보안 위험도 크게 낮춘다.
  • 페이지 간 전환 속도 우수: SSG 기반의 페이지는 이미 완성된 HTML을 갖고 있기 때문에, 클라이언트 측 라우팅을 적용하면 초기 로딩 이후의 페이지 전환 속도도 매우 빠르며, 사용자 체감 성능이 뛰어나다.

SSG는 빌드 시에 화면을 렌더링한 후, 이를 CDN에 캐싱하여 저장하기 때문에 사용자는 렌더링 시간을 느끼지 못한다. 하지만 SSG는 데이터가 변경되었을 때 이를 반영하지 못하는 문제가 있으며, 이에 따라 ISG(Incremental Static Regeneration)을 병행하게 된다.

ISG는 사용자가 특정 페이지에 최초로 접속하게 되면 이를 SSR로 서빙한 후, 서빙한 페이지는 SSG 형태로 CDN에 캐싱한다. 캐싱된 사이트는 Revalidate를 통해 폐기할 수 있다.

  • 초기 렌더링 속도는 SSG와 동일: 생성되지 않은 페이지에 접근하면 SSR처럼 다소 느리게 반응하지만, 생성된 페이지에 접근하는 경우에는 FCP 및 LCP 등 페이지 응답 속도는 순수 SSG 방식과 동일하게 빠르다. 유저는 여전히 "빠른 초기 콘텐츠 표시"를 경험할 수 있다.
  • 점진적인 페이지 생성: SSG는 모든 페이지를 빌드 시점에 생성해야 하지만, ISR은 미리 정의된 일부 페이지만 정적으로 만들고, 나머지는 요청이 있을 때 생성 후 캐시로 저장한다. 수천, 수만 개의 페이지를 가진 사이트에서도 빌드 시간을 획기적으로 줄일 수 있다.
  • 자동 캐시 갱신: revalidate 값을 통해 설정한 시간 이후에 페이지에 대한 요청이 들어오면, 백그라운드에서 새로운 HTML 파일을 다시 생성한다. 이후 요청부터는 새로운 HTML이 제공되어, 서버 부하 없이도 데이터가 최신 상태로 유지된다.
  • 데이터 변경 시 즉각적인 반영 가능: Webhook, API 호출 등과 연계해 특정 조건에서 강제 revalidate를 할 수 있으므로, CMS나 외부 데이터가 변경될 때 필요한 페이지만 선택적으로 갱신할 수 있다.
  • SSR보다 트래픽 대응이 강력함: SSR은 요청마다 렌더링이 일어나지만, ISR은 한번 생성된 페이지를 CDN에 저장하기 때문에, 트래픽 폭주에도 정적 자산처럼 대응 가능하다.

React에서 SSR을 구현하려면?

React는 본래 클라이언트 렌더링에 기반한 라이브러리이기 때문에, SSR이나 SSG, ISR과 같은 서버 기반 렌더링 전략을 직접 구현하려면 상당한 개발 리소스가 요구된다. 특히, 서버를 직접 구성하고, 라우팅을 수동으로 정의하는 등 구조적인 복잡함이 뒤따른다.

  • 서버 구성부터 빌드 파이프라인,
  • 라우팅 설계,
  • 데이터 패칭 전략,
  • 캐시 무효화 시스템

Server Side Rendering (SSR)

1. Node.js 기반의 서버 구성

SSR을 구현하기 위해서는 먼저 Node.js 환경에서 expressfastify와 같은 서버 프레임워크를 사용하여 요청을 처리할 수 있는 웹 서버를 구성해야 한다.

import express from "express";
import ReactDOMServer from "react-dom/server";
import App from "./App";

const app = express();

app.get("*", (req, res) => {
  const html = ReactDOMServer.renderToString(<App />);
  res.send(`
    <!DOCTYPE html>
    <html><body><div id="root">${html}</div></body></html>
  `);
});

app.listen(3000);

2. 클라이언트에서 Hydration 처리

브라우저에서는 위에서 렌더링된 HTML에 React의 이벤트 처리를 결합하기 위해 hydrateRoot()를 사용하여 클라이언트를 초기화해야 한다.

import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("root"), <App />);

3. 번들링 및 빌드 설정

서버와 클라이언트 각각에 대해 별도의 Webpack 설정이 필요하며, Babel을 통해 JSX와 최신 문법을 트랜스파일링해야 한다. 예를 들어, webpack-ssr.config.jswebpack-client.config.js와 같은 빌드 설정 파일을 따로 구성해야 한다.

4. 라우팅 수동 구현

React Router를 사용하는 경우, SSR에 적합한 StaticRouter를 이용하여 요청 URL에 따라 컴포넌트를 렌더링하도록 직접 설정해야 한다. 라우트 별 데이터 패칭 로직 또한 수동으로 구현해야 한다.

Static Site Generation (SSG)

1. 정적 HTML 생성

ReactDOMServer의 renderToStaticMarkup()을 활용하여 컴포넌트를 정적으로 렌더링하고, 해당 HTML을 파일로 저장한다.

import fs from "fs";
import ReactDOMServer from "react-dom/server";
import App from "./App";

const html = ReactDOMServer.renderToStaticMarkup(<App />);
fs.writeFileSync("./out/index.html", `<!DOCTYPE html><html><body>${html}</body></html>`);

2. 동적 라우팅 대응

다수의 페이지를 지원하려면, 예를 들어 게시글 ID나 슬러그에 따라 각각의 페이지를 별도로 렌더링하여 HTML 파일로 저장해야 한다.

const slugs = ["post-1", "post-2"];
for (const slug of slugs) {
  const html = renderToStaticMarkup(<Post slug={slug} />);
  fs.writeFileSync(`./out/${slug}.html`, html);
}

3. 정적 파일 서빙

생성된 HTML 파일은 Express, Nginx 등의 정적 파일 서버를 통해 별도로 서빙해야 하며, 이 또한 운영 인프라를 직접 구성해야 한다.

Incremental Static Regeneration (ISR)

1. 기본 구조는 SSG와 동일

초기에는 renderToStaticMarkup()을 사용하여 정적 페이지를 생성하고, 일정 주기마다 해당 파일을 갱신할 수 있는 로직을 추가해야 한다.

2. 재생성 조건 정의

파일이나 캐시의 lastModified 시간을 기준으로 일정 시간이 경과한 경우, 백엔드에서 페이지를 다시 렌더링하고 HTML을 갱신한다.

if (Date.now() - lastGenerated > 60_000) {
  const html = renderToStaticMarkup(<Page slug={slug} />);
  cache.set(slug, html);
}

3. 백그라운드에서 캐시 갱신

Redis와 같은 외부 캐시 시스템을 통해 각 페이지의 상태를 관리하고, 재생성 주기 및 타이밍을 직접 설계해야 한다.

4. CDN 무효화 수동 처리

Cloudflare나 Fastly 등 CDN을 사용할 경우, 변경된 HTML을 반영하기 위해 별도의 Webhook이나 API를 통해 캐시를 무효화하거나 수동으로 배포를 트리거해야 한다.