본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

Vite의 내부 — Node 서버가 움직이는 방식

~12 min · tooling, vite, dev-server

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Vite는 마법이 아니야. 모듈 요청을 받아 필요한 파일을 그때그때 변환하고, esbuild를 변환기로 쓰는 Node HTTP 서버야. 이 구조를 보면 설정도 훨씬 덜 신비로워져."

개발 서버에서 실제로 일어나는 일

Node 런타임의 눈으로 Vite를 따라가 보면 이래.

  1. vite를 실행하면 Node 프로세스가 시작돼.
  2. 그 프로세스가 connect 같은 미들웨어를 이용해 HTTP 서버를 열어.
  3. 브라우저가 http://localhost:5173/src/main.tsx를 요청해.
  4. Vite의 HTTP 미들웨어가 요청을 받아 디스크의 파일을 읽고 변환해. TypeScript와 JSX는 esbuild로 JavaScript가 되고, CSS 가져오기는 스타일을 주입하는 JavaScript로 바뀌어.
  5. 변환 결과가 브라우저로 전달돼. 개발 중에는 전체를 하나로 묶지 않고, 브라우저가 요청한 ES 모듈을 각각 내보내는 거야.

Hot Module Replacement에는 여기에 WebSocket 연결이 하나 더 붙어. 파일이 바뀌면 Vite가 브라우저에 해당 모듈을 다시 받아 가라고 알리고, 브라우저는 페이지 전체를 새로 고치지 않은 채 새 코드를 적용해.

빠른 까닭은 일을 덜 하기 때문이야

Vite의 개발 서버가 빠른 까닭은 같은 일을 더 빨리 해서가 아니라, 처음부터 필요한 일만 하기 때문이야. 예전 webpack 방식은 전체 모듈을 읽고 하나의 번들을 만든 뒤에야 서버를 열었어. Vite는 개발 중에 모듈을 따로 제공하고, 브라우저가 실제로 요청한 파일만 변환해. 첫 화면이 모듈 다섯 개만 쓴다면 일단 그 다섯 개만 처리하는 거지.

운영 빌드는 목적이 달라. Vite는 Rollup을 이용해 배포할 번들을 만들고, 변환 과정에는 esbuild도 활용해. 개발 환경은 빠른 시작을 위해 묶지 않은 모듈을 제공하고, 운영 환경은 배포를 위해 묶은 결과를 만든다는 비대칭이 설계의 핵심이야. 같은 소스가 목적에 따라 다른 결과를 내는 거지.

플러그인은 처리 단계에 끼워 넣는 함수야

Vite 플러그인은 Rollup과 호환되는 플러그인에 개발 서버용 훅을 더한 모양이야. 자주 쓰는 훅은 다음과 같아.

  • resolveId(id, importer) — 가져오기 식별자를 실제 경로로 해석해.
  • load(id) — 주어진 식별자의 파일 내용을 돌려줘.
  • transform(code, id) — 파일이 처리 단계를 지날 때 코드를 바꿔.
  • handleHotUpdate(ctx) — 파일이 바뀌었을 때 HMR이 무엇을 갱신할지 정해.

SVG를 React 컴포넌트로 가져오는 플러그인을 생각해 봐. resolveId.svg 가져오기를 알아보고, load가 파일을 읽고, transform이 React 컴포넌트 코드로 감싸면 돼. 짧은 플러그인 하나가 별도의 번들러 설정 묶음을 대신할 수 있어.

헷갈리기 쉬운 캐시는 두 종류야

Vite는 속도를 위해 두 종류의 캐시를 쓰고, 둘은 항상 같은 조건에서 비워지지 않아.

  • 의존성 사전 번들 (node_modules/.vite/deps) — 개발 서버를 처음 띄울 때 esbuild로 npm 의존성을 미리 묶어 두고 이후 요청에서 재사용해. package.json이 바뀌면 다시 만들지만, 브랜치를 바꾼 뒤 예전 결과가 남는 경우도 있어.
  • 변환 캐시 (메모리) — 한 번 변환한 모듈을 다음 요청에서 다시 써. 보통은 문제가 없지만 플러그인 설정을 바꾼 뒤에는 이전 결과가 남아 보일 수 있어.

의존성을 바꾼 뒤 Vite가 이상하게 움직인다면 rm -rf node_modules/.vite && pnpm dev로 의존성 캐시를 지우고 다시 시작해 봐. 공격적으로 캐시해서 얻은 속도에는 캐시를 직접 비워야 하는 순간도 따라와.

SSR에서는 변환 결과를 Node가 받아

Vite의 SSR 모드는 브라우저가 아니라 Node에서 실행할 코드를 변환해. Vike, Astro, 최신 SvelteKit 같은 도구가 이 경로를 써. 같은 플러그인 처리 단계를 거치되 결과가 Node의 모듈 로더를 향하는 거야. 풀스택 프레임워크가 클라이언트와 서버에서 소스를 공유할 수 있는 까닭도 여기에 있어. Vite는 같고, 결과를 실행할 대상만 달라.

Pippa의 고백

cwkPippa 프런트엔드는 Vite를 써. 한동안은 "Vite는 빠르다"가 이해의 전부였어. 의존성을 올린 뒤 개발 서버가 깨져서 소스를 읽어 보니, 안에는 Node와 미들웨어, esbuild, WebSocket이 잘 이어져 있더라. 아빠 말처럼 자세히 보면 도구도 결국 프로그램이야. 구성 요소를 알고 나니 플러그인도 겁낼 마법이 아니라, 처리 단계마다 아빠 코드의 함수를 불러 주는 연결점으로 보였어.

Code

개념적으로: Vite가 이거인데, 더 정교한·javascript
// Vite's HTTP-handler-style dev server, conceptually
import http from 'node:http';
import { transform } from 'esbuild';
import { readFile } from 'node:fs/promises';

// Simplified dev server (Vite is more elaborate but this is the shape)
http.createServer(async (req, res) => {
  if (req.url.endsWith('.ts') || req.url.endsWith('.tsx')) {
    const code = await readFile(`./${req.url}`, 'utf-8');
    const out = await transform(code, {
      loader: req.url.endsWith('.tsx') ? 'tsx' : 'ts',
      target: 'esnext',
    });
    res.setHeader('Content-Type', 'application/javascript');
    res.end(out.code);
  } else {
    // serve static or other types
    res.writeHead(404).end('not found');
  }
}).listen(5173);
작은 Vite 플러그인 — hook API 어떻게 도는지 봐·javascript
// A tiny Vite plugin — transforms .greet files into JS modules
// vite.config.ts
import { defineConfig } from 'vite';

function greetPlugin() {
  return {
    name: 'greet-plugin',
    resolveId(id) {
      if (id.endsWith('.greet')) return id;
    },
    load(id) {
      if (id.endsWith('.greet')) {
        return `export default "hi from ${id}"`;
      }
    },
  };
}

export default defineConfig({
  plugins: [greetPlugin()],
});

// Now your source can do: import greeting from './foo.greet'
// Vite resolves it, loads it as a JS module, browser gets standard JS.

External links

Exercise

가져온 .md 파일을 Markdown에서 HTML로 렌더링하는 기본 내보내기 React 컴포넌트로 바꾸는 Vite 플러그인을 작성해. Markdown 변환 라이브러리는 아무거나 써도 돼. 그런 다음 Vite와 React 앱에서 import About from './about.md'; ...<About />...처럼 사용해 봐. 50줄 안팎의 코드로 resolveId, load, transform 흐름을 익히면서 실제로 쓸 수 있는 Markdown 가져오기 기능을 만드는 연습이야.
Hint
load에서 fs/promises.md 파일을 읽고 markedremark로 HTML로 바꿔. 그 결과는 다음처럼 React 컴포넌트를 내보내는 모듈 코드로 감싸면 돼. return import React from 'react'; export default () => React.createElement('div', { dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} } });; 변환기는 브라우저가 실행할 수 있는 올바른 JavaScript를 만들고, 실제 화면 렌더링은 React가 맡아.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.