본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

React 빌드 환경 직접 구성하기: webpack·Babel·TypeScript

작성 2026. 10. 21.추가
목차9개 항목
출처와 참고

「로더 규칙」과 entry·output 해시는 「Frontend_TIL: Webpack」, 「Webpack, Babel 사용해서 React 개발 환경 구축」, 「webpack 이미지, 폰트 loader」를 바탕으로 webpack 5 기준으로 보강했다.

React 코드는 그대로 브라우저에서 돌지 않는다. JSX와 최신 문법은 Babel이 변환하고, 흩어진 파일들은 webpack 같은 번들러가 하나로 합친다. Vite나 Next.js 같은 도구는 이 설정을 숨겨 두는데, 한 번 직접 구성해 보면 각 도구가 어느 단계에서 무엇을 하는지 보인다.

가장 빠른 시작: CDN

설치 없이 HTML 파일 하나로 실습할 때는 CDN 스크립트를 쓴다.

<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  function App() {
    const [counter, setCounter] = React.useState(0)
    return <button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
  }
  ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>
  • 경고 메시지(PropTypes 등)를 보려면 production.min.js 대신 development.js 빌드를 써야 한다.
  • 브라우저에서 Babel이 즉석 변환하므로 학습용으로만 쓴다. React 19부터는 UMD 빌드가 제공되지 않아, 최신 버전은 ESM CDN(esm.sh 등)을 쓴다.

Create React App은 역사가 되었다

CRA(npx create-react-app)는 webpack·Babel·ESLint 설정을 내장해 명령 하나로 React 프로젝트를 만들어 주던 스캐폴딩 도구였다. 설정을 바꾸려면 되돌릴 수 없는 eject를 해야 했고, 2025년 공식적으로 지원이 종료되었다. 새 프로젝트는 Vite(npm create vite@latest)나 Next.js로 시작한다. CRA 시절 코드를 읽을 때 npm start·npm run build·REACT_APP_ 접두사가 나오면 이 도구의 흔적이다.

직접 구성하기: webpack + Babel + TypeScript

파일/도구역할
package.jsonnpm init으로 생성. react, react-dom, typescript, @types/react 등 설치
tsconfig.jsonTypeScript 설정. strict: true를 켜야 타입 검사가 의미 있다
webpack.config.tsts·css·json·최신 JS를 하나의 번들로 합침
Babel 프리셋@babel/preset-env(최신 문법 → 구형 브라우저), preset-react(JSX), preset-typescript(TS)
eslint.config.js(옛 .eslintrc)·.prettierrc코드 점검과 포맷. 설정 방법은 ESLint와 Prettier 설정

webpack 핵심 개념

flowchart LR
  E["entry(시작 파일)"] --> M["module.rules(로더로 변환)"] --> O["output(결과 번들)"]
  P["plugins(빌드 중 부가 작업)"]
  • entry: { app: './client' } → output.filename: '[name].js'이면 [name]이 app으로 바뀌어 dist/app.js가 생성된다. 키를 여러 개 두면 결과 번들도 여러 개 나온다(페이지마다 번들이 따로 필요한 MPA). 값에 배열을 주면 여러 파일을 번들 하나로 합친다.
  • output.filename의 자리표시자: [name](entry 키), [contenthash](파일 내용이 바뀔 때만 바뀌는 해시). 배포용은 '[name].[contenthash].js'로 두어, 바뀌지 않은 파일은 브라우저 캐시를 그대로 쓰게 한다. 빌드마다 바뀌는 [fullhash](webpack 4의 [hash])는 캐시를 매번 깨뜨린다.
  • devServer: 개발 서버(포트, historyApiFallback으로 SPA 라우팅 지원, proxy로 API 요청 전달, AJAX와 CORS 참고). 정적 파일 폴더는 static으로 지정한다(webpack-dev-server 3의 contentBase).
  • Hot reloading: @pmmmwh/react-refresh-webpack-plugin + react-refresh로 저장 시 state를 유지한 채 화면만 갱신.
  • fork-ts-checker-webpack-plugin: 타입 검사를 별도 프로세스에서 병렬로 돌려 빌드를 빠르게 한다.

로더 규칙: 무엇을 어떤 순서로 변환하나

webpack은 JavaScript와 JSON만 이해한다. 나머지 파일은 module.rules에서 test(정규식으로 파일 고르기)와 use(적용할 로더)로 변환한다. use 배열은 뒤에서 앞으로(아래에서 위로) 실행된다.

module.exports = {
  module: {
    rules: [
      { test: /\.[jt]sx?$/, exclude: /node_modules/, use: 'babel-loader' },
      {
        test: /\.scss$/,
        // sass-loader(SCSS → CSS) → css-loader(@import·url() 해석, JS 모듈로) → 마지막 로더
        use: [isProd ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader', 'sass-loader'],
      },
      { test: /\.(png|jpe?g|gif|svg|webp)$/i, type: 'asset' },
      { test: /\.(woff2?|ttf|otf)$/i, type: 'asset/resource', generator: { filename: 'fonts/[name].[contenthash][ext]' } },
    ],
  },
  plugins: [new HtmlWebpackPlugin({ template: './public/index.html' }), new MiniCssExtractPlugin()],
}
CSS 마지막 단계결과쓰는 때
style-loaderCSS를 JS 번들에 넣고 실행 시 <style> 태그로 삽입개발. HMR이 빠르다
MiniCssExtractPlugin.loader + 플러그인별도 .css 파일로 뽑아 <link>로 건다배포. JS와 병렬로 받고 따로 캐시된다

이미지·폰트는 webpack 5의 에셋 모듈(asset modules) 이 로더 없이 처리한다.

type하는 일옛 로더
asset/resource파일을 출력 폴더로 복사하고 URL을 돌려준다file-loader
asset/inlinebase64 data URI로 번들에 넣는다. 요청이 하나 줄지만 크기가 약 33% 커지고 따로 캐시되지 않는다url-loader
asset/source파일 내용을 문자열로raw-loader
asset기본 8KB(parser.dataUrlCondition.maxSize)보다 작으면 inline, 크면 resourceurl-loader의 limit

에셋은 코드에서 import하거나 CSS의 url()로 참조한 것만 번들에 들어간다. 참조하지 않은 이미지는 출력되지 않는다. 경로를 문자열로 하드코딩하면 빌드 후 위치(dist/fonts/…)와 어긋나므로 import logo from './logo.png'처럼 모듈로 불러와 webpack이 최종 URL을 넣게 한다. 결과 URL 앞에 붙는 경로는 output.publicPath로 정한다. 과거 file-loader를 require()로 불러오면 ES 모듈 객체가 와서 .default를 붙여야 했는데, 에셋 모듈로는 그럴 일이 없다.

HtmlWebpackPlugin은 템플릿 HTML에 번들 <script>·<link>를 자동으로 넣어 출력 폴더에 쓴다. 폴리필은 @babel/polyfill(7.4에서 폐기)이 아니라 @babel/preset-env의 대상 브라우저 설정과 core-js로 넣는다(Babel).

tsconfig 메모

  • lib: ["ES2020", "DOM"], module: "esnext"처럼 최신 문법으로 작성하되, target은 구형 브라우저까지 고려해 낮게 잡을 수 있다.
  • paths로 경로 별칭을 지정하면 ../../layouts/App 대신 @layouts/App처럼 쓸 수 있다("@layouts/*": ["./src/layouts/*"]). 예전에는 baseUrl을 함께 썼지만 TypeScript 6에서 폐기됐다. 별칭은 타입 검사에만 적용되므로 webpack에도 resolve.alias를 같은 내용으로 맞춘다(TypeScript 프로젝트 설정).
  • @types/xxx 패키지는 원래 JavaScript로 작성된 라이브러리에 다른 사람이 타입 정의를 붙여 둔 것이다. 타입이 헷갈리면 에디터의 "Go to Type Definition"으로 정의를 직접 읽는다.

빌드 환경을 잡은 뒤 pages·layouts·components·hooks 같은 폴더를 어떻게 나누는지는 프론트엔드 폴더 구조의 진화에 있다.

라우팅과 코드 스플리팅

  • react-router로 URL과 페이지 컴포넌트를 연결한다. 앱을 BrowserRouter로 감싸고 경로를 정의한다. (참고: v6부터 Switch → Routes, Redirect → Navigate, component={X} → element={<X />}로 바뀌었다. v7부터 react-router 패키지 하나로 합쳐졌고, v8에서는 호환용 react-router-dom도 빠졌다. react-router 레이아웃 참고.)
  • 코드 스플리팅: 페이지별로 번들을 나눠, 회원가입 페이지에 들어온 사람은 회원가입에 필요한 JS만 받게 한다. React.lazy + Suspense 또는 @loadable/component를 쓴다.

환경 변수

클라이언트 코드에 노출되는 환경 변수는 도구별 접두사가 필요하다. .env는 프로젝트 루트에 두고, 바꾼 뒤에는 개발 서버를 재시작한다. 클라이언트에 노출되는 값은 비밀이 아니므로 진짜 비밀 키는 서버에만 둔다.

도구접두사읽는 법
ViteVITE_import.meta.env.VITE_API_URL
Next.jsNEXT_PUBLIC_process.env.NEXT_PUBLIC_API_URL
CRA(종료)REACT_APP_process.env.REACT_APP_API_URL

Vite가 기본으로 넣어 주는 값은 다음과 같다. 모드별로 .env.development, .env.production을 나눠 두면 vite build --mode staging처럼 모드에 맞는 파일(.env.staging)이 함께 로드된다.

내장 변수값
import.meta.env.MODE현재 모드(development, production 등)
import.meta.env.BASE_URL앱이 제공되는 기본 URL(base 설정)
import.meta.env.PROD / DEV프로덕션 / 개발 환경 여부(항상 서로 반대)
import.meta.env.SSR서버에서 실행 중인지
증상원인해결
환경 변수가 undefined라 API 요청이 엉뚱한 경로로 가 404가 난다접두사가 없거나(API_URL), Vite에서 process.env로 읽었거나, .env를 바꾼 뒤 서버를 재시작하지 않았다도구의 접두사를 붙이고 import.meta.env로 읽은 뒤 개발 서버를 재시작한다. 그래도 이상하면 값 자체가 맞는 자리에 들어갔는지, 특수문자가 있으면 따옴표로 감쌌는지 확인한다

핵심 정리

  • Babel = 문법 변환(JSX, TS, 최신 JS), webpack = 파일들을 번들로 합치기.
  • 새 프로젝트는 CRA 대신 Vite나 Next.js로 시작한다. 직접 구성은 도구의 역할을 이해하는 용도다.
  • TypeScript는 strict: true, 경로 별칭은 tsconfig와 webpack 양쪽에 맞춘다.
  • 페이지 단위 코드 스플리팅으로 첫 로딩에 필요한 JS만 보낸다.
  • 클라이언트 환경 변수는 도구별 접두사가 필요하고, 공개된다는 것을 전제로 쓴다.

관련 노트: ESLint와 Prettier 설정, 프론트엔드 폴더 구조의 진화, 패키지 매니저: npm, yarn, pnpm, JavaScript는 인터프리터 언어일까, 컴파일 언어일까

원본 기록6

댓글

GitHub 계정으로 댓글을 남길 수 있어요.