리액트 공식문서를 50% 정도 읽고 만들어본 To Do List!
2024-02-25 업데이트
useContext
를 이용하여 리팩토링 한 게시글이 있습니다.
좀 더 공부한 후의 프로젝트를 보고 싶다면 해당 게시글 을 통해 확인해보세요 ><
useState
이용해서 만들어보기HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../src/index.css" />
<link rel="stylesheet" href="../src/App.css" />
<title>useState TodoList</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Bagel+Fat+One&display=swap');
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>
깔쌈한 폰트 하나 다운로드 해주고 body
태그에는 div
태그 하나만 생성해주고
컴포넌트들을 생성하러 가보자
각 컴포넌트들을 만들 때 컴포넌트의 역할을 명시하면서 해보도록 하겠다.
App.js
state
정의export default function App() {
const [tasks, setTasks] = useState([]);
const [text, setText] = useState('');
...
}
최상위 컴포넌트에서 사용할 state
는 두가지이다.
tasks
상태는 현재 투 두 리스트들의 내용을 담은 객체를 배열로 담은 상태이다.
tasks
는 다음과 같이 구성된다.
[
...
{
id : todolist 의 id,
content : 적힌 내용,
isEdit : 해당 todolist가 수정 중인지, 아닌지를 담은 boolean
}
... ]
text
는 input
값에서 적힌 값들을 저장하는 state
이다.
Input
function Input({ onChange }) {
return (
<input type='text' placeholder='할 일을 입력해주세요' onChange={onChange} />
);
}
인풋 컴포넌트는 입력값을 받아 props
로 받은 onChange
콜백함수를 통해 현재 입력된 값들을 담고 있는 state
값을 변경하는 컴포넌트이다.
Button
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
버튼 컴포넌트는 state
를 변경시키는 onClick
콜백함수를 props
로 받는다.
기본적인 기능은 일반적인 <button ..>
과 다를 것 없지만 컴포넌트를 구성 할 때 전부 이쁘게 대문자로 통일 하고 싶어서 만들어주었다.
ToDoText
function TodoText({ task, onEdit }) {
return (
<>
<p>{task.content}</p>
<Button
onClick={() => {
onEdit(task.id);
}}
>
Edit
</Button>
</>
);
}
ToDoText
컴포넌트는 props
로 받은 task
의 content
를 게시판에 띄우는 역할을 하는 p
태그와
수정 기능이 있는 Button
컴포넌트를 가지고 있다.
task
는 위에서 Input
컴포넌트에 의해 입력된 값을 content
라는 프로퍼티에 담고 있다.
ToDoInput
function TodoInput({ task, onSave }) {
const [localText, setLocalText] = useState(task.content);
return (
<>
<input
type='text'
onChange={(e) => setLocalText(e.target.value)}
value={localText}
/>
<Button
onClick={() => {
onSave(task.id, localText);
}}
>
Save
</Button>
</>
);
}
ToDoInput
컴포넌트는 ToDoText
에서 edit
버튼이 눌렸을 때 나타나는 컴포넌트이다.
수정 버튼을 눌렀을 때에는 이전에 입력된 값들을 가지고 있어야 하기 때문에 task
값을 props
로 받는다.
지역 state
인 localText
를 가지고 있어 input
값에서 입력된 값들로 지역 상태를 변경하고
Save
버튼이 눌리면 local state
를 상위 컴포넌트의 상태를 onSave
콜백함수를 이용해 변경시키도록 한다.
ToDoList
function TodoList({ tasks, onSave, onEdit, onRemove }) {
if (!tasks) return;
return (
<>
{tasks.map((task) => {
return (
<div key={task.id} className='container'>
{task.isEdit ? (
<TodoInput key={task.id} task={task} onSave={onSave} />
) : (
<TodoText key={task.id} task={task} onEdit={onEdit} />
)}
<Button onClick={() => onRemove(task.id)}>Remove</Button>
</div>
);
})}
</>
);
}
ToDoList
컴포넌트는 입력값에 따른 task
들을 컴포넌트에 넣어 렌더링 하는 컴포넌트이다.
렌더링 할 때 포인튼느 isEdit
값에 따라 ToDoInput
, ToDoText
컴포넌트를 결정하고 렌더링 한다는 것이다.
이를 통해 위에서 버튼들이 눌려 tasks
의 task
객체의 isEdit
이 변경되면
렌더링 되는 컴포넌트가 p
태그가 되기도, input
태그가 되기도 하게 하였다.
App
let nextId = 0;
export default function App() {
const [tasks, setTasks] = useState([]);
const [text, setText] = useState('');
function handleType(e) {
setText(e.target.value);
}
function handleAdd() {
setTasks([...tasks, { id: nextId++, content: text, isEdit: false }]);
}
function handleSave(targetId, newContent) {
setTasks(
tasks.map((task) => {
if (task.id === targetId)
return { id: targetId, content: newContent, isEdit: false };
return task;
}),
);
}
function handleEdit(targetId) {
setTasks(
tasks.map((task) => {
if (task.id === targetId) return { ...task, isEdit: true };
return task;
}),
);
}
function handleRemove(targetId) {
setTasks(tasks.filter((task) => task.id !== targetId));
}
return (
<>
<h1>To Do List</h1>
<div className='header'>
<Input onChange={handleType} />
<Button onClick={handleAdd}>Add</Button>
</div>
<TodoList
tasks={tasks}
onSave={handleSave}
onEdit={handleEdit}
onRemove={handleRemove}
/>
</>
);
}
최상위 컴포넌트인 App
컴포넌트에서 각 이벤트 핸들러들을 정의해주고 컴포넌트를 구성하였다.
이벤트 핸들러들은 type , add , save , edit , remove
등의 인터렉션을 담당하고 있다.
끝 ~!~!
useReducer
를 이용해 만들어보기React Learn - 이벤트 핸들링과 인터렉션 분해의 필요성 , useReducer
이전에 useReducer
의 개념과 필요성에 대해 공부해보았으니 useReducer
를 이용해서 App
컴포넌트를 수정해보자
let nextId = 0;
export default function App() {
const [tasks, dispatch] = useReducer(taskReducer, []);
const [text, setText] = useState('');
function handleType(e) {
setText(e.target.value);
}
function dispatchAdd() {
dispatch({
type: 'add',
text,
});
}
function dispatchSave(targetId, newContent) {
dispatch({
type: 'save',
targetId,
newContent,
});
}
function dispatchEdit(targetId) {
dispatch({
type: 'edit',
targetId,
});
}
function dispatchRemove(targetId) {
dispatch({
type: 'remove',
targetId,
});
}
return (
<>
<h1>To Do List</h1>
<div className='header'>
<Input onChange={handleType} />
<Button onClick={dispatchAdd}>Add</Button>
</div>
<TodoList
tasks={tasks}
onSave={dispatchSave}
onEdit={dispatchEdit}
onRemove={dispatchRemove}
/>
</>
);
}
/**
* @param {Array} tasks 컴포넌트의 State
* @param {Object} action 이벤트 핸들러에서 디스패치한 이벤트 객체
* 액션 타입과 이벤트 핸들시 필요한 파라미터를 프로퍼티로 가지고 있음
* @returns {Array} 새로 갱신 할 State
*/
function taskReducer(tasks, action) {
// state 변경이 일어날 떄 발생한 action 을 debuging 하기 좋음
console.log(action);
switch (action.type) {
// add 일 때 필요한 action 프로퍼티는 text
case 'add': {
return [...tasks, { id: nextId++, content: action.text, isEdit: false }];
}
case 'save': {
// save 일 때 필요한 action 프로퍼티는 targetId , newContent
return tasks.map((task) => {
if (task.id === action.targetId)
return {
id: action.targetId,
content: action.newContent,
isEdit: false,
};
return task;
});
}
case 'edit': {
// edit 일 떄 필요한 action 프로퍼티는 targetId
return tasks.map((task) => {
if (task.id === action.targetId) return { ...task, isEdit: true };
return task;
});
}
case 'remove': {
// remove 일 떄 필요한 action 프로퍼티는 targetId
return tasks.filter((task) => task.id !== action.targetId);
}
default: {
throw new Error('처음 보는 Type 인디요');
}
}
}
결과물은 동일하다.
CSS
두 파일 모두 CSS
는 같으니 마지막에 명시해두었다.
* {
margin: 0px;
padding: 0px;
color: white;
font-family: 'Bagel Fat One', system-ui;
}
body {
background: url('https://images.pexels.com/photos/807598/pexels-photo-807598.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2');
background-size: cover;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#root {
backdrop-filter: blur(30px);
padding: 50px;
width: 300px;
/* border: 5px solid white; */
border-radius: 20%;
}
.header {
display: flex;
gap: 10px;
padding: 10px 0px;
}
button {
width: 50x;
font-size: 10px;
background-color: green;
padding: 10px;
border-radius: 20px;
border: none;
}
input {
border-radius: 20px;
border: none;
padding: 10px;
color: black;
}
.container {
display: flex;
gap: 10px;
padding: 10px;
}
.container p {
width: 200px;
}
li {
display: flex;
gap: 5px;
}
한 달전 자바스크립트 공부를 끝내고 투 두 리스트를 만들어봤던 적이 있다.
이 때는 완전 러프하게 이벤트 별 인터렉션을 모두 정의해두었기 때문에
같이 공부하는 스터디원이 수정 기능이 있으면 좋겠다는 이야기를 들었을 때
기능을 추가할 엄두가 나지 않았다.
그 때는 상태 관리의 개념에 대해 모르고 있었기 때문에 기능을 추가하려면 새로 다시 만드는게 나은 수준이였다.
하지만 이번에 상태 관리의 개념을 알고 나서 해보니
이전에 이벤트 별 인터렉션을 모두 정의해두는 것이 얼마나 확장성이 낮은 것인지 더 체감 할 수 있었다.
물론 지금의 투 두 리스트도 완벽한 것은 아니다.
입력값이 들어올 때 마다 상위 컴포넌트의 text
가 변할 때 마다 모든 하위 컴포넌트가 재렌더링 되고 있기 때문이다.
(상위 컴포넌트의 state 가 변하고 있기 때문이다.)
이를 방지하기 위한 다양한 Hook 이 있던데 열심히 공부해봐야겠다 :)