CRUD의 두 번째 요소인 Read 기능을 이용하면 배열에 저장한 여러 할 일 아이템을 반복해서 페이지에 렌더링 가능하다.
배열을 리스트로 렌더링하기
1. App컴포넌트의 state변수 todo에 저장된 할 일 아이템들을 Props로 TodoList 컴포넌트에 전달한다.
//App.js
(...)
function App() {
const [todo, setTodo] = useState(mockTodo);
(...)
return (
<div className="App">
<Header />
<ToDoEditor onCreate={onCreate}/>
<TodoList todo={todo} /> **
</div>
);
}
export default App;
2. TodoList 컴포넌트에서 todo를 리스트로 렌더링한다.
📌 리액트에서 배열 데이터를 렌더링하는 법
=> 배열 메서드 map을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링할 수 있다.
① map을 이용한 HTML 반복하기
//TodoList.js
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => { // props를 구조분해할당하기
return (
<div className="TodoList">
<h4>Todo List🌱</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
<div className="list_wrapper">
{todo.map((it)=> ( // map 메서드를 이용해 배열 todo를 순회하며 HTML로 변환
<div>{it.content}</div>
))}
</div>
</div>
)
};
export default TodoList;
② map을 이용 컴포넌트 반복하기
여기서는 현재 순회 중인 배열 요소 it의 모든 프로퍼티를 스프레드 연산자를 이용해 Props로 전달한다.
//TodoList.js
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} /> // map 메서드의 콜백 함수가 TodoItem 컴포넌트 반환
))}
</div>
</div>
)
};
export default TodoList;
3. TodoItem 컴포넌트에 전달된 Props를 사용하도록 수정한다.
//TodoItem.js
import "./TodoItem.css";
const TodoItem = ({id, content, isDone, createdDate }) => { //props 전달받음
return (
<div className="TodoItem">
<div className="Checkbox_col">
<input checked={isDone} type="checkbox" /> // 체크 여부를 isDone으로 설정
</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;
페이지에서는 '독서하기' 항목을 추가하였을 때 목 데이터의 할 일 아이템들과 새로 입력한 아이템을 잘 렌더링한 것을 볼 수 있다. 하지만 이때 개발자 도구 콘솔창에서 2가지 경고 메시지를 발견할 수 있다.
① 리스트의 모든 자식 요소는 key라는 고유한 props를 반드시 가져야 한다.
② TodoItem 컴포넌트가 체크박스 입력폼에 onChange 이벤트 핸들러를 설정하지 않았다.
②의 경고사항은 나중에 추가할 것이니 지금은 무시하고 ①의 경고를 먼저 해결하도록 하자.
📌key 설정하기
key는 리스트에서 각각의 컴포넌트를 구분하기 위해 사용하는 값이다. 리액트는 리스트에서 특정 컴포넌트를 수정, 추가, 삭제하는 경우 key를 사용하여 어떤 컴포넌트를 업데이트할지 결정한다.
앞에서 우리는 목데이터를 설정할 때 각 아이템마다 고유한 id를 갖도록 모델링했기 때문에 이것을 key로 설정하면 된다.
//TodoList.js
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 key={it.id} {...it} /> //key로 id를 전달함
))}
</div>
</div>
)
};
export default TodoList;
검색어에 따라 필터링하기
이번에는 TodoList 컴포넌트에 특정 할 일을 검색하는 기능을 만들어보겠다.
1. 검색기능 만들기
사용자가 입력하는 검색어를 처리할 State 변수를 만들고, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능을 만든다.
//TodoList.js
import { useState } from "react";
(...)
const TodoList = ({ todo }) => {
const [search, setSearch] = useState(""); //state변수 search 생성
const onChangeSearch = (e) => { //검색폼의 이벤트 핸들러 onChangeSearch 만듦
setSearch(e.target.value);
};
return (
<div className="TodoList">
(...)
<input
value={search} //value로 state변수 search설정
onChange={onChangeSearch} //검색폼의 onChange 이벤트 핸들러를 onChangeSearch로 만듦
className="searchbar"
placeholder="검색어를 입력하세요"
/>
(...)
</div>
)
};
export default TodoList;
2. 필터링 기능 만들기
다음으로 검색어에 따라 할 일 아이템을 필터링하는 기능을 만든다.
여기서 대소문자를 구별하지 않도록 toLowerCase() 메서드를 사용해 코드를 수정하면 사용자가 더 쉽게 검색가능하도록 구현할 수 있다.
//TodoList.js
(...)
const TodoList = ({ todo }) => {
(...)
const getSearchResult = () => {
return search === ""
? todo //검색어인 search가 빈문자열이면 todo그대로 반환
: todo.filter((it) => it.content.includes(search)); // 그렇지 않으면 search 내용과 일치하는 아이템만 필터링해 반환
// : todo.filter((it) => it.content.toLowerCase().includes(search.toLowerCase())); //대소물자 구별하지 않도록
}
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => ( //getSearchResult의 결과를 map을 이용하여 리스트로 렌더링
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
)
};
export default TodoList;
CRUD의 세 번째 기능은 Update로 [할 일 관리] 앱에서 수정 기능을 만들어보겠다.
기능 흐름 살펴보기
① 사용자가 TodoItem의 체크박스에 틱을 한다.
② TodoItem 컴포넌트는 함수 onUpdate를 호출하고 어떤 체크박스에서 틱이 발생했는지 해당 아이템의 id를 인수로 전달한다.
③ App 컴포넌트의 함수 onUpdate는 틱이 발생한 아이템의 상태를 업데이트한다.
④ App 컴포넌트의 State 값이 변경되면 TodoList에 전달하는 Props값도 변경된다.
⑤ TodoList는 변경된 State값을 다시 리스트에 렌더링한다.
아이템 수정 함수 만들기
수정을 위해 함수 onUpdate를 만들고 이 함수를 TodoItem 컴포넌트까지 전달해야 한다.
1. onUpdate 함수 생성 후 TodoList 컴포넌트에 Props로 전달하기
리액트 컴포넌트는 바로 한 단계 아래의 자식 컴포넌트에만 데이터를 전달할 수 있다. TodoList는 해당 함수를 사용하지 않지만 TodoItem 컴포넌트에 함수 onUpdate를 전달해야 하므로 일종의 매개 역할을 수행한다.
이것은 리액트에서 State와 Props를 사용할 때 흔히 발생하는 일이며, Props Drilling이라고 한다.
//App.js
(...)
function App() {
(...)
//수정함수 onUpdate만들기
const onUpdate = (TargetId) => { // 틱이 발생한 할일 아이템의 id를 저장함
setTodo( //todo값을 업데이트하기 위한 함수 setTodo 호출
todo.map(
(it) => {
if (it.id === TargetId){ //targetId와 일치한 할일 아이템이 있으면
return { //isDone 프로퍼티 값을 토글한 새 배열을 만들어 인수로 전달
...it,
isDone: !it.isDone,
};
} else {
return it;
}
}
)
/*
todo.map(
(it)=> //삼항연산자를 이용한 표현
it.id===TargetId ? { ...it, isDone: !it.isDone } : it
)
*/
)
}
return (
<div className="App">
<Header />
<ToDoEditor onCreate={onCreate}/>
<TodoList todo={todo} onUpdate={onUpdate} /> // TodoList컴포넌트에 Props로 함수 onUpdate 전달
</div>
);
}
export default App;
2. TodoList에서 TodoItem 컴포넌트에 함수 onUpdate 전달하기
//TodoList.js
(...)
const TodoList = ({ todo, onUpdate }) => { // onUpdate 추가
(...)
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} onUpdate={onUpdate} /> // TodoItem 컴포넌트에 함수 onUpdate를 props 전달
))}
</div>
</div>
)
};
export default TodoList;
TodoItem 컴포넌트에서 아이템 수정 함수 호출하기
TodoItem 컴포넌트에서 이벤트가 발생하면 함수 onUpdate를 호출하도록 코드를 수정하겠다.
//TodoItem.js
import "./TodoItem.css";
const TodoItem = ({id, content, isDone, createdDate, onUpdate }) => { // onUpdate추가
const onChangeCheckbox = () => { // 체크박스 틱했을때 호출할 함수 onChangeCheckbox 생성
onUpdate(id);
};
return (
<div className="TodoItem">
<div className="Checkbox_col">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" /> // onChange 이벤트 핸들러 설
</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;
마지막으로 CRUD의 Delete 기능을 구현하여 할 일 아이템을 삭제해 보겠다.
기능 흐름 살펴보기
삭제는 수정 기능과 유사한 흐름으로 진행이 된다.
① 사용자가 삭제하려는 할 일 아이템에서 삭제 버튼을 클릭한다.
② 할 일을 삭제하는 함수 onDelete를 호출한다. 이 함수는 App의 State 값을 업데이트하므로 미리 App 컴포넌트에서 Props로 전달해야 한다.
③ 삭제 버튼을 클릭하면 삭제할 아이템만 빼고, 새 배열을 만들어 State 값을 업데이트한다.
④ State 변수 todo가 업데이트되면, App이 TodoList 컴포넌트에 전달한 Props 값도 변경된다.
⑤ TodoList는 Props가 변경되면 리렌더된다. 이때 새로운 배열 todo로 할 일 리스트를 다시 렌더링한다.
아이템 삭제 함수 만들기
수정과 마찬가지로 삭제를 위해 함수 onDelete를 만들고 이 함수를 TodoItem 컴포넌트까지 전달해야 한다.
1. onDelete 함수 생성 후 TodoList 컴포넌트에 Props로 전달하기
//App.js
(..)
function App() {
(...)
const onDelete = (TargetId) => { // 삭제 버튼을 클릭했을 때 호출하는 함수 onDelete 생성
setTodo(todo.filter((it)=> it.id !== TargetId));
};
return (
<div className="App">
<Header />
<ToDoEditor onCreate={onCreate}/>
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} /> // 먼저 TodoList에 props로 전달
</div>
);
}
export default App;
2. TodoList에서 TodoItem 컴포넌트에 함수 onDelete 전달하기
//TodoList.js
const TodoList = ({ todo, onUpdate, onDelete }) => { // onDelete 추가
(...)
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it}
onUpdate={onUpdate}
onDelete={onDelete} /> // TodoItem에 props로 전달
))}
</div>
</div>
)
};
export default TodoList;
TodoItem 컴포넌트에서 삭제 함수 호출하기
TodoItem에서 삭제 버튼을 클릭하면 함수 onDelete를 호출하도록 구현한다.
여기까지 구현하면 삭제버튼을 클릭했을 때 페이지에서 바로 삭제되는 것을 확인할 수 있다.
//TodoItem.js
import "./TodoItem.css";
const TodoItem = ({id, content, isDone, createdDate, onUpdate, onDelete }) => { // onDelete 추가
(...)
const onClickDelete = () => { // 삭제 함수 onClickDelete 생성
onDelete(id); // 인수로 id 전달
};
return (
<div className="TodoItem">
(...)
<div className="btn_col">
<button onClick={onClickDelete}>삭제</button> // onClick 이벤트 핸들러로 onClickDelete 설정
</div>
</div>
)
}
export default TodoItem;
1. CRUD의 기능에는 ( Create ) , ( Read ) , ( Update ), ( Delete )가 있다.
2. 리엑트에서 배열 메서드 ( map )을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링할 수 있다.
3. 리엑트에서 리스트의 모든 자식 요소는 ( key )라는 고유한 props를 반드시 가져야 한다.
4. (O/X 문제) 리액트 컴포넌트는 바로 한 단계 아래의 자식 컴포넌트에만 데이터를 전달할 수 있다. ( O )
5. 한 단계 이상 떨어져 있는 자식 컴포넌트에 데이트를 전달하기 위해, 전달에 전달을 반복하는 현상을 ( Props Drilling )이라고 한다.
6. ( toLowerCase() ) 메서드는 문자열에 있는 대문자를 모두 소문자로 바꿔준다. 대소문자를 구별하고 싶지 않을 때 사용한다.
7. [할 일 관리] 앱에서 배열에 저장한 여러 할 일 아이템을 반복해서 페이지에 렌더링하는 것은 CRUD 중 ( Read ) 기능이다.
이번 프로그래밍 문제에서는 검색 필터링 기능을 구현해 보도록 할 것이다.
1. 먼저 사용자가 입력하는 검색어를 처리할 State 변수를 만들고, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능을 만들어라. (주석문을 코드로 작성해라)
//TodoList.js
import { useState } from "react";
(...)
const TodoList = ({ todo }) => {
//state변수 search 생성
//검색폼의 이벤트 핸들러 onChangeSearch 만듦
return (
<div className="TodoList">
(...)
<input
//value로 state변수 search설정
//검색폼의 onChange 이벤트 핸들러를 onChangeSearch로 만듦
className="searchbar"
placeholder="검색어를 입력하세요"
/>
(...)
</div>
)
};
export default TodoList;
2. 다음으로 검색어에 따라 할 일 아이템을 필터링하는 기능인 getSearchResult를 완성해라. 이때 대소문자를 구별하지 않도록 코드를 구현하여라. (주석문을 코드로 작성해라)
//TodoList.js
(...)
const TodoList = ({ todo }) => {
(...)
const getSearchResult = () => {
// 검색어인 search가 빈문자열이면 todo그대로 반환
// 그렇지 않으면 search 내용과 일치하는 아이템만 필터링해 반환 (대소물자 구별하지 않도록)
}
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => ( //getSearchResult의 결과를 map을 이용하여 리스트로 렌더링
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
)
};
export default TodoList;
정답
1.
//TodoList.js
import { useState } from "react";
(...)
const TodoList = ({ todo }) => {
const [search, setSearch] = useState(""); //state변수 search 생성
const onChangeSearch = (e) => { //검색폼의 이벤트 핸들러 onChangeSearch 만듦
setSearch(e.target.value);
};
return (
<div className="TodoList">
(...)
<input
value={search} //value로 state변수 search설정
onChange={onChangeSearch} //검색폼의 onChange 이벤트 핸들러를 onChangeSearch로 만듦
className="searchbar"
placeholder="검색어를 입력하세요"
/>
(...)
</div>
)
};
export default TodoList;
2.
//TodoList.js
(...)
const TodoList = ({ todo }) => {
(...)
const getSearchResult = () => {
return search === ""
? todo //검색어인 search가 빈문자열이면 todo그대로 반환
: todo.filter((it) => it.content.toLowerCase().includes(search.toLowerCase())); // 그렇지 않으면 search 내용과 일치하는 아이템만 필터링해 반환 (대소물자 구별하지 않도록)
}
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => ( //getSearchResult의 결과를 map을 이용하여 리스트로 렌더링
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
)
};
export default TodoList;
출처 : 이정환, 『한 입 크기로 잘라 먹는 리액트』, 인사이트, p326-342
Corner React.js 3
Editor: via
[리액터 스타터3] 9장 컴포넌트 트리에 데이터 공급하기 (1) | 2024.01.05 |
---|---|
[리액터 스타터3] 7장 useReducer와 상태 관리 / 8장 최적화 (0) | 2023.12.29 |
[리액터 스타터3] project 2. [할 일 관리] 앱 만들기 1 (1) | 2023.12.01 |
[리액트 스타터3] 8장. hooks (0) | 2023.11.24 |
[리액트 스타터3] project 1 [카운터] 앱 만들기 6장. 라이프 사이클과 리액트 개발자 도구 (0) | 2023.11.17 |