├── .nvmrc ├── .prettierignore ├── .eslintignore ├── .browserslistrc ├── src ├── pages │ ├── HomePage │ │ └── index.tsx │ ├── AboutPage │ │ └── index.tsx │ ├── index.ts │ └── App.test.tsx ├── mocks │ ├── browser.ts │ ├── node.ts │ ├── fileMock.js │ ├── handlers.ts │ └── renderWithProviders.tsx ├── Layout.tsx ├── utils │ └── http.ts ├── index.tsx ├── Routes.tsx ├── App.tsx └── react-app-env.d.ts ├── jest.setup.js ├── .vscode ├── extensions.json └── settings.json ├── .prettierrc.js ├── public ├── index.html ├── manifest.json └── mockServiceWorker.js ├── .lighthouserc.js ├── .gitignore ├── jest.config.js ├── tsconfig.json ├── .github ├── workflows │ ├── lighthouse.yml │ └── ci.yml └── pull_request_template.md ├── .storybook ├── preview.js └── main.js ├── .eslintrc.js ├── .stylelintrc.js ├── webpack ├── webpack.dev.js ├── webpack.common.js └── webpack.prod.js ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | >= 0.1% in KR 2 | not dead -------------------------------------------------------------------------------- /src/pages/HomePage/index.tsx: -------------------------------------------------------------------------------- 1 | const HomePage = () => { 2 | return
HomePage
; 3 | }; 4 | 5 | export default HomePage; 6 | -------------------------------------------------------------------------------- /src/pages/AboutPage/index.tsx: -------------------------------------------------------------------------------- 1 | const AboutPage = () => { 2 | return
AboutPage
; 3 | }; 4 | 5 | export default AboutPage; 6 | -------------------------------------------------------------------------------- /src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from './handlers'; 2 | import { setupWorker } from 'msw'; 3 | 4 | export const worker = setupWorker(...handlers()); 5 | -------------------------------------------------------------------------------- /src/mocks/node.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from './handlers'; 2 | import { setupServer } from 'msw/node'; 3 | 4 | export const server = setupServer(...handlers()); 5 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const HomePage = lazy(() => import('pages/HomePage')); 4 | export const AboutPage = lazy(() => import('pages/AboutPage')); 5 | -------------------------------------------------------------------------------- /src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | 3 | const Layout = () => { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default Layout; 12 | -------------------------------------------------------------------------------- /src/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | process(src, filename) { 5 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | export function handlers() { 4 | return [rest.get('/example', getExample)]; 5 | } 6 | 7 | const getExample: Parameters[1] = (_, res, ctx) => { 8 | return res(ctx.status(200)); 9 | }; 10 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { server } from 'mocks/node'; 3 | 4 | import '@testing-library/jest-dom'; 5 | 6 | beforeAll(() => { 7 | server.listen({ onUnhandledRequest: 'bypass' }); 8 | }); 9 | 10 | afterAll(() => { 11 | server.close(); 12 | }); 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "stylelint.vscode-stylelint", 6 | "webben.browserslist", 7 | "aaron-bond.better-comments", 8 | "streetsidesoftware.code-spell-checker", 9 | "usernamehw.errorlens" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosRequestConfig } from 'axios'; 2 | 3 | const axios = Axios.create(); 4 | 5 | export const http = { 6 | get: async function get( 7 | url: string, 8 | options?: AxiosRequestConfig, 9 | ) { 10 | const res = await axios.get(url, options); 11 | return res.data; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | singleQuote: true, 4 | endOfLine: 'auto', 5 | importOrder: [ 6 | '^(@|hooks)(.*|$)', 7 | '^(@|pages)(.*|$)', 8 | '^(@|components/@shared)(.*|$)', 9 | '^(@|components)(.*|$)', 10 | '^(@|constants)(.*|$)', 11 | '^(@|styles)(.*|$)', 12 | ], 13 | importOrderSeparation: true, 14 | }; 15 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Boilerplate 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /.lighthouserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ci: { 3 | collect: { 4 | url: ['http://localhost:3000/'], 5 | collect: { 6 | numberOfRuns: 5, 7 | }, 8 | }, 9 | upload: { 10 | startServerCommand: 'yarn run start:dev', 11 | target: 'filesystem', 12 | outputDir: './lhci_reports', 13 | reportFilenamePattern: '%%PATHNAME%%-%%DATETIME%%-report.%%EXTENSION%%', 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | junit.xml 24 | 25 | .lighthouseci 26 | lhci_reports -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import App from 'App'; 2 | import { worker } from 'mocks/browser'; 3 | import { StrictMode } from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | 7 | if (process.env.NODE_ENV === 'development') { 8 | worker.start(); 9 | } 10 | 11 | const root = createRoot(document.getElementById('root') as HTMLElement); 12 | 13 | root.render( 14 | 15 | 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /src/pages/App.test.tsx: -------------------------------------------------------------------------------- 1 | import App from 'App'; 2 | import { renderWithProviders } from 'mocks/renderWithProviders'; 3 | 4 | import { screen } from '@testing-library/react'; 5 | 6 | describe('테스트', () => { 7 | test('Home', async () => { 8 | renderWithProviders(, { route: '/home' }); 9 | 10 | await screen.findByText(/HomePage/); 11 | }); 12 | 13 | test('About', async () => { 14 | renderWithProviders(, { route: '/about' }); 15 | 16 | await screen.findByText(/AboutPage/); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnPaste": false, 4 | "editor.formatOnType": false, 5 | "editor.rulers": [120], // 설명: 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "always", 8 | "source.organizeImports": "always" 9 | }, 10 | "prettier.configPath": ".prettierrc.js", 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "typescript.updateImportsOnFileMove.enabled": "always", 13 | "stylelint.validate": ["css", "typescript", "typescriptreact"], 14 | "stylelint.enable": true 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'json'], 3 | transform: { 4 | '^.+\\.(js|jsx|ts|tsx)?$': 'ts-jest', 5 | '^.+\\.svg$': 'jest-transformer-svg', 6 | }, 7 | testEnvironment: 'jsdom', 8 | testMatch: ['/**/*.test.(js|jsx|ts|tsx)'], 9 | moduleDirectories: ['node_modules', 'src'], 10 | moduleNameMapper: { 11 | '\\.(jpg|ico|jpeg|png|gif|webp)$': '/mocks/fileMock.js', 12 | }, 13 | setupFilesAfterEnv: ['./jest.setup.js'], 14 | reporters: ['default', 'jest-junit'], 15 | }; 16 | -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import Layout from 'Layout'; 2 | import { Route, Routes as ReactRouterRoutes, Navigate } from 'react-router-dom'; 3 | 4 | import { AboutPage, HomePage } from 'pages'; 5 | 6 | const Routes = () => { 7 | return ( 8 | 9 | }> 10 | } /> 11 | } /> 12 | } /> 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Routes; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "jsxImportSource": "@emotion/react" 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/lighthouse.yml: -------------------------------------------------------------------------------- 1 | name: lighthouse 2 | run-name: ${{github. actor}} has addded new commit. - lighthouse 3 | 4 | on: 5 | push: 6 | 7 | jobs: 8 | lighthouse: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | - name: Install dependencies 16 | run: yarn 17 | - name: Build 18 | run: yarn run build:dev 19 | - name: Run Lighthouse CI 20 | run: | 21 | npm install -g @lhci/cli@0.8.x 22 | lhci autorun 23 | env: 24 | LHCI_GITHUB_APP_TOKEN: ${{ env.LHCI_GITHUB_APP_TOKEN }} 25 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter } from 'react-router-dom'; 2 | import { RecoilRoot } from 'recoil'; 3 | 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | 6 | export const parameters = { 7 | actions: { argTypesRegex: '^on[A-Z].*' }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | }; 15 | 16 | const queryClient = new QueryClient(); 17 | 18 | export const decorators = [ 19 | (Story) => ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ), 28 | ]; 29 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:storybook/recommended', 12 | ], 13 | overrides: [], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react', '@typescript-eslint', 'prettier', 'compat'], 20 | rules: { 21 | 'prettier/prettier': 'error', 22 | 'react/react-in-jsx-scope': 'off', 23 | '@typescript-eslint/no-var-requires': 'off', 24 | 'react/no-unknown-property': ['error', { ignore: ['css'] }], 25 | 'compat/compat': 'warn', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-interactions', 7 | ], 8 | framework: '@storybook/react', 9 | webpackFinal: async (config) => { 10 | config.resolve.modules = [ 11 | ...config.resolve.modules, 12 | path.resolve(__dirname, '../src'), 13 | ]; 14 | 15 | const fileLoaderRule = config.module.rules.find( 16 | (rule) => rule.test && rule.test.test('.svg'), 17 | ); 18 | fileLoaderRule.exclude = /\.svg$/; 19 | 20 | config.module.rules.unshift({ 21 | test: /\.svg$/, 22 | enforce: 'pre', 23 | use: ['@svgr/webpack'], 24 | }); 25 | 26 | return config; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Routes from 'Routes'; 2 | import emotionNormalize from 'emotion-normalize'; 3 | import { RecoilRoot } from 'recoil'; 4 | 5 | import { css, Global } from '@emotion/react'; 6 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 7 | 8 | const queryClient = new QueryClient(); 9 | 10 | const App = () => { 11 | return ( 12 |
13 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /src/mocks/renderWithProviders.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import { RecoilRoot } from 'recoil'; 4 | 5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 6 | import { render, RenderResult } from '@testing-library/react'; 7 | 8 | export const generateTestQueryClient = () => { 9 | const client = new QueryClient({ 10 | defaultOptions: { 11 | queries: { 12 | retry: 0, 13 | refetchOnWindowFocus: false, 14 | }, 15 | }, 16 | }); 17 | 18 | return client; 19 | }; 20 | 21 | export function renderWithProviders( 22 | Component: ReactElement, 23 | options: { route: string }, 24 | ): RenderResult { 25 | return render( 26 | 27 | 28 | 29 | {Component} 30 | 31 | 32 | , 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const { rules } = require('stylelint-config-clean-order'); 2 | 3 | const [propertiesOrder, options] = rules['order/properties-order']; 4 | 5 | const propertiesOrderWithEmptyLineBefore = propertiesOrder.map( 6 | (properties) => ({ 7 | ...properties, 8 | emptyLineBefore: 'never', 9 | }), 10 | ); 11 | 12 | module.exports = { 13 | extends: ['stylelint-config-standard', 'stylelint-config-clean-order'], 14 | plugins: ['stylelint-no-unsupported-browser-features'], 15 | customSyntax: 'postcss-styled-syntax', 16 | rules: { 17 | 'plugin/no-unsupported-browser-features': [ 18 | true, 19 | { ignorePartialSupport: true, severity: 'warning' }, 20 | ], 21 | 'order/order': [ 22 | 'custom-properties', 23 | 'dollar-variables', 24 | 'at-variables', 25 | 'declarations', 26 | 'rules', 27 | 'at-rules', 28 | 'less-mixins', 29 | ], 30 | 'order/properties-order': [ 31 | propertiesOrderWithEmptyLineBefore, 32 | { 33 | ...options, 34 | severity: 'warning', 35 | }, 36 | ], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.avif' { 2 | const src: string; 3 | export default src; 4 | } 5 | 6 | declare module '*.bmp' { 7 | const src: string; 8 | export default src; 9 | } 10 | 11 | declare module '*.gif' { 12 | const src: string; 13 | export default src; 14 | } 15 | 16 | declare module '*.jpg' { 17 | const src: string; 18 | export default src; 19 | } 20 | 21 | declare module '*.jpeg' { 22 | const src: string; 23 | export default src; 24 | } 25 | 26 | declare module '*.png' { 27 | const src: string; 28 | export default src; 29 | } 30 | 31 | declare module '*.webp' { 32 | const src: string; 33 | export default src; 34 | } 35 | 36 | declare module '*.svg' { 37 | import * as React from 'react'; 38 | 39 | export const ReactComponent: React.FunctionComponent< 40 | React.SVGProps & { title?: string } 41 | >; 42 | 43 | const src: string; 44 | export default src; 45 | } 46 | 47 | declare namespace NodeJS { 48 | interface ProcessEnv { 49 | readonly NODE_ENV: 'development' | 'production' | 'test'; 50 | readonly PUBLIC_URL: string; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PR Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - develop 8 | paths: 9 | - /** 10 | - .github/** # Github Actions 작업을 위한 포함 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./ 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: 16.13.0 24 | cache: 'yarn' 25 | cache-dependency-path: '**/yarn.lock' 26 | 27 | - name: Install node packages 28 | run: yarn 29 | 30 | - name: Jest Component Test 31 | run: yarn run test-ci 32 | 33 | - name: Jest Test Comment 34 | uses: MishaKav/jest-coverage-comment@main 35 | with: 36 | coverage-summary-path: coverage/coverage-summary.json 37 | title: Jest Test 38 | summary-title: Coverage 39 | badge-title: Coverage 40 | hide-comment: false 41 | create-new-comment: true 42 | hide-summary: false 43 | junitxml-title: Result 44 | junitxml-path: junit.xml 45 | 46 | - name: TypeScript test 47 | run: yarn run typecheck 48 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Issue ticket number and link 5 | 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## Motivation and Context 17 | 18 | 19 | 20 | ## Screenshots (if appropriate): 21 | 22 | ## How Has This Been Tested? 23 | 24 | 25 | 26 | 27 | ## Checklist before requesting a review 28 | - [ ] I have performed a self-review of my code 29 | - [ ] If it is a core feature, I have added thorough tests. 30 | - [ ] Do we need to implement analytics? 31 | - [ ] Will this be part of a product update? If yes, please write one phrase about this update. 32 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common'); 3 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); 4 | const smp = new SpeedMeasurePlugin(); 5 | 6 | module.exports = smp.wrap( 7 | merge(common, { 8 | mode: 'development', 9 | devtool: 'eval-cheap-module-source-map', 10 | cache: { 11 | type: 'filesystem', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx|ts|tsx)$/i, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | options: { 20 | cacheCompression: false, 21 | cacheDirectory: true, 22 | presets: [ 23 | '@babel/preset-env', 24 | [ 25 | '@babel/preset-react', 26 | { runtime: 'automatic', importSource: '@emotion/react' }, 27 | ], 28 | '@babel/preset-typescript', 29 | ], 30 | plugins: [ 31 | [ 32 | '@emotion', 33 | { 34 | sourceMap: true, 35 | autoLabel: 'dev-only', 36 | labelFormat: '[local]', 37 | cssPropOptimization: false, 38 | }, 39 | ], 40 | ], 41 | }, 42 | }, 43 | ], 44 | }, 45 | }), 46 | ); 47 | -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const ProgressPlugin = require('progress-webpack-plugin'); 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | entry: './src/index.tsx', 8 | output: { 9 | publicPath: '/', 10 | path: path.join(__dirname, '../dist'), 11 | filename: 'bundle.[name].[chunkhash].js', 12 | clean: true, 13 | }, 14 | resolve: { 15 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 16 | modules: [ 17 | path.resolve(__dirname, '../src'), 18 | path.resolve(__dirname, '../node_modules'), 19 | ], 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(png|webp)$/, 25 | type: 'asset', 26 | generator: { 27 | filename: 'assets/[name][hash][ext]', 28 | }, 29 | }, 30 | { 31 | test: /\.svg$/, 32 | use: ['@svgr/webpack'], 33 | }, 34 | ], 35 | }, 36 | plugins: [ 37 | new HtmlWebpackPlugin({ 38 | template: './public/index.html', 39 | }), 40 | new CopyWebpackPlugin({ 41 | patterns: [{ from: './public/manifest.json', to: '.' }], 42 | }), 43 | new ProgressPlugin(true), 44 | ], 45 | devServer: { 46 | historyApiFallback: true, 47 | port: 3000, 48 | hot: true, 49 | open: true, 50 | }, 51 | performance: { 52 | hints: false, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common'); 3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 4 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); 5 | const smp = new SpeedMeasurePlugin(); 6 | 7 | module.exports = smp.wrap( 8 | merge(common, { 9 | mode: 'production', 10 | devtool: false, 11 | target: ['web', 'es5'], 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx|ts|tsx)$/i, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | options: { 19 | presets: [ 20 | [ 21 | '@babel/preset-env', 22 | { 23 | useBuiltIns: 'usage', 24 | corejs: { 25 | version: 3, 26 | }, 27 | }, 28 | ], 29 | [ 30 | '@babel/preset-react', 31 | { runtime: 'automatic', importSource: '@emotion/react' }, 32 | ], 33 | '@babel/preset-typescript', 34 | ], 35 | plugins: [ 36 | [ 37 | '@emotion', 38 | { 39 | cssPropOptimization: true, 40 | }, 41 | ], 42 | ], 43 | }, 44 | }, 45 | ], 46 | }, 47 | plugins: [new ForkTsCheckerWebpackPlugin()], 48 | }), 49 | ); 50 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The name of your application", 3 | "short_name": "This name will show in your Windows taskbar, in the start menu, and Android homescreen", 4 | "start_url": "The URL that should be loaded when your application is opened", 5 | "display": "standalone", 6 | "description": "A description for your application", 7 | "lang": " The default language of your application", 8 | "dir": "auto", 9 | "theme_color": "#000000", 10 | "background_color": "#000000", 11 | "orientation": "any", 12 | "icons": [ 13 | { 14 | "src": "https://www.pwabuilder.com/assets/icons/icon_512.png", 15 | "sizes": "512x512", 16 | "type": "image/png", 17 | "purpose": "maskable" 18 | }, 19 | { 20 | "src": "https://www.pwabuilder.com/assets/icons/icon_192.png", 21 | "sizes": "192x192", 22 | "type": "image/png", 23 | "purpose": "any" 24 | } 25 | ], 26 | "screenshots": [ 27 | { 28 | "src": "https://www.pwabuilder.com/assets/screenshots/screen1.png", 29 | "sizes": "2880x1800", 30 | "type": "image/png", 31 | "description": "A screenshot of the home page" 32 | } 33 | ], 34 | "related_applications": [ 35 | { 36 | "platform": "windows", 37 | "url": " The URL to your app in that app store" 38 | } 39 | ], 40 | "prefer_related_applications": "false", 41 | "shortcuts": [ 42 | { 43 | "name": "The name you would like to be displayed for your shortcut", 44 | "url": "The url you would like to open when the user chooses this shortcut. This must be a URL local to your PWA. For example: If my start_url is /, this URL must be something like /shortcut", 45 | "description": "A description of the functionality of this shortcut" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Boilerplate 2 | 3 | ## 소개 4 | 5 | React 프로젝트 빌드 시 사용하는 기초 환경설정입니다. 6 | 7 | CRA를 사용하지 않고 Webpack, Babel로 개발, 프로덕션을 구분한 빌드 환경설정을 하였습니다. 8 | 9 | Jest, RTL을 사용하여 테스트 환경설정을 하였고, PR을 만들면 자동 테스트 후 PR에 테스트 결과 코멘트가 달리도록 기본 설정했습니다. 10 | 11 | ## 템플릿 사용방법 12 | 13 | 해당 템플릿으로 새 저장소 만들어서 프로젝트 시작하기 14 | 15 | 스크린샷 2022-12-19 오후 3 49 57 16 | 17 | ## 사용 라이브러리 18 | 19 | - [React](https://ko.reactjs.org/) 20 | - [TypeScript](https://www.typescriptlang.org/) 21 | - 라우팅 : [React Router](https://reactrouter.com/en/main) 22 | - CSS 스타일링 : [emotions](https://emotion.sh/docs/introduction) 23 | - 코드 포매팅 : [Prettier](https://prettier.io/) 24 | - 린팅 : [ESlint](https://eslint.org/), [StyleLint](https://stylelint.io/) 25 | - 전역상태 관리 : [Recoil](https://recoiljs.org/ko/) 26 | - API 연동 : [Axios](https://axios-http.com/), [React Query](https://react-query-v3.tanstack.com/) 27 | - 테스트 : [Storybook](https://storybook.js.org/), [Jest](https://jestjs.io/), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), [MSW](https://mswjs.io/) 28 | - 패키지 매니저: [Yarn](https://yarnpkg.com/) 29 | - 빌드 환경설정 : [Webpack](https://webpack.kr/), [babel-loader](https://www.npmjs.com/package/babel-loader) 30 | 31 | - 기타 특징 32 | - 절대경로 33 | - import 순서 자동정렬 34 | - CSS 순서 자동정렬 35 | - github workflow(PR 테스트 후 코멘트) 36 | - PR template 37 | - development, production 모드에 따라 최적화된 빌드 환경설정 구분 38 | - WebAPI, ECMAScript, CSS 브라우저 호환성 체크 린트 플러그인 추가 39 | 40 | ## Scripts 41 | 42 | - node version : 18.16.1 43 | 44 | - 설치 45 | 46 | ```bash 47 | yarn 48 | ``` 49 | 50 | - development 모드로 구동 51 | 52 | ```bash 53 | yarn run start:dev 54 | ``` 55 | 56 | - production 모드로 빌드 57 | 58 | ```bash 59 | yarn run build:prod 60 | ``` 61 | 62 | - 테스트 63 | 64 | ```bash 65 | yarn run test 66 | ``` 67 | 68 | - 스토리북 69 | 70 | ```bash 71 | yarn run storybook 72 | ``` 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boilerplate", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/wonsss/react-boilerplate.git", 6 | "author": "wonsss ", 7 | "license": "MIT", 8 | "scripts": { 9 | "typecheck": "tsc --build && tsc --build --clean", 10 | "start:prod": "webpack serve --config webpack/webpack.prod.js", 11 | "start:dev": "webpack serve --config webpack/webpack.dev.js", 12 | "build:prod": "webpack --config webpack/webpack.prod.js", 13 | "build:dev": "webpack --config webpack/webpack.dev.js", 14 | "storybook": "start-storybook -p 6006", 15 | "build-storybook": "build-storybook", 16 | "pretty": "prettier --write \"src/**/*.{ts,tsx,json}\"", 17 | "test": "jest --watchAll", 18 | "test-ci": "jest -ci --coverage --coverageReporters json-summary" 19 | }, 20 | "dependencies": { 21 | "@emotion/react": "^11.10.5", 22 | "@emotion/styled": "^11.10.5", 23 | "@tanstack/react-query": "^4.20.4", 24 | "axios": "^1.2.1", 25 | "emotion-normalize": "^11.0.1", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-router-dom": "^6.4.5", 29 | "recoil": "^0.7.6" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.20.5", 33 | "@babel/preset-env": "^7.20.2", 34 | "@babel/preset-react": "^7.18.6", 35 | "@babel/preset-typescript": "^7.18.6", 36 | "@emotion/babel-plugin": "^11.10.5", 37 | "@storybook/addon-actions": "^6.5.14", 38 | "@storybook/addon-essentials": "^6.5.14", 39 | "@storybook/addon-interactions": "^6.5.14", 40 | "@storybook/addon-links": "^6.5.14", 41 | "@storybook/builder-webpack4": "^6.5.14", 42 | "@storybook/manager-webpack4": "^6.5.14", 43 | "@storybook/react": "^6.5.14", 44 | "@storybook/testing-library": "^0.0.13", 45 | "@svgr/webpack": "^6.5.1", 46 | "@testing-library/jest-dom": "^5.16.5", 47 | "@testing-library/react": "^13.4.0", 48 | "@trivago/prettier-plugin-sort-imports": "^4.0.0", 49 | "@types/react": "^18.0.26", 50 | "@types/react-dom": "^18.0.9", 51 | "@types/react-router-dom": "^5.3.3", 52 | "@typescript-eslint/eslint-plugin": "^5.46.1", 53 | "@typescript-eslint/parser": "^5.46.1", 54 | "babel-loader": "^9.1.0", 55 | "copy-webpack-plugin": "^11.0.0", 56 | "dotenv": "^16.0.3", 57 | "eslint": "^8.29.0", 58 | "eslint-config-prettier": "^8.5.0", 59 | "eslint-plugin-compat": "^4.2.0", 60 | "eslint-plugin-prettier": "^4.2.1", 61 | "eslint-plugin-react": "^7.31.11", 62 | "eslint-plugin-storybook": "^0.6.8", 63 | "fork-ts-checker-webpack-plugin": "^7.2.14", 64 | "html-webpack-plugin": "^5.5.0", 65 | "jest": "^29.3.1", 66 | "jest-environment-jsdom": "^29.3.1", 67 | "jest-junit": "^15.0.0", 68 | "jest-transformer-svg": "^2.0.0", 69 | "msw": "^0.49.2", 70 | "prettier": "^2.8.1", 71 | "progress-webpack-plugin": "^1.0.16", 72 | "postcss": "^8.4.32", 73 | "postcss-styled-syntax": "^0.5.0", 74 | "speed-measure-webpack-plugin": "^1.5.0", 75 | "stylelint": "15.11.0", 76 | "stylelint-config-clean-order": "^5.0.1", 77 | "stylelint-config-standard": "^34.0.0", 78 | "stylelint-no-unsupported-browser-features": "7.0.0", 79 | "ts-jest": "^29.0.3", 80 | "typescript": "^4.9.4", 81 | "webpack": "^5.75.0", 82 | "webpack-cli": "^5.0.1", 83 | "webpack-dev-server": "^4.11.1", 84 | "webpack-merge": "^5.8.0" 85 | }, 86 | "engines": { 87 | "node": ">=16", 88 | "npm": "please-use-yarn" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /public/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | 4 | /** 5 | * Mock Service Worker (0.49.1). 6 | * @see https://github.com/mswjs/msw 7 | * - Please do NOT modify this file. 8 | * - Please do NOT serve this file on production. 9 | */ 10 | 11 | const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'; 12 | const activeClientIds = new Set(); 13 | 14 | self.addEventListener('install', function () { 15 | self.skipWaiting(); 16 | }); 17 | 18 | self.addEventListener('activate', function (event) { 19 | event.waitUntil(self.clients.claim()); 20 | }); 21 | 22 | self.addEventListener('message', async function (event) { 23 | const clientId = event.source.id; 24 | 25 | if (!clientId || !self.clients) { 26 | return; 27 | } 28 | 29 | const client = await self.clients.get(clientId); 30 | 31 | if (!client) { 32 | return; 33 | } 34 | 35 | const allClients = await self.clients.matchAll({ 36 | type: 'window', 37 | }); 38 | 39 | switch (event.data) { 40 | case 'KEEPALIVE_REQUEST': { 41 | sendToClient(client, { 42 | type: 'KEEPALIVE_RESPONSE', 43 | }); 44 | break; 45 | } 46 | 47 | case 'INTEGRITY_CHECK_REQUEST': { 48 | sendToClient(client, { 49 | type: 'INTEGRITY_CHECK_RESPONSE', 50 | payload: INTEGRITY_CHECKSUM, 51 | }); 52 | break; 53 | } 54 | 55 | case 'MOCK_ACTIVATE': { 56 | activeClientIds.add(clientId); 57 | 58 | sendToClient(client, { 59 | type: 'MOCKING_ENABLED', 60 | payload: true, 61 | }); 62 | break; 63 | } 64 | 65 | case 'MOCK_DEACTIVATE': { 66 | activeClientIds.delete(clientId); 67 | break; 68 | } 69 | 70 | case 'CLIENT_CLOSED': { 71 | activeClientIds.delete(clientId); 72 | 73 | const remainingClients = allClients.filter((client) => { 74 | return client.id !== clientId; 75 | }); 76 | 77 | // Unregister itself when there are no more clients 78 | if (remainingClients.length === 0) { 79 | self.registration.unregister(); 80 | } 81 | 82 | break; 83 | } 84 | } 85 | }); 86 | 87 | self.addEventListener('fetch', function (event) { 88 | const { request } = event; 89 | const accept = request.headers.get('accept') || ''; 90 | 91 | // Bypass server-sent events. 92 | if (accept.includes('text/event-stream')) { 93 | return; 94 | } 95 | 96 | // Bypass navigation requests. 97 | if (request.mode === 'navigate') { 98 | return; 99 | } 100 | 101 | // Opening the DevTools triggers the "only-if-cached" request 102 | // that cannot be handled by the worker. Bypass such requests. 103 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 104 | return; 105 | } 106 | 107 | // Bypass all requests when there are no active clients. 108 | // Prevents the self-unregistered worked from handling requests 109 | // after it's been deleted (still remains active until the next reload). 110 | if (activeClientIds.size === 0) { 111 | return; 112 | } 113 | 114 | // Generate unique request ID. 115 | const requestId = Math.random().toString(16).slice(2); 116 | 117 | event.respondWith( 118 | handleRequest(event, requestId).catch((error) => { 119 | if (error.name === 'NetworkError') { 120 | console.warn( 121 | '[MSW] Successfully emulated a network error for the "%s %s" request.', 122 | request.method, 123 | request.url, 124 | ); 125 | return; 126 | } 127 | 128 | // At this point, any exception indicates an issue with the original request/response. 129 | console.error( 130 | `\ 131 | [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, 132 | request.method, 133 | request.url, 134 | `${error.name}: ${error.message}`, 135 | ); 136 | }), 137 | ); 138 | }); 139 | 140 | async function handleRequest(event, requestId) { 141 | const client = await resolveMainClient(event); 142 | const response = await getResponse(event, client, requestId); 143 | 144 | // Send back the response clone for the "response:*" life-cycle events. 145 | // Ensure MSW is active and ready to handle the message, otherwise 146 | // this message will pend indefinitely. 147 | if (client && activeClientIds.has(client.id)) { 148 | (async function () { 149 | const clonedResponse = response.clone(); 150 | sendToClient(client, { 151 | type: 'RESPONSE', 152 | payload: { 153 | requestId, 154 | type: clonedResponse.type, 155 | ok: clonedResponse.ok, 156 | status: clonedResponse.status, 157 | statusText: clonedResponse.statusText, 158 | body: 159 | clonedResponse.body === null ? null : await clonedResponse.text(), 160 | headers: Object.fromEntries(clonedResponse.headers.entries()), 161 | redirected: clonedResponse.redirected, 162 | }, 163 | }); 164 | })(); 165 | } 166 | 167 | return response; 168 | } 169 | 170 | // Resolve the main client for the given event. 171 | // Client that issues a request doesn't necessarily equal the client 172 | // that registered the worker. It's with the latter the worker should 173 | // communicate with during the response resolving phase. 174 | async function resolveMainClient(event) { 175 | const client = await self.clients.get(event.clientId); 176 | 177 | if (client?.frameType === 'top-level') { 178 | return client; 179 | } 180 | 181 | const allClients = await self.clients.matchAll({ 182 | type: 'window', 183 | }); 184 | 185 | return allClients 186 | .filter((client) => { 187 | // Get only those clients that are currently visible. 188 | return client.visibilityState === 'visible'; 189 | }) 190 | .find((client) => { 191 | // Find the client ID that's recorded in the 192 | // set of clients that have registered the worker. 193 | return activeClientIds.has(client.id); 194 | }); 195 | } 196 | 197 | async function getResponse(event, client, requestId) { 198 | const { request } = event; 199 | const clonedRequest = request.clone(); 200 | 201 | function passthrough() { 202 | // Clone the request because it might've been already used 203 | // (i.e. its body has been read and sent to the client). 204 | const headers = Object.fromEntries(clonedRequest.headers.entries()); 205 | 206 | // Remove MSW-specific request headers so the bypassed requests 207 | // comply with the server's CORS preflight check. 208 | // Operate with the headers as an object because request "Headers" 209 | // are immutable. 210 | delete headers['x-msw-bypass']; 211 | 212 | return fetch(clonedRequest, { headers }); 213 | } 214 | 215 | // Bypass mocking when the client is not active. 216 | if (!client) { 217 | return passthrough(); 218 | } 219 | 220 | // Bypass initial page load requests (i.e. static assets). 221 | // The absence of the immediate/parent client in the map of the active clients 222 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 223 | // and is not ready to handle requests. 224 | if (!activeClientIds.has(client.id)) { 225 | return passthrough(); 226 | } 227 | 228 | // Bypass requests with the explicit bypass header. 229 | // Such requests can be issued by "ctx.fetch()". 230 | if (request.headers.get('x-msw-bypass') === 'true') { 231 | return passthrough(); 232 | } 233 | 234 | // Notify the client that a request has been intercepted. 235 | const clientMessage = await sendToClient(client, { 236 | type: 'REQUEST', 237 | payload: { 238 | id: requestId, 239 | url: request.url, 240 | method: request.method, 241 | headers: Object.fromEntries(request.headers.entries()), 242 | cache: request.cache, 243 | mode: request.mode, 244 | credentials: request.credentials, 245 | destination: request.destination, 246 | integrity: request.integrity, 247 | redirect: request.redirect, 248 | referrer: request.referrer, 249 | referrerPolicy: request.referrerPolicy, 250 | body: await request.text(), 251 | bodyUsed: request.bodyUsed, 252 | keepalive: request.keepalive, 253 | }, 254 | }); 255 | 256 | switch (clientMessage.type) { 257 | case 'MOCK_RESPONSE': { 258 | return respondWithMock(clientMessage.data); 259 | } 260 | 261 | case 'MOCK_NOT_FOUND': { 262 | return passthrough(); 263 | } 264 | 265 | case 'NETWORK_ERROR': { 266 | const { name, message } = clientMessage.data; 267 | const networkError = new Error(message); 268 | networkError.name = name; 269 | 270 | // Rejecting a "respondWith" promise emulates a network error. 271 | throw networkError; 272 | } 273 | } 274 | 275 | return passthrough(); 276 | } 277 | 278 | function sendToClient(client, message) { 279 | return new Promise((resolve, reject) => { 280 | const channel = new MessageChannel(); 281 | 282 | channel.port1.onmessage = (event) => { 283 | if (event.data && event.data.error) { 284 | return reject(event.data.error); 285 | } 286 | 287 | resolve(event.data); 288 | }; 289 | 290 | client.postMessage(message, [channel.port2]); 291 | }); 292 | } 293 | 294 | function sleep(timeMs) { 295 | return new Promise((resolve) => { 296 | setTimeout(resolve, timeMs); 297 | }); 298 | } 299 | 300 | async function respondWithMock(response) { 301 | await sleep(response.delay); 302 | return new Response(response.body, response); 303 | } 304 | --------------------------------------------------------------------------------