Vite 빌드 도구의 원리
Vite는 개발 서버와 배포용 빌드를 제공하는 프론트엔드 빌드 도구다. 코드 변환과 번들링, 개발 서버의 모듈 처리 방식, HMR과 의존성 사전 번들링을 살펴본다.
배포가 필요해지는 이유
- 웹페이지는 HTML, CSS, JS 파일로 이루어진다.
- 이 파일들이 내 컴퓨터에만 있으면 다른 사람은 접속할 수 없다.
- 그래서 인터넷에 연결된 채 계속 켜져 있는 컴퓨터, 즉 서버에 파일을 올려야 한다.
- 서버에는 웹 서버 프로그램이 돌고 있어서, 브라우저가 요청하면 해당 파일을 응답으로 보내준다.
- 브라우저는 받은 파일을 해석하고 실행해서 화면을 그린다.
- 이렇게 파일을 서버에 올려 누구나 접속할 수 있게 만드는 일을 배포라고 한다.
- 다만 개발할 때 작성한 코드는 그대로 올리기 어려운 경우가 많아서, 배포 전에 빌드라는 단계를 거친다.
빌드 1: 브라우저가 읽을 수 있는 문법으로 변환
-
브라우저가 해석할 수 있는 코드는 HTML, CSS, JavaScript이다.
-
그런데 개발할 때는 편의를 위해 브라우저가 직접 이해하지 못하는 문법을 함께 쓴다.
JSX는 JS 안에 HTML처럼 생긴 태그를 쓰는 문법이다.TypeScript는 JS에 타입을 붙인 언어이다.SCSS는 CSS에 변수, 중첩, 함수 같은 기능을 더한 문법이다.
-
따라서 이 코드들을 브라우저가 읽을 수 있는 HTML, CSS, JS로 바꿔주는 과정이 필요하다.
-
예를 들어
JSX태그는 아래처럼 일반 JS 함수 호출로 바뀐다.// 변환 전: 브라우저가 읽지 못한다 const el = <h1 className="title">Hello</h1>; // 변환 후: 일반 JS 함수 호출이 된다 const el = jsx("h1", { className: "title", children: "Hello" }); -
여기서
jsx는 변환 도구가 파일 위쪽에 자동으로 추가하는import를 통해react/jsx-runtime에서 가져오는 함수이다. -
그래서 JSX만 쓰는 파일에서는
React를 직접import하지 않아도 동작한다. -
같은 방식으로
TypeScript는 타입이 제거된 JS로,SCSS는 일반 CSS로 바뀐다.
빌드 2: 번들링
- 컴포넌트마다 파일을 나누고 패키지 코드까지 더하면 파일이 수백 개 이상이 되기 쉽다.
- 브라우저는 파일 하나를 받아서 읽어 봐야, 그 안의
import가 가리키는 다음 파일이 무엇인지 알 수 있다. - 그래서
import가 여러 단계로 이어지면 요청과 응답도 그 단계만큼 차례로 반복된다. - 이처럼 파일 수와
import단계가 늘어날수록 서버와 주고받는 횟수가 많아져 로딩이 느려질 수 있다. - 그래서
import로 연결된 파일들을 따라가며 소수의 파일로 합치는데, 이를 번들링이라고 한다. - 합치는 과정에서 공백과 주석을 지우고 변수명을 짧게 줄여 용량도 함께 낮춘다. (압축)
import만 해두고 실제로 쓰지 않는 코드는 결과물에서 빼기도 한다. (트리 셰이킹)
빌드가 끝나면
-
이 변환 + 번들링 과정 전체를 빌드라고 한다.
-
빌드가 끝나면 결과물은 보통
dist폴더에 다음처럼 생성된다.dist/ ├── index.html ├── favicon.svg └── assets/ ├── index-B3xK9a2f.js ← main.jsx + App.jsx + Header.jsx + Button.jsx + react ├── index-D8hLm4Qp.css ← index.css + App.css └── logo-C7dN1eRt.png -
여러 파일에 흩어져 있던 내 코드와 패키지 코드가
assets안의 JS, CSS 파일로 모인 것을 볼 수 있다. -
파일 이름에 붙은
B3xK9a2f같은 문자열은 빌드할 때 파일 내용을 바탕으로 붙는 해시이다. -
내용이 바뀌면 이름도 바뀌므로, 브라우저가 예전에 저장해 둔 파일을 새 버전으로 착각해 재사용하는 일을 줄일 수 있다.
-
dist/index.html안의<script>주소도 해시가 붙은 이 파일 이름으로 바뀌어 들어간다.
빌드 before/after
-
빌드 전의 코드는 아래처럼 파일이 나뉘어 있고 사람이 읽기 쉽다.
// Button.jsx export function Button({ label }) { return <button>{label}</button>; } // App.jsx import { Button } from './Button'; export function App() { return <Button label="로그인" />; } -
빌드 후에는
import와export가 사라지고, 한 파일 안에 압축된 형태로 이어 붙는다.// index-B3xK9a2f.js 의 일부 function a({label:e}){return t("button",{children:e})}function n(){return t(a,{label:"로그인"})} -
여담) 프로젝트가 커지면 페이지별로 파일을 나눠서 필요한 시점에 불러오기도 하므로, 결과물이 꼭 하나의 JS 파일인 것은 아니다. (코드 스플리팅)
빌드 도구: VITE
-
변환과 번들링을 사람이 직접 하기는 어려우므로, 이를 대신해주는 빌드 도구를 사용한다.
-
요즘 React 프로젝트에서는 Vite를 많이 사용한다.
-
Vite는 크게 두 부분으로 이루어진다. (이하 Vite 8 문서 기준)
-
하나는 코드를 고치는 동안 결과를 바로 확인하게 해 주는 개발 서버이다.
-
다른 하나는 앞에서 본 변환과 번들링을 수행해
dist를 만드는 빌드 명령이다. -
두 부분은
package.json의scripts에 아래처럼 연결되어 있다."scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" } -
그래서
npm run dev는 개발 서버를 띄우고,npm run build는dist를 만들며,npm run preview는 만들어진dist를 내 컴퓨터에서 열어 본다. -
여기서 눈여겨볼 점은 개발 서버가 앞에서 설명한 번들링을 거치지 않는다는 것이다.
Vite 1: 개발 서버는 요청받은 파일만 변환한다
-
번들링은 프로젝트 전체를 처리해야 끝나므로, 앱이 커질수록 서버를 띄우거나 수정 결과를 확인하기까지 기다리는 시간이 길어진다.
-
Vite는 이 대기 시간을 줄이기 위해, 개발 중에는 내가 작성한 소스 코드를 번들링하지 않는다.
-
대신 브라우저가
import를 직접 따라가며 파일을 하나씩 요청하는 ES 모듈 방식에 맡긴다.- ES모듈 방식
-
브라우저가
/src/main.jsx를 요청하면, Vite는 그 시점에 그 파일 하나만 JS로 변환해서 응답한다. -
응답받은 코드 안에
import './App.jsx'가 있으면 브라우저가 이어서/src/App.jsx를 요청하고, 같은 과정이 반복된다.브라우저: GET /src/main.jsx -> Vite: 이 파일만 변환 -> JS 응답 브라우저: GET /src/App.jsx -> Vite: 이 파일만 변환 -> JS 응답 -
결국 지금 화면에 필요한 파일만 변환되므로, 프로젝트가 커져도 서버가 뜨는 시간은 크게 늘지 않는 편이다.
-
파일을 저장했을 때도 전체를 다시 만들지 않고 바뀐 모듈만 브라우저에 다시 보내는데, 이를 HMR(Hot Module Replacement)이라고 한다.
- HMR
-
React 템플릿에서는
@vitejs/plugin-react가 HMR을 React에 연결해 주어서, 페이지를 새로고침하지 않고 수정한 컴포넌트만 갱신된다.
Vite 2: 패키지만은 미리 번들링해 둔다
-
다만
node_modules에 있는 패키지까지 같은 방식으로 내보내면 두 가지 문제가 생긴다. -
첫째로, 브라우저는
import { useState } from 'react'처럼 주소가 아닌 이름으로 된import를 처리하지 못한다.- 왜?
-
둘째로, 패키지 하나가 수백 개의 내부 파일로 나뉘어 있으면 요청도 그 수만큼 한꺼번에 발생한다.
-
그래서 Vite는 개발 서버를 처음 띄울 때 패키지들만 미리 번들링해 두는데, 이를 의존성 사전 번들링이라고 한다.
-
이 과정에서 CommonJS 형식으로 배포된 패키지는 ES 모듈로 바뀌고, 내부 파일이 많은 패키지는 하나의 파일로 합쳐진다.
- CommonJS 형식
-
결과는
node_modules/.vite폴더에 저장되고, 소스 코드의import는 그 파일을 가리키는 주소로 바뀌어 브라우저에 전달된다.// 내가 작성한 코드 import { format } from 'date-fns' // 브라우저가 실제로 받는 코드 (주소는 예시) import { format } from '/node_modules/.vite/deps/date-fns.js?v=1a2b3c4d' -
저장된 결과는
package-lock.json같은 lockfile이나 Vite 설정이 바뀌기 전까지 그대로 재사용된다. -
그래서 패키지가 예전 상태로 남아 있는 것처럼 보일 때는
npm run dev -- --force로 다시 만들거나node_modules/.vite폴더를 지워 볼 수 있다. -
사전 번들링은 개발 서버에만 해당하며,
npm run build에서는 패키지도 내 코드와 함께 번들링된다.