[할 일 관리] 앱 만들기

로이·2024년 4월 24일

프로젝트 준비하기

요구사항 분석하기

[할 일 관리] 앱의 UI 요소를 컴포넌트 단위로 분리

- Header: 오늘의 날짜를 표시 형식에 맞게 보여줌
- TodoEditor: 새로운 할 일 아이템을 등록
- TodoList: 검색어에 맞게 필터링된 할 일 리스트를 렌더링
- TodoItem: 낱낱의 할 일 아이템에는 기본 정보 외에도 체크박스와 삭제 버튼이 있으며, 체크박스 토글과 아이템 삭제 기능 있음

리액트 앱 만들기

> npx create-react-app .

UI 구현하기

페이지 레이아웃 만들기

// App.js

import "./App.js"

function App() {
	return (
    	<div className="App">
        	<Header />
        	<TodoEditor />
        	<TodoList />
        </div>
    );
}
export default App;

Header 컴포넌트 만들기

// component/Header.js

import "./Header.css";

const Header = () => {
	return (
    	<div className="Header">
        	<h3>오늘은 📅</h3>
            <h1>{new Date().toDateString()}</h1>
        </div>
    );
}
export default Header;

TodoEditor 컴포넌트 만들기

// component/TodoEditor.js

import "./TodoEditor.css"

const TodoEditor = () => {
	return (
    	<div className="TodoEditor">
        	<h4>새로운 Todo 작성하기 ✏️</h4>
            <div className="editor_wrapper">
            	<input placeholder="새로운 Todo..." />
                <button>추가</button>
            </div>
        </div>
    );
}
export default TodoEditor;

TodoList, TodoItem 컴포넌트 만들기

TodoList 컴포넌트 만들기

// component/TodoList.js

import "./TodoList.css";

const TodoLsit = () => {
	return (
    	<div className="TodoList">
        	<h4>todo List 🌱</h4>
            <input className="searchbar" placeholder="검색어를 입력하세요" />
        </div>
    );
}
export default TodoList;

TodoItem 컴포넌트 만들기

// component/TodoItem.js

import "./TodoItem.css";

const TodoItem = () => {
	return (
    	<div className="TodoItem">
        	<div className="check_col">
            	<input type="checkbox" />
            </div>
            <div className="title_col">할 일</div>
            <div className="date_col">{new Date().toLocaleDateString()}</div>
            <div className="btn_col">
            	<button>삭제</button>
            </div>

        </div>
    );
}
export default TodoItem;

TodoList에 TodoItem 컴포넌트 배치하기

// component/TodoList.js

import "./TodoList.css";

const TodoLsit = () => {
	return (
    	<div className="TodoList">
        	<h4>todo List 🌱</h4>
            <input className="searchbar" placeholder="검색어를 입력하세요" />
            <div className="list_wrapper">
            	<TodoItem />
            	<TodoItem />
            	<TodoItem />
            </div>
        </div>
    );
}
export default TodoList;

기능 구현 준비하기

[할 일 관리] 앱의 기능을 컴포넌트 단위로 분리

- App 컴포넌트: 할 일 데이터 관리하기
- Header 컴포넌트: 오늘의 날짜 표시
- TodoEditor 컴포넌트: 새로운 할 일 아이템 생성
- TodoList 컴포넌트: 검색에 따라 필터링된 할 일 아이템 렌더링
- TodoItem 컴포넌트: 할 일 아이템의 수정 및 삭제

데이터를 다루는 4개의 기본 기능, 즉 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞 글자만 따서 CRUD라고 함

- Create: 할 일 아이템 생성
- Read: 할 일 아이템 렌더링
- Update: 할 일 아이템 수정
- Delete: 할 일 아이템 삭제

기초 데이터 설정하기

State 변수 todo는 [할 일 관리] 앱에서 데이터를 저장하는 배열이면서 동시에 일종의 데이터베이스 역할을 수행함
// App.js

import { useState } from "react";
(...)

function App() {
	const [todo, setTodo] = useState([]);
    return (
    	(...)
    );
}
export default App;

데이터 모델링하기

현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현하는 행위를 '데이터 모델링'이라고 함
{
	id: 0,
    isDone: false,
    content: "React Study",
    createdDate: new Date().getTime(),
}

목 데이터 설정하기

목(Mock) 데이터란 모조품 데이터라는 뜻으로, 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용하는 데이터
// App.js

import { useState } from "react";
(...)

const mockTodo = [
	{
		id: 0,
    	isDone: false,
    	content: "React Study",
    	createdDate: new Date().getTime(),
	},
    {
		id: 1,
    	isDone: false,
    	content: "Laundry",
    	createdDate: new Date().getTime(),
	},
    {
		id: 2,
    	isDone: false,
    	content: "Sing a Song",
    	createdDate: new Date().getTime(),
	},
];

function App() {
	const [todo, setTodo] = useState(mockTodo);
    return (
    	(...)
    );
}
export default App;

Create: 할 일 추가하기

기능 흐름 살펴보기

1. 사용자가 새로운 할 일 입력
2. TodoEditor 컴포넌트에 있는 <추가> 버튼 클릭
3. TodoEditor 컴포넌트는 부모인 App에게 아이템 추가 이벤트가 발생했음을 알리고 사용자가 추가한 할 일 데이터를 전달
4. App 컴포넌트는 TodoEditor 컴포넌트에서 받은 데이터를 이용해 새 아이템이 추가된 배열을 만들고 State 변수 todo 값을 업데이트 함
5. TodoEditor 컴포넌트는 자연스러운 사용자 경험을 위해 할 일 입력 폼을 초기화 함

아이템 추가 함수 만들기

App 컴포넌트에서 새 할 일 아이템을 추가하는 함수 onCreate를 만들되, idRef를 이용해 아이템을 생성할 때마다 id가 1씩 늘어나도록 하고, 사용자가 TodoEditor 컴포넌트에서 <추가> 버튼을 클릭해야 호출되기 때문에 TodoEditor 컴포넌트에 onCreate 함수를 Props로 전달해야 함
// App.js

(...)

function App() {
	const [todo, setTodo] = useState(mockTodo);
    
    const idRef = useRef(3);
    const onCreate = (content) => {
    	const newItem = {
        	id: idRef.current,
            content,
            isDone: false,
            createdDate: new Date().getTime(),
        };
        setTodo([newItem, ...todo]);
    	idRef += 1;
    };
    
    return (
    	<div className="App">
        	<Header />
            <TodoEditor onCreate={onCreate} />
            <TodoList />
        </div>
    );
}
export default App;
! TodoEditor에서 Props를 받을 때는 Props 객체를 구조분해 할당 함

아이템 추가 함수 호출하기

TodoEditor 컴포넌트의 할 일 입력 폼에서 사용자가 입력하는 새 할 일 데이터를 저장할 State와 onChangeContent 함수를 만들고, <추가> 버튼을 클릭하면 함수 onCreate를 호출하는 버튼 클릭 이벤트 핸들러 함수 onSubmit을 만듦
// component/TodoEditor.js

(...)

const TodoEditor = ({ onCreate }) => {
	const [contet, setContent] = useState("");
    const onChangeContent = (e) => {
    	setContent(e.target.value);
    };
    const onSubmit = () => {
    	onCreate(content);
    };
    
    return (
    	<div className="TodoEditor">
        	<div className="editor_wrapper">
              <input value={content} onChange={onChangeContent} placeholder="new Todo..." />
              <button onClick={onSubmit}>추가</button>
        	</div>
        </div>
    );
};
export default TodoEditor;

Create 완성도 높이기

빈 입력 방지하기

아무것도 입력하지 않은 상태로 아이템을 추가하는 것을 방지하기 위한 방법으로, 함수 onSubmit에서 content 값이 비어 있으면 입력 폼에 포커스를 구현함
// component/TodoEditor.js

(...)

const TodoEditor = ({ onCreate }) => {
	(...)
    const inputRef = useRef();
    const onSubmit = () => {
        if (!content) {
            inputRef.current.focus();
            return;
        }
        onCreate(content);
    };

    (...)
};
export default TodoEditor;

아이템 추가 후 입력 폼 초기화하기

아이템을 추가하면 자동으로 할 일 입력 폼을 초기화하는 기능 구현
// component/TodoEditor.js

(...)

const TodoEditor = ({ onCreate }) => {
	(...)
    const inputRef = useRef();
    const onSubmit = () => {
        (...)
        setContent("");
    };

    (...)
};
export default TodoEditor;

Enter 키를 눌러 아이템 추가하기

Enter 키를 누르면, <추가> 버튼을 클릭핳ㄴ 것과 동일한 동작을 수행하는 키 입력 이벤트 구현
// component/TodoEditor.js

(...)

const TodoEditor = ({ onCreate }) => {
	(...)
    
    const onKeyDown = (e) => {
        if (e.keyCode === 13) {
            onSubmit();
        }
    };

    return (
    	<div className="TodoEditor">
        	<div className="editor_wrapper">
            	<input value={content} onChange={onChangeContent} onKeyDown={onKeyDown} placeholder="new Todo..." />
                <button onClick={onSubmit}>추가</button>
            </div>
        </div>
    );

    (...)
};
export default TodoEditor;

Read: 할 일 리스트 렌더링하기

배열을 리스트로 렌더링하기

App 컴포넌트에서는 todo를 TodoList 컴포넌트에 Props로 전달하고, TodoList 컴포넌트에서는 Props로 전달된 todo를 리스트로 렌더링 해야 함
// App.js

(...)
function App() {
	const [todo, setTodo] = setState(mockTodo);
    (...)
    return (
    	(...)
        <TodoList todo={todo} />
    );
}
export default App;

map을 이용한 HTML 반복하기

배열 메서드 map을 이용해 HTML 요소를 반복해서 렌더링 함
// component/TodoList.js

(...)
const TodoList = ({ todo }) => {
	return (
    	(...)
        <div className="list_wrapper">
        	{todo.map((it) => (
            	<div>{it.content}</div>
            ))}
        </div>
    );
};
export default TodoList;
(it) => {} 가 아니라 왜 (it) => () 인거지?

map을 이용해 컴포넌트 반복하기

map 메서드의 콜백 함수가 HTML이 아닌 컴포넌트를 반환하도록 수정
// component/TodoList.js

(...)
const TodoList = ({ todo }) => {
	return (
    	(...)
        <div className="list_wrapper">
        	{todo.map((it) => (
            	<TodoItem {...it} />
            ))}
        </div>
    );
};
export default TodoList;
it={it} 가 아닌 {... it} 로 사용된 이유?
TodoItem 컴포넌트에 전달된 Props를 이 컴포넌트에서 사용할 수 있도록 수정
// component/TodoItem.js

import "./TodoItem.css";

const TodoItem = ({ id, content, isDone, createdDate }) => {
	return (
    	<div className="TodoItem">
        	<div className="check_col">
            	<input checked={isDone} type="checkbox" />
            </div>
            <div className="title_col">{content}</div>
            <div className="date_col">{new Date().toLocaleDateString()}</div>
            <div className="btn_col">
            	<button>삭제</button>
            </div>

        </div>
    );
}
export default TodoItem;

key 설정하기

map을 이용해 컴포넌트를 리스트 형태로 반복적으로 렌더링하려면 반드시 리스트 내의 고유한 key를 Props로 전달해야 함
// component/TodoList.js

(...)
const TodoList = ({ todo }) => {
	return (
    	(...)
        <div className="list_wrapper">
        	{todo.map((it) => (
            	<TodoItem key={it.id} {...it} />
            ))}
        </div>
    );
};
export default TodoList;

검색어에 따라 필터링하기

검색 기능 만들기

사용자가 입력하는 검색어를 처리할 State 변수를 만든 다음, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능 구현
// component/TodoList.js

(...)
const TodoList = ({ todo }) => {
	const [search, setSearch] = useState("");
    const onChangeSearch = (e) => {
    	setSearch(e.target.value);
    };
	return (
    	(...)
        <input value={search} onChange={onChangeSearch}
        	className="searchBar" placeholder="검색어를 입력하세요" />
        (...)
    );
};
export default TodoList;
사용자가 입력한 검색어에 따라 할 일 아이템을 필터링하는 기능 구현
// component/TodoList.js

(...)
const TodoList = ({ todo }) => {
	(...)
    const getSearchResult = () => {
    	return search === "" ? todo :
        	todo.filter((it) => it.content.includes(search));
    };
	return (
    	(...)
        <div className="list_wrapper">
        	{getSearchResult().map((it) => (
            	<TodoItem key={it.id} {...it} />
            ))}
        </div>
    );
};
export default TodoList;

대소 문자를 구별하지 않게 하기

검색에서 대소 문자를 구별하지 않도록 하는 기능 구현
// component/TodoList.js

(...)
const TodoList = ({ todo }) => {
	(...)
    const getSearchResult = () => {
    	return search === "" ? todo :
        	todo.filter((it) =>
            	it.content.toLowerCase().includes(search.toLowerCase()));
    };
	(...)
};
export default TodoList;

Update: 할 일 수정하기

기능 흐름 살펴보기

아이템 수정 함수 만들기

할 일 수정 함수 onUpdate를 생성하고 TodoList 컴포넌트에 Props로 전달
// App.js

(...)

function App() {
    const onUpdate = (targetId) => {
    	setTodo(
        	todo.map((it) => 
            	it.id === targetId ? { ...it, isDone: !it.isDone } : it
            )
        );
    };
    
    return (
    	<div className="App">
        	<Header />
            <TodoEditor onCreate={onCreate} />
            <TodoList todo={todo} onUpdate={onUpdate} />
        </div>
    );
}
export default App;
TodoList에서 TodoItem 컴포넌트에 함수 onUpdate를 전달
// component/TodoList.js

(...)
const TodoList = ({ todo, onUpdate }) => {
	(...)
	return (
    	(...)
        <div className="list_wrapper">
        	{getSearchResult().map((it) => (
            	<TodoItem key={it.id} {...it} onUpdate={onUpdate} />
            ))}
        </div>
    );
};
export default TodoList;

TodoItem 컴포넌트에서 아이템 수정 함수 호출하기

TodoItem 컴포넌트에서 틱 이벤트가 발생하면 함수 onUpdate를 호출
// component/TodoItem.js

import "./TodoItem.css";

const TodoItem = ({ id, content, isDone, createdDate, onUpdate }) => {
	const onChangeCheckbox = () => {
    	onUpdate(id);
    };
	return (
    	<div className="TodoItem">
        	<div className="check_col">
            	<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
            </div>
            (...)
        </div>
    );
}
export default TodoItem;

Delete: 할 일 삭제하기

기능 흐름 살펴보기

수정 기능과 유사한 흐름으로 진행

아이템 삭제 함수 만들기

App 컴포넌트에서 할 일을 삭제하는 함수 onDelete를 작성
// App.js

(...)

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;
// component/TodoList.js

(...)
const TodoList = ({ todo, onUpdate, onDelete }) => {
	(...)
	return (
    	(...)
        <div className="list_wrapper">
        	{getSearchResult().map((it) => (
            	<TodoItem key={it.id} {...it} onUpdate={onUpdate} onDelete={onDelete} />
            ))}
        </div>
    );
};
export default TodoList;

TodoItem 컴포넌트에서 삭제 함수 호출하기

// component/TodoItem.js

import "./TodoItem.css";

const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => {
	(...)
    const onClickDelete = () => {
    	onDelete(id);
    };
	return (
    	<div className="TodoItem">
        	(...)
            <div className="btn_col">
            	<button onClick={onClickDelete}>삭제</button>
            </div>
        </div>
    );
}
export default TodoItem;

0개의 댓글