๊ฐ์ธ ํ๋ก์ ํธ TODO LIST์ ์์์ ๋๋ค!!! ์ง์ง์ง ๐๐ป๐๐ป๐๐ป
์ดํ์ด์ง๋ง ๊ฝค๋ ๊ธด ์ฌ์ ์ด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ฐจ๊ทผ์ฐจ๊ทผ ๋ธ๋ก๊ทธ์ ๊ธฐ๋กํด๊ฐ๋ฉด์ LIST๋ฅผ ์์๋๊ฐ๊ฒ ์ต๋๋ค.
CodeStates์์ ๋ฐฐ์ด๋๋ก๋ง ๋ง๋ญ๋๋ค. ๋ฐ๋ก ํ ์ค๋ ๋ค๋ฅธ ๊ธฐ๋ฒ์ ์ฌ์ฉํ์ง ์๊ฒ ์ต๋๋ค.
๋ํ, ์ํ๊ฐ ๊ผญ ํ์ํ ๋ถ๋ถ์ด ์๋๋ฉด ๋ชจ๋ ์ํ๋ App.js์์ ๊ด๋ฆฌํ๊ฒ ์ต๋๋ค.
์ด๋ป๊ฒ ๊ตฌ์ฑํด์ผ ํ ๊น์? ๋ง๋ค๊ธด ์ด๋ ต๊ฒ ์ง๋ง, ์ ์ฝ๋ฉ์ด๋ผ๋ ํด๋ด ์๋ค!
1. ํฐ ํ๋ก ์ ์ฒด๋ฅผ ๊ฐ์ธ์ฃผ์ด์ผ ํ ๋ฏ ํฉ๋๋ค. ์ฑ์ฒ๋ผ ๋ณด์ด๊ฒ์!
2. ์ถ๊ฐ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์ ์ฅ์ด๋๋ฉด์, ์๋ก์ด ๋ฆฌ์คํธ๊ฐ ๋์์ผ ๊ฒ ๋ค์!
3. ์ ์ผ ์์ชฝ์ Title์ ๊ตฌ์ฑํ๊ตฌ์. ์ค๋ฅธ์ชฝ์ ๋ ์ง๋ ์์ผ๋ฉด ์ข๊ฒ ๋ค์.
4. Input์ฐฝ์์ ์ ๋ ฅํ๋ฉด ๋ฐ๋ก ๊ธ์จ๊ฐ ์จ์ง๊ณ , submit์ผ๋ก ๋ฐ๋ก ๊ทธ ์๋ฆฌ์ ์ถ๊ฐํด์ค์๋ค.
5. ๊ทธ๋ฆฌ๊ณ ๋ฐ๋ก ๋ค์ ์ ๋ ฅ์นธ์ผ๋ก ํฌ์ปค์ค๊ฐ ๋์ด๊ฐ๊ณ , ํ ์ผ์ ์์ฑํด์ผ์ฃ .
6. ๊ฐ ํ ์ผ์ ํด๋ฆญํ๋ฉด ์๋ฃํ ์ผ์ ํ์ํด์ค์ผํฉ๋๋ค.
7. ๊ฐ ํ ์ผ๋ค์ ์ญ์ ๋ ํด์ค์ผ๊ฒ ์ฃ ? ์ค๋ฅธ์ชฝ ์ฏค์ ๋ฒํผํ๋ ์ถ๊ฐํฉ์๋ค
๊ธฐ๋ณธ ์ ํ ์ ํด๋ด ์๋ค.
๋จผ์ CRA๋ฅผ ์ด์ฉํ์ฌ ํ๋ก์ ํธ๋ฅผ ์์ฑํฉ๋๋ค.
npx create-react-app <ํ๋ก์ ํธ ์ด๋ฆ>
ํ๋ก์ ํธ๋ฅผ ๊น๋ํ๊ฒ ๊ตฌ์ฑํ๊ธฐ ์ํด ESLint์ Prettier๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค.
๋ง์ผํ๋ ์ด์ค์์ ๋ ๊ฐ๋ฅผ ์ค์น๋ถํฐ ํด์ฃผ์๊ณ ์๋๋ก ๋ฐ๋ผ์์ฃผ์ ์ผํด์ :)
package.json์ ์๋๋ฅผ ๋ถ์ฌ๋ฃ๊ณ yarn install
๋ก ์ค์นํฉ๋๋ค.
// package.json
"devDependencies": {
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.14.3",
"husky": "^3.0.2",
"lint-staged": "^9.2.1",
"prettier": "1.18.2"
},
"lint-staged": {
"*.{js,jsx}": [
"eslint --fix",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
๋ค์ .eslintrc
ํ์ผ์ ์์ฑํ์ฌ ์๋์ ๊ฐ์ด ์์ฑํฉ๋๋ค.
// .eslintrc
{
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:react/recommended",
"prettier",
"prettier/react"
],
"plugins": ["prettier", "react"],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true
},
"rules": {
"prettier/prettier": [
"error",
{
"singleQuote": true,
"printWidth": 120
},
{
"usePrettierrc": false
}
],
"no-console": "warn",
"semi": 2,
"no-undef": "warn"
}
}
๋ง์ง๋ง์ผ๋ก, ESLint์ Prettier ์ค์ ์ Setting.json์ ์ถ๊ฐํฉ๋๋ค.
// Setting.json
{
// ...์๋ต...
"eslint.autoFixOnSave": true,
"eslint.packageManager": "yarn",
"eslint.validate": [
"javascript",
"javascriptreact",
"html",
"typescriptreact"
],
"editor.formatOnSave": true,
"javascript.format.enable": false,
"prettier.eslintIntegration": true
}
์ด์ App.js๋ก ๋ค์ด๊ฐ์ Command
+ s
ํด์ฃผ์๋ฉด ์๋ ์ ๋ ฌ์ด ๋ฉ๋๋ค!
ํน์ ์ฝ๋ ์์ฑํ์๋ค๊ฐ babel๊ด๋ จ Lint์๋ฌ๊ฐ ๋์ ๋ค๋ฉด ์๋๋ฅผ .eslintrc์ ์ถ๊ฐํด๋ณด์ธ์!
// .eslintrc
"parser": "babel-eslint",
์ด๋ฒ ํ๋ก์ ํธ์์๋ Sass๋ฅผ ์ฐ์ตํด๋ณด๊ธฐ ์ํด์ ์ดํ์ scssํ์ผ์ ๋ง๋ค๊ฑฐ์์.
App.js / App.css / index.js / index.css ๋ง ๋จ๊ฒจ๋๊ณ ์ญ์ ํ ๊ฒ์!
๊ทธ ๋ค ๊ฐ๋จํ ํ๋ฉด ๊ตฌ์ฑ์ ์ํด์ ๋ด์ฉ์ ์์ ํฉ๋๋ค!
/* index.css */
body {
margin: 1rem;
padding: 0;
background: #2196f3;
}
// App.js
import React, { Component } from 'react';
import './App.css';
class App extends Component {
render() {
return <div className="App">TODO LIST๋ฅผ ๋ง๋ค์ด๋ณผ๊ฑฐ์์!</div>;
}
}
export default App;
/* App.css */
.App {
width: 760px;
margin: 0 auto;
padding: 2rem;
background: #fff;
border-radius: 1em;
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.3);
}
์ด์ yarn start
๋ก ํ์ธํด๋ณผ๊น์? ๐๐ป๐๐ป๐๐ป
๊ฐ๋จํ๊ฒ ํ์ดํ๋ถํฐ ๋ง๋ค์ด๋ณด๊ฒ ์ต๋๋ค.
srcํด๋์ components ํด๋๋ฅผ ์์ฑํ๊ณ , Title.jsx์ Title.scss๋ฅผ ์์ฑํฉ๋๋ค.
Sass๋ฅผ ์ฌ์ฉํ๊ณ ์ถ์ผ์๋ฉด yarn add node-sass
๋ฅผ ์ฌ์ฉํ์๋ฉด ๋ฉ๋๋ค!
๋ฌผ๋ก css๋ฅผ ์ฌ์ฉํ์ ๋ ๋ง๋๋ ๊ฑฐ์ ํฐ ์ง์ฅ์ ์์ง๋ง, ์ ๋ ์ฐ์ต์ ์ํด ์ฌ์ฉํด๋ณผ๊ฒ์!
์ด๋ฒ๊น์ง๋ง import์ export๋ฅผ ์ ๊ณ , ์ดํ์๋ ์๋ตํ๊ฒ ์ต๋๋ค!
// Title.jsx
import React from 'react';
import './Title.scss';
const Title = () => {
return <div className="title">TODO LIST</div>;
};
export default Title;
// Title.scss
.title {
padding-bottom: 1rem;
font-size: 2.5rem;
font-weight: 700;
text-align: center;
border-bottom: 0.8px solid rgba($color: #0000ff, $alpha: 0.2);
}
์ด์ App.js์์ Title์ ๋ถ๋ฌ์์ฃผ์ธ์.
์๋์ ๊ฐ์ด ๋์จ๋ค๋ฉด ์์ฑ์
๋๋ค! css๋ ์์ ๋กญ๊ฒ ๋ง๋ค์ด์ฃผ์๋ฉด ๋ฉ๋๋ค.
์ด๋ ์ ๋๊น์ง ๋ง๋ค๊ณ ํฌ์คํ ์ ํด์ผํ ์ง ์ ๋งคํ๋ฐ์~ ์ผ๋จ ๋ด ์๋ค!
Form.jsx, Form.scss๋ฅผ ๋ง๋ค๊ณ ์์ฑํฉ๋๋ค.
๊ฐ์๊ธฐ PropTypes๊ฐ ๋์์ฃ ...?!
๊ฐ prop์ ํ์ ์ ์ง์ ํด์ฃผ๋๊ฑด๋ฐ์, ESLint์์ ์๋ฌ๋ฅผ ์ฃผ์ด์ ์ค์นํ์ต๋๋ค.
์ ๋ฌผ๋ก , ํ์ ์ง์ ์ ํด์ฃผ๋ ๊ฑด ์์ฃผ ๋ฐ๋์งํ ์ฝ๋ฉ๋ฐฉ๋ฒ์ ๋๋ค!
yarn add prop-types
๋ฅผ ํ์๊ณ ์ถ๊ฐํ์๋ฉด ๋ฉ๋๋ค!
TypsScript๋ฅผ ์ฌ์ฉํ ๋๊น์ง ํ์ ์ง์ ์ ํด์ฃผ์ด์ ์ค๋ฅ๋ฅผ ๋ฐฉ์งํด์ค์๋ค!
// Form.jsx
import PropTypes from 'prop-types';
const Form = ({ inputRef, onSubmit, onChange, inputText }) => {
return (
<form className="form" onSubmit={e => onSubmit(e)}>
<input className="form-input" ref={input => inputRef(input)} value={inputText} onChange={e => onChange(e)} />
<input className="form-submit" type="submit" />
</form>
);
};
Form.propTypes = {
inputRef: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
inputText: PropTypes.string.isRequired
};
Form์ ref๋ App.js์์ ์ด๊ธฐ์ input์ focus๋ฅผ ์ฃผ๊ธฐ ์ํด์ ์ฌ์ฉํฉ๋๋ค.
๋๋จธ์ง ํจ์๋ค๋ ๋ชจ๋ App.js์์ ๊ด๋ฆฌํ๊ธฐ ์ํด prop์ผ๋ก ์ ๋ฌ๋ฐ์์ต๋๋ค!
onChange๋ onClick event๋ ํ์ดํ ํจ์๋ก ํจ์์ ์ง์ ์ ๋ฌํด์ฃผ๋ฉด ๋ฉ๋๋ค.
// Form.scss
.form {
.form-input {
width: 90%;
margin: 1.5rem;
padding: 1rem;
font-size: 1.2rem;
color: #2196f3;
border: 1px solid #eee;
border-radius: 3px;
&:focus {
outline-color: rgba($color: #6cccf8, $alpha: 0.1);
}
}
.form-submit {
display: none;
}
}
์ด์ ์ด ๋ชจ๋ ๊ฑธ ๊ด๋ฆฌํ App.js๋ฅผ ์์ ํด์ค๋๋ค!
๊ฐ๋จํ๊ฒ ์ฃผ์์ผ๋ก ์ฝ์ฝ ์ค๋ช ํ ๊ฒ์.
// App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: ''
};
}
// TODO : ์ฒ์ ์์ ์์ Input์ฐฝ์ ํฌ์ปค์ค
componentDidMount() {
this.textInput.focus();
}
// TODO : Input์
๋ ฅ ์ ๊ฐ ๋ณํ
onChange = e => {
this.setState({ text: e.target.value });
};
// TODO : input๊ฐ์ ์
๋ ฅํ๊ณ submitํ๋ฉด ๊ฐ์ ๋น์๋๋ค.
onSubmit = e => {
e.preventDefault();
this.setState({
text: ''
});
};
// TODO : input focus๋ฅผ ์ํ ํจ์
inputRef = input => {
this.textInput = input;
};
render() {
// state destructuring
const { text } = this.state;
// func destructuring
const { onChange, onSubmit, inputRef } = this;
return (
<div className="App">
<Title />
<Form inputText={text} inputRef={inputRef} onChange={onChange} onSubmit={onSubmit} />
</div>
);
}
}
render์์ ์ฌ์ฉํ state์ function์ ํ๋์ฉ ๋น๊ตฌ์กฐํ ํ ๋น์ ํด์ฃผ์๋ฉด
์ฝ๋๋ ๊น๋ํด์ง๊ณ , ์ฌ์ฉํ ์ํ๋ ํจ์๊ฐ ๋ฌด์์ธ์ง ๋ฐ๋ก๋ฐ๋ก ํ์ธํ์ค ์ ์์ด์!
์ด์ ํ๋ฉด์ ๋ณด๋ฉด...
ํ๋ฉด์ ๋ณด์๋ง์ Input ํฌ์ปค์ฑ์ด ๋ค์ด๊ฐ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค! ๐๐ป
์ด์ ํ ์ผ ๋ชฉ๋ก์ ๋ง๋ค์ด ๋ณผํ ๋ฐ์,
๋ชฉ๋ก๊ณผ ๋ชฉ๋ก์ ๋ค์ด๊ฐ ์์ดํ ์ ๋ถ๋ฆฌํฉ๋๋ค.
๊ทธ๋์ผ ๋ชฉ๋ก์ ๊ฐ ํ ์ผ๋ค์ ๋ด์์ ํํํด์ค ์ ์์ต๋๋ค.
์ ํด์ค๋ ๋์ง๋ง, ๋์ค์ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ์์ ํธ๋ฆฌํ๋๋ก ํฉ๋๋ค.
๋จผ์ , App.js์์ ์ํ๋ฅผ ์ถ๊ฐํฉ๋๋ค. ๊ธฐ๋ณธ ๋ฐ์ดํฐ๋ฅผ 2๊ฐ ์ ๋ ๋ง๋ค์ด๋๊ฒ์.
// App.jsx
// ... ์๋ต...
this.state = {
id: 3,
text: '',
checked: false,
todoList: [{ id: 1, text: '๊ฐ์ ์ถ์๋จน๊ธฐ', checked: true }, { id: 2, text: '์ท ์ฌ๋ฌ ๊ฐ๊ธฐ', checked: false }]
};
...
์ด์ TodoList.jsx, TodoListItem.jsx ๋ฅผ ์์ฑํฉ๋๋ค.
๋ง๋ค์ด ๋์๋ todoList๋ฐฐ์ด์ ์ํํ๋ฉด์ TodoListItem์ ์์ฑํฉ๋๋ค.
TodoListItem์๋ ์ ํ ๋ฒํผ, ํ ์คํธ, ์ญ์ ๋ฒํผ์ ๋ฏธ๋ฆฌ ์์ฑํด ๋ก๋๋ค.
className์ checked๋ฅผ ์ผํญ ์ฐ์ฐ์๋ก ๊ฒ์ฌํ์ฌ ์ฒดํฌ ๋์์ ๊ตฌํํด ์ค๊ฒ๋๋ค.
// TodoList.jsx
const TodoList = ({ todoList }) => {
return (
<div className="todo-list">
{todoList.map(list => (
<TodoListItem key={list.id} text={list.text} checked={list.checked} />
))}
</div>
);
};
TodoList.propTypes = {
todoList: PropTypes.array.isRequired
};
// TodoListItem.jsx
const TodoListItem = ({ text, checked }) => {
return (
<div className={`todo-list-item ${checked ? 'checked' : ''}`}>
<button className="check-button"></button>
<span className="todo-text">{text}</span>
<button className="delete-button">โ</button>
</div>
);
};
TodoListItem.propTypes = {
text: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired
};
์ด์ App.jsx์ ์ปดํฌ๋ํธ๋ฅผ ์ถ๊ฐํ๊ณ ๊ฐ๊ฐ์ scssํ์ผ์ ์์ฑํ์ฌ ์ฝ๊ฐ์ ๋ชจ์์ ์์ ํฉ๋๋ค.
์์ง TodoList.scss๋ ๋ณ๋ค๋ฅธ ์์ ์ ํ์ง ์๊ฒ ์ต๋๋ค.
// App.jsx
// ...์๋ต...
render() {
// state destructuring
const { text, todoList } = this.state;
// func destructuring
const { onChange, onSubmit, inputRef } = this;
return (
<div className="App">
<Title />
<Form inputText={text} inputRef={inputRef} onChange={onChange} onSubmit={onSubmit} />
<TodoList todoList={todoList} />
</div>
);
}
// TodoListItem.scss
@mixin button() {
width: 20px;
height: 20px;
border: none;
border-radius: 5px;
transition: all 0.2s;
outline: none;
}
.todo-list-item {
margin: 0.3rem;
padding: 0.5rem;
.todo-text {
font-size: 1.3rem;
margin-left: 12px;
}
.check-button {
@include button;
background: rgba(0, 0, 0, 0.3);
&:hover {
background: rgba(0, 0, 0, 0.2);
}
}
.delete-button {
@include button;
font-size: 1.2rem;
font-weight: 800;
color: rgba(255, 0, 0, 0.2);
&:hover {
color: red;
}
}
}
.checked {
margin-left: 1.5rem;
}
๋์์ ๊ตฌํํ๊ธฐ ์ ์ ๋จผ์ ํ๋ฉด์ ํ์ธํด ๋ด ์๋ค.
์ด์ onSubmit ๋์ ์ ๋ฆฌ์คํธ๋ฅผ ์ถ๊ฐํ๋ฉด ๋๊ฒ ๋ค์!
์ ์ฝ์์ต๋๋ค. ๋๋ถ์ husky์ git-hooks๋ฅผ ๊ณต๋ถํ๊ณ ๊ฐ๋๋ค ใ ใ
์๊ฐํด๋ ๋์์ธ์ Figma ๊ฐ์ ํ๋กํ ํ์ ํด์ ์ด์ฉํด ๋ง๋ค์ด๋๋ฉด ์ข์ ๊ฒ ๊ฐ์์ ใ ใ