CRA에서 Vite로 마이그레이션 여정 (이라 쓰고 삽질이라 읽는다)
파일 갯수가 도합 1500개 가량 되는 대형 프로젝트를 Vite 로 마이그레이션 한 경험에 대하여 공유합니다. 5년 전에 CRA (create-react-app) 를 사용하여 생성이 된 프로젝트입니다. 구체적인 패키지 스펙은 다음과 같아요.
{
"name": "...",
"version": "0.1.0",
"dependencies": {
"@types/lodash": "^4.14.149",
"@types/node": "^12.0.0",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/react-redux": "^7.1.7",
"@types/react-router-dom": "^5.1.3",
"@types/styled-components": "^5.0.1",
"axios": "^0.19.2",
"classnames": "^2.2.6",
"date-fns": "^2.16.1",
"dayjs": "^1.10.7",
"lodash": "^4.17.20",
"moment": "^2.27.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"react-scripts": "^5.0.1",
"redux": "^4.0.5",
"styled-components": "^5.0.1",
"typescript": "3.8"
},
"scripts": {
"start": "cp .env.localhost .env.development.local; react-scripts start",
"build": "react-scripts build --max_old_space_size=4096; rm -rf dist; mv -f build dist",
"test": "react-scripts test",
"eject": "react-scripts eject",
"prettier": "prettier \"**/*.{js,ts,tsx}\" --write"
},
"eslintConfig": {
"extends": "react-app"
}
}
들어가기 전에, 먼저 왜 CRA 프로젝트를 Vite 로 변경하기로 했는지에 대해 이야기하고 싶은데요,
- CRA가 가지고 있던 Cache Busting과 같은 문제를 해결하고 싶었고,
- 어떤 이유에서인지 Hot Reloading 동작이 안 됐음. 원인 파악이 불가능했고, 밀려드는 Task 를 처리하면서도 매번 페이지를 새로고침해야 했습니다.
- 또 다른 프레임워크인 Next.js 도 염두해 두고 있었으나 당장 한번에 변경하기에는 라우터 설정, 미들웨어 등 코드 변경점이 너무 컸고, Vite 로 옮긴 이후 추후에 점진적으로 도입할 계획을 구상했습니다.
결국 언젠가는 해야 할 작업을 조금이라도 이르게 진행하고 싶었어요. 개발자 관점에서는 HMR 동작이 원활하지 않은 것이 가장 불편했습니다.
큰 레거시 프로젝트를 (비교적) 최신 기술로 탈바꿈해야 하는 것에 대한 부담감이 있었기 때문에 상세한 계획을 수립하고, 명확하게 원하는 결과물을 도출해야 했습니다.
- Vite 도입 후 CRA 의 잔재 (react-scripts) 제거
- TypeScript 버전 업데이트 minimum 5.0 maximum 5.5
- 패키지 교체
- lodash 를 조금 더 가벼운 라이브러리로 변경
- date 관련 라이브러리 (dayjs, moment. date-fns 등) 통일
- React.FC 와 같은 구형 문법 제거 및 class component를 functional component로 변경
- Code splitting
- 기타 오래된 라이브러리 버전 업
- 코드 컨벤션
- 산발적으로 정의되어 있는 타입이나 상수 일원화
- tsconfig 의 절대 경로를 상대 경로로 가지고 오는 코드 수정
- alert() 등의 자바스크립트 코드를 toastify 교체
- 사용하지 않는 코드 제거
고심 끝에 위 리스트 순서대로 달성하기로 목표를 구체화했고, 실제 코드 레벨의 진행 단계로 도입하였습니다. 이 글에서는 Vite 도입 및 react-scripts 제거 및 기타 리팩토링만 다룹니다.
먼저, 커맨드로 Vite 를 설치합니다.
yarn add -D vite @vitejs/plugin-react
설치 후 기존 react-scripts 는 더 이상 사용되지 않으므로 제거합니다.
yarn remove react-scripts
그리고 동시에 package.json 내의 스크립트를 변경합니다. 아래는 기존 커맨드이고,
"scripts": {
"start": "cp .env.localhost .env.development.local; react-scripts start",
"start:dev": "cp .env.develop .env.development.local; react-scripts start",
"build": "react-scripts build --max_old_space_size=4096; rm -rf dist; mv -f build dist",
"build:dev": "cp .env.develop .env.production.local; react-scripts build --max_old_space_size=4096; rm -rf dist; mv -f build dist",
"build:patch": "cp .env.patch .env.production.local; react-scripts build --max_old_space_size=4096; rm -rf dist; mv -f build dist",
"build:stg": "cp .env.stg .env.production.local; react-scripts build --max_old_space_size=4096; rm -rf dist; mv -f build dist",
"build:product": "cp .env.production .env.production.local; react-scripts build --max_old_space_size=4096; rm -rf dist; mv -f build dist",
"test": "react-scripts test",
"eject": "react-scripts eject",
"prettier": "prettier \"**/*.{js,ts,tsx}\" --write"
},
다음과 같이 Vite 커맨드로 변경합니다.
"scripts": {
"dev": "cp .env.localhost .env.development.local; vite start",
"build": "tsc && vite build --max_old_space_size=4096; rm -rf dist; mv -f build dist",
"prettier": "prettier \"**/*.{js,ts,tsx}\" --write"
},
start 커맨드 제거 후 dev 로 커맨드를 교체하여 앞으로 프로젝트는 yarn dev 로 실행합니다.
Vite 는 성능 측면에서 이점을 취하기 위해 빌드 시 타입 체크와 빌드 프로세스가 분리되어 있습니다. 경우에 따라서는 tsc 를 생략해도 되지만, 생략하게 되면 타입 에러가 있어도 빌드 성공하는 케이스가 발생할 수 있습니다. 따라서 프로젝트의 안정성을 위해 빌드 전 타입을 체크하고 싶다면 build 커맨드 앞에 tsc && 를 붙여주어야 합니다. (물론 build 커맨드만 있을 때보다 tsc && build 커맨드를 실행하면 시간이 조금 더 소요됩니다.)
다음은 vite 설정 파일인 vite.config.ts 을 루트에 생성한 후 다음 코드를 작성합니다.
물론 지금은 설정이 더 추가되어 있지만, 이 시점에서는 아주 기본적인 설정만 다룹니다.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: 'localhost',
port: 3000,
open: true,
strictPort: true,
},
build: {
outDir: 'dist',
},
});
Vite는 기본 포트가 5173 으로 열립니다. 기존 개발 환경과 싱크를 맞추기 위해 port는 3000으로 설정합니다. 아무것도 설정하지 않으면 기본값인 5173으로 열립니다.
open: true 는 서버가 실행되면 자동으로 브라우저를 열 수 있도록 설정합니다.
strictPort: true 는 3000 포트가 다른 개발 서버로 열려 있을 때, 3000 포트를 대신하여 3001 포트로 열지 설정하는 물음입니다. true 로 설정하면 다른 대체 포트를 열지 않습니다.
tsconfig.json 에도 Vite 구문을 추가로 설정해 주어야 합니다.
{
"compilerOptions": {
"types": ["@emotion/react/types/css-prop", "vite/client"]
},
"include": ["src", "vite.config.ts"]
}
compile 옵션에서 TypeScript가 Vite 를 인식할 수 있도록 vite/client 를 추가하고 vite.config.ts 도 인지할 수 있도록 includes에 추가합니다.
그리고 react-app-env.d.ts 파일을 제거하고 vite-env.d.ts 파일을 루트에 생성합니다. 이 파일은 타입스크립트가 Vite의 특정 전역 변수나 기능을 이해할 수 있도록 도와줍니다.
/// <reference types="vite/client" />
다음은 public/index.html 파일을 루트 경로로 이동합니다. 또한, 해당 파일 내에 있는 %PUBLIC_URL% 구문을 지워 줍니다.
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
Webpack 을 활용하던 CRA 와 번들링 방식에 차이가 있기 때문에 Vite 는 이렇게 직접적으로 스크립트 안에 진입점을 추가해야 합니다.
여기까지는 Vite 의 기초 세팅을 진행했고, 더 이상 필요하지 않은 react-scripts 패키지의 흔적을 지웠습니다.
이제부터는 계속 코드를 수정하고 빌드를 돌려 에러가 없는지 확인하고, 또 서버를 실행해 보고 빌드가 성공하여 서버가 잘 켜지는지 반복적으로 확인하는 일이 남았습니다.
먼저 yarn build 커맨드로 dev 커맨드를 수행하기 전에 빌드를 먼저 실행합니다.
에러 발생 1

CRA는 webpack의 SVGR 로더가 기본 내장되어 SVG를 React 컴포넌트로 자동 변환하지만, Vite는 SVG를 단순 URL 문자열로만 처리합니다. 따라서 { ReactComponent }라는 named export가 존재하지 않아 에러가 발생합니다.
svgr 설치
yarn add -D vite-plugin-svgr
vite-env.d.ts 에 추가
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
/// <reference types="./Icons.d.ts" />
vite.config.ts에 다음 구문 추가
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import path from 'path';
export default defineConfig({
plugins: [react(), svgr()], // 이 부분
...
});
에러가 뜨는 해당 파일에서 export 구문을 default 로 변경합니다.
export { ReactComponent as Exclamation } from '../images/icons/exclamation.svg';
export { default as Exclamation } from '../images/icons/exclamation.svg?react';
에러 발생 2
빌드 후 두번째 에러가 발생합니다.
vite Rollup failed to resolve import "components/common/Topbar" from "/Users/vivi/Documents/repo_name/src/pages/Auth/Agree/index.tsx". This is most likely unintended because it can break your application at runtime.

이 에러는 다음 링크를 참고하여 수정했습니다. 참고 링크
vite.config.ts 내의 alias 설정을 변경하여 절대 경로를 추가합니다.
resolve: {
alias: {
components: path.resolve(__dirname, './src/components'),
constants: path.resolve(__dirname, './src/constants'),
hooks: path.resolve(__dirname, './src/hooks'),
images: path.resolve(__dirname, './src/images'),
modules: path.resolve(__dirname, './src/modules'),
services: path.resolve(__dirname, './src/services'),
pages: path.resolve(__dirname, './src/pages'),
},
},

그런 다음에는 또 빌드를 실행해 보니 갑작스러운 에러가 발생합니다. 이를 해결하기 위해 vite.config.mts 에 구문을 추가합니다.
define: {
global: 'globalThis',
},
global 값은 코드 내의 global이라는 식별자가 있으면, 이를 globalThis 로 치환합니다.
globalThis는 ECMAScript 2020(ES11)에서 도입된 표준 전역 객체로, 브라우저와 Node.js 등 모든 환경에서 사용할 수 있는 전역 객체를 가리킵니다. 일부 Node.js 기반 라이브러리들이 브라우저 환경에서도 global 객체를 참조하려 할 때 에러가 발생합니다. 이 설정으로 코드의 모든 global을 globalThis로 자동 치환하여 브라우저 호환성을 확보합니다.
다음은 빌드를 실행하기 전에 한 가지 잊은 사실이 있는데, env 파일을 모두 변경합니다.
REACT_APP 을 VITE prefix 로 변환하는 작업입니다. 물론 REACT_APP 을 그대로 사용하는 방법도 있지만 직관성을 위해 VITE 로 변경한 뒤 process.env 를 전부 import.meta.env 로 변환합니다.
*as is*
process.env.REACT_APP_API_PATH
*to be*
import.meta.env.VITE_API_PATH
여기까지 작업한 뒤 숨 한번 고르고 다음 빌드를 실행합니다. 에러 3번째에서 대부분의 에러는 보이지 않고, 경고 라인만 한가득입니다. 배럴 파일의 순환 참조 문제가 감지되었습니다.
배럴 파일이 무엇인지, 그리고 이것을 왜 지양해야 하는지는 다음 링크를 보면 알기 쉽게 되어 있습니다. https://tkdodo.eu/blog/please-stop-using-barrel-files
결론적으로는 하나의 파일 안에서 다른 파일을 참조하고 있어서 순환 참조 문제가 발생한다, 로 요약할 수 있는데 이 부분은 에러까지는 아니고 경고이기 때문에 잠재적 성능 저하를 방지하기 위해 이 코드도 수정합니다.
당시 해당 경고 사항 발생 상황:

/** Elements */
export { default as Divider } from './Divider';
export { default as Panel } from './Panel';
...
export { default as Input } from './Input';
export { default as Tooltip } from './Tooltip';
export { default as Table } from './Table';

Table.tsx 파일에서 동일하게 export 되는 index.ts 배럴 파일 내의 Input 을 참조하고 있기 때문에 순환 파일 참조 문제가 발생하게 되는 구조입니다.
이 부분도 index.ts 가 아니라 Input.tsx 에서 직접 가지고 올 수 있도록 수정하여 순환 참조 문제를 깨트립니다. 이런 식으로 다수의 파일을 수정합니다. 더 나아가서는, index.ts 파일을 삭제하는 경우도 있었습니다.
이 시점에서는 에러 메시지 두 번과 경고 수정 한 번으로 빌드가 성공한 상황입니다. 여기까지 빌드 후 청크 사이즈를 측정해 봅니다.

CRA 를 사용하던 때의 빌드 시간 (100s+) 보다 Vite 의 빌드 시간이 현저히 줄어들었지만, 청크 사이즈가 과도하게 큰 것을 확인할 수 있습니다.
청크 사이즈가 너무 커서 정확한 측정을 위해 vite.config.mts 파일에 빌드 관련 설정 몇 가지를 추가합니다.
build: {
outDir: 'dist',
sourcemap: true,
manifest: true,
},
이렇게 하면 빌드 후에 dist 안에 .vite 가 생기고 manifest.json 파일이 생성됩니다. 정확한 내용을 파악하기 위해 파일 들여다 봅니다.
두번째로 큰 파일 (1,703kb) 먼저 보면,
"_index-CIsZJGgT.js": {
"file": "assets/index-CIsZJGgT.js", // 실제 결과물
"name": "index", // entry 포인트
"isDynamicEntry": true, // 동적
"imports": [ // 참조 모듈들
"index.html",
...
],
"dynamicImports": [ // 동적 import로 로드할 수 있는 다른 모듈
...
],
"css": [
"assets/index-DhZwF7rb.css"
],
"assets": [
"assets/top-logo@3x-C6ZpEqmc.png",
"assets/img-slide-01@2x-Ln4SbJfP.png",
"assets/img-slide-01@3x-DYcioUSv.png",
"assets/text-ok@2x-B1PkcUtI.png",
"assets/text-ok@3x-BIqLhKB1.png",
"assets/img-tu-02@2x-xNyGbzr8.png",
"assets/img-tu-02@3x-XNz_kcfa.png",
"assets/text-editor@2x-Cw7S8w9P.png",
"assets/text-editor@3x-DVHGYXdC.png",
"assets/ars-logo-C9Qvq3d1.svg",
"assets/loading_50-Ch3-uIdb.gif",
"assets/loading_100-DQdymZYW.gif",
"assets/loading_150-C0FpNRej.gif"
]
},
각각의 필드에 맞게 어떤 파일들이 활용되고 있는지 확인할 수 있습니다. 이렇게 manifest.json 을 확인하면 크기가 큰 파일 청크 이름 확인 -> 해당 청크를 구성하고 있는 파일들을 알 수 있습니다. 이름이 해시화되어 있기는 하지만 못 알아볼 정도는 아니기 때문에, 해당 파일들을 확인한 이후에는 가능하면 해당 파일의 최적화를 진행합니다. 파일이 너무 큰 경우에는 분리를 할 수 있고, 재사용되는 로직들을 찾아 리팩토링도 할 수도 있습니다. 그런 다음 빌드 커맨드를 다시 실행해 보면 미세하게나마 줄어든 청크 크기를 확인할 수 있습니다.
이제 에러는 거의 잡았으니 본격적으로 번들 사이즈 다이어트를 시작합니다. 번들이 크다의 기준은 디폴트 값을 활용했는데, 이 기본값은 500kb 입니다. 빌드된 번들의 크기가 500kb 이상이면 크다고 워닝이 나오게 됩니다. 사실 다이어트는 대단한 방법은 아니고 최적화 및 리팩토링, 공통 함수를 사용하거나 컴포넌트의 단위를 쪼개고 쪼개면 됩니다. 제가 작업하고 있던 프로젝트에는 파일 하나가 몇 천 줄에 해당하는 코드를 담고 있는 경우가 많았기 때문에, 의존성들만 조금 분리해 주어도 큰 효과가 있을 거라고 생각했습니다.

다이어트 전

다이어트 후 ...
큰 변화가 없었습니다 ...
파일 크기가 큰 것들을 또 manifest.json 으로 살펴 보니 굵직굵직한 라이브러리들이 있었습니다.
이 시점에서는 이미 같은 기능을 하는 여러 라이브러리를 일원화하거나, 사용하지 않는 패키지 등은 제거를 한 시점이라 그 외의 라이브러리를 가볍게 만들거나 아예 교체하는 등의 이상적인 방안은 당장은 수행할 수 없었기 때문에 이들을 청크로 자르는 코드를 vite.config.mts 에 추가합니다.
build: {
outDir: 'dist',
manifest: true,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules/lottie-web')) {
return 'lottie-web';
} else if (id.includes('node_modules/react-beautiful-dnd')) {
return 'react-beautiful-dnd';
} else if (id.includes('node_modules/react-json-view')) {
return 'react-json-view';
}
},
},
external: './src/App.tsx',
},
},
참고로 external로 지정된 파일은 런타임에 별도로 로드되어야 하므로, 실제로 필요한 경우가 아니면 사용을 권장하지 않습니다. 대부분의 경우 코드 스플리팅(dynamic import)으로 동일한 효과를 얻을 수 있습니다.
다시 빌드를 수행합니다.

현재 파일 구조
// src/modules/Renewal/Components/Toast/index.tsx
import { toast } from 'react-toastify';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faX, faTriangleExclamation, faCircleCheck, faCircleExclamation } from '@fortawesome/pro-solid-svg-icons';
import { Flex } from '../index';
import { BaseToast } from '../../Styles/Toast';
import './custom-toast.css';
import { Props } from '../../Interfaces/Toast';
const MessageIcon = ({ type, message }: Omit<Props, 'status'>) => {
switch (type) {
...
}
};
export const notify = ({ status, message, type }: Props) => {
toast(
({ closeToast }) => {
return (
...
);
},
{ autoClose: type !== 'error' ? 4000 : false },
);
};
// src/modules/Renewal/Components/index.ts
export { default as Text } from './Text';
export { default as Tab } from './Tab';
export { default as Box } from './Box';
...
export { default as Badge } from './Badge';
export { default as Input } from './Input';
...
export { notify } from './Toast';
export { default as Table } from './Table';
Flex 를 index.ts (배럴 파일)에서 가지고 오고 있었는데 이를 직접 import 하여 순환 참조를 깨트립니다.
import Flex from '../Flex';
그런 다음 머리를 식힐 겸 코드 내에 아주 많고 고르게 퍼져 있던 any 타입 제거를 해 봅니다. 이 작업은 Vite 로 마이그레이션과 밀접한 관계는 없었지만 빌드 -> 오류 수정 -> 또 빌드 -> 또 오류 수정 -> 또또 빌드 -> 또또 오류 수정 ... 의 작업을 거치다 보니 휴식 시간이 필요했습니다.

주로 Select 등의 선택지에서 인자 값이 any 라던가, 마우스 이벤트, 키보드 이벤트 혹은 onChange 등에서 사용되는 any 를 모두 교정해 줍니다. 또한, 위 작업들을 진행하면서 리팩토링 이후 타입 추론이 가능해진 코드들도 any 를 제거합니다.

열심히 했는데 280개 줄었네요.
한 턴 머리를 식힌 후에는 다시 빌드 작업에 돌입합니다. 이 시점에서는 빌드가 성공해서 실제 개발 서버에서 기능들이 정상 동작하는지 파악하는 과정이 있었는데요, 개발자 도구를 열어보니 폴리필이 제대로 이루어지지 않아서 브라우저에 경고가 많이 누적되어 있었습니다.

참고 문서 https://tech.wonderwall.kr/articles/vite/ https://github.com/vitejs/vite/discussions/14966
이 링크들을 참고하여 위 경고들을 없앱니다.
여기까지 작업한 뒤의 제 vite.config.ts 파일입니다.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import { visualizer } from 'rollup-plugin-visualizer';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
svgr({ include: '**/*.svg' }),
nodePolyfills({ include: ['fs', 'path', 'url'] }),
visualizer({
emitFile: true,
filename: 'stats.html',
}),
],
server: {
host: '127.0.0.1',
port: 3000,
open: true,
strictPort: true,
},
define: {
global: 'globalThis',
},
build: {
outDir: 'dist',
manifest: true,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules/lottie-web')) {
return 'lottie-web';
} else if (id.includes('node_modules/react-beautiful-dnd')) {
return 'react-beautiful-dnd';
} else if (id.includes('node_modules/react-json-view')) {
return 'react-json-view';
}
},
},
external: './src/App.tsx',
},
},
optimizeDeps: {
esbuildOptions: {
plugins: [NodeGlobalsPolyfillPlugin({ process: true })],
},
},
resolve: {
alias: {
components: path.resolve(__dirname, './src/components'),
constants: path.resolve(__dirname, './src/constants'),
hooks: path.resolve(__dirname, './src/hooks'),
images: path.resolve(__dirname, './src/images'),
modules: path.resolve(__dirname, './src/modules'),
services: path.resolve(__dirname, './src/services'),
pages: path.resolve(__dirname, './src/pages'),
'source-map-js': 'source-map',
},
},
});
마지막으로 빌드 테스트를 한 번 더 수행합니다. 운영 서비스와 동일한 환경으로 수행해 보기 위해 Dockerfile을 로컬에 가져와 빌드를 테스트 합니다. (여기서의 테스트 커맨드는 정말 "테스트" 용도이므로 임의로 설정합니다. 물론 커밋에 stage 하지는 않습니다.)
"tt": "docker build -t test -f Dockerfile ."
실행을 해 보는데, 역시 처음부터 성공하는 게 이상하지 않을까 싶을 정도로 익숙하게 에러가 발생합니다.

다시 로컬에서 yarn build 를 수행합니다. 이 역시도 안 되는 거 보니까 Node.js 메모리가 딸려서
"build": "NODE_OPTIONS=--max-old-space-size=4096 tsc && vite build",
커맨드를 수정하여 메모리를 임의로 늘린 뒤... (NODE_OPTIONS=--max-old-space-size=4096은 Node.js의 메모리 제한을 4GB로 증가시킵니다)

다시 수행하니까 됩니다. 빌드 캐시를 사용하지 않고 패키지 install 부터 전체를 수행하고 있어 로컬 빌드와 다르게 컨테이너 빌드는 시간이 다소 소요되는 편입니다.
이후에는 Docker server 배포를 마지막으로 실제 개발 환경 호스트에 배포합니다.
그리고 나서 빌드, 배포가 성공한 이후에 정말로 Vite 로 배포가 된 건지? 싶어서 로그를 추가했다. next.js 를 사용한 프로젝트는 브라우저 콘솔에 NEXT_DATA 를 하면 해당 객체가 나오니까 이 아이디어를 이용해 보았습니다.
먼저
vite.config.mts 내 define 에 객체를 정의합니다.
define: {
global: 'globalThis',
__BUILD_INFO__: JSON.stringify({
buildTool: 'vite',
buildTime: new Date().toISOString(),
version: process.env.npm_package_version,
}),
},
그리고 vite-env.d.ts
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
interface BuildInfo {
buildTool: string;
buildTime: string;
version: string;
}
declare const __BUILD_INFO__: BuildInfo;
마지막으로 루트에 있는 src/index.tsx
(window as any).__BUILD_INFO__ = __BUILD_INFO__;
... 이전 코드
로 로그성 배포를 한번 더 돌렸습니다.

그랬더니 잘 나오네요. 이 코드는 추후 상용에서도 테스트 후 지울 거라 일단은 킵하고, 마이그레이션을 빙자한 삽질은 이렇게 일단락을 하게 됩니다...