728x90
반응형
안녕하세요! delay100입니다. 이번 포스팅에서는 간단한 투두리스트를 만들어봅시다. 지난 1~9 포스팅들은 작은 개념에 대한 빌드업을 해왔는데, 드디어 무언가를 만들어본다니 설레네요ㅎㅎ.
대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요!
책 리액트를 다루는 기술, 개정판의 10장 내용을 다루고 있습니다.
이번 포스팅의 Github 링크
https://github.com/delay-100/study-react/tree/main/ch10/todo-app
1. 프로젝트 준비하기
프로젝트 생성 명령어
yarn create react-app todo-app
cd todo-app
설치할 라이브러리
* sass, classnames에 대한 설명은 여기를 참고해주세요. (클릭)
yarn add sass classnames react-icons
+ react-icons?
- 리액트에서 다양한 아이콘을 사용할 수 있는 라이브러리
- SVG 형태로 이루어진 아이콘을 리액트 컴포넌트처럼 사용할 수 있음
- 아이콘의 크기나 색상은 props 혹은 css 스타일로 변경하여 사용 가능
+ Prettier 설정
코드 스타일을 깔끔하게 하기 위해 Prettier을 설정합니다.
VSCode에서 f1을 누른 후 format 입력 후 Enter (-> format Document)을 누르면 됩니다. 그 후 /todo-app의 최상위 디렉터리에 .prettierrc 파일을 생성했습니다.
- .prettier 파일 생성 및 설정 - .prettierrc
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
2. UI 만들기
각 파일들 설명
TodoTemplate: 화면을 가운데에 정렬시켜 주며, 앱 타이틀(일정 관리)를 보여줌
TodoInsert: 새로운 항목을 입력하고 추가할 수 있는 컴포넌트
TodoList: map을 사용해 여러 개의 TodoListItem 컴포넌트를 보여줌
TodoListItem: 각 할일 항목에 대한 정보를 보여주는 컴포넌트
scss 파일들은 github를 참고해주세요!
https://github.com/delay-100/study-react/tree/main/ch10/todo-app/src/components
- src/App.js
import { useState } from 'react';
import TodoInsert from './components/TodoInstert';
import TodoTemplate from './components/TodoTemplate';
import TodoList from './components/TodoList';
const App = () => {
const [todos, setTodos] = useState([
// 나중에 추가할 일정 항목에 대한 상태들은 모두 App 컴포넌트에서 관리함
// useState를 사용하여 todos라는 상태를 정의
{
id: 1, // 각 항목의 고유 id
text: '리액트의 기초 알아보기', // 내용
checked: true, // 완료 여부
},
{
id: 2,
text: '컴포넌트 스타일링해 보기',
checked: true,
},
{
id: 3,
text: '투두리스트 만들어 보기',
checked: false,
},
]);
return (
<TodoTemplate>
<TodoInsert />
<TodoList todos={todos} /> {/* todos를 TodoList의 props로 전달*/}
</TodoTemplate>
);
};
export default App;
- src/components/TodoTemplate.js
import './TodoTemplate.scss';
const TodoTemplate = ({ children }) => {
return (
<div className="TodoTemplate">
<div className="app-title">일정 관리</div>
<div className="content">{children}</div>
</div>
);
};
export default TodoTemplate;
- src/components/TodoInsert.js
import { MdAdd } from 'react-icons/md'; // import 아이콘이름 from 'react-icons-md';
// https://react-icons.netlify.com/#/icons/md 페이지에 들어가서 Material Design icons에서 아이콘이름을 고름
import './TodoInsert.scss';
const TodoInsert = () => {
return (
<form className="TodoInsert">
<input placeholder="할 일을 입력하세요" />
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
- src/components/TodoList.js
import TodoListItem from './TodoListItem';
import './TodoList.scss';
// App.js에서 todos={todos}로 props를 넘겨주었기 때문에 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정해야 함
const TodoList = ({ todos }) => {
return (
<div className="TodoList">
{/* props로 받아온 todos 배열을 배열 내장 함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링함
map을 사용하여 컴포넌트로 변환할 때는 key props를 전달해주어야 함(여기서는 key값에 각 항목마다 가지고 있는 고유값인 id를 넣어 줌)
그리고 여러 종류의 값을 전달해줘야 하는 todo데이터의 경우 통째로 전달하는 편이 나중에 성능 최적화 시 편리함
=> TodoListItem으로 이동 */}
{todos.map((todo) => (
<TodoListItem todo={todo} key={todo.id} />
))}
</div>
);
};
export default TodoList;
- src/components/TodoListItem.js
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames'; // 조건부 스타일링을 위해 classnames를 이용함
import './TodoListItem.scss';
// TodoList에서 todo 값을 넘겨줌
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;
3. 기능 구현하기
이번에는 위의 UI에 기능을 추가해봅시다.
3-1. 추가(CREATE)
- 투두 추가하기 - src/App.js 中 일부
import { useState, useRef, useCallback } from 'react';
...
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(4); // id값은 useRef를 사용하여 관리 => id 값은 렌더링되는 정보가 아니고, 새로운 항목을 만들 때 참조되는 값일 뿐이기 때문
// onInsert 함수 - App 컴포넌트에서 todos 배열에 새 객체를 추가하는 함수
const onInsert = useCallback(
// 컴포넌트의 성능을 아낄 수 있도록 useCallback으로 감싸줌(props로 전달해야 할 함수를 만들 때는 useCallback을 사용해 함수를 감싸줌)
(text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo));
nextId.current += 1; // nextId 1씩 더하기
},
[todos],
);
...
- 투두 추가하기 - src/components/TodoInsert.js
import { useState, useCallback } from 'react';
import { MdAdd } from 'react-icons/md'; // import 아이콘이름 from 'react-icons-md';
// https://react-icons.netlify.com/#/icons/md 페이지에 들어가서 Material Design icons에서 아이콘이름을 고름
import './TodoInsert.scss';
// App.js에서 TodoInsert에 onInsert 함수를 넣어줌
const TodoInsert = ({ onInsert }) => {
const [value, setValue] = useState(''); // TodoInsert 컴포넌트에서 input에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 함수를 정의함
const onChange = useCallback((e) => {
// input에 넣어 줄 onChange 함수도 작성해줘야 함
// => 이 과정에서 컴포넌트가 리렌더링될 때마다 함수를 새로 만드는 것이 아니라 한번 함수를 만들고 재사용 할 수 있도록 useCallback Hook을 사용함
setValue(e.target.value);
}, []);
// onSubmit 함수 - form의 onSubmit으로 설정
// 이 함수 호출 시 props로 받아 온 onInsert 함수에 현재 value 값을 파라미터로 넣어서 호출하고 현재 value 값을 초기화 함
// onClick도 가능한데, onSubmit은 input에서 Enter을 넣었을 때도 실행되기 때문에 사용했음(onClick은 onKeyPress 이벤트를 통해 Enter 감지 로직을 따로 해줘야 함)
const onSubmit = useCallback(
(e) => {
onInsert(value);
setValue(''); // value 값 초기화
// submit 이벤트는 브라우저에서 새로고침을 발생시킵니다.
// e.preventDefault() - 새로고침을 방지하기 위해 이 함수를 호출합니다.
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;
3-2. 삭제(DELETE)
리액트 컴포넌트에서 배열의 불변성을 지키면서 배열 원소를 제거해야 할 경우, 배열 내장 함수인 filter를 사용하면 매우 간편합니다.
filter은 지난 포스팅에서 다룬 적이 있습니다. (클릭 후 filter 검색 - ctrl+f)
- 투두 삭제하기 - src/App.js 中 일부
// onRemove 함수 - App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수
const onRemove = useCallback(
(id) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos],
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} />
{/* todos, onRemove를 TodoList의 props로 전달*/}
</TodoTemplate>
);
- 투두 삭제하기 - src/components/TodoList.js
import TodoListItem from './TodoListItem';
import './TodoList.scss';
// App.js에서 todos={todos}로 props를 넘겨주었기 때문에 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정해야 함
const TodoList = ({ todos, onRemove }) => {
return (
<div className="TodoList">
{/* props로 받아온 todos 배열을 배열 내장 함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링함
map을 사용하여 컴포넌트로 변환할 때는 key props를 전달해주어야 함(여기서는 key값에 각 항목마다 가지고 있는 고유값인 id를 넣어 줌)
그리고 여러 종류의 값을 전달해줘야 하는 todo데이터의 경우 통째로 전달하는 편이 나중에 성능 최적화 시 편리함
=> TodoListItem으로 이동 */}
{todos.map((todo) => (
<TodoListItem todo={todo} key={todo.id} onRemove={onRemove} />
))}
</div>
);
};
export default TodoList;
- 투두 삭제하기 - src/components/TodoListItem.js
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames'; // 조건부 스타일링을 위해 classnames를 이용함
import './TodoListItem.scss';
// TodoList에서 todo 값을 넘겨줌
const TodoListItem = ({ todo, onRemove }) => {
const { id, text, checked } = todo;
return (
<div className="TodoListItem">
<div className={cn('checkbox', { checked })}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
3-3. 체크하기
투두의 핵심인 체크하는 기능을 만들어봅시다.
- 투두 체크하기 - src/App.js 中 일부
...
const onToggle = useCallback(
(id) => {
setTodos(
todos.map(
(todo) =>
// 배열 내장 함수 map을 사용해 특정 id를 가지고 있는 객체의 checked 값을 반전시켜 줌
// 특정 배열 원소를 업데이트해야 할 때 이렇게 map을 사용하면 짧은 코드로 쉽게 작성 가능
todo.id === id ? { ...todo, checked: !todo.checked } : todo, // todo.id와 현재 파라미터로 사용된 id 값이 같을 때는 새로운 객체를 생성하지만, id 값이 다를 때는 처음 받아왔던 상태 그대로를 반환함
),
);
},
[todos],
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
{/* todos, onRemove를 TodoList의 props로 전달*/}
</TodoTemplate>
);
...
- 투두 체크하기 - src/components/TodoList.js
import TodoListItem from './TodoListItem';
import './TodoList.scss';
// App.js에서 todos={todos}로 props를 넘겨주었기 때문에 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정해야 함
const TodoList = ({ todos, onRemove, onToggle }) => {
return (
<div className="TodoList">
{/* props로 받아온 todos 배열을 배열 내장 함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링함
map을 사용하여 컴포넌트로 변환할 때는 key props를 전달해주어야 함(여기서는 key값에 각 항목마다 가지고 있는 고유값인 id를 넣어 줌)
그리고 여러 종류의 값을 전달해줘야 하는 todo데이터의 경우 통째로 전달하는 편이 나중에 성능 최적화 시 편리함
=> TodoListItem으로 이동 */}
{todos.map((todo) => (
<TodoListItem
todo={todo}
key={todo.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
};
export default TodoList;
- 투두 체크하기 - src/components/TodoListItem.js
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames'; // 조건부 스타일링을 위해 classnames를 이용함
import './TodoListItem.scss';
// TodoList에서 todo 값을 넘겨줌
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;
읽어주셔서 감사합니다. 잘못된 정보는 댓글로 알려주세요!
728x90
반응형
'Study > React' 카테고리의 다른 글
12. 불변성 유지하기(immer) (0) | 2022.07.21 |
---|---|
11. 컴포넌트 성능 최적화 (0) | 2022.07.20 |
9. 컴포넌트 스타일링(CSS) (0) | 2022.07.18 |
8. Hooks (0) | 2022.07.16 |
7. Lifecycle method(라이프사이클 메서드) (0) | 2022.07.14 |