HMR(Hot Module Replacement)의 동작 원리

HMR(Hot Module Replacement)는 코드를 수정했을 때 웹 페이지 전체를 새로고침하지 않고도 변경된 부분만 즉시 업데이트하는 기술이다.


HMR의 동작 방식

1️⃣ 파일 변경 감지 (File Watcher 기반)

  • Webpack이나 Vite가 파일 시스템(File System)을 감시하여 소스 코드가 변경되는지 확인한다.

    파일 감지 방식

    Webpack: fs.watch() 또는 chokidar 사용

    Vite: esbuild의 감지 기능과 chokidar 결합

2️⃣ 변경 사항을 클라이언트에 전달 (WebSocket 기반)

파일이 변경되면 서버(Webpack Dev Server, Vite Dev Server)가 WebSocket 메시지를 보내고, 브라우저(클라이언트)는 메시지를 수신하여 변경된 모듈 정보를 확인한다.

3️⃣ 클라이언트에서 새 코드로 교체

ESM (ES 모듈): import.meta.hot.accept() 사용

=> Vite는 ESM(ES 모듈) 기반으로 변경된 모듈만 다시 요청하여 적용

Webpack HMR Runtime: module.hot.accept() 사용

=> Webpack은 변경된 모듈 ID를 기반으로 내부 번들에서 찾아 교체

  • 클라이언트에서 변경된 모듈을 다시 불러오고, 기존 모듈을 대체한다.

4️⃣ UI 상태 유지 (Fast Refresh)

  • react-refresh 같은 Fast Refresh 플러그인를 활용해 UI 상태를 유지하며 변경된 컴포넌트만 업데이트할 수 있다.

Fast Refresh 플러그인: 컴포넌트의 변경 사항을 감지하고, 기존 상태를 유지한 채 새 코드만 교체하는 역할


live-server와의 차이

html, css, js 같은 정적 파일의 변경 사항을 감지하고 브라우저에 즉시 반영하기 위해 live-server를 사용해 본 적이 있을 것이다.

Live Server

Live Server는 fs.watch() 또는 chokidar 같은 파일 감지 기능을 사용하여 파일 변경을 감지하고, 변경이 발생하면 브라우저에게 새로고침 명령을 내린다.

즉, 변경된 파일뿐만 아니라 전체 HTML 문서를 다시 불러오기 때문에 속도가 느리고, 기존 상태가 모두 초기화되는 문제가 발생한다.

HMR

반면, HMR은 Live Server와 다르게, 전체 페이지를 다시 로드하지 않고 변경된 모듈듈만 교체하는 방식으로 동작한다.

또한, React/Vue 같은 프레임워크를 사용할 경우, 상태(State)를 유지하면서 UI 변경을 적용할 수 있다.


Vite와 Webpack의 HMR 구조 및 동작

Webpack과 Vite는 모두 HMR(Hot Module Replacement)을 지원하지만, 내부적인 구조동작 방식이 다르다.

  • Webpack은 번들링된 모듈을 다시 로드하는 방식
  • Vite는 네이티브 ESM을 활용하여 변경된 모듈만 다시 로드하는 방식

Webpack은 번들링 과정이 포함되어 속도가 느릴 수 있고, Vite는 ESM(ES 모듈) 기반으로 동작하여 더 빠르게 반영된다.


Next.js의 HMR

Next.js에서는 Vite 없이도 자체적인 개발 서버(next dev)가 실행되며, HMR이 자동으로 적용된다.

그럼 뭘로 HMR이 적용되는 거지?

=> Next.js는 내부적으로 Webpack 기반의 HMR 시스템을 사용하여 변경 사항을 자동으로 반영한다.

Next.js가 Webpack을 최적화해서 사용하기 때문에 기존 Webpack HMR보다 훨씬 빠르게 변경 사항 반영 가능하다.

Next.js의 Webpack 최적화

1️⃣ 자동 코드 분할 (Automatic Code Splitting)

Next.js는 페이지 단위로 번들링하므로, 불필요한 코드 로드가 없다. Webpack의 기본 번들링 방식보다 훨씬 가벼운 초기 로드를 제공한다.

2️⃣ 서버 사이드 렌더링(SSR) 최적화

Webpack 번들링 시 SSR에 불필요한 코드(클라이언트 전용 코드)를 제외하여 불필요한 빌드 시간을 단축함.

import dynamic from "next/dynamic";

// 이 컴포넌트는 클라이언트에서만 실행되며, SSR 시 렌더링되지 않음
const ClientOnlyComponent = dynamic(
  () => import("../components/ClientOnlyComponent"),
  {
    ssr: false,
  }
);

3️⃣ 파일 기반 라우팅 시스템

Webpack을 직접 설정할 필요 없이, Next.js의 파일 기반 라우팅 시스템이 자동으로 Webpack 번들을 생성하고 최적화한다.

각 페이지(pages/ 디렉토리 내부의 파일)는 자동으로 엔트리 포인트로 간주되며, Webpack이 최적화된 방식으로 번들링을 수행한한다.

엔트리 포인트: 애플리케이션의 실행이 시작되는 파일을 의미한다.

Webpack은 엔트리 포인트를 기준으로 모든 의존성을 분석하여 하나의 번들 파일을 생성한다.

4️⃣ 변경된 모듈만 다시 빌드

Next.js는 페이지 단위 번들링을 활용하여 불필요한 재번들링을 최소화한다.

또한, Next.js는 Webpack의 기본 HMR이 아니라 React Fast Refresh를 사용하여 HMR을 최적화한다. React Fast Refresh로 인해 컴포넌트 상태를 유지하면서 UI만 업데이트되며, 코드 변경은 최소한의 부분만 교체될 수 있는 것이다.


하지만 Next.js는 Webpack을 최적화해 사용했음에도 Vite보다 HMR이 빠르진 않다.

Next.js는 왜 Vite로 전환하지 않을까?

1️⃣ Next.js는 SSR을 지원하는데, Webpack은 SSR에 최적화된 번들링 환경을 제공한다.

Vite는 기본적으로 클라이언트 사이드 번들링을 위한 도구이므로, SSR에 Webpack만큼 최적화되지 않는다.

2️⃣ Next.js의 안정성

Webpack은 강력한 빌드 최적화 기능을 제공해 대규모 애플리케이션에 적합하다.

Next.js의 내장 최적화 시스템과도 잘 맞아 안정적인 성능을 보장한다.