[React] 23. TODO LIST 앱 / Read - 투두리스트 렌더링

서회정's avatar
Mar 06, 2026
[React] 23. TODO LIST 앱 / Read - 투두리스트 렌더링

 
이번 단계에서는 todos에 담긴 상태를
List컴포넌트에서 렌더링 될 수 있도록 해보자.
 
import "./App.css"; import { useState, useRef } from "react"; import Header from "./components/Header"; import Editor from "./components/Editor"; import List from "./components/List"; const mockData = [ { id: 0, isComplete: false, content: "React 공부하기", date: new Date().getTime(), }, { id: 1, isComplete: false, content: "빨래하기", date: new Date().getTime(), }, { id: 2, isComplete: false, content: "노래 연습하기", date: new Date().getTime(), }, ]; function App() { const [todos, setTodos] = useState(mockData); const isRef = useRef(3); const onCreate = (content) => { const newTodo = { id: isRef.current++, isComplete: false, content: content, date: new Date().getTime(), }; setTodos([newTodo, ...todos]); }; return ( <div className="App"> <Header /> <Editor onCreate={onCreate} /> <List /> </div> ); } export default App;
 

1. todos 상태 렌더링하기

 

1. props 전달

 
먼저 todosList컴포넌트에 props로 전달해주자.
 
<div className="App"> <Header /> <Editor onCreate={onCreate} /> <List todos={todos} /> </div>;
 
다음으로 List컴포넌트에서 구조분해할당 문법으로
todos props를 받아주자.
 
import "./List.css"; import TodoItem from "./TodoItem"; const List = ({ todos }) => { return ( <div className="List"> <h3>Todo List ☘️</h3> <input type="search" placeholder="검색어를 입력하세요" /> <div> <TodoItem /> <TodoItem /> <TodoItem /> </div> </div> ); }; export default List;
 
리액트 개발자 도구에서 다음과 같이
List컴포넌트에 props로 존재하는 todos를 확인할 수 있다.
 
notion image
 

2. map 메서드로 리스트 반환

 
다음으로, TodoItem 리스트가 있던 차리에 todosmap메서드를 활용하여
리스트 형태로 뿌려줄 수 있다.
 
map 메서드는 콜백 함수를 인수로 넣어 모든 요소에 대한 콜백 함수를 실행한 뒤
이 콜백 함수가 리턴한 값을 모아 리스트로 반환한다.
 
이해를 위해 일단 콜백 함수의 인수로 todo를 넣고
이를 단순히 todo라는 문자열을 반환하게 해보자.
 
<div> {todos.map((todo) => { return <div>todo</div>; })} </div>;
 
현재 props로 전달된 리스트가 총 세 개의 항목을 가지고 있고
이를 세 번 실행하여 세 번의 div를 반환하게 된다.
 
notion image
 
이런 형태로 content 키를 꺼내주면 화면에도 출력된다.
 
<div> {todos.map((todo) => { return <div>{todo.content}</div>; })} </div>;
notion image
 
또한, 이런 문법으로 컴퍼넌트를 리턴할 수도 있는데
중괄호로 props를 해당하는 컴퍼넌트에 전달할 수 있다.
 
<div> {todos.map((todo) => { return <TodoItem {...todo} />; })} </div>;
 
전달된 props는 각각의 키를 잘 가지고 있다.
 
 
notion image
 

3. props 화면에 렌더링

 
다음으로 TodoItem에서 구조분해할당으로 각각의 props를 받자.
 
import "./TodoItem.css"; const TodoItem = ({ id, isCpmplete, content, date }) => { return ( <div className="TodoItem"> <input type="checkbox" /> <p className="content">할 일 1</p> <p className="date">Date</p> <button>삭제</button> </div> ); }; export default TodoItem;
 
다음으로 props로 전달받은 값을 화면에 렌더링되도록
각각의 태그에 넣어주자.
 
date는 그냥 넣지 않고, 새로운 Date객체를 만들어서 인수로 넣고
toLocaleDateString()메서드를 호출해주자.
 
import "./TodoItem.css"; const TodoItem = ({ id, isComplete, content, date }) => { return ( <div className="TodoItem"> <input checked={isComplete} type="checkbox" /> <p className="content">{content}</p> <p className="date">{new Date(date).toLocaleDateString()}</p> <button>삭제</button> </div> ); }; export default TodoItem;
 
원하는 정보가 알맞게 렌더링 되고 있다.
 
notion image
 

⚠️ 오류 수정

 
하지만 콘솔 탭에서는 몇 가지 오류가 발생한 것을 확인할 수 있다.
해석해보자면 input에서 onChange에 이벤트 핸들러 없이 checked속성에
값을 받아서 오류가 났다는 것이다.
 
notion image
 
일단 이번 단계에서는 해당하는 이벤트 핸들러를
만들지 않을 예정이니 input의 속성을 readOnly로 변경하자.
 
<input readOnly checked={isComplete} type="checkbox" />
 
다음 오류도 살펴보자.
해석해보자면 각각의 리스트의 자식은 고유의 키 값이 필요하다는 뜻이다.
 
notion image
 
리액트에서는 리스트 형태로 렌더링된 컴퍼넌트를 구분하기 위해
내부적으로 각각의 요소를 key라는 prop을 사용한다.
 
따라서 다음과 같이 TodoItemkey 속성에
todo.id가 들어갈 수 있도록 추가해주자.
 
<div> {todos.map((todo) => { return <TodoItem key={todo.id} {...todo} />; })} </div>;
 
오류 메세지도 사라지고, 리액트 개발자 도구에서
각각의 컴포넌트에 부여된 key값이 들어가 있는 걸 확인할 수 있다.
 
notion image
 

2. 검색 기능 추가

 
검색 기능을 추가해보자.
빨래라고 입력을 하면 빨래하기만 리스트에 렌더링 되어야한다.
 
notion image
 

1. 검색어 상태 저장

 
따라서 검색창에 사용자가 입력하는 값을 스테이트로 관리해야한다.
 
새로운 스테이트를 만들고 inputvalue값의 변경에 따라
상태가 변경될 수 있도록 이벤트 핸들러를 만들어주자.
 
const [search, setSearch] = useState(""); const onChangeSearch = (e) => { setSearch(e.target.value); };
 
valuesearch를 넣고 onChange속성에도 만들어둔
이벤트 핸들러를 넣어주자.
 
<input value={search} onChange={onChangeSearch} type="search" placeholder="검색어를 입력하세요" />;
 
상태가 잘 저장된다.
 
notion image
 

2. 필터링 기능 추가

 
필터링을 하는 함수를 만들자.
함수 내부에서 조건문을 통해 search가 빈 문자열일 때
그냥 todos라는 리스트를 반환하고, 그렇지 않은 경우라면
 
todos에 filter메서드를 호출하여 콜백 함수로 todo
contentsearch에 저장된 값이 포함되어 있는지 확인하여
그 값이 참인 것만 반환하게 될 것이다.
 
const getFilteredData = () => { if (search === "") { return todos; } return todos.filter((todo) => todo.content.includes(search)); };
 
그런 다음 그 결과값을 filteredTodos라는 변수에 저장해준다.
 
const filteredTodos = getFilteredData();
 
마지막으로 todos가 아닌 filteredTodosmap메서드로
렌더링 될 수 있도록 수정하자.
 
<div> {filteredTodos.map((todo) => { return <TodoItem key={todo.id} {...todo} />; })} </div>;
 

⚠️ 대소문자를 구분하지 않고 search 기능 만들기

 
대소문자를 구분하지 않고 검색할 수 있게 기능을 수정해보자.
 
필터링이 진행되는 로직에서 비교할 문자 둘을 모두 소문자로
변환해서 비교할 수 있도록 모든 문자를 소문자로 변환시키는
.toLowerCase() 메서드를 추가해보자.
 
const getFilteredData = () => { if (search === "") { return todos; } return todos.filter((todo) => todo.content.toLowerCase().includes(search.toLowerCase()) ); };
 
대소문자를 구분하지 않고 검색이 잘 된다.
 
notion image
Share article

clubnerdy