© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
👔
프로젝트 세팅
요약 Framework : React
, React BoilerPlate(create-react-app)
Route: react-router-dom(5.3.0)
State Management Tool: redux
Convention: ESLint
, Prettier
Style: Emotion
, styled component
etc: formik + Yup
, momentjs
, react-dnd
, emoji-picker-react
1) create-react-app & Typescript
tsconfig.json 2) ESLint & Prettier 설정 package.json 설치 완료 후 아래의 devDependencies
가 추가된다. .eslintrc.js 자동 생성된 .eslintrc.js
코드를 아래와 같이 수정하여 rule을 추가한다. .prettierrc 루트 폴더에 .prettierrc
파일 생성 후 아래 코드를 작성한다.
타입스크립트 경로 설정 추가한 패키지 tsconfig-paths-webpack-plugin
craco-alias
tsconfig.json
tsconfig.paths.json tsconfig의 절대경로 별칭 지정
craco.config.js craco의 절대경로 별칭을 지정
스토리북 설정 mian.js
내용 추가스토리북 경로 alias 설정 (feat.tsconfig-paths-webpack-plugin
패키지) 참고자료
preview.js
내용 추가커스텀 뷰포트 추가 MemoryRouter
추가스토리북상에서 링크 이동 관련 액션발생시 홈으로 라우팅
3) emotion, styled javascript 파일 내부에서 스타일 적용 패키지
craco(create-react-app-config-override) 위의 패키지 3개만 추가하면 styled 패키지를 사용할 때 마다 /** @jsxImportSource @emotion/react */
fragma 주석을 추가해야 되는데, 이를 해결하기 위해 craco
를 설치한다.
craco.config.js 루트 폴더에 craco.config.js 파일을 생성하여 아래와 같이 추가한다.
package.json package.json 파일에서 scripts 부분을 react-scripts
에서 craco
로 변경한다.
4) Storybook UI 컴포넌트를 모아 문서화하고 보여주는 오픈소스 Tool
storybook 설치 후 start 오류 storybook 설치 후 react-scripts와 storybook의 babel-loader 버전이 달라서 yarn start로 로컬 환경 Open이 되지 않는 현상이 발생한다. package.json 파일에 아래 코드를 추가하여 해결할 수 있다.
5) Axios 브라우저, Node.js를 위한 Promise API를 활용하는 HTTP 비동기 통신 라이브러리
6) .env ESLint
설치 후 package.json과 yarn.lock에서의 package version 차이가 발생해서 yarn start 같은 로컬 환경이 Open되지 않는다.루트 폴더에 .env 파일을 생성 후 아래 코드를 추가한다. 7) react-router-dom react-router-dom을 설치하면 최신버전인 6버전으로 설치가 된다.
기술 스택 1. Recoil 부모 트리(보통 App.js
)에 RecoilRoot
태그로 한 번 감싸준 후 Atom(State) 등을 통해 상태를 관리, 사용한다.
사용 방법 1) state.js ⇒ 상태들을 저장, 관리한다.
2) 사용될 컴포넌트
3) App.js 2. formik + Yup form 태그 내부에서 사용되는 input 태그의 입력 값의 Change, Blur, Submit 등의 폼 기능을 간편하게 사용할 수 있다. Yup 라이브러리와 함께 사용해서 입력 값의 유효성 검사를 쉽게 작성할 수 있다. Yup을 사용하지 않고 validation을 직접 작성할 수 있다. 회원가입, 로그인 페이지에서 사용될 수 있다.
기본 사용 방법
3. moment js
durationTime 구하기 4. react-dnd
5. emoji-picker
최종 package.json
yarn create react-app yas --template typescript
{
"compilerOptions": {
"target": "es6", // 기본값: es5 => es6로 수정
"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,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
],
}
yarn add -D eslint prettier
yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser // ESLint Rule 추가 플러그인
yarn add -D eslint-config-airbnb // airbnb ESLint 규칙
yarn add -D eslint-config-prettier eslint-plugin-prettier // prettier Rule 플러그인
yarn add -D eslint-plugin-react eslint-plugin-react-hooks // airbnb ESLint 규칙
yarn add -D eslint-plugin-jsx-a11y eslint-plugin-import // airbnb ESLint 규칙
// ...
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint": "^8.3.0",
"eslint-config-airbnb": "^19.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react-hooks": "^4.3.0",
"prettier": "^2.5.0"
}
// ...
module.exports = {
env: {
browser: true,
node: true,
commonjs: true,
es2021: true,
},
extends: [
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint'],
rules: {
'react/react-in-jsx-scope': 0,
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
};
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80,
"arrowParens": "always",
"orderedImports": true,
"bracketSpacing": true,
"jsxBracketSameLine": false
}
{
"extends": "./tsconfig.paths.json", // 추가, 해당 파일에서 속성을 상속받는다.
"compilerOptions": {
"target": "es6",
"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,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
],
"exclude": [ // 추가, 내용 더 찾아보기
"node_modules",
"**/node_modules/*"
]
}
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
}
}
// eslint-disable-next-line
const CracoAlias = require('craco-alias'); // 추가
module.exports = {
babel: {
presets: ['@emotion/babel-preset-css-prop'],
},
plugins: [ // 추가, craco의 절대경로 별칭을 지정
{
plugin: CracoAlias,
options: {
source: 'tsconfig',
baseUrl: './src',
tsConfigPath: './tsconfig.paths.json',
},
},
],
};
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/reset-css@5.0.1/reset.min.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap"
rel="stylesheet"
/>
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/preset-create-react-app',
],
webpackFinal: async (config) => {
config.resolve.plugins.push(new TsconfigPathsPlugin({}));
return config;
},
};
import './index.css';
import React from 'react';
import { addDecorator } from '@storybook/react';
import { MemoryRouter } from 'react-router-dom';
addDecorator((story) => (
<MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>
));
const customViewports = {
mobile: {
name: 'Mobile',
styles: {
width: '320px',
height: '100%',
},
},
tablet: {
name: 'Tablet',
styles: {
width: '768px',
height: '100%',
},
},
pc: {
name: 'PC',
styles: {
width: '1920px',
height: '100%',
},
},
};
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
viewport: { viewports: customViewports },
};
yarn add @emotion/react // emotion
yarn add --dev @emotion/babel-plugin // babel
yarn add @emotion/styled // styled
// ...
"dependencies": {
"@emotion/react": "^11.6.0",
"@emotion/styled": "^11.6.0",
// ...
yarn add -D @craco/craco
yarn add -D @emotion/babel-preset-css-prop
module.exports = {
babel: {
presets: ["@emotion/babel-preset-css-prop"]
}
}
// ...
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject"
},
// ...
npx -p @storybook/cli sb init
// package.json
// ...
"resolutions": { "babel-loader": "8.1.0" }
// ...
SKIP_PREFLIGHT_CHECK=true
yarn add react-router-dom@5.3.0
import { atom } from "recoil";
// atom은 상태를 나타낸다.
export const textState = atom({
key: "textState", //
default: "",
});
import { useRecoilState } from "recoil";
import { textState } from "./state";
const RecoilTest = () => {
const [value, setValue] = useRecoilState(textState);
// ...
}
import { RecoilRoot } from "recoil";
import "./App.css";
function App() {
return (
// 사용될 컴포넌트를 RecoilRoot로 감싸주면 자식 컴포넌트에서 전역으로 사용할 수 있다.
<RecoilRoot>
// ...
</RecoilRoot>
);
}
export default App;
const formik = useFormik({
initialValues: {
userEmail: "",
userPassword: "",
},
validationSchema: Yup.object({
userEmail: Yup.string()
.email(invalidErrorMessage.email)
.required(invalidErrorMessage.email),
userPassword: Yup.string()
.min(8, invalidErrorMessage.password)
.max(15, invalidErrorMessage.password)
.matches(/^[a-zA-Z0-9]+$/, invalidErrorMessage.password)
.required(invalidErrorMessage.password),
}),
onSubmit: (value) => {
alert(value);
},
});
// {...}
<form onSubmit={formik.handleSubmit}>
<input
id="userEmail"
name="userEmail"
type="email"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.userEmail}
placeholder="이메일"
/>
{formik.errors.userEmail}
<input
id="userPassword"
name="userPassword"
type="password"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.userPassword}
placeholder="비밀번호"
/>
{formik.errors.userPassword}
</form>
// {...}
const startTime = moment(); // 미션이 시작될 때 startTime 변수 생성
const endTime = moment(); // 미션이 끝났을 때 endTime 변수 생성
const durationTime = moment.duration(endTime.diff(startTime)).asSeconds();
// 초(second) 단위 Number type으로 변환된다.
// duration값에서는 asDays(), asHours() 등으로 원하는 값을 얻을 수 있다.
{
"name": "yas",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.6.0",
"@emotion/styled": "^11.6.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"axios": "^0.24.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
},
"resolutions": { "babel-loader": "8.1.0" },
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@craco/craco": "^6.4.2",
"@emotion/babel-plugin": "^11.3.0",
"@emotion/babel-preset-css-prop": "^11.2.0",
"@storybook/addon-actions": "^6.3.12",
"@storybook/addon-essentials": "^6.3.12",
"@storybook/addon-links": "^6.3.12",
"@storybook/node-logger": "^6.3.12",
"@storybook/preset-create-react-app": "^3.2.0",
"@storybook/react": "^6.3.12",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint": "8.3.0",
"eslint-config-airbnb": "^19.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react-hooks": "^4.3.0",
"prettier": "^2.5.0"
}
}