├── .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 |
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 |
--------------------------------------------------------------------------------