create-react-app을 사용해서 프로젝트를 생성합니다.
npx create-react-app todo-app
sass를 사용해서 스타일을 꾸며 주도록 하겠습니다.
npm add sass classnames react-icons
classnames는 조건부 스타일링을 좀 더 편하게 하기 위함
react-icons는 리액트에서 다양하고 예쁜 아이콘을 사용할 수 있는 라이브러리
=> https://react-icons.netlify.com/
npm start
생성된 create-react-app을 실행해 볼 수 있습니다.
컴포넌트
TodoTemplate : 화면을 가운데에 정렬시켜 주며, 앱 타이틀(일정 관리)을 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해 줍니다.
TodoInsert : 새로운 항목을 입력하고 추가할 수 있는 컴포넌트입니다. state를 통해 인풋의 상태를 관리합니다.
TodoListItem : 각 할 일 항목에 대한 정보를 보여 주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다.
TodoList : todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoListItem 컴포넌트로 변환하여 보여 줍니다.
App.js
// App.js
import { 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]
);
const onRemove = useCallback(
(id) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos]
);
const onToggle = useCallback(
(id) => {
setTodos(todos.map((todo) => (todo.id === id ? { ...todo, checked: !todo.checked } : todo)));
},
[todos]
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
);
};
export default App;
useState를 사용해서 todos의 상태를 정의합니다.
nextId는 todos에 새로운 값이 추가될 때 id가 겹치지 않게 넣어주기 위해서 사용합니다.
id 값은 렌더링 되는 정보가 아니므로 useRef를 사용하여 관리합니다.
onInsert 함수는 입력된 text로 todo 객체를 생성합니다. setTodos를 사용해 todos의 상태를 변경합니다. concat함수를 사용해서 추가된 todo를 todos에 붙여줍니다.
todos가 변경 될 때만 함수를 재생성하게 useCallback을 사용해주었습니다.
* props로 전달해야 할 함수를 만들 때 useCallback을 사용하여 함수를 감싸는 것을 습관화 하세요.
해당 함수를 TodoInsert 컴포넌트의 props로 설정해주었습니다.
onRemove 함수는 filter함수를 사용하여 App 컴포넌트에서 id를 파라미터로 받아 와서 같은 id를 가진 항목을 todos 배열에서 지워 줍니다.
todos가 변경 될 때 함수를 재생성하게 useCallback을 사용해주었습니다.
해당 함수를 TodoList 컴포넌트의 props로 설정해주었습니다.
onToggle 함수는 id가 동일한 경우 해당 값의 checked값을 not연산 해줍니다. (true -> false, false -> true)
todos가 변경 될 때 함수를 재생성하게 useCallback을 사용해주었습니다.
해당 함수를 TodoList 컴포넌트의 props로 설정해주었습니다.
TodoTemplate.js
// TodoTemplate.js
import "./scss/TodoTemplate.scss";
const TodoTemplate = ({ children }) => {
return (
<div className="TodoTemplate">
<div className="app-title">일정 관리</div>
<div className="content">{children}</div>
</div>
);
};
export default TodoTemplate;
TodoTemplate.js는 Todo의 전체적인 틀을 만들어주는 컴포넌트입니다.
TodoTemplate의 children을 <div className="content">안에 넣어주었습니다.
TodoInsert.js
// TodoInsert.js
import { useState, useCallback } from "react";
import { MdAdd } from "react-icons/md";
import "./scss/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;
TodoInsert.js는 입력받는 부분과 해당 기능에 대해서 구현되어 있는 컴포넌트입니다.
useState를 사용해서 value의 상태를 정의합니다.
onChange함수는 input값을 변경하기 위해서 사용합니다.
함수는 바뀌지 않으며, 재사용할 수 있으므로 mount될 때 한번만 만들어지도록 useCallback을 사용해주었습니다.
onSubmit함수가 실행되면 onInsert함수를 실행하고 value의 상태를 ""로 만들어줍니다.
e.preventDefault()를 사용해 화면이 넘어가지 않도록 해줍니다.
함수내에서 onInsert함수와 value를 사용하므로 해당 값이 바뀔 때만 새로 생성하게 해주었습니다.
TodoList.js
// TodoList.js
import TodoListItem from "./TodoListItem";
import "./scss/TodoList.scss";
const TodoList = ({ todos, onRemove, onToggle }) => {
return (
<div className="TodoList">
{todos.map((todo) => (
<TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToggle={onToggle} />
))}
</div>
);
};
export default TodoList;
TodoList는 할일 목록을 보여주는 컴포넌트입니다.
props로 todos와 onRemove, onToggle을 받습니다.
todos는 '할일'들을 가지고 있습니다. map을 사용해서 todos안의 객체 수만큼 TodoListItem을 만들어줍니다.
TodoListItem.js
// TodoListItem.js
import { MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline } from "react-icons/md";
import cn from "classnames";
import "./scss/TodoListItem.scss";
const TodoListItem = ({ todo, onRemove, onToggle }) => {
const { id, text, checked } = todo;
return (
<div className="TodoListItem">
<div className={cn("checkbox", { checked })} onClick={() => onToggle(id)}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
TodoListItem.js는 할일을 나타내는 컴포넌트입니다.
props로 받은 todo로 할일을 나타내고 onRemove와 onToggle함수를 사용해서 기능을 추가해주었습니다.
scss
// TodoInsert.scss
.TodoInsert {
display: flex;
background: #495057;
input {
background: none;
outline: none;
border: none;
padding: 0.5rem;
font-size: 1.125rem;
line-height: 1.5;
color: white;
&::placeholder {
color: #dee2e6;
}
flex: 1;
}
button {
background: none;
outline: none;
border: none;
background: #868e96;
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: #adb5bd;
}
}
}
// TodoList.scss
.TodoList {
min-height: 320px;
max-height: 513px;
overflow-y: auto;
}
// 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: #22b8cf;
}
.text {
color: #adb5bd;
text-decoration: line-through;
}
}
}
.remove {
display: flex;
align-items: center;
font-size: 1.5rem;
color: #ff6b6b;
cursor: pointer;
&:hover {
color: #ff8787;
}
}
& + & {
border-top: 1px solid #dee2e6;
}
}
// TodoTemplate.scss
.TodoTemplate {
width: 512px;
margin-left: auto;
margin-right: auto;
margin-top: 6rem;
border-radius: 4px;
overflow: hidden;
.app-title {
background: #22b8cf;
color: white;
height: 4rem;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.content {
background: white;
}
}
& 상위 선택자
https://webclub.tistory.com/396
결과 화면
'React' 카테고리의 다른 글
[React] 리덕스 미들웨어 (0) | 2023.05.30 |
---|---|
[React] 리액트 라우터 (1) | 2023.05.02 |
[React] JSX (0) | 2023.04.18 |
[React] props, defaultProps, propTypes (0) | 2021.03.06 |
[React] React Native (0) | 2021.02.09 |