✨바로가기 https://soonmac.github.io/react-todolist/
✨깃허브 https://github.com/soonmac/react-todolist
<리액트를 다루는 기술> 10장 일정 관리 웹 애플리케이션 만들기를 보면서 만든 투두리스트 앱입니다.
책에서는 할 일 추가, 삭제, 체크 기능만 나와있는데 거기에 덧붙여 개린이르아나 님의 강의를 참고해 수정하기
기능도 구현해봤습니다.
투두리스트 앱을 제작하면서 배운 것들을 정리했습니다📚
투두리스트 앱의 컴포넌트 구조를 그림으로 그려봤습니다.
ToDoTemplate 안에 입력창, 할 일 목록들을 넣었습니다.
JSX 형식으로 표현하면 이런 느낌입니다.
<TodoTemplate> //앱을 이루는 컨테이너 박스
<ToDoInsert /> //할 일 입력창
<ToDoList> //할 일 목록(ul)
<ToDoListItem /> //할 일 (li)
</ToDoList>
<ToDoEdit /> //수정하기 창(팝업창이라서 대충 빼놓음)
</TodoTemplate>
위의 설계도에서 일정 항목에 필요한 것은 텍스트(내용), 체크박스, 삭제버튼, 수정버튼입니다.
여기서 수정버튼은 텍스트의 내용을 바꾸는 것이고, 삭제버튼은 해당 항목 자체를 없애버리는 것이니 데이터로 만들 때 필요한 것은 텍스트, 체크유무, 그리고 각 항목을 구분해줄 고유한 id입니다.
{
id: 1,
text: '리액트 기초 알아보기',
checked: true,
}
id, text, checked 세가지의 key를 가진 객체로 구성했습니다.
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링 하기',
checked: true,
},
{
id: 3,
text: '투두리스트 만들기',
checked: false,
},
]);
useState를 사용하여 todos의 기본값을 정했습니다.
나중에 setTodos를 이용해 todos 배열에 항목을 추가, 수정, 삭제할거임
TodoList 컴포넌트에 props로 todos를 전달합니다.
App.js
<TodoList
todos={todos}
/>
map() 메서드로 todos 배열의 각 항목들을 ToDoListItem 컴포넌트로 가공해줍니다.
ToDoListItem
에 체크유무, 텍스트, id를 props로 전달해줍니다.
TodoList.js
function TodoList({ todos }) {
return (
<ul className="TodoList">
{todos.map((todo) => (
<ToDoListItem
todo={todo}
key={todo.id}
/>
))}
</ul>
);
}
ToDoListItem.js
function ToDoListItem({ todo }) {
const { id, text, checked } = todo;
return (
<li className="TodoListItem">
<div className={cn('checkbox', { checked })}>
//checked=true일 때 checked라는 class를 추가
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
//checked=true면 체크된 박스 아이콘이 false면 빈 박스 아이콘이 뜸
<div className="text">{text}</div>
</div>
<div className="edit">
<MdModeEditOutline />
</div>
<div className="remove">
<MdRemoveCircleOutline />
</div>
</li>
);
}
🍧classnames(cn) 함수 : 인자에 들어간 값이 true면 class로 넣어주고 false면 그없
첫번째 인자 'checkbox'는 {checkbox:true}의 줄임말입니다.
체크유무에 상관없이 checkbox 클래스는 항상 필요하기 때문에 true 처리했습니다.
cn 예시)
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames('foo', { bar: false }); // => 'foo'
출처: https://www.npmjs.com/package/classnames
ToDoInsert.js
function ToDoInsert({onInsert}) {
const [value, setValue] = useState('');
const onChange = useCallback(e=>{
setValue(e.target.value);
},[])
const onSubmit = useCallback(
e => {
setValue(''); //value 초기화
//기본이벤트(새로고침) 방지
e.preventDefault();
}
,[value])
return (
<form className="TodoInsert" onSubmit={onSubmit}>
<input
onChange={onChange}
value={value} placeholder="할 일을 입력하세요" />
<button type="submit">
<MdAdd />
</button>
</form>
)
}
❓useCallback이 뭔가요 :
컴포넌트는 자신의 state 혹은 부모에게서 받은 props가 변경될 때마다 리렌더링됩니다.
근데 굳이 렌더링 안해도 괜찮은 부분까지 리렌더링 되면 코스트 낭비로 이어질 수 있고,
거기에 리렌더링 될 때마다 함수도 다시 생성되는 불상사가 일어납니다.
이를 방지하기 위해 전에 생성된 함수를 다시 재활용할 수 있도록 해주는 기능입니다.useCallback(생성하고 싶은 함수,[배열 안의 값이 바뀌었을 때 함수가 새로 생성됩니다.])
근데 솔직히 왜 쓰는지는 잘 모르겟음ㅎㅎ;
App.js
const nextId = useRef(4);
const onInsert = useCallback(
(text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo)); //concat(): 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열 반환
nextId.current++; //nextId 1씩 더하기
},
[todos],
);
🍧useRef: useRef(초기값) 여기서는 useRef를 로컬 변수로 활용했습니다.
❓로컬변수 : 렌더링과 상관없이 바뀔 수 있는 값
useRef의 current 속성은 인자로 넘어온 초기값을 current에 할당합니다.
useRef는 currnet 값이 바뀌어도 컴포넌트가 리렌더링 되지 않고, 컴포넌트가 리렌더링 되어도 current의 값을 잃지 않는다는 장점이 있습니다.
{
id: 1,
text: '리액트 기초 알아보기',
checked: true,
}
❓concat( )이 뭔데요:
인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환하는 메서드입니다.
예시)
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2); //=> Array ["a", "b", "c", "d", "e", "f"]
App.js
<ToDoInsert onInsert={onInsert} />
ToDoInsert.js
const onSubmit = useCallback(
e => {
onInsert(value);
setValue(''); //value 초기화
//기본이벤트(새로고침) 방지
e.preventDefault();
}
,[onInsert, value])
App.js
const onRemove = useCallback(
(id) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos],
);
🍧filter( ): https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
ToDoListItem.js
function ToDoListItem({ todo, onRemove }) {
const { id, text, checked } = todo;
return (
<li className="TodoListItem">
<div className={cn('checkbox', { checked:checked })}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="edit">
<MdModeEditOutline />
</div>
<div className="remove" onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</li>
);
}
ToDoList.js
function ToDoListItem({ todo, onRemove }) {
const { id, text, checked } = todo;
return (
<li className="TodoListItem">
<div className={cn('checkbox', { checked:checked })}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
.
.
.
</li>
);
}
삼항 연산자를 이용해 checked가 true 일 때 체크박스를 보여주고 checked가 false일 때 빈 박스를 보이는 형식인데요.
{checked ? ✅ : 🟩 }
checked가 true임 =? ✅
checked가 false임 => 🟩
문제는? 각 일정 항목을 클릭했을 때 해당 todo 객체 안에 있는 checked의 값을 바꿔야한다는 거죠...
아까 만든 삭제하기 기능과 거의 비슷합니다. id를 인자로 받아서 해당 id의 todo 객체의 checked의 값을 바꿔줍니다.
App.js
const onToggle = useCallback(
(id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo,
),
);
},
[todos],
);
ToDoInsert.js와 거의 비슷합니다.
ToDoEdit.js
function ToDoEdit({ selectedTodo, onUpdate }) {
const [value, setValue] = useState('');
const onChange = useCallback((e) => {
setValue(e.target.value);
}, []);
const onSubmit = useCallback(
(e) => {
setValue(''); //value 초기화
//기본이벤트(새로고침) 방지
e.preventDefault();
},
[value],
);
return (
<div className="background">
<form onSubmit={onSubmit} className="todoedit__insert">
<h2>수정하기</h2>
<input
onChange={onChange}
value={value}
placeholder="할 일을 입력하세요"
/>
<button type="submit">수정하기</button>
</form>
</div>
);
}
ToDoEdit.scss
.TodoInsert {
display: flex;
background: #495057;
input {
width: 100%;
background: none;
outline: none;
border: none;
padding: 0.5rem;
font-size: 1.125rem;
line-height: 1.5;
color: white;
&::placeholder {
color: #dee2e6;
}
}
button {
background: none;
outline: none;
border: none;
background: #868e96;
color: white;
padding: 0 1rem;
font-size: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
transition: 0.1s background ease-in;
&:hover {
background: #adb5bd;
}
}
}
먼저 insertToggle이라는 state를 만듭니다.
App.js
const [insertToggle, setInsertToggle] = useState(false); //플래그 역할을 해줄 state
.
.
return (
<TodoTemplate>
.
.
.
{insertToggle && (
<ToDoEdit />
)}
</TodoTemplate>
);
insertToggle의 값이 true면 todoEdit 팝업창을 부르고 false면 그없인데요.
이제 일정 항목을 클릭했을 때 insertToggle이 true로 바뀌는 함수를 작성합니다.
const [selectedTodo, setSelectedTodo] = useState(null);
const [insertToggle, setInsertToggle] = useState(false);
const onInsertToggle = () => {
if (selectedTodo) {
setSelectedTodo(null);
}
setInsertToggle((prev) => !prev);
};
const onChangeSelectedTodo = (todo) => {
setSelectedTodo(todo);
};
onChangeSelectedTodo
함수 : toDoListItem의 수정 아이콘에 onClick 이벤트로 넣어줄 함수입니다.onInsertToggle
함수 : toDoListItem의 '수정 아이콘'과 수정하기 팝업창의 '수정하기 버튼'에 onClick 이벤트로 넣어줄 함수입니다.함수들을 ToDoListItem.js와 ToDoEdit.js에 props로 전달해서 써주세용
ToDoListItem
return (
<li className="TodoListItem">
.
.
<div className="edit" onClick={() =>
{onChangeSelectedTodo(todo)
onInsertToggle();
}
}>
<MdModeEditOutline />
</div>
.
.
</li>
);
}
ToDoListItem
useEffect(() => {
if (selectedTodo) {
setValue(selectedTodo.text);
}
}, [selectedTodo]);
return (
<div className="background">
<form onSubmit={onSubmit} className="todoedit__insert">
<h2>수정하기</h2>
<input
onChange={onChange}
value={value}
placeholder="할 일을 입력하세요"
/>
<button type="submit">수정하기</button>
</form>
</div>
);
❓아니? 뜬끔없이 왜 useEffect를?
todoListItem 컴포넌트를 클릭했을 때 해당 todo객체의 text 내용이 input에 뜨도록 useEffect를 썼습니다.useEffect(2번째 인자에 해당하는 state가 변할 때만 실행될 코드, {해당 state})
todoEdit.js 수정하기 폼의 onSubmit 이벤트로 들어갈 함수입니다.
App.js
const onUpdate = (id, text) => {
onInsertToggle();
setTodos(todos.map((todo) => (todo.id === id ? { ...todo, text } : todo)));
};
ToDoEdit.js에 해당 함수를 props로 전달해서 써주세요^_^
ToDoEdit.js
const onSubmit = useCallback(
(e) => {
onUpdate(selectedTodo.id, value);
setValue(''); //value 초기화
//기본이벤트(새로고침) 방지
e.preventDefault();
},
[onUpdate, value],
//이거 React Hook useEffect has a missing dependency 오류 생김; 돌아가긴하는데..
);
만드는 것보다 블로그에 포스트 쓰는 시간이 훨씬 오래 걸리네요...😫
그래도 복습하면서 몰랐던 부분을 짚어볼 수 있어서 좋았습니다.😁
정보 공유해주시는 분들 항상 감사합니다~
<리액트를 다루는 기술> - 김민준(벨로퍼트), 길벗
classnames - npm
18. useCallback 을 사용하여 함수 재사용하기 - 벨로퍼트
useCallback을 사용해보자 - kysung95
React Hooks: useRef 사용법 - DaleSeo
Array.prototype.concat()- MDN
[React로 Todo App 만들기] 4. Update와 Delete 기능 개발 - 개린이르아나 채널