├── .env.development
├── .env.production
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── __mocks__
├── fileMock.js
└── svgMock.js
├── babel.config.js
├── jest.config.js
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.test.tsx
├── App.tsx
├── assets
│ ├── bigSvgTest.svg
│ ├── fonts
│ │ ├── Pretendard-Medium.subset.woff
│ │ └── Pretendard-Medium.subset.woff2
│ ├── logo.svg
│ ├── sheep.jpg
│ ├── test.webp
│ └── videos
│ │ ├── videoTest.mp4
│ │ └── videoTest.webm
├── components
│ ├── Example.test.tsx
│ └── Example.tsx
├── index.tsx
├── pages
│ ├── Home
│ │ ├── Home.test.tsx
│ │ └── index.tsx
│ └── Test
│ │ └── index.tsx
├── setupTests.ts
├── style.css
└── types
│ └── index.d.ts
├── tsconfig.json
├── tsconfig.paths.json
├── webpack
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
└── yarn.lock
/.env.development:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | REACT_APP_EXAMPLE=development
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | NODE_ENV=production
2 | REACT_APP_EXAMPLE=production
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | parser: '@typescript-eslint/parser',
8 | plugins: [
9 | '@typescript-eslint',
10 | 'react',
11 | 'react-hooks',
12 | 'prettier', // eslint-plugin-prettier: ESLint 규칙에 Prettier의 규칙을 적용
13 | ],
14 | extends: [
15 | 'eslint:recommended',
16 | 'plugin:react/recommended',
17 | 'plugin:@typescript-eslint/recommended',
18 | 'plugin:prettier/recommended', // eslint-config-prettier: Prettier의 설정과 충돌하는 ESLint의 규칙을 자동으로 비활성화
19 | ],
20 | rules: {
21 | '@typescript-eslint/no-var-requires': 'off', // require 감지 off
22 | '@typescript-eslint/explicit-function-return-type': 'off', // 함수 반환 타입 지정
23 | '@typescript-eslint/explicit-module-boundary-types': 'off',
24 | '@typescript-eslint/no-explicit-any': 'off', // any 타입사용을 방지
25 | 'prettier/prettier': 'error', // Prettier와 충돌하는 규칙을 에러 발생
26 | 'react/react-in-jsx-scope': 'off',
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "semi": true,
4 | "singleQuote": true,
5 | "jsxBracketSameLine": true,
6 | "eslintIntegration": true,
7 | "parser": "typescript",
8 | "useTabs": false,
9 | "tabWidth": 2,
10 | "endOfLine": "auto"
11 | }
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 💻 리액트 커스텀 개발 환경 보일러 플레이트
2 | - 해당 저장소는 `리액트 커스텀 개발 환경 보일러 플레이트 저장소`로서, react, webpack, babel, eslint 등 리액트 프로젝트를 진행 할 때 기본적으로 셋팅해야되는 개발 환경을 구축한 저장소입니다.
3 | - 기본적으로 react v18, webpack v5, esbuild-loader v3 등 `2023년 6월 기준` 최신 버전으로 구축되어 있습니다.
4 | - README 문서 하단에는 참고 내용이라던가 개인적으로 더 추가하면 좋은 로더/플러그인 내용도 있으니 참고하면 좋을 것 같습니다.
5 | - 개선 할 내용이 있다면 issue 또는 Pull Request도 등록해주시면 감사드립니다.
6 |
7 |
8 |
9 | ## 📗 사용 절차
10 | ### 1. Repo Clone
11 | ```
12 | git clone https://github.com/ssi02014/react-dev-env-boilerplate.git
13 | ```
14 |
15 |
16 |
17 | ### 2. Branch & Checkout & Pull
18 | - 해당 레포는 다양한 환경에 맞춰 브랜치가 구성되어 있다. 기호에 맞게 참고 or 사용하면 된다.
19 | - 만약, `esbuild-loader` 브랜치 코드를 확인하려면 아래의 절차를 진행한다.
20 |
21 | ```
22 | git branch esbuild-loader
23 |
24 | git checkout -m esbuild-loader
25 |
26 | git pull origin esbuild-loader
27 | ```
28 |
29 |
30 |
31 | ### 3. 패키지 설치
32 | - 해당 레포는 `yarn` 패키지 매니저를 기반으로 패키지를 관리하고 있다. 따라서 yarn으로 패키지를 설치하자.
33 |
34 | ```
35 | yarn
36 | or
37 | yarn install
38 | ```
39 |
40 |
41 |
42 | ## 👨🏻💻 master Branch
43 | - 가장 기본적인 react, babel, typescript, eslint, prettier, jest, React Testing Library 셋팅 브랜치
44 |
45 |
46 |
47 | **셋팅 목록**
48 | - react v18
49 | - typeScript
50 | - webpack v5
51 | - **babel**
52 | - eslint
53 | - prettier
54 | - **jest**
55 | - **react testing library**
56 |
57 |
58 |
59 | ## 👨🏻💻 esbuild-loader Branch
60 | - master Branch 셋팅에서 babel-loader대신 `esbuild-loader`를 적용해서 빌드 타임을 감소시킨 브랜치
61 |
62 |
63 |
64 | **셋팅 목록**
65 | - react v18
66 | - typeScript
67 | - webpack v5
68 | - **esbuild-loader**
69 | - eslint
70 | - prettier
71 |
72 |
73 |
74 | ## 👨🏻💻 esbuild-jest Branch
75 | - esbuild-loader Branch 셋팅에서 `jest`, `react testing library` 테스트 환경 추가 브랜치
76 | - 테스트 환경을 위해 `@babel/preset-typescript`, `@babel/preset-env`, `@babel/preset-react` 및 babel 추가 셋팅
77 |
78 |
79 |
80 | **셋팅 목록**
81 | - react v18
82 | - typeScript
83 | - webpack v5
84 | - esbuild-loader
85 | - eslint
86 | - prettier
87 | - **jest**
88 | - **react testing library**
89 | - jest를 위한 babel 일부 적용
90 |
91 |
92 |
93 | ## 🚀 그 외 보일러 플레이트 저장소
94 | ### react-npm-deploy-boilerplate
95 | - [react-npm-deploy-boilerplate](https://github.com/ssi02014/react-npm-deploy-boilerplate)
96 | - Design System와 같은 npm 오픈소스 배포에 최적화 보일러 플레이트 저장소
97 | - React, rollup, Typescript, Storybook 등으로 구성
98 | - github actions를 이용한 자동 빌드 및 배포 구현
99 | - Storybook을 github page로 배포함으로써 데모 페이지로서 활용
100 |
101 |
102 |
103 | ### React 차세대 개발 툴 적용 보일러 플레이트 저장소
104 | - [react-vite-berry-boilerplate](https://github.com/ssi02014/react-vite-berry-boilerplate)
105 | - React, TypeScript, Vite, Yarn Berry로 구성한 리액트 프로젝트 보일러 플레이트 저장소
106 | - `Vite`를 이용한 리액트(+타입스크립트) 프로젝트 구성
107 | - Yarn Berry의 `Plug’n’Play(PnP)`와 `Zero-Install` 적용
108 |
109 |
110 |
111 |
112 | ## 🙋🏻 참고 문서
113 | ### 📄 1. esbuild-loader?
114 | - esbuild-loader는 `멀티 스레드 기반`으로 동작하는 Go언어로 작성된 로더이다. 그래서 싱글 스레드 기반인 자바스크립트로 만들어진 babel보다 언어가 동작하는 본질적인 차이때문에 퍼포먼스 측면에서 차이가 크다.
115 | - [kakao esbuild-loader 문서](https://fe-developers.kakaoent.com/2022/220707-webpack-esbuild-loader/)
116 |
117 |
118 |
119 | **기존 babel-loader 빌드 타임**
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | **esbuild-loader 도입 후 빌드 타임**
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | ### 📄 2. @svgr/webpack
136 | - @svgr/webpack 패키지는 svg를 리액트 컴포넌트 형식으로 사용할 수 있게 변환해주는 웹팩 모듈이다.
137 | - https://react-svgr.com/
138 |
139 |
140 |
141 | **예제**
142 | ```jsx
143 | import Star from './star.svg'
144 |
145 | const Example = () => (
146 |
147 |
148 |
149 | )
150 | ```
151 | - @svgr/webpack를 적용하면 위 예제처럼 svg를 import해서 컴포넌트로 형식으로 사용할 수 있다.
152 |
153 |
154 |
155 | ### 📄 3. Font Preload?
156 | - Create React App(CRA)로 만든 프로젝트는 font를 preload하려면 `craco`와 같은 라이브러리를 활용해서 `preload-webpack-plugin`, `webpack-font-preload-plugin`을 적용했어야 했다.
157 | - TMI: preload-webpack-plugin는 webpack5부터 호환되지 않아서 webpack-font-preload-plugin를 사용해야 함
158 | - 하지만, 해당 보일러 플레이트는 `html-loader`가 적용되어 있기 때문에 preload를 위한 플러그인을 설치하지 않아도 된다.
159 | - CRA는 html-loader가 적용되어있지 않고, html-webpack-plugin 만 적용되어 있어서 이런 차이가 있음.
160 | - 자세한 내용은 [블로그: 커스텀 웹팩 환경에서 font preload하면서 겪은 문제 고찰](https://blog.naver.com/ssi02014/223079553571) 참고
161 | - 실제 적용 방법은 폰트 파일들이 `src/assets/fonts` 경로에 있다고 가정하고, `index.html`파일을 아래처럼 작성하면 된다.
162 | ```html
163 |
164 |
165 |
172 |
173 | ```
174 | - 현재 상태에서 `build` 해보고 확인해보면 제대로된 경로로 파일을 가져오는 것을 확인할 수 있다.
175 |
176 | 
177 |
178 |
179 |
180 |
181 | ## 👍 그 외 추가하면 좋은 로더 및 플러그인
182 | ### ⭐️ 1. SASS(SCSS)
183 | - Sass는 Syntactically Awesome Style Sheets의 약자로, CSS의 확장된 문법을 제공하는 CSS 전처리기(preprocessor)다.
184 | - Sass는 빌드 시에 CSS 파일로 컴파일되야 한다. 따라서, webpack에서 Sass 파일을 읽고, CSS 파일로 컴파일하여 번들링하려면 sass-loader를 추가해야 합니다.
185 |
186 |
187 |
188 | **패키지 설치**
189 | ```
190 | yarn add -D sass sass-loader
191 | ```
192 |
193 |
194 |
195 | **webpack 셋팅**
196 | - `[MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']` 와 같이 배열을 넣었는데 여기서 순서가 중요하다.
197 | - 이는 배열 마지막 요소부터 로더가 적용되기 때문인데, sass-loader를 통해서 먼저 sass파일을 css로 컴파일 한 후, css-loader가 컴파일 된 css 파일을 모듈로 읽어들인 다음, MiniCssExtractPlugin.loader 가 별도의 파일로 추출된 css 파일을 로드하기 때문이다.
198 | - 배열 순서를 정확하게 작성하지 로더가 정상적으로 작동하지 않아 에러가 발생할 수 있으니..😱 순서를 꼭 맞춰주자!
199 | ```js
200 | module.exports = {
201 | module: {
202 | rules: [
203 | // ...webpack rules
204 | {
205 | test: /\.(sa|sc|c)ss$/i, // scss, sass, css 파일 매칭
206 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], // sass-loader 추가
207 | },
208 | ]
209 | }
210 | }
211 | ```
212 |
213 |
214 |
215 | **scss파일 생성 후 적용**
216 | ```scss
217 | // style.scss
218 | body {
219 | font-family: Pretendard;
220 | background-color: rgb(192, 249, 230);
221 | }
222 | ```
223 | ```js
224 | // App.tsx
225 | import './style.scss';
226 | import './style2.css';
227 |
228 | function App() {
229 | // ...
230 | }
231 |
232 | export default App;
233 | ```
234 |
235 |
236 |
237 | ### ⭐️ 2. webpack-bundle-analyzer
238 | 
239 |
240 | - webpack-bundle-analyzer는 webpack 번들링 결과를 시각화하여 번들링 크기를 확인할 수 있도록 도와주는 도구다.
241 | - webpack-bundle-analyzer는 webpack으로 번들링된 결과물을 시각적으로 파악하여 번들링 크기를 줄이는 데 도움을 줄 수 있다.
242 | - [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer)
243 |
244 |
245 |
246 | **패키지 설치**
247 | ```
248 | yarn add -D webpack-bundle-analyzer
249 | ```
250 |
251 |
252 |
253 | **webpack 셋팅**
254 | ```js
255 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
256 |
257 | module.exports = {
258 | plugins: [
259 | // webpack plugins...
260 |
261 | new BundleAnalyzerPlugin({
262 | analyzerMode: "static", // 분석 파일 html을 build폴더에 저장
263 | reportFilename: 'bundle-report.html', // 분석 파일 보고서 이름(자유롭게 지정)
264 | openAnalyzer: false, // 분석 파일을 실행 시 자동으로 열지 않는다.
265 | generateStatsFile: true, // 분석 파일을 json으로 저장한다.
266 | statsFilename: "bundle-report.json", // 분석 파일 json 파일 이름 (자유롭게 지정)
267 | })
268 | ]
269 | }
270 | ```
271 |
272 |
273 |
274 | **package.json 셋팅**
275 | ```json
276 | {
277 | // ...
278 | "scripts": {
279 | // ...
280 | "preanalyze": "yarn build:prod",
281 | "analyze": "webpack-bundle-analyzer ./build/bundle-report.json --default-sizes gzip",
282 | }
283 | }
284 | ```
285 |
286 |
287 |
288 | **yarn analyze 실행**
289 | - yarn analyze 실행 시 빌드 후 분석 진행
290 | ```
291 | yarn analyze
292 | ```
293 |
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = '';
2 |
--------------------------------------------------------------------------------
/__mocks__/svgMock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SvgrMock = React.forwardRef((props, ref) => (
4 |
5 | ));
6 |
7 | SvgrMock.displayName = 'SvgrMock';
8 |
9 | export const ReactComponent = SvgrMock;
10 | export default SvgrMock;
11 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@babel/preset-env',
4 | '@babel/preset-typescript',
5 | [
6 | '@babel/preset-react',
7 | {
8 | runtime: 'automatic', // jest로 테스트 시 React 컴폰너트 읽기 위함.
9 | },
10 | ],
11 | ],
12 | plugins: [
13 | [
14 | '@babel/plugin-transform-runtime',
15 | {
16 | corejs: 3,
17 | },
18 | ],
19 | ],
20 | };
21 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
3 | testEnvironment: 'jsdom',
4 | testMatch: [
5 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
6 | '/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
7 | ],
8 | moduleNameMapper: {
9 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy', // import 문으로 불러온 CSS 모듈에 대한 Mock 생성
10 | '.+\\.svg?.+$': '/__mocks__/svgMock.js', // svg에 대한 Mock 생성
11 | '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
12 | '/__mocks__/fileMock.js', // jpg, jpeg, webp, woff 등등 Mock 생성
13 | '^@components(.*)$': '/src/components$1', // jest가 alias 경로 알아먹을 수 있게 적용
14 | '^@(components|assets)(.*)$': '/src/$1$2', // jest가 alias 경로 알아먹을 수 있게 적용, 경로 추가 시 key 정규표현식에다 추가
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_development_environment_boilerplate",
3 | "version": "1.0.0",
4 | "description": "리액트 개발 환경 보일러플레이트",
5 | "scripts": {
6 | "test": "jest",
7 | "start": "webpack serve --open --config webpack/webpack.dev.js",
8 | "start:build": "serve -s build -l 8080",
9 | "build:prod": "webpack --config webpack/webpack.prod.js",
10 | "build:dev": "webpack --config webpack/webpack.dev.js"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/ssi02014/React_Development_Environment_Boilerplate.git"
15 | },
16 | "author": "Gromit",
17 | "license": "ISC",
18 | "bugs": {
19 | "url": "https://github.com/ssi02014/React_Development_Environment_Boilerplate/issues"
20 | },
21 | "homepage": "https://github.com/ssi02014/React_Development_Environment_Boilerplate/blob/master/README.md",
22 | "dependencies": {
23 | "react": "^18.2.0",
24 | "react-dom": "^18.2.0",
25 | "react-router": "^6.11.2",
26 | "react-router-dom": "^6.11.2",
27 | "react-scripts": "^5.0.1",
28 | "typescript": "^5.1.3"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "^7.22.1",
32 | "@babel/plugin-transform-runtime": "^7.22.4",
33 | "@babel/preset-env": "^7.22.4",
34 | "@babel/preset-react": "^7.22.3",
35 | "@babel/preset-typescript": "^7.21.5",
36 | "@babel/runtime-corejs3": "^7.22.3",
37 | "@svgr/webpack": "^8.0.1",
38 | "@testing-library/jest-dom": "^5.16.5",
39 | "@testing-library/react": "^14.0.0",
40 | "@testing-library/user-event": "^14.4.3",
41 | "@types/node": "^20.2.5",
42 | "@types/react": "^18.2.8",
43 | "@types/react-dom": "^18.2.4",
44 | "@types/react-router-dom": "^5.3.3",
45 | "@typescript-eslint/eslint-plugin": "^5.59.8",
46 | "@typescript-eslint/parser": "^5.59.8",
47 | "babel-loader": "^9.1.2",
48 | "clean-webpack-plugin": "^4.0.0",
49 | "compression-webpack-plugin": "^10.0.0",
50 | "copy-webpack-plugin": "^11.0.0",
51 | "css-loader": "^6.8.1",
52 | "css-minimizer-webpack-plugin": "^5.0.0",
53 | "dotenv": "^16.1.4",
54 | "eslint": "^8.42.0",
55 | "eslint-config-prettier": "^8.8.0",
56 | "eslint-plugin-prettier": "^4.2.1",
57 | "eslint-plugin-react": "^7.32.2",
58 | "eslint-plugin-react-hooks": "^4.6.0",
59 | "eslint-webpack-plugin": "^4.0.1",
60 | "fork-ts-checker-webpack-plugin": "^8.0.0",
61 | "html-loader": "^4.2.0",
62 | "html-webpack-plugin": "^5.5.1",
63 | "identity-obj-proxy": "^3.0.0",
64 | "jest": "^29.5.0",
65 | "jest-environment-jsdom": "^29.5.0",
66 | "mini-css-extract-plugin": "^2.7.6",
67 | "prettier": "^2.8.8",
68 | "serve": "^14.2.0",
69 | "terser-webpack-plugin": "^5.3.9",
70 | "ts-jest": "^29.1.0",
71 | "tsconfig-paths-webpack-plugin": "^4.0.1",
72 | "webpack": "^5.85.1",
73 | "webpack-cli": "^5.1.3",
74 | "webpack-dev-server": "^4.15.0",
75 | "webpack-merge": "^5.9.0"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 | React Setting Boilerplate
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 | import { BrowserRouter } from 'react-router-dom';
4 |
5 | test('Renders main element', async () => {
6 | render(
7 |
8 |
9 |
10 | );
11 | const mainElement = await screen.findByRole('main');
12 | expect(mainElement).toBeInTheDocument();
13 | });
14 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy } from 'react';
2 | import { Route, Routes } from 'react-router-dom';
3 |
4 | import './style.css';
5 | import { Link } from 'react-router-dom';
6 |
7 | const Home = lazy(() => import('@pages/Home'));
8 | const Test = lazy(() => import('@pages/Test'));
9 |
10 | function App() {
11 | return (
12 |
13 |
14 | 로딩중...
}>
15 |
16 | -
17 | 홈으로
18 |
19 | -
20 | 테스트
21 |
22 |
23 |
24 | 프론트엔드 개발 환경 커스텀 구축
25 |
26 |
27 | } />
28 | } />
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/src/assets/bigSvgTest.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/fonts/Pretendard-Medium.subset.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/src/assets/fonts/Pretendard-Medium.subset.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Pretendard-Medium.subset.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/src/assets/fonts/Pretendard-Medium.subset.woff2
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/sheep.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/src/assets/sheep.jpg
--------------------------------------------------------------------------------
/src/assets/test.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/src/assets/test.webp
--------------------------------------------------------------------------------
/src/assets/videos/videoTest.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/src/assets/videos/videoTest.mp4
--------------------------------------------------------------------------------
/src/assets/videos/videoTest.webm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssi02014/react-dev-env-boilerplate/49c3cf54ce26ccea5cdc31fb4cd3d921fc9cc79c/src/assets/videos/videoTest.webm
--------------------------------------------------------------------------------
/src/components/Example.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import Example from './Example';
3 |
4 | test('Example Test', () => {
5 | render();
6 | const test = screen.getByText('zzz');
7 | expect(test).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/components/Example.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Logo from '@assets/logo.svg';
3 | import logo from '@assets/logo.svg?url';
4 | import bigSvgTest from '@assets/bigSvgTest.svg?url';
5 | import BigSvgTest from '@assets/bigSvgTest.svg';
6 | import webpImage from '@assets/test.webp';
7 |
8 | interface Props {
9 | value: string;
10 | }
11 |
12 | const Example = ({ value }: Props) => {
13 | return (
14 | <>
15 | {value}
16 |
17 |
18 |
SVG as React Component Test
19 |
20 |
21 |
22 |
23 |
asset svg url Test
24 |

25 |
26 |
27 |
28 |
big size SVG as React Component Test
29 |
30 |
31 |
32 |
33 |
asset big size SVG url Test
34 |

35 |
36 |
37 |
38 |
webp Image Test
39 |

40 |
41 | >
42 | );
43 | };
44 |
45 | export default Example;
46 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as ReactDOM from 'react-dom/client';
3 | import App from './App';
4 | import { BrowserRouter } from 'react-router-dom';
5 |
6 | const rootNode = document.getElementById('root');
7 |
8 | ReactDOM.createRoot(rootNode as HTMLElement).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import Home from '.';
3 |
4 | test('Renders main element', () => {
5 | render();
6 | const test = screen.getByText('환경 변수 테스트');
7 | expect(test).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/pages/Home/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Example from '@components/Example';
3 | import image from '@assets/sheep.jpg';
4 | import video from '@assets/videos/videoTest.mp4';
5 | import videoWebm from '@assets/videos/videoTest.webm';
6 |
7 | const Home = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
환경 변수 테스트
16 |
{process.env.NODE_ENV}
17 |
{process.env.REACT_APP_EXAMPLE}
18 |
19 |
20 |
21 |
일반적인 jpg 이미지 테스트
22 |

23 |
24 |
25 |
26 |
video 테스트
27 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default Home;
37 |
--------------------------------------------------------------------------------
/src/pages/Test/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | interface List {
4 | body: string;
5 | id: number;
6 | title: string;
7 | userId: number;
8 | }
9 |
10 | const Test = () => {
11 | const [list, setList] = useState([]);
12 | useEffect(() => {
13 | fetch('https://jsonplaceholder.typicode.com/posts')
14 | .then((response) => response.json())
15 | .then((data) => setList(data));
16 | }, []);
17 |
18 | return (
19 |
20 |
API Fetch Test
21 |
22 | {list.map((el) => (
23 | - {el.title}
24 | ))}
25 |
26 |
27 | );
28 | };
29 |
30 | export default Test;
31 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Pretendard;
3 | background-color: rgb(192, 249, 230);
4 | }
5 |
6 | @font-face {
7 | font-family: 'Pretendard';
8 | font-style: normal;
9 | font-display: block;
10 | font-weight: 500;
11 | src: url(./assets/fonts/Pretendard-Medium.subset.woff2) format('woff2'),
12 | url(./assets/fonts/Pretendard-Medium.subset.woff) format('woff');
13 | unicode-range: U+AC00-D7A3, U+0041-005A, U+0061-007A, U+0030-0039;
14 | }
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.jpg' {
2 | const content: string;
3 | export default content;
4 | }
5 |
6 | declare module '*.jpeg' {
7 | const content: string;
8 | export default content;
9 | }
10 |
11 | declare module '*.png' {
12 | const content: string;
13 | export default content;
14 | }
15 |
16 | declare module '*.gif' {
17 | const content: string;
18 | export default content;
19 | }
20 |
21 | declare module '*.webp' {
22 | const content: string;
23 | export default content;
24 | }
25 |
26 | declare module '*.svg' {
27 | const content: React.ReactComponent>;
28 | export default content;
29 | }
30 |
31 | declare module '*.svg?url' {
32 | const content: string;
33 | export default content;
34 | }
35 |
36 | declare module '*.mp4' {
37 | const content: string;
38 | export default content;
39 | }
40 |
41 | declare module '*.webm' {
42 | const content: string;
43 | export default content;
44 | }
45 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "noEmit": true,
22 | "jsx": "react-jsx"
23 | },
24 | "include": [
25 | "src"
26 | ],
27 | "extends": "./tsconfig.paths.json"
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.paths.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@pages/*": ["src/pages/*"],
5 | "@components/*": [ "src/components/*"],
6 | "@assets/*": [ "src/assets/*"],
7 | },
8 | }
9 | }
--------------------------------------------------------------------------------
/webpack/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebPackPlugin = require('html-webpack-plugin');
4 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const CopyWebpackPlugin = require('copy-webpack-plugin');
7 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
8 | const ESLintPlugin = require('eslint-webpack-plugin');
9 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
10 |
11 | module.exports = {
12 | entry: './src/index.tsx',
13 | target: 'web',
14 | output: {
15 | filename: 'static/js/[name].[contenthash:8].js',
16 | path: path.resolve('build'),
17 | chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',
18 | publicPath: '/',
19 | },
20 | resolve: {
21 | extensions: ['.ts', '.tsx', '.js', '.jsx'],
22 | plugins: [new TsconfigPathsPlugin()],
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.(tsx|ts|js|jsx)$/,
28 | exclude: /node_modules/,
29 | use: ['babel-loader'],
30 | },
31 | // fonts
32 | {
33 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
34 | type: 'asset',
35 | parser: {
36 | dataUrlCondition: {
37 | maxSize: 50 * 1024, // 50kb 미만은 base64형태로 사용
38 | },
39 | },
40 | generator: {
41 | filename: 'static/media/[name].[contenthash:8][ext]',
42 | },
43 | },
44 | // image & video
45 | {
46 | test: /\.(png|jpe?g|gif|ico|webp|mp4|webm)$/i,
47 | type: 'asset',
48 | parser: {
49 | dataUrlCondition: {
50 | maxSize: 4 * 1024, // 4kb 미만은 base64형태로 사용
51 | },
52 | },
53 | generator: {
54 | filename: 'static/media/[name].[contenthash:8][ext]',
55 | },
56 | },
57 | // svg
58 | {
59 | test: /\.svg$/i,
60 | type: 'asset',
61 | resourceQuery: /url/,
62 | parser: {
63 | dataUrlCondition: {
64 | maxSize: 2 * 1024, // 2kb 미만은 base64형태로 사용
65 | },
66 | },
67 | generator: {
68 | filename: 'static/media/[name].[contenthash:8][ext]',
69 | },
70 | },
71 | {
72 | test: /\.svg$/i,
73 | issuer: /\.[jt]sx?$/,
74 | resourceQuery: { not: [/url/] },
75 | use: ['@svgr/webpack'],
76 | },
77 | // style
78 | {
79 | test: /\.(css)$/,
80 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
81 | },
82 | // html
83 | {
84 | test: /\.html$/,
85 | use: [
86 | {
87 | loader: 'html-loader',
88 | },
89 | ],
90 | },
91 | ],
92 | },
93 | plugins: [
94 | new HtmlWebPackPlugin({
95 | template: './public/index.html',
96 | filename: 'index.html',
97 | favicon: './public/favicon.ico',
98 | // minify 속성 참고 https://github.com/terser/html-minifier-terser
99 | minify: {
100 | collapseWhitespace: true,
101 | keepClosingSlash: true,
102 | removeComments: true,
103 | removeRedundantAttributes: true,
104 | removeScriptTypeAttributes: true,
105 | removeStyleLinkTypeAttributes: true,
106 | useShortDoctype: true,
107 | },
108 | }),
109 | new CopyWebpackPlugin({
110 | patterns: [
111 | {
112 | from: 'public',
113 | to: './',
114 | globOptions: {
115 | ignore: ['**/index.html'],
116 | },
117 | },
118 | ],
119 | }),
120 | new webpack.ProvidePlugin({
121 | React: 'react',
122 | }),
123 | new MiniCssExtractPlugin({
124 | filename: 'static/css/[name].[contenthash:8].css',
125 | chunkFilename: 'static/css/[id].[contenthash:8].css',
126 | }),
127 | new ForkTsCheckerWebpackPlugin(),
128 | new ESLintPlugin(),
129 | new CleanWebpackPlugin(),
130 | ],
131 | };
132 |
--------------------------------------------------------------------------------
/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const webpack = require('webpack');
3 | const common = require('./webpack.common.js');
4 |
5 | require('dotenv').config({ path: './.env.development' });
6 |
7 | // merge를 이용해서 webpack.common 와 병합
8 | module.exports = merge(common, {
9 | mode: 'development',
10 | devtool: 'source-map',
11 | optimization: {
12 | minimize: false,
13 | splitChunks: false,
14 | },
15 | devServer: {
16 | static: false,
17 | client: {
18 | overlay: true,
19 | },
20 | historyApiFallback: true,
21 | port: 3000,
22 | compress: true, // gzip 압축 여부
23 | hot: true, // HMR 적용
24 | proxy: {
25 | '/api': 'http://localhost:8000',
26 | },
27 | },
28 | plugins: [
29 | new webpack.EnvironmentPlugin({
30 | ...process.env,
31 | }),
32 | ],
33 | });
34 |
--------------------------------------------------------------------------------
/webpack/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const webpack = require('webpack');
3 | const CompressionPlugin = require('compression-webpack-plugin');
4 | const common = require('./webpack.common.js');
5 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
6 | const TerserPlugin = require('terser-webpack-plugin');
7 |
8 | require('dotenv').config({ path: './.env.production' });
9 |
10 | // merge를 이용해서 webpack.common 와 병합
11 | module.exports = merge(common, {
12 | mode: 'production',
13 | optimization: {
14 | minimize: true,
15 | minimizer: [
16 | new CssMinimizerPlugin(),
17 | new TerserPlugin({
18 | terserOptions: {
19 | compress: {
20 | drop_console: true,
21 | },
22 | },
23 | }),
24 | ],
25 | splitChunks: {
26 | chunks: 'all',
27 | name: false,
28 | },
29 | },
30 | plugins: [
31 | new CompressionPlugin({
32 | test: /\.(js|css|html)$/,
33 | algorithm: 'gzip', // gzip으로 압축
34 | threshold: 10240, // 10kb 이상 압축
35 | minRatio: 0.8,
36 | }),
37 |
38 | new webpack.EnvironmentPlugin({
39 | ...process.env,
40 | }),
41 | ],
42 | });
43 |
--------------------------------------------------------------------------------