며칠전 회사에서 디자인 시스템을 구축하라는 명을 받았다.
이때다 싶어 평소에 사용해보고 싶었던 Vite
와 Storybook
을 사용해서 디자인 시스템을 구축하고 더불어 npm
에 배포 후 원하는 프로젝트에서 편리하게 사용해보기로 했다.
(시니어 프론트엔드 개발자가 없어서 나름대로 사전 조사 후 하기로 결정함. 막 한거 아님.)
하쥐만 문득 vite
를 지금부터 게속 사용하게되면 CRA
없이 react
설정과 webpack
설정 등을 직접 전부 해보려고 했던 내가.. 시간이 흐를수록 절대 하지 않을 것 같다..
그래서 CRA 없이 처음부터 세팅을 해보기로 맘 먹었다.
세팅하기로 이왕 맘 먹은김에 완전 제대로 하기위해 세팅에 필요한 각각의 요소들의 역할이나 자세한 정보들까지 조사하고 공부하기로 했다.
npm init -y
npm i react react-dom react-router-dom
우선 npm init
을 해주고 React를 사용하는 프로젝트에서 거~의 필수적으로 사용되는 패키지들을 설치 해주겠다.
react
: React를 사용하기 위해 설치 해야하며 component, state, props 등의 react에서 주요 기능들을 포함하고 있다.react-dom
: React의 컴포넌트들을 DOM에 렌더링을 해주기 위한 라이브러리이다.react-router-dom
: SPA 중 React의 라우팅 처리를 도와준다.Babel
은 Javascript
의 컴파일러로서 주로 ECMAScript 2015(ES6) 이상 버전의 코드들을 그 이전 버전으로 트랜스파일링 해주며, 특정 기능이 브라우저에서 기본적으로 지원 되지 않는 경우 폴리필을 제공해준다.
또, 프론트엔드 프레임워크 및 라이브러리들(React, Vue, Angular)의 특정 문법들을 vanila javascript로 변환해준다!
정리하자면, Babel
은 최신 Javascript
를 안전하게 사용하게 해주고, 브라우저 간의 호환성 문제도 해결해주며, React를 사용할 때 JSX 문법으로 작성된 코드들을 Javascript로 변환 시켜줄 수 있다. (플러그인이나 프리셋을 통해 더 확장해서 변환 처리도 가능)
npm i -D @babel/preset-env @babel/preset-react @babel/preset-typescript core-js
babel
에서 필수로 사용해야하는 패키지들을 설치해준다.
@babel/preset-env
: 구문 변환 및 폴리필 같은 다양한 처리를 해준다.@babel/preset-react
: JSX 문법을 JS로 바꿔주는 등의 처리를 해준다.@babel/preset-typescript
: typescript를 javascript로 바꿔주는 등의 처리를 해준다.core-js
: pollyfill을 위해서 설치해주는 패키지babel
패키지를 설치한 후 bable.config.js
파일을 루트 디렉토리에 생성하고 설정을 해준다.
const presets = [
"@babel/preset-react", // jsx 구문들을 javascript로 변환하는데 사용.
[
"@babel/preset-env", // 브라우저의 버전과 node.js의 버전 등에 대해 호환성을 생각하지 않고 최신 javascript의 기능들을 사용할 수 있음.
{
modules: false, // 컴파일할 모듈 시스템을 결정. => false로 설정하면 모듈이 변환되지 않아서 ES6 모듈을 사용함.
useBuiltIns: "usage", // 코드에서 실제로 사용하고 있는 pollyfill만 포함하도록 처리(babel 자동감지 처리).
corejs: 3, // 사용중인 core-js의 버전을 지정. => 위의 useBuiltIns를 제대로 작동하게 하기 위해서 사용.
},
],
"@babel/preset-typescript", // tsc를 사용하여 ts를 js로 변환하지 않아도 ts를 작성하고 js를 출력할 수 있도록 사용. => 트랜스파일 처리는 되지만 타입 검사는 안해줌.
];
const plugins = [];
module.exports = { presets, plugins };
위의 설정값들을 옆에 주석값들을 자세히 달아 두었으니 참고해서 사용하면 된다!
한 가지 정확하게 짚어줄 부분은 modules: false
부분이다.
@babel/preset-env
은 babel을 사용하는 이유 중 가장 큰 이유이다(그렇다고 다른 기능이 필요 없다는게 절대 아님.)
하지만 @babel/preset-env
설정이 되어 있으면 CommonJS
모듈 방식으로 자동 변환이 이뤄진다. CommonJS
모듈 방식은 Tree Shaking
을 해줄수가 없기 때문에 modules: false
로 설정하면 모듈 방식을 CommonJS
로 변환하지 않는다는 뜻이므로 webpack
이 Tree Shaking
을 해줄 수 있다.
webpack
이란 javascript
를 중심으로 한 모듈 번들러 이다. webpack
자체적으로는 javascript
와 json
만 번들링이 가능한데, loader
가 있으면 다른 유형의 파일들도 웹팩이 처리할 수 있는 모듈로 변환이 가능하다.
webpack
이 나오기 한참 전으로 거슬러 가본다면 javascript
는 모듈(module)도 없었다. 그리고 시간이 지남에 따라 CJS
, ESM
방식의 모듈이 만들어졌다.
이제는 모듈은 사용할 수 있는데, js 파일을 동시에 여러 개 호출하게되면 속도 문제가 발생하게 되었다. 또, 특정 js 파일의 로딩이 지연되면 전체가 늦어져서 문제가 되었다.
결국 여러 문제를 해결하기 위해서 나온 것이 번들러이고, 현재 가장 유명한 번들러로는 Webpack이라고 할 수 있다. 하지만 빌드가 오래걸리는 것이 문제점이라고 볼 수 있다.
Vite는 Esbuild 기반으로 만들어진 Snowpack의 컨셉을 모두 흡수하면서도 좀 더 간결하고 사용성이 좋게 만들어진 번들러이다.
// 웹 팩에 필요한 패키지들
npm i -D webpack webpack-cli webpack-dev-server webpack-merge
// JS가 아닌 다른 유형의 파일들을 번들링 하기위한 패키지들
npm i -D css-loader style-loader babel-loader
// Plugin
npm i -D html-webpack-plugin css-minimizer-webpack-plugin webpack-bundle-analyzer react-refresh @pmmmwh/react-refresh-webpack-plugin
webpack
, loader
, plugin
패키지들을 설치해준다.
webpack
: webpack을 사용하기 위해 설치해야한다.
webpack-cli
: CLI에서 webpack 명령어를 사용하기 위해 사용한다.
webpack-dev-server
: 개발 서버로 실행하기 위해서 사용한다.
webpack-merge
: 모드에 따라 다른 설정을 적용한 파일을 번들링 하는데 사용한다.
css-loader
: css 파일을 읽기 위해 사용한다.
style-loader
: css를 inline으로 <style></style>
안에 넣어주기 위해 사용한다.
babel-loader
: webpack이 번들링 처리를 할 때 babel을 사용하게 해주기 위해 사용한다.
html-webpack-plugin
: 번들링 결과를 포함한 HTML을 제공하기 위해 사용한다.
css-minimizer-webpack-plugin
: css를 압축하기 위해서 사용한다.
webpack-bundle-analyzer
: 번들 사이즈를 UI로 보여주기 위해 사용
@pmmmwh/react-refresh-webpack-plugin
: React에서 HMR(Hot Module Replacement)을 적용하기 위해서 사용 (이게 없으면 코드 변경 시 새로고침됨)
webpack
의 패키지를 설치한 후 webpack
의 config
설정을 해주면된다.
webpack
의 config
설정은 3가지로 나눠서 처리했다.
webpack
의 설정 파일들에 대한 내용은 주석으로 아주 자세하게 달아 두었으니 참고하실 분은 참고 바람!
webpack.common.ts
- 공통으로 사용할 webpack
설정// webpack.common.ts
import path from "path";
import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
const configuration: webpack.Configuration = {
resolve: {
extensions: [".ts", ".tsx", ".js", "jsx"], // import 할 때 확장자를 설정해주지 않아도 됌. => ex) import './index.tsx' -> import './index'
alias: {
"@src": path.resolve(__dirname, "../src/"), // 상대 경로들에 대해서 절대 경로를 지정해 줄 수 있음. => ex) ../../../src/ -> @src/
},
},
entry: "./src/index", // webpack이 종속성 그래프를 만들 때의 진입점 파일을 설정.
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/, // 규칙을 적용시킬 확장자들을 선언. => ts,tsx,js,jsx 지정
use: ["babel-loader"], // 사용할 트랜스파일러를 지정. => 위의 test에서 지정된 확장자들을 babel을 사용해서 트랜스파일 처리.
exclude: /node_modules/, // 제외할 디렉토리 지정. => babel이 node_modules를 트랜스파일 처리 하지 않게 제외 시킴.
},
],
},
plugins: [
// webpack 번들에 대한 스크립트가 포함된 index.html을 기본적으로 생성하고, 추가 설정 시 생성되는 html파일 내의 설정을 조정할 수 있다. => public폴더 안의 index.html을 기본 템플릿으로 설정
new HtmlWebpackPlugin({
template: path.join(__dirname, "..", "public", "index.html"),
}),
new webpack.ProvidePlugin({ React: "react" }), // import 하지 않아도 자동으로 로드되는 모듈을 지정 => React를 직접 import하지 않아도 webpack에 의해 번들로 제공되는 파일에서 참조될 땜마다 React를 자동으로 import함.
],
};
export default configuration;
webpack.dev.ts
- 개발 모드에서 사용할 webpack
설정// webpack.dev.ts
import path from "path";
import webpack from "webpack";
import "webpack-dev-server";
import { merge } from "webpack-merge";
import common from "./webpack.common";
import ReactRefreshPlugin from "@pmmmwh/react-refresh-webpack-plugin";
const configuration: webpack.Configuration = {
mode: "development", // 개발 모드를 활성화하여 디버깅 및 개발에 최적화된 번들링과 HMR이 가능해짐.
devtool: "inline-source-map", // 소스맵 생성 방식 정의 - 디버깅을 수월하게 하기위해 소스 맵 파일을 번들리에 포함 시킴. => 번들링된 파일에서 에러가 났을 때 소스 맵 파일로 인해 React에서 에러가 난 부분의 정확한 위치를 찾을 수 있음.
output: {
path: path.resolve(__dirname, "../dist"), // 번들 파일들을 출력할 디렉토리를 지정.
filename: "[name].bundle.js", // 출력된 번들 파일의 이름 지정.
},
module: {
rules: [
{
test: /\.css$/, // .css 확장자를 타겟팅
use: ["style-loader", "css-loader"], // test에 선언된 확장자 파일들을 'style-loader', 'css-loader'를 사용해 파일을 변환함.
exclude: /node_modules/, // node_modules는 제외하고 적용
},
],
},
plugins: [new ReactRefreshPlugin()], // webpack dev 서버에 react의 변경 사항을 실시간으로 반영 => react용 HMR
devServer: {
static: path.join(__dirname, "public"), // 정적 파일을 제공할 디렉토리 설정
port: 3000, // 개발 서버의 포트 번호 설정 => auto로 설정 가능
open: true, // 서버가 시작할 때 자동으로 브라우저를 여는 설정
compress: true, // 모든 자원들을 gzip 압축하여 제공
historyApiFallback: true, // 404 에러 대신 index.html 페이지로 리디렉션 시킴 => SPA를 위한 설정
hot: true, // HMR을 활성화 시켜 코드 변경 시 전체 페이지 새로고침 없이 해당 모듈만 반영 시킴. => RefreshWebpackPlugin이 없으면 react에서 state나 props를 일반 HMR로는 빠른 대응이 불가능 해서 RefreshWebpackPlugin를 플러그인에 포함시켜 react에 특화된 HMR을 제공
},
watchOptions: {
ignored: /node_modules/, // 변경된 부분을 감지하는 파일 목록 중 node_moduels의 변경은 감지 옵션에서 제외 시킴.
},
};
export default merge(common, configuration);
webpack.prod.ts
- 프로덕션용 webpack
설정// webpack.prod.ts
import path from "path";
import webpack from "webpack";
import { merge } from "webpack-merge";
import common from "./webpack.common";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
const configuration: webpack.Configuration = {
mode: "production", // 최적화와 압축을 위한 여러 플러그인들이 기본적으로 활성화 됌.
devtool: "cheap-module-source-map", // 소스 맵 생성방식 정의 - 각 열에 대한 정보 제공하지 않고 행에 대한 매핑 정보만 제공하고 프로젝트 원본 소스 코드의 매핑 뿐만 아니라 loader에 의해 변환된 모든 모듈들에 대한 소스 맵도 생성
output: {
path: path.resolve(__dirname, "../dist"),
filename: "[name].[contenthash].js", // 출력 파일의 이름을 결정. => [contenthash] 사용 시 파일 이름이 같을 때 캐시에 저장하는 브라우저의 특성을 활용해 파일 내용이 변경 될 때만 파일 이름을 바꿔줌.
clean: true, // 빌드 전 'output.paht' 경로에 있는 디렉토리 내용을 자동으로 삭제.
},
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"], // MiniCssExtractPlugin.loader와 css-loader을 같이 사용하게 되면, css를 별도의 파일로 추출해 줄 수 있어 js와 css를 병렬 처리가 가능하게함.
},
],
},
plugins: [new MiniCssExtractPlugin()], // 플러그인에 포함시켜 MiniCssExtractPlugin를 사용하게함. => css를 별도의 파일로 추출하여 캐싱 및 병렬 처리를 통해 성능을 최적화 함.
optimization: {
usedExports: true, // tree shaking을 활성화. => 사용하지 않는 코드를 제거 (import하고 사용하지 않는 코드는 번들링 후 제거)
minimize: true, // 번들을 최소화 시킴.
minimizer: [new CssMinimizerPlugin()], // css의 공백, 주석등을 제거해서 파일 크기를 줄여줌.
},
};
export default merge(common, configuration);
webpack
설정을 마무리한 후 package.json
안 scripts
를 아래와 같이 해주면 된다!
"scripts": {
"dev": "npx webpack serve --config config/webpack.dev.ts",
"build:dev": "npx webpack --config config/webpack.dev.ts",
"build:prod": "npx webpack --config config/webpack.prod.ts"
},
npm i -D typescript ts-node @types/react @types/react-dom @types/webpack @types/webpack-dev-server @types/node
typescript
관련 패키지들을 설치해준다.
typescript
: typescript 사용을 위해 설치해준다.ts-node
: node.js에서 typescript를 실행하기 위해 사용한다.@types/react
: react에 대한 타입 정의를 해주기 위해서 사용한다.@types/react-dom
: reactDOM에 대한 타입 정의를 해주기 위해서 사용한다.@types/webpack
: webpack에 대한 타입 정의를 해주기 위해서 사용한다.@types/webpack-dev-server
: webpack dev server에 대한 타입 정의를 해주기 위해서 사용한다.@types/node
: node에 대한 타입을 정의해주기 위해서 사용한다.typescript
관련 패키지들을 설치해준 후 tsconfig.json
의 설정을 아래와 같이 해주면 된다.
tsconfig.json
안에 주석으로 자세하게 설명을 달아두었으니 참고 하실 분은 참고 바람!
{
"compilerOptions": {
"outDir": "./dist", // 컴파일러가 컴파일된 javascript 파일을 배치하는 위치를 지정. => 컴파일된 파일을 ./dist 경로에 저장
"target": "ES5", // 컴파일러가 코드들을 지정한 버전으로 변환. => ES5 Javascript 구문으로 변환
"module": "ESNext", // 사용하려는 모듈 시스템을 정함. => ES6 스타일 모듈 구문을 사용함.
"jsx": "react-jsx", // JSX 구문을 처리하는 방법을 typescript에 알려줌. => react-jsx로 설정 시 React 17 부터 도입된 새로운 JSX Transform을 사용하게된다. 이 때문에 jsx 구문을 사용하는 파일 맨 위에 "import React from 'react'"를 불러오지 않아도 jsx문법을 사용할 수 있다.
"noImplicitAny": true, // true로 설정 시 typescript가 유추할 수 없는 모든 변수가 컴파일 오류를 발생 시킴.
"allowSyntheticDefaultImports": true, //
"lib": ["dom", "dom.Iterable", "esnext"], //
"allowJs": true, // true로 설정 시 컴파일에 javascript 파일을 포함시킬 수 있음.
"skipLibCheck": true, // true로 설정 시 모든 선언 파일(.d.ts)에 대한 타입 검사를 건너 뜀.
"esModuleInterop": true, // true로 설정 시 ES6 모듈 가져오기/내보내기의 상호 운용성을 활성화하여 모든 가져오기에 대한 네임스페이스 개체를 생성.
"strict": true, // 타입 검사를 더 강력하게 시킴.
"forceConsistentCasingInFileNames": true, // 대소문자를 구분하는 파일 시스템에서 문제를 방지하기위해 파일이 동일한 대소문자로 일관되게 참조되도록 함.
"moduleResolution": "node", //
"resolveJsonModule": true, // json 모듈을 가져올 수 있게 함.
"isolatedModules": true, //
// 절대 경로 설정
"baseUrl": ".", // paths의 base가 되는 url경로
"paths": {
// 절대 경로를 설정하기 위해 선언
"@src/*": ["src/*"] // ex) ../../../components/button -> @src/components/button
}
},
"include": ["src"], // 컴파일에 포함할 파일 및 폴더를 지정하는 glob 패턴의 배열. => src 폴더 안에 있는 파일만 컴파일에 포함 됌.
"exclude": ["node_modules", "dist"], // 컴파일에서 제외할 파일 또는 폴더를 지정하는 glob 패턴의 배열. => node_modules, dist 폴더 내에 파일들을 컴파일 하는 것을 제외
// webpack 설정 파일들을 typescript로 처리하기 위해 사용
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}
위의 설정까지 전부 끝났다면, 이제 public
폴더안에 index.html
파일을 생성하고 src
폴더안에 App.tsx
, index.tsx
를 만들어 주고 아래와 같이 코드를 작성해주면 된다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<title>Title</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
const App = () => <div>Hello, Webpack!</div>;
export default App;
import { createRoot } from "react-dom/client";
import App from "@src/App";
const container = document.getElementById("root");
const root = createRoot(container as Element);
root.render(<App />);
여기까지 설정이 끝났다면 디렉토리 구조가 아래와 같이 나오게 된다.
├── babel.config.js
├── config
│ ├── webpack.common.ts
│ ├── webpack.dev.ts
│ └── webpack.prod.ts
├── package.json
├── public
│ └── index.html
├── src
│ ├── App.tsx
│ └── index.tsx
├── tsconfig.json
└── yarn.lock
이제 yarn dev
or npm run dev
명령어를 사용해 실행시켜보면 아주 잘 작동한다!
정리가 잘 된 글이네요. 도움이 됐습니다.