React & Typescript (without CRA)

homewiz·2024년 3월 29일

React & typescript

목록 보기
1/18
post-thumbnail

INTRO

CRA를 이용 하지 않고 웹팩으로 리액트 프로젝트 세팅을 한다.
velog의 시리즈중 react(with CRA) & react(without CRA) 는 자바스크립트 기반본 시리즈는 React & typescript를 이용한 프로젝트에 대해 기록 한다.


ESM

ESM(ECMAScript Modules)을 사용해야 하는 이유는 현대 JavaScript 환경의 표준이기 때문이며, 다음과 같은 기술적·실용적 이점이 있습니다:

✅ 1. 표준화된 모듈 시스템

ESM은 JavaScript의 공식 모듈 시스템입니다 (ECMAScript 2015부터 표준).

CommonJS(require, module.exports)는 Node.js만의 비표준 방식입니다.

브라우저, Deno, Node.js 모두 ESM을 지원하며, ESLint, TypeScript, Webpack, Vite 등 주요 도구들도 ESM을 기본으로 전환 중입니다.

✅ 2. 정적 분석 가능

ESM은 import/export가 정적으로 분석 가능하여:

트리 쉐이킹(Tree shaking)이 가능 → 사용하지 않는 코드를 제거해서 번들 크기 줄임

자동 코드 완성 및 타입 추론 정확도 향상

// 정적 분석 가능
import { add } from './utils.js';

vs

// 동적 분석 불가
const utils = require('./utils'); // 런타임까지 분석 불가

✅ 3. 비동기 로딩 지원 (브라우저 및 Node.js)

ESM은 브라우저 환경에서 <script type="module"> 사용 가능

Node.js에서도 import()로 동적 import 가능 → 지연 로딩, 코드 분할 가능

✅ 4. 미래 호환성

Node.js 20+ 및 대부분의 현대 툴링은 ESM 중심으로 움직이고 있음

일부 npm 패키지들은 더 이상 CommonJS를 지원하지 않거나, ESM만 배포함

React, Vite, Astro 등 프론트엔드 도구들도 ESM 기반

✅ 5. Top-level await 지원

ESM은 모듈 최상단에서 await 사용 가능 (top-level await)

CommonJS에서는 불가능

// ESM
const data = await fetch(...);

⚠️ 단점 및 고려 사항

항목 설명
.js, .mjs, type: "module" 문제 Node.js에서 ESM을 사용하려면 파일 확장자 또는 package.json 설정 필요
동적 import 제한 import() 사용 시에는 비동기 코드가 필요
일부 구버전 패키지 호환성 오래된 CommonJS 전용 패키지는 직접 호환처리 필요

🧠 결론

선택 사용 시기
✅ ESM (import, export) 신규 프로젝트, 모던 툴체인 사용 시 권장
⚠️ CommonJS (require) 기존 Node.js 레거시 시스템 유지 시

Create Project - yarn

mkdir ProjectName && cd ProjectName
yarn init
// 본인 입맛에 맞게 답변을 하면되고 귀찮으면 enter 또는 yarn init -y

yarn init v1.22.19
question name (ProjectName):
question version (1.0.0):
question description:
question entry point (index.tsx):
question repository url:
question author:
question license (MIT):
question private:         
success Saved package.json
Done in 6.64s.

위 과정을 통해 Project root > package.json이 생성된다.

{
  "name": "ProjectName",
  "version": "1.0.0",
  "main": "index.tsx",
  "license": "MIT"
}

Install Packages

1. install react

yarn add react react-dom

  • React는 View를 만들기위한 라이브러리
  • ReactDOM은 UI를 실제로 브라우저에 렌더링 할 때 사용하는 라이브러리

2. install typescript

yarn add -D typescript @types/react @types/react-dom

  • typescript: TypeScript compiles to readable, standards-based JavaScript.
  • @types/react: This package contains type definitions for react
  • @types/react-dom: This package contains type definitions for react-dom

3. install webpack

yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin ts-loader terser-webpack-plugin ignore-loader

  • webpack: javaScript application staic module buddler
    웹팩을 사용하면 응용 프로그램을 구성 요소로 분할하고 이를 종속성 관리를 통해 정적 에셋으로 변환하여 번들링할 수 있다.

  • webpack-cli: webpack commnd interface
    웹팩 빌드 및 관련명령어를 터미널에서 사용할 수 있다.

  • webpack-dev-server: dev server
    실시간으로 변경되는 소스를 확인 하고 디버깅 할수 있다.

  • html-webpack-plugin: 웹팩 빌드 시 자동으로 HTML 파일을 생성.
    웹팩 빌드에서 생성된 JavaScript나 CSS 파일을 HTML 파일에 자동으로 추가할 수 있습니다.

  • webpack-merge: webpack-config merge
    ex) webpack.common.js , webpack.dev.js, webpack.prod.js
    tip) 하나의 config에서 분기를 통해 처리 하는 것보다 파일 나누는게 해보니 나음.

  • ts-loader: typescript loader

  • terser-webpack-plugin; 빌드시 [name].LICENCE.txt 등과 같이 불필요한 파일 생성 방지

  • ignore-loader: .md파일과 같이 빌드에 필요 없는 파일을 무시 하는 기능

  • clean-webpack-plugin: 빌드 결과물 정리 플러그인
    webpack.config > output: {clean: true} 옵션 추가를 통해 할수 있다

4. install babel

낡은 것은 버리기로 한다. 혹 필요 할수도 있으니...
JavaScript 최신 문법을 구형 브라우저에서도 실행 가능한 ES5로 변환 도구.

yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/preset-typescript

  • @babel/core: Babel Core Package

  • @babel/preset-env: 이 preset은 최신 JavaScript 문법을 사용하여 작성된 코드를 지원할 브라우저 환경에 맞게 변환해줍니다. 브라우저 지원 목록을 설정하고 필요한 폴리필을 추가하여 최신 문법을 구형 브라우저에서도 실행 가능하게 만들어줍니다.

  • @babel/preset-react: 이 preset은 React 애플리케이션에서 JSX 문법을 사용할 수 있게 해줍니다. JSX는 JavaScript 내에서 HTML을 작성할 수 있게 해주는 문법으로, React 애플리케이션을 개발할 때 필수적인 도구입니다.

  • babel-loader: Webpack에서 바벨을 사용 하기 위한 로더

  • @babel/preset-typescript: typescript preset


Set Configs

bash code: 직접 하나씩 생성 해보길 권장
touch tsconfig.json && mkdir config && touch config/webpack.common.js && touch config/webpack.dev.js && touch config/webpack.prod.js

간결한 폴더 트리를 위해 설정관련 폴더(config)를 만들고 안에 추가한다.

./tsconfig.js

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "typeRoots": ["src/@types", "./node_modules/@types"],
    "target": "ESNext",
    "jsx": "react",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "strictNullChecks": false,
    "noImplicitAny": true,
    "skipLibCheck": true,
    "allowJs": true,
    "outDir": "./build",
    "strictBindCallApply": false,
    "sourceMap": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "plugins": [{ "transform": "ts-transformer-keys/transformer" }]
  },
  "include": ["src", "jsoneditor-react"],
  "exclude": ["node_modules", "build", "public"]
}

yarn tsc --init 명령어를 통해 생성해보기 권한다.
위 코드는 필자 입맛에 맞게 구성한 가장 기본값이다.

tsc --init을 통해 생성시 "jsx": "react" 구문을 반드시 추가 해준다.

"jsx": "react",

## ./config/babel.config.js
구 브라우져 지원시 추가

module.exports = {
  presets: ["@babel/preset-react", "@babel/preset-env"],
};

./config/webpack.common.js

import path from "path";
import HtmlWebpackPlugin from "html-webpack-plugin";
import webpack from "webpack";

const Webpack = (env, argv, custom) => {
  const { appName, version, minify } = custom;
  console.log(appName, "build version : ", version, JSON.stringify(argv), JSON.stringify(env), JSON.stringify(custom));
  return {
    entry: { main: "./src/index.tsx" },
    resolve: {
      extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".scss"],
      alias: { "@": path.resolve(process.cwd(), "src") },
      symlinks: false,
      cacheWithContext: false
    },
    performance: {
      maxEntrypointSize: 10240000,
      maxAssetSize: 10240000
    },
    module: {
      rules: [
        {
          test: /\.(ts|tsx|js|jsx)$/,
          use: "ts-loader",
          exclude: /node_modules/
        },
        {
          test: /\.md$/,
          use: "ignore-loader"
        }
      ]
    },
    plugins: [
      new webpack.ProvidePlugin({ React: "react" }),
      new HtmlWebpackPlugin({
        title: appName,
        template: "./public/index.html",
        favicon: "./public/favicon.ico",
        minify
      })
    ]
  };
};

export default Webpack;

./config/webpack.dev.js

import { merge } from "webpack-merge";
import common from "./webpack.common.js";

const Webpack = (env, argv) => {
  const custom = {
    appName: argv.name || "client",
    version: env.version || "v0.0.1",
    minify: {
      collapseWhitespace: true,
      removeComments: true
    }
  };

  return merge(common(env, argv, custom), {
    mode: "development",
    devtool: env.debug ? "inline-source-map" : "eval",
    output: {
      publicPath: "/",
      clean: true
    },
    devServer: {
      host: "localhost",
      port: argv.port || 80,
      historyApiFallback: true,
      open: true,
      client: {
        overlay: {
          errors: true,
          warnings: false
        },
        logging: "info"
      }
    }
  });
};

export default Webpack;

./config/webpack.prod.js

import path from "path";
import TerserPlugin from "terser-webpack-plugin";
import { merge } from "webpack-merge";
import common from "./webpack.common.js";

const Webpack = (env, argv) => {
  const version = env.version || "v0.0.1";
  const buildDir = env.dist ? path.resolve(process.cwd(), env.dist) : "build";

  return merge(common(env, argv, { version }), {
    mode: "production",
    output: {
      publicPath: "auto",
      path: buildDir,
      filename: `${version}/[name].js`,
      clean: true
    },
    optimization: {
      minimize: true,
      minimizer: [new TerserPlugin({ extractComments: false })]
    }
  });
};

export default Webpack;

./package.json

"name": "projectname",
  "version": "1.0.0",
  "description": "project description",
  "main": "index.js",
  "type": "module",
  "author": "MIT",
  "license": "MIT",
  "private": true,
  "engines": {
    "node": "22.15.0",
    "yarn": "1.22.22"
  },
  "scripts": {
    "clean": "rm -rf node_modules && yarn cache clean && yarn",
    "start": "yarn webpack-dev-server --port=80 --progress --config config/webpack.dev.js --name=app ",
    "build": "yarn webpack --progress --config config/webpack.prod.js --name=app --env version=v0.0.1 dist=build"
  },
  • ESM 방식을위해 type: "module"을 추가
  • 협업시 엔진 통일을 위해 엔진 추가
  • 실행 스크립트 추가

5. Create Source

bash code: 직접 하나씩 생성 해보길 권장
mkdir -p public && mkdir -p src && touch src/index.tsx && touch src/App.tsx && touch public/index.html

5-1 ./public/index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <link rel="shortcut icon" sizes="any" href="favicon.ico" />
    <link rel="icon" sizes="any" href="favicon.ico" type="image/x-icon" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Expires" content="0" />
    <meta http-equiv="Pragma" content="no-cache" />
    <title>REACT</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

5-2 ./src/App.tsx

import React from "react";

const App = () => {
  return <div>This is App.jsx</div>;
};

export default App;

5-3 ./src/index.tsx

import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const root = createRoot(document.getElementById("root")!);
root.render(
  // StricMode 개발 초기에 구성 요소의 일반적인 버그를 찾을 수 있습니다.
  <StrictMode>
    <App />
  </StrictMode>
);
  • StricMode
    전체 앱에 대해 StricMode 활성화
    앱의 일부에 대해 StricMode 활성화
    개발 중 이중 렌더링으로 발견된 버그 수정
    개발 중 Effects를 다시 실행하여 발견된 버그 수정
    엄격 모드로 활성화된 지원 중단 경고 수정

5-4 ./public/favicon.ico

  1. https://favicon.io/
  2. 위사이트에서 제작후 해당 경로에 저장

6. Execution

yarn dev

yarn start
yarn run v1.22.22
$ yarn webpack-dev-server --port=80 --progress --config config/webpack.dev.js --name=app 
$ C:\work\sources\react-webpack2025\node_modules\.bin\webpack-dev-server --port=80 --progress --config config/webpack.dev.js --name=app
app build version :  v0.0.1 {"port":80,"progress":true,"config":["config/webpack.dev.js"],"name":"app","env":{"WEBPACK_SERVE":true}} {"WEBPACK_SERVE":true} {"appName":"app","version":"v0.0.1","minify":{"collapseWhitespace":true,"removeComments":true}}
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:80/, http://[::1]:80/
<i> [webpack-dev-server] Content not from webpack is served from 'C:\work\sources\react-webpack2025\public' directory
<i> [webpack-dev-server] 404s will fallback to '/index.html'
<i> [webpack-dev-middleware] wait until bundle finished: /
asset main.js 1.19 MiB [emitted] (name: main)
asset favicon.ico 15 KiB [emitted]
asset index.html 565 bytes [emitted]
runtime modules 26.3 KiB 12 modules
modules by path ./node_modules/ 1.09 MiB
  modules by path ./node_modules/webpack-dev-server/client/ 84.8 KiB 8 modules
  modules by path ./node_modules/webpack/hot/*.js 5.17 KiB 4 modules
  modules by path ./node_modules/react-dom/ 947 KiB 4 modules
  modules by path ./node_modules/react/ 44.2 KiB 2 modules
  modules by path ./node_modules/scheduler/ 12 KiB
    ./node_modules/scheduler/index.js 194 bytes [built] [code generated]
    ./node_modules/scheduler/cjs/scheduler.development.js 11.9 KiB [built] [code generated]
  ./node_modules/events/events.js 14.5 KiB [built] [code generated]
  ./node_modules/ansi-html-community/index.js 4.16 KiB [built] [code generated]
modules by path ./src/*.tsx 2.34 KiB
  ./src/index.tsx 1.96 KiB [built] [code generated]
  ./src/App.tsx 387 bytes [built] [code generated]
app (webpack 5.99.8) compiled successfully in 2549 ms
<i> [webpack-dev-middleware] wait until bundle finished: /
assets by path *.js 1.19 MiB
  asset main.js 1.19 MiB [emitted] (name: main)
  asset main.826510bdba39566a87f1.hot-update.js 860 bytes [emitted] [immutable] [hmr] (name: main)
asset favicon.ico 15 KiB [emitted]
asset index.html 565 bytes [emitted]
asset main.826510bdba39566a87f1.hot-update.json 28 bytes [emitted] [immutable] [hmr]
Entrypoint main 1.19 MiB = main.js 1.19 MiB main.826510bdba39566a87f1.hot-update.js 860 bytes
cached modules 1.09 MiB [cached] 24 modules
runtime modules 26.3 KiB 12 modules
app (webpack 5.99.8) compiled successfully in 115 ms


7. Output Tree

├─ config
	~~├─ babel.config.js~~
	├─ webpack.common.js
	├─ webpack.dev.js
	├─ webpack.pro.js
├─ node_modules
├─ public
	├─ favicon.ico
	└─ index.html
├─ src
    ├─ App.jsx
    └─ index.js
├─ package.json
├─ jsconfig.json
└─ yarn.lock

8. package.json

{
  "name": "react-webpack2025",
  "version": "1.0.0",
  "description": "react practics",
  "main": "index.js",
  "type": "module",
  "author": "MIT",
  "license": "MIT",
  "private": true,
  "engines": {
    "node": "22.15.0",
    "yarn": "1.22.22"
  },
  "scripts": {
    "clean": "rm -rf node_modules && yarn cache clean && yarn",
    "start": "yarn webpack-dev-server --port=80 --progress --config config/webpack.dev.js --name=app ",
    "build": "yarn webpack --progress --config config/webpack.prod.js --name=app --env version=v0.0.1 dist=build"
  },
  "dependencies": {
    "react": "^19.1.0",
    "react-dom": "^19.1.0"
  },
  "devDependencies": {
    "@types/react": "^19.1.4",
    "@types/react-dom": "^19.1.4",
    "html-webpack-plugin": "^5.6.3",
    "ignore-loader": "^0.1.2",
    "terser-webpack-plugin": "^5.3.14",
    "ts-loader": "^9.5.2",
    "typescript": "^5.8.3",
    "webpack": "^5.99.8",
    "webpack-cli": "^6.0.1",
    "webpack-dev-server": "^5.2.1"
  }
}

0개의 댓글