todo-app
프로젝트 생성npx create-react-app todo-app
npm add node-sass classnames react-icons
/* 최상위 디렉토리에 .prettierrc */
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
index.css
수정body {
margin: 0;
padding: 0;
background: #e9ecef;
}
App.js
컴포넌트 초기화import React from 'react';
const App = () => {
return <div>Todo App을 만들자!</div>;
};
export default App;
만들자!
이번 프로젝트에서 쓰일 컴포넌트는 아래와 같다.
TodoTemplate
: 화면을 가운데에 정렬 / children
으로 내부 JSX를 props
로 받아 와서 렌더링TodoInsert
: 새로운 항목을 입력하고 추가 / state
를 통해 인풋의 상태 관리TodoListItem
: 각 할 일 항목 / todo
객체를 props
로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줌TodoList
: todos
배열을 props
로 받아 map
으로 여러 개의 TodoListItem
컴포넌트로 변환해서 보여줌TodoTemplate.js
import React from 'react';
import './TodoTemplate.scss';
const TodoTemplate = ({ children }) => {
return (
<div className="TodoTemplate">
<div className="app-title">일정 관리</div>
<div className="content">{children}</div>
</div>
);
};
export default TodoTemplate;
TodoTemplate.scss
.TodoTemplate {
width: 512px;
margin-left: auto;
margin-right: auto;
margin-top: 6rem;
border-radius: 4px;
overflow: hidden;
.app-title {
background: salmon;
color: white;
height: 4rem;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.content {
background: white;
}
}
overflow: hidden;
넘치는 부분은 잘려서 보여지지 않음
TodoInsert.js
import React from 'react';
import { MdAdd } from 'react-icons/md';
import './TodoInsert.scss';
const TodoInsert = () => {
return (
<form className="TodoInsert">
<input placeholder="할 일을 입력하세요" />
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
TodoInsert.scss
.TodoInsert {
display: flex;
background: rgb(179, 179, 179);
input {
background: none;
outline: none;
border: none;
padding: 0.5rem;
font-size: 1.125rem;
line-height: 1.5;
color: white;
&::placeholder {
color: rgb(224, 224, 224);
}
flex: 1;
}
button {
background: none;
outline: none;
border: none;
background: rgb(141, 141, 141);
color: white;
padding-left: 1rem;
padding-right: 1rem;
font-size: 1.5rem;
display: flex;
align-items: center;
cursor: pointer;
transition: 0.1s background ease-in;
&:hover {
background: rgb(247, 169, 160);
}
}
}
transition: 0.1s background ease-in;
- background: (property) 애니메이션 시킬 속성
- 0.1s: (duration) 시작해서 끝날 때까지의 시간
- ease-in: (timingfunction) 속도 변화
TodoListItem.js
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import './TodoListItem.scss';
const TodoListItem = () => {
return (
<div className="TodoListItem">
<div className="checkbox">
<MdCheckBoxOutlineBlank />
<div className="text">할 일</div>
</div>
<div className="remove">
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
TodoListItem.scss
.TodoListItem {
padding: 1rem;
display: flex;
align-items: center;
&:nth-child(even) {
background: #f8f9fa;
}
.checkbox {
cursor: pointer;
flex: 1;
display: flex;
align-items: center;
svg {
font-size: 1.5rem;
}
.text {
margin-left: 0.5rem;
flex: 1;
}
&.checked {
svg {
color: salmon;
}
.text {
color: #adb5bd;
text-decoration: line-through;
}
}
}
.remove {
cursor: pointer;
display: flex;
align-items: center;
font-size: 1.5rem;
color: rgb(141, 141, 141);
&:hover {
color: salmon;
}
}
& + & {
border-top: 1px solid #dee2e6;
}
}
&
는 중첩된 규칙의 부모 선택자를 참조;& + & = .TodoListItem + .TodoListItem
TodoList.js
import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';
const TodoList = () => {
return (
<div className="TodoList">
<TodoListItem />
<TodoListItem />
<TodoListItem />
</div>
);
};
export default TodoList;
TodoList.scss
.TodoList {
min-height: 320px;
max-height: 513px;
overflow-y: auto; //리스트 안은 스크롤 가능하게 함
}
import React from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
const App = () => {
return(
<TodoTemplate>
<TodoInsert />
<TodoList />
</TodoTemplate>
);
};
export default App;
컴포넌트 스타일링 완료
App.js
import React, { useState }from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트의 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링해 보기',
checked: true,
},
{
id: 3,
text: '일정 관리 앱 만들어 보기',
checked: false,
},
]);
return(
<TodoTemplate>
<TodoInsert />
<TodoList todos={todos}/>
</TodoTemplate>
);
};
export default App;
TodoList.js
import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';
const TodoList = ({ todos }) => {
return (
<div className="TodoList">
{todos.map(todo => (
<TodoListItem todo={todo} key={todo.id} />
))}
</div>
);
};
export default TodoList;
TodoListItem.js
classnames
사용import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = ({ todo }) => {
const { text, checked } = todo;
return (
<div className="TodoListItem">
<div className={cn('checkbox', {checked})}>
{checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove">
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
useState -> value
useCallback -> onChange, onSubmit
(컴포넌트가 리렌더링될 때마다 함수를 새로 만드는 것이 아니라, 한 번 함수를 만들고 재사용할 수 있도록 Hook 사용)
onClick
대신에 onSubmit
쓰는 이유 -> Enter
감지
TodoInsert.js
import React, { useState, useCallback } from 'react';
import { MdAdd } from 'react-icons/md';
import './TodoInsert.scss';
const TodoInsert = ({ onInsert }) => {
const [value, setValue] = useState('');
const onChange = useCallback(e => {
setValue(e.target.value);
}, []);
const onSubmit = useCallback(
e => {
onInsert(value);
setValue('');
e.preventDefault();
},
[onInsert, value],
);
return (
<form className="TodoInsert" onSubmit={onSubmit}>
<input
placeholder="할 일을 입력하세요"
value={value}
onChange={onChange}
/>
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
onInsert
-> id
값은 렌더링되는 정보가 아니기 때문에 useRef
를 사용하여 관리
-> props
로 전달해야 할 함수이므로 useCallback
을 사용
App.js
import React, { useState, useRef, useCallback }from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트의 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링해 보기',
checked: true,
},
{
id: 3,
text: '일정 관리 앱 만들어 보기',
checked: false,
},
]);
const nextId = useRef(4);
const onInsert = useCallback(
text => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo));
nextId.current +=1;
},
[todos],
);
return(
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos}/>
</TodoTemplate>
);
};
export default App;
filter
함수로 todos
를 걸러 준다. App.js
에 아래 onRemove
함수를 정의해 주고 TodoList
, TodoListItem
에 props로 전달 전달
const onRemove = useCallback(
id => {
setTodos(todos.filter(todo => todo.id !== id));
},
[todos],
);
현재 onRemove
를 호출한 id
를 가진 todo
만 남기고 새로운 todos
배열로 바뀜
위와 비슷하게 filter
함수로 todos
를 걸러 준다. App.js
에 아래 onToggle
함수를 정의해 주고 TodoList
, TodoListItem
에 props로 전달 전달
const onToggle = useCallback(
id => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo,
),
);
},
[todos],
);
현재 onToggle
을 호출한 id
를 가진 todo
의 checked
값을 반전시켰다(나머지는 그대로 todo
반환). 불변성을 유지하면서 특정 배열 원소를 업데이트해야 할 때 map
을 사용하면 짧은 코드로 쉽게 작성할 수 있다.