[React.js 2팀] project 2 [할 일 관리] 앱 만들기 2 (Read: 할 일 리스트 렌더링하기 ~ Delete: 할 일 삭제하기)
지난 시간까지는 TodoEditor 컴포넌트를 활용하여 할 일을 추가하는 단계까지 구현해 보았다. 지난번에 만들었던 컴포넌트를 이용하여 이번에는 할 일 리스트를 렌더링하고 수정하며, 마지막으로 삭제하는 단계까지 구현하여 할 일 관리 앱을 완성하여 본다.
먼저, TodoList 컴포넌트의 기능이면서 CRUD의 두 번째 요소인 Read 기능을 만들어보자. Read 기능을 이용하여 배열에 저장한 여러 할 일 아이템을 반복하여 렌더링할 수 있다.
[ 배열을 리스트로 렌더링 ]
App 컴포넌트의 State 변수 todo는 배열 형태로 여러 개의 할 일 아이템을 저장한다. 따라서 배열 todo를 TodoList 컴포넌트에 Props로 다음과 같이 전달한다.
(...)
function App() {
(...)
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} />
</div>
);
}
export default App;
src/App.js
TodoList 컴포넌트에서는 App에서 Props로 전달받은 todo를 리스트로 렌더링해야 하며, 이때 배열 메서드 map을 이용한다. 배열 메서드 map은 리액트에서 배열 데이터를 렌더링할 때 주로 이용하는 방법이다. map을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링할 수 있다.
[ map을 이용한 HTML 반복하기 ]
TodoList 컴포넌트에서 배열 메서드 map을 다음과 같이 이용할 수 있다.
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => { ①
return (
<div className="TodoList">
<h4>Todo List 💕</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
<div className="list_wrapper">
{todo.map((it) => ( ②
<div>{it.content}</div>
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
1. Props를 구조 분해 할당한다.
2. map 메서드를 이용하여 배열 todo의 모든 요소를 순차적으로 순회하며 HTML로 변환한다. 이 식의 결과값은 배열 todo에 저장된 모든 할 일을 <div> 태그로 감싼 것과 동일하다.
*map 메서드의 매개변수 it은 item을 줄여 쓴 것이다.
위와 같이 수정한 후 State 변수 todo를 초기화하기 위해 페이지를 새로고침하고 렌더링 결과를 확인해본다.
위 화면과 같이 map 메서드를 이용하여 todo에 저장되어 있는 3개의 할 일을 HTML로 반복해 페이지에 렌더링할 수 있다. 그런데 개발자 도구의 콘솔을 보면 "Each child in a list should have a unique "key" prop."란 경고문이 출력되었다. 이 메세지는 뒤에서 자세히 살펴보자.
[ map을 이용해 컴포넌트 반복하기 ]
이번에는 map 메서드의 콜백함수가 HTML이 아닌 컴포넌트를 반환하도록 해 보자.
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => {
return (
<div className="TodoList">
<h4>Todo List 💕</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
<div className="list_wrapper">
{todo.map((it) => (
<TodoItem {...it} />
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
위와 같이 map 메서드의 콜백 함수가 TodoItem 컴포넌트를 반환하도록 수정할 수 있다. 이때 TodoItem 컴포넌트에 현재 순회 중인 배열 요소 it의 모든 프로퍼티를 스프레드 연산자(...)를 이용해 props로 전달한다. 배열 todo에는 할 일 아이템 객체가 저장되어 있으므로 결과적으로 TodoItem 컴포넌트에는 이 객체 각각의 프로퍼티가 props로 전달된다.
이어서 TodoItem 컴포넌트에 전달된 props를 이 컴포넌트에서 사용할 수 있도록 다음과 같이 수정한다.
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createdDate }) => { ①
return (
<div className="TodoItem">
<div className="checkbox_col">
<input checked={isDone} type="checkbox" /> ②
</div>
<div className="title_col">{content}</div> ③
<div className="date_col">
{new Date(createdDate).toLocaleDateString()} ④
</div>
<div className="btn_col">
<button>삭제</button>
</div>
</div>
);
};
export default TodoItem;
src/component/TodoItem.js
1. props를 구조 분해 할당한다.
2. 체크박스 입력 폼의 체크 여부를 isDone으로 설정한다.
3. 할 일을 페이지에 표시하기 위해 content를 렌더링한다.
4. new Date로 새로운 객체를 만들고 생성자의 인수로 createDate를 전달해 타임 스탬프 값을 Date 형식으로 변환한다. 그 다음 toLocaleDateString 메서드를 사용하여 문자열로 변환해 렌더링한다.
다음은 할 일 입력 폼에 '독서하기'라는 새 아이템을 추가한 결과이다. 그런데 개발자 도구 콘솔을 보면 앞서 확인했던 것과 같은 경고메시지가 출력되는 것을 볼 수 있다.
위 경고 메시지를 직역하면 "리스트의 모든 자식 요소는 key라는 고유한 prop을 반드시 가져야 한다"라고 해석할 수 있다.
이 경고 메시지는 TodoItem 컴포넌트가 체크박스 입력 폼에 onChange 이벤트 핸들러를 설정하지 않아서 발생한 경고이다. 나중에 체크박스에 onChange 이벤트 핸들러를 설정할 예정이므로 지금은 무시해도 된다.
[ key 설정하기 ]
key는 리스트에서 컴포넌트를 구분하기 위해 사용하는 값으로, 리액트는 리스트에서 특정 컴포넌트를 수정, 추가, 삭제하는 경우 key로 어떤 컴포넌트를 업데이트할지 결정한다. 앞서 App 컴포넌트의 할 일 아이템 생성 과정에서 Ref 객체를 이용해 아이템마다 고유 id를 갖도록 만들었는데, id를 key로 전달하면 문제를 간단히 해결할 수 있다.
이를 위해 TodoList 컴포넌트를 다음과 같이 수정한다.
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => {
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{todo.map((it) => (
<TodoItem key={it.id} {...it} /> ①
b ))}
</div>
</div>
);
};
export default TodoList;
코드 수정 후 개발자 도구의 콘솔 탭을 다시 확인해 보면 key와 관련된 오류는 더 이상 발생하지 않는다.
즉, map을 이용해 컴포넌트를 리스트 형태로 반복적으로 렌더링하려면 반드시 리스트 내의 고유한 key를 props로 전달해야 한다.
[ 검색 기능 만들기 ]
이번에는 TodoList 컴포넌트에서 특정 할 일을 검색하는 기능을 만들어보자.
먼저 사용자가 입력하는 검색어를 처리할 State 변수를 만든 다음, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능을 만든다. 이를 위해 TodoList.js를 다음과 같이 수정한다.
import { useState } from "react"; // ①
(...)
const TodoList = ({ todo }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => { // ②
setSearch(e.target.value);
};
return (
<div className="TodoList">
<h4>Todo List 💕</h4>
<input
value={search} // ③
onChange={onChangeSearch} // ④
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{todo.map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
1. react 라이브러리에서 useState 리액트 훅을 불러온다.
2. 검색 폼의 onChange 이벤트 핸들러 onChangeSearch를 만든다.
3. 검색 폼의 value로 State 변수 search를 설정한다.
4. 검색 폼의 onChange 이벤트 핸들러를 onChangeSearch로 설정한다.
이어서 사용자가 입력한 검색어에 따라 할 일 아이템을 필터링하는 기능을 만든다.
(...)
const TodoList = ({ todo }) => {
(...)
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) => it.content.includes(search));
};
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => ( ②
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
1. 함수 getSearchResult는 현재 입력한 검색어인 serach가 빈 문자열이면 todo를 그대로 반환, 그렇지 않으면 todo 배열에서 search의 내용과 일치하는 아이템만 필터링해 반환한다.
2. 함수 getSearchResult의 결과값을 map 메서드를 사용해 리스트로 렌더링한다.
코드를 수정하였으면 검색어로 'React'를 입력하여 결과를 확인해보자.
다만 한 가지 아쉬운 점은 검색어에서 대소 문자를 구별하므로 'react'로 검색하면 검색 결과가 나타나지 않는다.
[ 대소 문자를 구별하지 않게 하기 ]
이번에는 검색에서 대소 문자를 구별하지 않도록 기능을 업그레이드 해보자.
(...)
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase()) ①
);
};
(...)
src/component/TodoList.js
toLowerCase() 메서드는 문자열에 있는 대문자를 모두 소문자로 변환한다. 따라서 toLowerCase 메서드를 이용해 검색어와 todo 아이템의 content를 모두 소문자로 바꾸면 대소 문자를 구별하지 않고 검색할 수 있다.
이번에는 CRUD의 세 번째 기능인 Update를 구현해보자.
[ 기능 흐름 살펴보기 ]
할 일 관리 앱에서 할 일 아이템의 수정은 다음 그림과 같이 진행된다. 사용자가 TodoItem의 체크박스에 틱(Tik, 체크 표시)하면 할 일 아이템이 미완료에서 완료, 완료에서 미완료로 바뀌는 토글 기능이 동작하며, 이를 위해 위와 같은 일련의 과정이 필요하다.
1. 사용자가 TodoItem의 체크박스에 틱(체크 표시)한다.
2. TodoItem 컴포넌트는 함수 onUpdate를 호출하고 어떤 체크박스에 틱이 발생했는지 해당 아이템의 id를 인수로 전달한다. (그 전에 함수 onUpdate를 App 컴포넌트에서 Props로 TodoItem에 전달해야 한다.)
3. App 컴포넌트의 함수 onUpdate는 틱이 발생한 아이템의 상태(완료 / 미완료)를 토글하기 위해 State 값을 업데이트 한다.
4. App 컴포넌트의 State 값이 변경되면 TodoList에 전달하는 Props의 값 또한 변경된다.
5. TodoList는 변경된 State 값을 다시 리스트로 렌더링하고, 결과적으로 수정 사항이 반영된다.
[ 아이템 수정 함수 만들기 ]
할 일 수정을 위해 함수 onUpdate를 만들어보자. 이 함수는 TodoItem 컴포넌트에 전달되어야 한다.
(...)
function App() {
(...)
const onUpdate = (targetId) => { ①
setTodo(
todo.map((it) => { ②
if (it.id === targetId) {
return {
...it,
isDone: !it.isDone,
};
} else {
return it;
}
})
);
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} /> ③
</div>
);
}
export default App;
src/App.js
1. 함수 onUpdate는 TodoItem 체크박스에 틱이 발생했을 때 호출하는 함수이다. 이때 어떤 아이템에 틱이 발생했는지 알아야 하므로, 매개변수 targetId로 틱이 발생한 아이템의 id를 저장한다.
2. todo 값을 업데이트하기 위해 함수 setTodo를 호출한다. 이때 앞에서와 같이 map 메서드를 이용해 배열 todo에서 id가 targetId와 일치하는 요소를 찾으면 isDone 프로퍼티 값을 토글한 새 배열을 생성해 인수로 전달하게 된다.
3. TodoList 컴포넌트에 Props로 함수 onUpdate를 전달한다.
위 코드를 삼항 연산자를 이용하여 간결하게 작성해보면 다음과 같다.
(...)
const onUpdate = (targetId) => {
setTodo(
todo.map((it) =>
it.id === targetId ? { ...it, isDone: !it.isDone } : it
)
);
};
(...)
src/App.js
이번에는 TodoList에서 TodoItem 컴포넌트에 함수 onUpdate를 전달해보자.
(...)
const TodoList = ({ todo, onUpdate }) => { ①
(...)
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} onUpdate={onUpdate} /> ②
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
1. Props를 구조분해 할당한다.
2. TodoItem 컴포넌트에 함수 onUpdate를 Props로 전달한다.
리액트 컴포넌트는 바로 한 단계 아래의 자식 컴포넌트에만 데이터를 전달할 수 있으므로 한 단계 이상 떨어져 있는 자식 컴포넌트로 데이터를 전달하려면 전달을 반복해야 한다. 따라서 TodoList의 자식은 해당 함수를 사용하지 않더라도 TodoItem 컴포넌트에 함수 onUpdate를 전달해야 하므로, Props로 받아 다시 전달하는 매개 역할을 수행한다.
이것은 리액트에서 State와 Props를 사용할 때 흔히 발생하는 일로, 이런 상황을 'Props Drilling' 이라고 한다.
[ TodoItem 컴포넌트에서 아이템 수정 함수 호출하기 ]
이제 TodoItem 컴포넌트에서 틱 이벤트가 발생하면 함수 onUpdate를 호출해보자. 이를 위해 TodoItem.js를 다음과 같이 수정한다.
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createdDate, onUpdate }) => { ①
const onChangeCheckbox = () => { ②
onUpdate(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input onChange={onChangeCheckbox} // ③
checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button>삭제</button>
</div>
</div>
);
};
export default TodoItem;
src/component/TodoItem.js
1. Props를 구조 분해 할당하고 함수 onUpdate를 추가한다.
2. 체크박스를 틱 했을 때 호출한 함수 onChangeCheckbox를 만든다. 이 함수는 onUpdate를 호출하며 인수로 현재 틱이 발생한 할 일 아이템의 id를 전달한다.
3. 체크박스의 입력 폼의 onChange 이벤트 핸들러를 함수 onChangeCheckbox로 설정한다.
이제 코드가 잘 작성되었는지 결과를 확인해보자.
위와 같이 'React 공부하기'를 틱했을 때 Components 탭을 열어보면 TodoItem 컴포넌트에서 이 아이템의 isDone 프로퍼티가 true로 변경된 것을 확일할 수 있다.
마지막으로 CRUD의 Delete 기능을 구현하여 할 일 아이템을 삭제해보자.
[ 기능 흐름 살펴보기 ]
할 일 관리 앱에서 삭제 기능은 위와 같은 흐름으로 구현된다. 앞에서 다루었던 수정 기능과 유사한 흐름으로 진행된다.
1. 삭제하려는 할 일 아이템의 <삭제> 버튼을 클릭한다.
2. 할 일을 삭제하는 함수 onDelete를 호출한다. (App의 State 값을 업데이트하므로 미리 App 컴포넌트에서 Props로 전달해야 한다.)
3. <삭제> 버튼을 클릭하면 삭제할 아이템만 제외한 새 배열을 생성해 State 값을 업데이트 한다.
4. State 변수 todo가 업데이트되면 App이 TodoList 컴포넌트에 전달한 Props의 값도 변경된다.
5. TodoList 컴포넌트는 Props의 값이 변경되면 리렌더되며, 이 때 새 배열 todo로 할 일 리스트를 다시 렌더링한다.
[ 아이템 삭제 함수 만들기 ]
이제 App 컴포넌트에서 할 일을 삭제하는 함수 onDelete를 만들어보자.
(...)
function App() {
(...)
const onDelete = (targetId) => { ①
setTodo(todo.filter((it) => it.id !== targetId));
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} /> ②
</div>
);
}
export default App;
src/App.js
1. TodoItem의 <삭제> 버튼을 클릭했을 때 호출되는 함수 onDelete는 매개변수 targetId에 삭제할 아이템의 id를 저장한다. 그리고 해당 id 요소를 제외한 새 배열로 todo를 업테이트하여 해당 아이템을 삭제한다.
2. 함수 onDelete는 TodoItem에서 <삭제> 버튼을 클릭할 때 호출하므로 먼저 TodoList에 Props로 전달해야 한다.
Props로 받은 함수 onDelete를 다시 TodoItem 컴포넌트에 전달하려면 다음과 같이 TodoList.js를 수정한다.
(...)
const TodoList = ({ todo, onUpdate, onDelete }) => { ①
(...)
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem
key={it.id}
{...it}
onUpdate={onUpdate}
onDelete={onDelete} ②
/>
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
1, Props를 구조분해 할당하고 함수 onDelete를 추가한다.
2. 함수 onDelete를 리스트의 모든 TodoItem에 Props로 전달한다.
[ TodoItem 컴포넌트에서 삭제 함수 호출하기 ]
이번에는 TodoItem에서 <삭제> 버튼을 클릭하면 함수 onDelete를 호출하도록 구현해보자.
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => { ①
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDelete = () => { ②
onDelete(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button onClick={onClickDelete}>삭제</button> ③
</div>
</div>
);
};
export default TodoItem;
src/component/TodoItem.js
1. Props를 구조분해 할당하고 함수 onDelete를 추가한다.
2. <삭제> 버튼을 클릭하면 호출할 함수 onClickDelete를 만든다. 이 함수는 함수 onDelete를 호출하며 인수로 해당 아이템의 id를 전달한다.
3. <삭제> 버튼의 onClick 이벤트 핸들러로 함수 onClickDelete를 설정한다.
코드 수정 후 실행 결과를 확인해보면, 'React 공부하기'의 삭제 버튼을 누르면 Todo List에서 삭제된 것을 페이지와 component 탭에서 확인할 수 있다.
여기까지 할 일 관리 앱을 모두 완성하였다. 그러나 앞에서 잠깐 언급하였던 Props Drilling, 최적화 문제, 분리되지 않은 상태 관리 등 리액트 서비스와 관련해 알아야 할 내용들이 더 있다. 이 개념들은 다음 과정에서 더 자세히 다룰 것이다.
**코드 작성 문제**
(...)
function App() {
(...)
const onDelete = (________) => {
setTodo(todo.filter((it) => it.id !== targetId));
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} (__________) />
</div>
);
}
export default App;
답) 1. Read, Update, Delete 2. map 3. key 4. key 5. 틱(Tik) 6. 틱, true 7. onClickDelete
코드 작성 문제)
1.
{todo.map((it) => (
<TodoItem {...it} /> ))}
2.
(...)
function App() {
(...)
const onDelete = (targetId) => {
setTodo(todo.filter((it) => it.id !== targetId));
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
출처: 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023), https://reactjs.winterlood.com/.
Corner React.js 2
Editor: 엔초_
[React.js 2팀] project 2 [할 일 관리] 앱 만들기 1 (프로젝트 준비하기 ~ Create: 할 일 추가하기) (1) | 2024.12.27 |
---|---|
[React.js 2팀] 8장. Hooks (0) | 2024.11.29 |
[React.js 2팀] project 1 [카운터] 앱 만들기 ~ 6장. 라이프 사이클과 리액트 개발자 도구 (0) | 2024.11.22 |
[React.js 2팀] 5장. 리액트의 기본 기능 다루기 (2) (1) | 2024.11.15 |
[React.js 2팀] 5장. 리액트의 기본 기능 다루기 (1) (0) | 2024.11.08 |