Woongjae Lee
NHN Dooray - Frontend Team
Lead Software Engineer @ProtoPie
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
npx create-react-app tic-tac-toe
npm 5.2.0 이상부터 함께 설치된 커맨드라인 명령어
이런 곳에 있습니다.
npx create-react-app 프로젝트이름
{
"name": "tic-tac-toe",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
"react": "^16.13.1"
"react-dom": "^16.13.1"
"react-scripts": "3.4.1"
"@testing-library/jest-dom": "^4.2.4"
"@testing-library/react": "^9.5.0"
"@testing-library/user-event": "^7.2.1"
npm install serve -g
serve -s build
eject 를 이용하면, cra 로 만든 프로젝트에서 cra 를 제거합니다.
이는 돌이킬 수 없기 때문에 결정하기 전에 신중해야 합니다.
보통 cra 내에서 해결이 안되는 설정을 추가해야 할 때 합니다.
{
"name": "tic-tac-toe",
"version": "0.1.0",
"private": true,
"dependencies": {
"@babel/core": "7.9.0",
"@svgr/webpack": "4.3.3",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"babel-eslint": "10.1.0",
"babel-jest": "^24.9.0",
"babel-loader": "8.1.0",
"babel-plugin-named-asset-import": "^0.3.6",
"babel-preset-react-app": "^9.1.2",
"camelcase": "^5.3.1",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"css-loader": "3.4.2",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.6.0",
"eslint-config-react-app": "^5.2.1",
"eslint-loader": "3.0.3",
"eslint-plugin-flowtype": "4.6.0",
"eslint-plugin-import": "2.20.1",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "^1.6.1",
"file-loader": "4.3.0",
"fs-extra": "^8.1.0",
"html-webpack-plugin": "4.0.0-beta.11",
"identity-obj-proxy": "3.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-resolve": "24.9.0",
"jest-watch-typeahead": "0.4.2",
"mini-css-extract-plugin": "0.9.0",
"optimize-css-assets-webpack-plugin": "5.0.3",
"pnp-webpack-plugin": "1.6.4",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.1",
"react": "^16.13.1",
"react-app-polyfill": "^1.0.6",
"react-dev-utils": "^10.2.1",
"react-dom": "^16.13.1",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"sass-loader": "8.0.2",
"semver": "6.3.0",
"style-loader": "0.23.1",
"terser-webpack-plugin": "2.3.5",
"ts-pnp": "1.1.6",
"url-loader": "2.3.0",
"webpack": "4.42.0",
"webpack-dev-server": "3.10.3",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "4.3.1"
},
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"jest": {
"roots": [
"<rootDir>/src"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
],
"setupFiles": [
"react-app-polyfill/jsdom"
],
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
],
"testEnvironment": "jest-environment-jsdom-fourteen",
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
"^.+\\.module\\.(css|sass|scss)$"
],
"modulePaths": [],
"moduleNameMapper": {
"^react-native$": "react-native-web",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node"
],
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
},
"babel": {
"presets": [
"react-app"
]
}
}
webpack
파일 확장자에 맞는 loader 에게 위임
babel-loader
js
jsx
css
css-loader
최종 배포용 파일
babel config
어떤 문법을 번역할건지 설정
The pluggable linting utility for JavaScript and JSX
mkdir eslint-test
cd eslint-test
npm init -y
npm install eslint -D
npx eslint --init
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"semi": [
"error",
"always"
]
}
}
rules 을 추가합니다.
// index.js
console.log("hello")
{
"name": "tic-tac-toe",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app",
"rules": {
"semi": [
"error",
"always"
]
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
.eslintrc.json
An opinionated code formatter
mkdir prettier-test
cd prettier-test
npm init -y
npm i prettier -D
// index.js
console.log('hello')
// index.js
console.log("hello")
* replace *
Prettier 에서 불필요하거나, Prettier 와 충돌할 수 있는 모든 규칙을 끕니다.
이 구성은 규칙을 끄기만 하기 때문에 다른 설정과 함께 사용하는 것이 좋습니다.
{
...
"eslintConfig": {
"extends": [
"react-app",
"prettier"
]
},
...
}
Git hooks made easy
mkdir husky-test
cd husky-test
npm init -y
git init
npm i huskey -D
{
"name": "husky-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"husky": {
"hooks": {
"pre-commit": "npm test"
}
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"husky": "^3.0.8"
}
}
git add -A
git commit -m "husky-test"
Run linters on git staged files
npm i lint-staged -D
{
"name": "husky-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.js": [
"git add"
]
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"husky": "^3.0.8",
"lint-staged": "^9.4.1"
}
}
npm i eslint prettier -D
{
"name": "husky-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.js": [
"eslint --fix",
"prettier --write",
"git add"
]
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"husky": "^3.0.8",
"lint-staged": "^9.4.1"
},
"eslintConfig": {}
}
개발 모드
컴포넌트를 만들어봅시다.
Game Componenet
Board Componenet
Square Componenet
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
// index.css
body {
font: 14px 'Century Gothic', Futura, sans-serif;
margin: 20px;
}
ol,
ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
// App.js
import React from 'react';
import Game from './components/Game';
function App() {
return (
<div className="App">
<Game />
</div>
);
}
export default App;
// components/Game.jsx (state)
import React from 'react';
import Board from './Board';
export default class Game extends React.Component {
state = {
history: [
{
squares: Array(9).fill(null),
},
],
stepNumber: 0,
xIsNext: true,
};
render() {
// ...
}
}
// components/Game.jsx (render)
render() {
const {history, stepNumber} = this.state;
const current = history[stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>
{move ? 'Go to move #' + move : 'Go to game start'}
</button>
</li>
));
const status = winner ? 'Winner: ' + winner : 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
<div className="game">
<div className="game-board">
<Board squares={current.squares} onClick={i => this.handleClick(i)} />
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
// components/Game.jsx (handleClick)
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([
{
squares: squares,
},
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
// components/Game.jsx (jumpTo)
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: step % 2 === 0,
});
}
// components/Game.jsx
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
// components/Board.jsx (render)
import React from 'react';
import Square from './Square';
export default class Board extends React.Component {
renderSquare(i) {...}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
// components/Board.jsx (renderSquare)
import React from 'react';
import Square from './Square';
export default class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {...}
}
// components/Square.jsx
import React from 'react';
export default function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
By Woongjae Lee
Fast Campus Frontend Developer School 17th