├── .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 | 스크린샷 2023-03-12 오후 4 58 35 124 | 125 |
126 | 127 | **esbuild-loader 도입 후 빌드 타임** 128 | 129 |
130 | 131 | 스크린샷 2023-03-12 오후 4 55 08 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 | ![스크린샷_2023-04-19_오후_7 33 26](https://user-images.githubusercontent.com/64779472/235470848-ff150182-1979-4b32-9c19-c0611c751292.png) 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 | ![스크린샷_2022-06-04_오후_11 39 30](https://github.com/ssi02014/react-dev-env-boilerplate/assets/64779472/265954b6-160b-4796-9f00-3697f3acc9cc) 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | --------------------------------------------------------------------------------