티스토리 뷰
우아한테크코스 레벨 3 (VoTogether팀) 2주차 - Webpack을 이용해 React 프로젝트 환경 설정하기
YG - 96년생 , 강아지 있음, 개발자 희망 2023. 7. 23. 17:46Webpack을 이용해 React 프로젝트를 설정하게 되었습니다. 생각보다 간단하지만은 않아서 환경 설정하는 과정을 기록해두려고 합니다.
모든 세팅이 끝난 Wepack-React 보일러플레이트 저장소
1. package.json파일 생성
npm init -y
2. 기본 패키지 설치 및 설정
.gitignore 설정
/node_modules
.env
/dist
React 필수 패키지 설치
npm i react react-dom react-router-dom
타입스크립트 및 타입 패키지 설치
npm i -D typescript @types/react @types/react-dom
tsconfig.json 파일 생성
절대 경로가 설정되어 있고, 테스트 폴더 및 styled-components.d.ts가 include 되어 있는 파일이니 사용하시는 용도에 따라 수정이 필요합니다.
{
"compilerOptions": {
"target": "es2021",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"noEmit": false,
"baseUrl": "src",
"paths": {
"@assets/*": ["assets/*"],
"@pages/*": ["pages/*"],
"@components/*": ["components/*"],
"@hooks/*": ["hooks/*"],
"@styles/*": ["styles/*"],
"@utils/*": ["utils/*"],
"@constants/*": ["constants/*"],
"@type/*": ["types/*"],
"@atoms/*": ["atoms/*"],
"@selectors/*": ["selectors/*"],
"@routes/*": ["routes/*"],
"@api/*": ["api/*"],
"@mocks/*": ["mocks/*"]
},
"outDir": "./dist"
},
"include": ["src", "src/custom.d.ts", "__tests__", "styled-components.d.ts"],
"exclude": ["node_modules"]
}
.prettierrc.js 설정
module.exports = {
printWidth: 100, // 한줄당 문자 100개로 제한
singleQuote: true, // "" => ''
arrowParens: 'avoid', // arrow function parameter가 하나일 경우 괄호 생략
};
Webpack 설정
npm install --save-dev webpack
npm install --save-dev webpack-cli
npm install webpack-dev-server --save-dev
npm i --save-dev html-webpack-plugin
npm install --save-dev clean-webpack-plugin
npm install dotenv-webpack --save-dev
npm install ts-loader --save-dev
npm install --save-dev css-loader
npm install --save-dev style-loader
webpack.common.js
빌드 시 dist 폴더에 결과물이 생깁니다. 스토리북에서 msw를 설정하기 위해 devServer의 static을 public으로 해주었습니다. dotenv를 사용하기 위한 설정이 있습니다.
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const DotenvWebpack = require('dotenv-webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.tsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: '/',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: {
'@assets': path.resolve(__dirname, 'src/assets'),
'@pages': path.resolve(__dirname, 'src/pages'),
'@components': path.resolve(__dirname, 'src/components'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@styles': path.resolve(__dirname, 'src/styles'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@constants': path.resolve(__dirname, 'src/constants'),
'@type': path.resolve(__dirname, 'src/types'),
'@atoms': path.resolve(__dirname, 'src/atoms'),
'@selectors': path.resolve(__dirname, 'src/selectors'),
'@routes': path.resolve(__dirname, 'src/routes'),
'@api': path.resolve(__dirname, 'src/api'),
'@mocks': path.resolve(__dirname, 'src/mocks'),
},
},
module: {
rules: [
{
test: /\.(js|ts|tsx)$/i,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
},
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.svg/,
type: 'asset/inline',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new CleanWebpackPlugin(),
new DotenvWebpack(),
],
devtool: 'inline-source-map',
devServer: {
static: 'public',
hot: true,
open: true,
},
};
webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development', // 현재 개발 모드
devtool: 'eval', // 최대성능, 개발환경에 추천
devServer: {
historyApiFallback: true,
port: 3000,
hot: true,
},
});
webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production', // 현재 배포 모드
devtool: 'hidden-source-map', // 느리지만 안전 배포에 추천
});
webpack 명령어 설정
package.json
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js --open --hot",
"build": "webpack --config webpack.prod.js",
"start": "webpack --config webpack.dev.js",
}
public/index.html , public 폴더에 index.html 생성 (react code를 집어넣을 html)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
src/index.tsx, src 폴더에 index.tsx 생성 (html과 App 컴포넌트를 연결해 주는 파일)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src/App.tsx 생성
import React from 'react';
const App = () => (
<>
<h1>Hi!</h1>
</>
);
export default App;
추가적인 패키지 설치 및 설정
MSW
npx msw init public/ save을 하면 public 폴더에 mockServiceWorker 파일이 생기게 됩니다.
npm install msw --save-dev
npx msw init public/ --save
src/mocks/worker.ts
import { setupWorker } from 'msw';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
src/mocks/handlers.ts
import { mockPostList } from './postList';
export const handlers = [...mockPostList];
src/mocks/postList.ts
예제 파일입니다.
import { rest } from 'msw';
export const MOCK_POST_LIST = [
{
id: 1,
text: 'hi',
},
{
id: 2,
text: 'hi2',
},
{
id: 3,
text: 'hi3',
},
];
export const mockPostList = [
rest.get('/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(MOCK_POST_LIST));
}),
];
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./mocks/worker');
worker.start();
}
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Storybook
npx storybook@latest init
npm install --save-dev tsconfig-paths-webpack-plugin
npm i msw msw-storybook-addon -D
스토리북 MSW 설정
.storybook/public/mockServiceWorker.js 파일 생성 후 public 폴더에 있는 mockServiceWorker.js를 그대로 복사 붙여 넣기 해줍니다.
.storybook/preview.tsx
import type { Preview } from '@storybook/react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { handlers } from '../src/mocks/handlers';
initialize();
const preview: Preview = {
parameters: {
msw: handlers,
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
mswDecorator,
Story => (
<BrowserRouter>
<Story />
</BrowserRouter>
),
],
};
if (typeof global.process === 'undefined') {
const { worker } = require('../src/mocks/worker');
worker.start();
}
export default preview;
스토리북 절대경로 설정
.storybook/main.ts
import path from 'path';
import type { StorybookConfig } from '@storybook/react-webpack5';
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {
autodocs: 'tag',
},
webpackFinal: async config => {
if (!config.resolve) {
config.resolve = {};
}
if (!config.resolve.plugins) {
config.resolve.plugins = [];
}
config.resolve.plugins.push(
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, '../tsconfig.json'),
})
);
return config;
},
staticDirs: ['./public'],
};
export default config;
Jest
npm install --save-dev jest jest-environment-jsdom @types/jest
// 리엑트 컴포넌트, 훅 테스팅 패키지
npm install --save-dev @testing-library/react
// node 환경에서 fetch를 테스트할 수 있도록 하기 위해 설치하는 패키지
npm install --save-dev whatwg-fetch
jest.config.js
절대경로 설정이 되어있는 세팅입니다. 따로 수정이 필요해요
module.exports = {
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
testEnvironment: 'jsdom',
moduleNameMapper: {
'^@assets/(.*)$': '<rootDir>/src/assets/$1',
'^@pages/(.*)$': '<rootDir>/src/pages/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@styles/(.*)$': '<rootDir>/src/styles/$1',
'^@api/(.*)$': '<rootDir>/src/api/$1',
'^@type/(.*)$': '<rootDir>/src/types/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^@constants/(.*)$': '<rootDir>/src/constants/$1',
'^@hooks/(.*)$': '<rootDir>/src/hooks/$1',
'^@mocks/(.*)$': '<rootDir>/src/mocks/$1',
},
setupFilesAfterEnv: ['./jest.setup.js'],
transformIgnorePatterns: ['<rootDir>/node_modules/'],
};
jest.setup.js
테스트에 필요한 환경설정을 하는 파일입니다. msw 테스트에 필요한 세팅이 적용된 파일입니다.
import 'whatwg-fetch';
import { setupServer } from 'msw/node';
import { handlers } from './src/mocks/handlers';
export const server = setupServer(...handlers);
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
루트에 __tests__ 폴더 생성 후 테스트 파일 생성
__tests__/unit.test.ts
describe('테스트 설정한다.', () => {
test('1 + 1 = 2', () => {
expect(1 + 1).toBe(2);
});
});
__tests_/hook.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCount } from '@hooks/useCount';
test('useCount hook을 테스트한다.', () => {
const { result } = renderHook(() => useCount());
act(() => {
result.current.increase();
});
expect(result.current.count).toBe(1);
});
src/hooks/useCount.ts
예제 파일입니다.
import { useState } from 'react';
export const useCount = () => {
const [count, setCount] = useState(0);
const increase = () => {
setCount(count + 1);
};
return { count, increase };
};
__tests__/postList.ts
import { MOCK_POST_LIST } from '@mocks/postList';
describe('게시글 목록을 통신하여 불러올 수 있다.', () => {
test('게시글 목록을 볼러올 수 있다.', async () => {
const data = await fetch('/posts').then(response => response.json());
expect(data).toEqual(MOCK_POST_LIST);
});
});
styled-component
npm install styled-components -D
src/styles/reset.ts
export const reset = /*css*/ `
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
html,
body,
p,
ol,
ul,
li,
dl,
dt,
dd,
blockquote,
figure,
fieldset,
legend,
textarea,
pre,
iframe,
hr,
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
padding: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
font-weight: normal;
}
ul {
list-style: none;
}
button,
input,
select {
margin: 0;
}
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
img,
video {
height: auto;
max-width: 100%;
}
iframe {
border: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
button{
background: none;
}
a{
color: inherit;
text-decoration: none;
}
`;
src/styles/globalStyle.ts
import { createGlobalStyle } from 'styled-components';
import { reset } from './reset';
export const GlobalStyle = createGlobalStyle`
${reset}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
border:none
}
ul,
li {
list-style: none;
}
html,
body {
font-family: sans-serif;
font-size: 62.5%;
}
:root {
/* Colors *****************************************/
--primary-color: #FA7D7C;
--white: #ffffff;
--slate: #94A3B8;
--gray: #F4F4F4;
--red: #F51A18;
--dark-gray: #929292;
--header: #1f1f1f;
--graph-color-purple:#853DE1;
--graph-color-green:#5AEAA5;
/* Fonts *****************************************/
--text-title: 600 2rem/2.4rem san-serif;
--text-subtitle: 600 1.8rem/2.8rem san-serif;
--text-body: 400 1.6rem/2.4rem san-serif;
--text-caption: 400 1.4rem/2rem san-serif;
--text-small: 400 1.2rem/1.8rem san-serif;
}
`;
src/styles/theme.ts
import { DefaultTheme } from 'styled-components';
const breakpoint = {
/** @media (min-width: 576px) { ... } */
sm: '576px',
/** @media (min-width: 960px) { ... } */
md: '960px',
/** @media (min-width: 1440px) { ... }*/
lg: '1440px',
};
const zIndex = {
header: 100,
modal: 200,
};
export type ZIndex = typeof zIndex;
export type Breakpoint = typeof breakpoint;
export const theme: DefaultTheme = {
breakpoint,
zIndex,
};
src/styles/styled-components.d.ts 파일 생성
이 파일에 theme에 사용되는 CSS 속성들을 선언함으로써 theme을 사용할 때 자동 완성이 가능하게 됩니다.
import { Breakpoint, ZIndex } from '@styles/theme';
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
breakpoint: Breakpoint;
zIndex: ZIndex;
}
}
src/App.tsx
App.tsx에 글로벌 스타일 적용 및 theme 적용
import React from 'react';
import { GlobalStyle } from '@styles/globalStyle';
import { theme } from '@styles/theme';
import { ThemeProvider } from 'styled-components';
const App = () => (
<ThemeProvider theme={theme}>
<GlobalStyle />
</ThemeProvider>
);
export default App;
.storybook/preview.tsx
GlobalStyle을 추가해 줍니다.
import type { Preview } from '@storybook/react';
import { GlobalStyle } from '../src/styles/globalStyle';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { handlers } from '../src/mocks/handlers';
initialize();
const preview: Preview = {
parameters: {
msw: handlers,
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
mswDecorator,
Story => (
<BrowserRouter>
<GlobalStyle />
<Story />
</BrowserRouter>
),
],
};
if (typeof global.process === 'undefined') {
const { worker } = require('../src/mocks/worker');
worker.start();
}
export default preview;
SVG 파일 인식 못하는 문제
svg를 사용하려고 하면 선언해 달라는 에러가 나옵니다.
src/svg.d.ts 생성
declare module '*.svg' {
const content: any;
export default content;
}
@tanstack-react-query 설치
npm i @tanstack/react-query
src/App.tsx 앱에 React-Query 적용
QueryClientProvider를 추가해 줍니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'styled-components';
import { GlobalStyle } from '@styles/globalStyle';
import { theme } from '@styles/theme';
const queryClient = new QueryClient();
const App = () => (
<ThemeProvider theme={theme}>
<GlobalStyle />
<QueryClientProvider client={queryClient}></QueryClientProvider>
</ThemeProvider>
);
export default App;
. storybook/preview.tsx 스토리북에 React-Query 적용
QueryClient, QueryClientProvider과 같은 설정을 해줍니다.
import type { Preview } from '@storybook/react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { GlobalStyle } from '../src/styles/globalStyle';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { handlers } from '../src/mocks/handlers';
const queryClient = new QueryClient();
initialize();
const preview: Preview = {
parameters: {
msw: handlers,
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
mswDecorator,
Story => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<GlobalStyle />
<Story />
</BrowserRouter>
</QueryClientProvider>
),
],
};
if (typeof global.process === 'undefined') {
const { worker } = require('../src/mocks/worker');
worker.start();
}
export default preview;
router 설정
src/App.tsx
import { RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'styled-components';
import router from '@routes/router';
import { GlobalStyle } from '@styles/globalStyle';
import { theme } from '@styles/theme';
const queryClient = new QueryClient();
const App = () => (
<ThemeProvider theme={theme}>
<GlobalStyle />
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</ThemeProvider>
);
export default App;
src/routes/router.tsx
import { createBrowserRouter } from 'react-router-dom';
import Home from '@pages/Home';
import Post from '@pages/Post';
const router = createBrowserRouter([
{
path: '/',
children: [
{ path: '', element: <Home /> },
{
path: 'posts',
element: <Post />,
},
],
},
]);
export default router;
eslint, eslint 순서 정렬 설정
npm i eslint -D
npm i eslint-config-prettier -D
npm i eslint-config-react-app -D
npm i eslint-plugin-import -D
npm install eslint-plugin-storybook --save-dev
.eslintrc
import 순서가 설정되어 있습니다. 임의대로 수정하여 사용하시면 됩니다.
{
"extends": ["react-app", "eslint:recommended", "react-app/jest", "plugin:storybook/recommended"],
"rules": {
"no-var": "error", // var 금지
"no-multiple-empty-lines": "error", // 여러 줄 공백 금지
"no-console": ["error", { "allow": ["warn", "error", "info"] }],
"eqeqeq": "error", // 일치 연산자 사용 필수
"dot-notation": "error", // 가능하다면 dot notation 사용
"no-unused-vars": "error", // 사용하지 않는 변수 금지
"import/order": [
"error",
{
"groups": [
"type",
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"unknown"
],
"newlines-between": "always",
"pathGroups": [
{
"pattern": "react*",
"group": "external",
"position": "before"
},
{
"pattern": "@type/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@hooks/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@atoms/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@selectors/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@routes/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@api/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@pages/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@components/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@constants/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@utils/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@styles/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@assets/**",
"group": "internal",
"position": "after"
},
{
"pattern": "@mocks/**",
"group": "internal",
"position": "after"
}
],
"alphabetize": {
"caseInsensitive": true,
"order": "asc"
},
"pathGroupsExcludedImportTypes": []
}
]
}
}
설정 끝!
참고 자료
'우아한테크코스' 카테고리의 다른 글
우아한테크코스 레벨 3 (VoTogether팀) 2주차 - 깃 PR, 이슈 템플릿 등록하는 방법 ,PR이 merge 되었을 때 관련 이슈를 자동으로 closed 하는 방법 , PR을 팀원 몇명 이상이 Approve 해줘야 머지할 수 있도록.. (0) | 2023.07.06 |
---|---|
우아한테크코스 레벨 3 (VoTogether팀) 1주차 - 팀 프로젝트 시작하는 방법 (0) | 2023.07.04 |
우아한테크코스 프론트앤드 5기 합격 후기 (일기장) (4) | 2022.12.30 |
우아한테크코스 프리코스 5기 1~4주 하면서 성장한 점 (0) | 2022.11.23 |
- Total
- Today
- Yesterday
- NextRequest
- 북클럽
- React
- 우아한테크코스
- 원티드
- 윤성우 열혈C프로그래밍
- 노개북
- WSL2
- 아차산
- javascript
- createPortal
- jest
- nextjs
- electron
- CLASS
- 위코드
- 초보
- import/order
- 노마드코더
- error
- Storybook
- NextApiRequest
- 프론트앤드
- 스토리 북
- 프리온보딩
- TopLayer
- C언어
- env
- nodejs
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |