🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [30]일차
🚀30일차에는 리액트로 Todo리스트를 구현하면서 추가, 삭제, 수정 기능을 어떻게 구현하면 좋을지 배울 수 있었다.
useState가 중요하게 자주 쓰였고 props로 함수객체와 데이터객체를 전달하여
다른 컴포넌트에서 수행한 값을 전달할 수 있었다. 회고를 통해 코드를 다시 해석해봐야겠다고 생각했다.
Todo 리스트 만들기
- 전체 컴포넌트를 감싸는 컴포넌트 = TodoBox 컴포넌트
- 그 안에서 Input을 받는 Todo = TodoInput 컴포넌트
- Todo들을 보여주는 컴포넌트 = TodoList 컴포넌트

추가 기능
- 할 일 입력 후 엔터 입력 시 “할일이 등록되면서” 리스트에 정보 출력 (input태그에 이벤트onKeyDown 활용)
- 리스트의 항목들을 배열로 관리
- 리스트가 "상태관리"되어야함
const [toDoList, setTodoList] = useState([
{ id: 1, title: "리액트 공부하기" },
{ id: 2, title: "스프링 공부하기" },
{ id: 3, title: "블로그 회고작성" },
]);
- 데이터를 가진 배열은 TodoList와 TodoInput컴포넌트 둘다 접근이 가능한 TodoBox 컴포넌트가 관리해야함
- 이 데이터 배열을 props를 통해 객체로써 각각의 컴포넌트에 보내줄 수 있을 것이다.
const TodoList = ({ todoList }) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}>{toDo.title}</li>
))}
</ul>
);
};
export default TodoList;
- const TodoList = ( { todoList })
➡️ TodoBox 컴포넌트로부터 "데이터 배열"을 전달받아서 사용 - return <ul> ... </ul>
➡️<ul>태그 밑에 { } 자바스크립트 코드로 감싼 map()함수를 사용하여 <li>태그를 만든다. - <li key={todDo.id}>
➡️map 사용 시 "기본키 값이 필요"하므로 id값을 키 값으로 설정한다.
// TodoBox컴포넌트
const addTodoList = (title) => {
console.log(title);
};
return (
<div>
<TodoInput addTodo={addTodoList} />
<TodoList todoList={todoList} />
</div>
);
- TodoInput 컴포넌트에서 사용자에게 값을 입력받은 후 수정하는 로직은
TodoBox컴포넌트에서 관리해야할 것이 맞을 것이다. - TodoBox컴포넌트에서 TodoInput컴포넌트로 props를 넘길때
"addTodo" 라는 “키”를 넘겨야한다. ⚠️"addTodoList"라는 “value”를 넘기면 안된다.
// TodoInput 컴포넌트
const TodoInput = ({ addTodo }) => {
const [toDo, setToDo] = useState("");
const keyDownHandler = (e) => {
if (e.key === "Enter") {
// Enter가 입력되면 리스트에 값을 저장
if (e.target.value === "") return;
addTodo(e.target.value); // 실제 리스트에 저장하는 부분은 TodoBox컴포넌트가 관리
setToDo("");
}
};
//...
- TodoBox컴포넌트로부터 addTodo “키”를 props로 전달받아서 keyDownHandler() 에서 사용
- addTodo(e.target.value)
➡️TodoBox컴포넌트에 e.target.value 를 "인자"넘기게된다.
➡️즉 TodoInput 컴포넌트에서 keyDownHandler 이벤트 수행을 통해
TodoBox컴포넌트의 함수인 addTodo()에게 (e.target.value)인자를 전달하게 되는 것이다. - setToDo("")
➡️값이 전달되면 “” 로 초기화시킨다. (useState의 set함수) - TodoInput 컴포넌트에서 사용자에게 값을 입력받을때 title값만 입력을 받지만
TodoBox컴포넌트에서 "데이터 배열"을 관리하기 위해서는 배열에 있는 속성인 id값도 필요할 것이다. - 그러기 위해서는 id값을 1씩 증가하면서 값을 넣기위해 useRef를 사용할 수 있다.
(내부적으로 값이 계속 변할 수 있는 기능제공 ↔ ⚠️let 사용 X (리렌더링 시 값이 초기화됨)
➡️useRef로 관리하는 변수는 "값이 바뀌어도 컴포넌트가 리렌더링되지 않는 특징"을 활용
Todo리스트 - 할 일 "추가" 구현
▶️실습 - TodoBox 컴포넌트 : useRef()를 활용한 id값 추가
const id = useRef(Math.max(...todoList.map((todo) => todo.id)) + 1);
const addTodoList = (title) => {
console.log("새로 추가한 값 : id[" + id + "], title[" + title + "]");
const newTodo = {
id: id.current,
title: title,
};
setTodoList([...todoList, newTodo]);
id.current += 1;
};
- const newTodo = { id: id.current, title:title };
➡️새로운 값을 넣기위한 배열을 만든다.
➡️id(key), 값(value)의 구조로된 배열로 기존 배열과 동일한 구조를 가진다. - id.current 의미
➡️useRef의 현재 값을 가져온다. (현재 id값)
➡️useRef의 구조는 { current:0 } 처럼 "객체"로 만들어져있다.
id.current대신 id를 참조하면 { current : 0 } 처럼 객체 자체가 리턴되므로
id.current를 참조해주어야 id 객체의 current를 직접 참조하여 그 값을 가져올 수 있게되는 것
⚠️useRef사용시에는 current를 사용해야 useRef의 값을 사용 가능 - setTodoList([…todoList, newTodo]);
➡️useState의 set함수(setTodoList)를 사용하여 (기존 배열의 값들 + 새로운 배열)로 TodoList 배열을 업데이트
➡️(… 스프레드 연산자)를 사용하여 기존배열(todoList)의 값 요소만 가져와야한다.
👀 위 코드 대신 사용할 수 있는 코드로
setTodoList(toDoList.concat(newTodo));
➡️concat()를 활용해 기존 배열 toDoList에 concat을 통해서 newTodo라는 새로운 배열을 이어 붙여줄 수 있다.
Math.max(...todoList.map((todo) => todo.id))
- "데이터 배열"의 id값이 반드시 (1, 2, 3, 4) 이지 않을 수 있고, 특정 id값(ex.3번)이 삭제된 상태일 수도 있다.
- id는 primary key이므로 중간 id값이 비어있어도 오류로 발생되진 않아 문제를 찾기 힘들 수 있다.
- map()을 활용하여 현재 "데이터 배열"의 최대 id값을 구해야한다.
- Math.max()
➡️만약 todoList.map()을 사용하게되면 배열 자체를 가져오게되므로
max()함수가 객체에 대한 최대값을 반환하지 못한다.
➡️해결방법 : …배열(스프레드 문법)을 활용한다 (ex. 1, 2, 3, 4 중 최대값을 추출해낼 수 있다. ( "4" ))
▶️실습 - TodoInput 컴포넌트 : keyDownHandler() 수정
// TodoInput 컴포넌트 - keyDownHandler()에 추가
e.target.value("");
e.target.focus();
- e.target.value(""); ➡️할 일을 입력(Enter) 한 후 input 박스에 입력했던 값이 비워지도록 구현
- e.target.focus(); ➡️비워진 후에는 focus()를 통해 비워진 input박스에 커서가 위치할 수 있도록 구현
Todo리스트 - 할 일 "수정/삭제" 구현
- 수정, 삭제는 “데이터 배열"(=리스트)가 수정, 삭제 되어야하므로
"데이터 배열"(=리스트)가 담긴 TodoBox컴포넌트에서 수행
▶️실습 - TodoBox컴포넌트 : deleteTodoList() = 삭제 기능
// TodoBox 컴포넌트
// 2. 할 일 삭제 (추가)
const deleteTodoList = (id) => {
setTodoList(todoList.filter((todo) => todo.id !== id));
}
- const deleteTodoList = (id) => { }
➡️id값을 가져와서 그 id에 해당하는 할 일을 삭제할 수 있도록 구현 - todoList.filter()
⭐filter()함수를 활용
➡️조건을 만족하는 것만 뽑아내어 새로운 배열을 리턴해줌 (map()과의 차이점은 조건을 검사하는지 안하는지) - todo.id !== id
➡️filter()함수의 조건식을 의미
현재 TodoBox컴포넌트에서 useRef로 관리되고 있는 todo.id와 deleteTodoList 함수에서 전달받은 id를 비교
➡️filter()를 통해 id값이 일치하지 않는 것만 새로운 배열로 만들어냄
(즉 삭제할 할 일의 id값을 제외하고 새로운 배열로 만들어냄)
➡️삭제할 할 일을 제외하고 새 배열로 만들어지므로 “삭제하는 듯한 기능이 구현됨”
▶️실습 - TodoBox컴포넌트 : updateTodoList() = 수정 (리렌더링 X)
// TodoBox 컴포넌트
// 3. 할 일 수정 (리렌더링 X)
const updateTodoList = (todo) => {
todoList.map((item) => {
if (item.id === todo.id) {
item.title = todo.title;
}
});
}
- 할 일 수정 후 리렌더링이 필요하지 않을때의 코드
- "수정"의 기능을 하는 컴포넌트로부터 todo라는 객체를 받아와서 id와 title을 모두 관리할 수 있도록 함
💡todo 객체 { id: undefined, title : undefined} - map( (item) => { ... })
➡️TodoList에서 요소들을 모두 꺼내서 진행해야하므로 map() 사용 - item ➡️임의로 이름을 설정한 todo 객체
- item.id === todo.id
➡️item.id은 현재 TodoBox컴포넌트에서 관리되고 있는 리스트의 id를 가리킨다.
➡️todo.id는 updateTodoList 함수에서 전달받은 todo객체의 id를 가리킨다.
따라서 이 둘이 같으면 현재 TodoBox컴포넌트의 리스트의 title을
새롭게 받아온 todo객체의 title로 바꿔주는 코드를 의미
▶️실습 - TodoBox컴포넌트 : updateTodoList() = 수정 (리렌더링 O)
// TodoBox 컴포넌트
// 3. 할 일 수정 (리렌더링 O)
const updateTodoList = (todo) => {
const updating = todoList.map((item) => {
item.id === todo.id ? {...item, title:todo.title} : item;
});
setTodoList(updating);
}
- 할 일 수정 후 리렌더링이 필요할때의 코드
- item.id === todo.id ? {...item, title:todo.title} : item;
➡️삼항연산자를 활용해 item.id === todo.id 가 일치한다면 {...item, title:todo.title} 수행 - {…item, title:todo.title}
➡️조건식 (item.id === todo.id)가 true일때 실행하는 문장이다.
...item는 기존 배열의 요소들을 updating에 저장하고
title:todo.title에서 title은 todo.title로 갱신하는 기능을 한다. - item
➡️조건식 (item.id === todo.id)가 false일때 실행하는 문장이다.
수정할 id가 기존 배열에 존재하지 않을때 기존 배열만 그대로 updating에 저장 - setTodoList(updating);
➡️useState의 set함수를 이용해 위의 조건으로부터 수정되었던 배열(updating)을 업데이트 시킨다.
▶️실습 - 삭제 기능 적용
TodoBox컴포넌트
// TodoBox컴포넌트
return (
<div>
<TodoInput addTodo={addTodoList} />
<TodoList
todoList={todoList}
deleteHandler={deleteTodoList}
updateHandler={updateTodoList}
/>
</div>
);
- TodoBox 컴포넌트의 <TodoList>컴포넌트가 담긴 부분에서 deleteHandler와 updateHandler에 각각의 함수를 전달
➡️TodoList 컴포넌트에서 deleteHandler와 updateHandler를 props로 전달받아 사용 가능
TodoList컴포넌트
// TodoList컴포넌트
const TodoList = ({ todoList, deleteHandler, updateHandler}) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}>{toDo.title}</li>
))}
</ul>
);
};
export default TodoList;
- const TodoList = ( { todoList, deleteHandler, updateHandler } ) => { ... }
➡️props로 "데이터 배열"과 deleteHandler, updateHandler를 전달받음
하지만 이렇게되면 TodoList 컴포넌트에서 해야하는 기능이 많아질 수 있으므로 “컴포넌트 분리”를 고려
Todo 컴포넌트로 기능 분리
const Todo = () => {
return(
);
}
export default Todo;
- return은 <div>처럼 묶여있어야 사용할 수 있는데
이 경우 Todo컴포넌트를 다른 컴포넌트에서 사용하게될때 <div>가 묶인 상태로 전달받게되므로
<div>로 한번 더 감싸져 원치않는 결과가 나올 수 있다. - ➡️해결방법 : Fragment (<> </>) 사용
return문에서 묶는 기능을 하면서도 태그가 전달되진 않아 원하는 결과를 얻을 수 있다.
▶️실습 - Todo 컴포넌트 : 삭제 버튼 만들기
const Todo = ({todo, deleteHandler}) => {
return(
<>
{todo.title}
<button onClick={() => deleteHandler(todo.id)}>삭제</button>
</>
);
}
export default Todo;
- {todo.title}
➡️TodoList컴포넌트로부터 받아온 todo객체으로부터 title을 만들어줌 - onClick={ () => deleteHandler(todo.id) }
➡️props로 받아온 deleteHandler함수 안에 todo.id를 넘겨준다. (TodoBox컴포넌트까지 전달됨)
❓onClick = {deleteHandler(todo.id)}
onClick={ () ⇒ deleteHandler(todo.id)} 의 차이점
➡️실행 시점의 차이이다.
- deleteHandler(todo.id)
함수 호출 결과를 `onClick`에 전달하는 방식으로 "컴포넌트가 렌더링되는 시점에 함수가 즉시 실행"된다.
따라서 deleteHandler(todo.id)의 반환값이 onClick에 할당되므로 이벤트와는 상관없이 "렌더링 중에 함수가 실행"
- onClick={() => deleteHandler(todo.id)}
람다 함수를 사용해 deleteHandler(todo.id) 함수를 호출할 수 있다.
onClick이벤트가 발생했을때 "() 람다함수"가 실행되어 deleteHandler(todo.id)를 호출하는 것이기때문에
컴포넌트가 렌더링되는 시점에는 실행되지 않고, onClick 이벤트가 발생할때 deleteHandler()함수가 실행된다.
즉 람다 함수 ( () => )를 사용해야 이벤트가 발생할 때 원하는 함수가 실행된다.
▶️실습 - TodoList 컴포넌트 : 삭제 기능을 Todo컴포넌트에 적용
import Todo from "./Todo";
const TodoList = ({ todoList, deleteHandler, updateHandler}) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}><Todo todo={toDo} deleteHandler={deleteHandler}/></li>
))}
</ul>
);
};
export default TodoList;
- TodoList 컴포넌트에서는 {toDo.title}부분을 <Todo/>컴포넌트로 대체가능
➡️(Todo컴포넌트에서 <> 버튼 </>을 return하게 되므로) - 속성으로는 map()을 통해 관리한 값인 toDo가 담겨있는 todo 속성을 전달
- deleteHandler={deleteHandler};
➡️ToboBox컴포넌트로부터 받아온 deleteHandler를 다시 Todo컴포넌트로 넘겨주는 역할을 한다.


- 삭제 버튼을 클릭하면 deleteHandler가 발생하여
TodoBox컴포넌트의 리스트가 수정되는 것까지 구현
▶️실습 - Todo 컴포넌트 : 수정 버튼 만들기
- 요청이 2개일 것이다. “수정할 폼(틀)” 요청과 “수정한 값으로 전달”하는 요청을 수행해야한다.
ex. 마치 로그인에서 “로그인 폼(틀)” 요청과 “로그인 요청” 두개 인 것과 유사하다.

- 위의 그림에서 위에 있는 수정버튼은 “수정해달라”라는 요청이고
밑에 있는 수정버튼은 “수정 폼”을 요청하는 요청이다.
▶️실습 - TodoList 컴포넌트 : 수정 기능을 Todo컴포넌트에 적용
import { useState } from "react";
const Todo = ({ todo, deleteHandler }) => {
const [updateMode, setUpdateMode] = useState(false); // 기본값은 false
return (
<>
{todo.title}
<button onClick={() => deleteHandler(todo.id)}>삭제</button>
<button>수정</button>
</>
);
};
export default Todo;
- 수정폼 요청 부분과과 수정 요청 부분을 추가
- const [updateMode, setUpdateMode] = useState(false);
➡️updateMode를 useState로 상태를 관리하여 updateMode가 true바뀌면 수정폼이 보여지게끔 구현

- 수정버튼이 클릭되면 수정 폼이 요청되어 수정폼을 보여줄 수 있도록 하는 기능을 추가 구현해야한다.
➡️updateMode를 true로 바꿔주어 수정 폼이 보여지는 기능 필요
❓<button onClick={updateModeHanlder}>수정</button> 호출 방식에 대하여
삭제 기능을 구현할때 { () => deleteHandler(todo.id) } 처럼 람다를 활용한 방식과 호출방식이 똑같다.
그 이유는 updateModeHandler에 따로 매개변수를 전달하지 않기 때문에, 별도의 람다함수(() =>)가 필요없는 것이다.
따라서 <button onClick={updateModeHandler}>는 이벤트가 발생할때만 이벤트핸들러가 실행되므로
람다 함수 없이도 적절하게 동작한다.
onChange 속성
- 리액트에서 <input> 에 value속성을 설정했을때,
onChange 속성을 제공하지 않으면 "읽기 전용"으로 판단되기때문에 onChange속성을 적용해주어야한다. - 리액트에서 value 속성이 설정되면 "제어 컴포넌트"로 판단되어 입력 값이 부모 컴포넌트의 상태에 의해 제어된다.
- 💡따라서 onChange 속성을 추가하여 입력 값을 상태로 관리해야한다.
▶️실습 - TodoList컴포넌트 :
TodoBox컴포넌트에서 받은 updateHandler를 Todo컴포넌트에 다시 전달
import Todo from "./Todo";
const TodoList = ({ todoList, deleteHandler, updateHandler }) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}>
<Todo
todo={toDo}
deleteHandler={deleteHandler}
updateHandler={updateHandler}
/>
</li>
))}
</ul>
);
};
export default TodoList;
- <Todo todo={toDo} deleteHandler={deleteHandler} updateHandler={updateHandler}
➡️TodoBox컴포넌트로부터 받아온 updateHandler를 Todo컴포넌트로 다시 전달
Todo컴포넌트
import { useState } from "react";
const Todo = ({ todo, deleteHandler, updateHandler }) => {
//...
const [title, setTitle] = useState(todo.title);
const updateModeHanlder = () => {
setUpdateMode(true);
console.log("updateMode가 " + updateMode + "입니다.");
};
// ...
export default Todo;
- const [title, setTitle] = useState(todo.title);
➡️초기값이 todo.title인 이유는 Todo컴포넌트가 TodoList컴포넌트로부터 todo 객체를 입력받아왔으므로
받아온 todo객체의 title을 수정하겠다고 명시해주어야하므로 todo객체의 title을 초기값으로 설정
▶️실습 - Todo컴포넌트 : onChange 적용
const changeHandler = (e) => {
setTitle(e.target.value);
};
if (updateMode) {
// 수정폼을 활성화시켰을때 (true)
return (
<>
<input type="text" value="수정할 값" onChange={changeHandler} />
<button>수정</button>
</>
);
}
- onChange를 이용하여 (e)이벤트가 발생했을때 수행할 changeHandler 이벤트를 요청
onChange의 코드를 onChange={(e) => setTitle(e.target.value)} 로 바꿔줄 수도 있다. - setTitle(e.target.value)
➡️changeHandler 함수에서 이벤트가 발생한 객체(e.target)의 값(.value)으로 현재 title을 바꿔주는 코드
⚠️setTitle()을 통해 Todo컴포넌트에서만 title이 수정되었기때문에
TodoBox컴포넌트의 "데이터 배열"까지는 수정된 title이 전달되지 않는다.
▶️실습 - Todo컴포넌트
import { useState } from "react";
const Todo = ({ todo, deleteHandler, updateHandler }) => {
// 수정폼, 수정
const [updateMode, setUpdateMode] = useState(false); // 기본값은 false
// updateMode가 true바뀌면 수정폼이 보여지게끔 구현 가능
const [title, setTitle] = useState(todo.title);
const updateModeHanlder = () => {
setUpdateMode(true);
console.log("updateMode가 " + updateMode + "입니다.");
};
const changeHandler = (e) => {
setTitle(e.target.value);
};
const updateValue = () => {
updateHandler({ id: todo.id, title: title });
setUpdateMode(false); // 다시 false로 바꿔주어 수정 폼을 비활성화 시킴
};
if (updateMode) {
// 수정폼을 활성화시켰을때 (true)
return (
<>
<input type="text" value="수정할 값" onChagne={changeHandler} />
<button onClick={updateValue}>수정</button>
</>
);
}
return (
<>
{todo.title}
<button onClick={() => deleteHandler(todo.id)}>삭제</button>
<button onClick={updateModeHanlder}>수정</button>
</>
);
};
export default Todo;
- const Todo = ({ todo, deleteHandler, updateHandler})
➡️TodoList 컴포넌트로부터 이 속성들을 전달받는다. (이 속성들은 TodoBox컴포넌트로부터 전달되었다) - const [title, setTitle] = useState(todo.title);
➡️todo객체의 title 속성으로 초기값을 설정해주는데
이 값은 TodoBox컴포넌트에서 관리되고 있는 리스트의 title속성이 가져와지는 것이다.
➡️id값은 primary key (기본키)로 고유한 값을 가지기때문에 title만 바꿔주어 데이터를 관리한다. - const updateValue = () ⇒ { ... }
➡️활성화된 수정폼 if문안에서 버튼 안에 onClick으로 작성된 이 이벤트핸들러는
다시 TodoBox컴포넌트 리스트를 수정할 수 있도록 TodoBox컴포넌트까지 수정된 값을 보내는 기능 수행updateHandler() 에 id와 title값을 전달한다. 이 updateHandler()는 TodoBox컴포넌트로부터 전달된 함수이다.
- setUpdateMode(false);
➡️다시 false로 바꿔주는 기능을 하여 수정이 끝나면 수정폼을 다시 닫아주는 기능
▶️실습 - TodoBox컴포넌트 적용
// 3. 할 일 수정
const updateTodoList = (todo) => {
todoList.map((item) => {
if (item.id === todo.id) {
item.title = todo.title;
}
});
};
- 함수의 인자로는 (todo)를 받고 있다. 이는 객체이므로 updateHandler()안에는 객체를 전달하게된다.



- 수정이 잘 되고 있는 것을 확인할 수 있다.
- 이 후 자바스크립트의 express 서버 등을 활용하여 백엔드와 프론트엔드를 연결하는 것도 가능하다.
Express회고는 별도의 회고로 작성해야겠다!
🚀 회고를 통해 TodoBox컴포넌트로부터 Todo컴포넌트까지의 프로젝트 흐름 등을 깨달을 수 있었고
구조적으로는 이해할 수 있었지만
Todo리스트를 처음부터 다시 설계해보려할때 이 과정을 생각해낼 수 있는지가 관건인것 같다.
비슷한 프로젝트를 진행하여서 익숙하게 만드는 방법 밖에 없을 것 같다!
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_31일차_"리액트 useEffect, Memo프로젝트" (1) | 2025.01.15 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_30일차_"리액트 Express" (0) | 2025.01.14 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_29일차_"useState, useRef" (0) | 2025.01.13 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_28일차_"리액트 React" (1) | 2025.01.10 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_27일차_"자바스크립트 이벤트" (0) | 2025.01.09 |
🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [30]일차
🚀30일차에는 리액트로 Todo리스트를 구현하면서 추가, 삭제, 수정 기능을 어떻게 구현하면 좋을지 배울 수 있었다.
useState가 중요하게 자주 쓰였고 props로 함수객체와 데이터객체를 전달하여
다른 컴포넌트에서 수행한 값을 전달할 수 있었다. 회고를 통해 코드를 다시 해석해봐야겠다고 생각했다.
Todo 리스트 만들기
- 전체 컴포넌트를 감싸는 컴포넌트 = TodoBox 컴포넌트
- 그 안에서 Input을 받는 Todo = TodoInput 컴포넌트
- Todo들을 보여주는 컴포넌트 = TodoList 컴포넌트

추가 기능
- 할 일 입력 후 엔터 입력 시 “할일이 등록되면서” 리스트에 정보 출력 (input태그에 이벤트onKeyDown 활용)
- 리스트의 항목들을 배열로 관리
- 리스트가 "상태관리"되어야함
const [toDoList, setTodoList] = useState([
{ id: 1, title: "리액트 공부하기" },
{ id: 2, title: "스프링 공부하기" },
{ id: 3, title: "블로그 회고작성" },
]);
- 데이터를 가진 배열은 TodoList와 TodoInput컴포넌트 둘다 접근이 가능한 TodoBox 컴포넌트가 관리해야함
- 이 데이터 배열을 props를 통해 객체로써 각각의 컴포넌트에 보내줄 수 있을 것이다.
const TodoList = ({ todoList }) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}>{toDo.title}</li>
))}
</ul>
);
};
export default TodoList;
- const TodoList = ( { todoList })
➡️ TodoBox 컴포넌트로부터 "데이터 배열"을 전달받아서 사용 - return <ul> ... </ul>
➡️<ul>태그 밑에 { } 자바스크립트 코드로 감싼 map()함수를 사용하여 <li>태그를 만든다. - <li key={todDo.id}>
➡️map 사용 시 "기본키 값이 필요"하므로 id값을 키 값으로 설정한다.
// TodoBox컴포넌트
const addTodoList = (title) => {
console.log(title);
};
return (
<div>
<TodoInput addTodo={addTodoList} />
<TodoList todoList={todoList} />
</div>
);
- TodoInput 컴포넌트에서 사용자에게 값을 입력받은 후 수정하는 로직은
TodoBox컴포넌트에서 관리해야할 것이 맞을 것이다. - TodoBox컴포넌트에서 TodoInput컴포넌트로 props를 넘길때
"addTodo" 라는 “키”를 넘겨야한다. ⚠️"addTodoList"라는 “value”를 넘기면 안된다.
// TodoInput 컴포넌트
const TodoInput = ({ addTodo }) => {
const [toDo, setToDo] = useState("");
const keyDownHandler = (e) => {
if (e.key === "Enter") {
// Enter가 입력되면 리스트에 값을 저장
if (e.target.value === "") return;
addTodo(e.target.value); // 실제 리스트에 저장하는 부분은 TodoBox컴포넌트가 관리
setToDo("");
}
};
//...
- TodoBox컴포넌트로부터 addTodo “키”를 props로 전달받아서 keyDownHandler() 에서 사용
- addTodo(e.target.value)
➡️TodoBox컴포넌트에 e.target.value 를 "인자"넘기게된다.
➡️즉 TodoInput 컴포넌트에서 keyDownHandler 이벤트 수행을 통해
TodoBox컴포넌트의 함수인 addTodo()에게 (e.target.value)인자를 전달하게 되는 것이다. - setToDo("")
➡️값이 전달되면 “” 로 초기화시킨다. (useState의 set함수) - TodoInput 컴포넌트에서 사용자에게 값을 입력받을때 title값만 입력을 받지만
TodoBox컴포넌트에서 "데이터 배열"을 관리하기 위해서는 배열에 있는 속성인 id값도 필요할 것이다. - 그러기 위해서는 id값을 1씩 증가하면서 값을 넣기위해 useRef를 사용할 수 있다.
(내부적으로 값이 계속 변할 수 있는 기능제공 ↔ ⚠️let 사용 X (리렌더링 시 값이 초기화됨)
➡️useRef로 관리하는 변수는 "값이 바뀌어도 컴포넌트가 리렌더링되지 않는 특징"을 활용
Todo리스트 - 할 일 "추가" 구현
▶️실습 - TodoBox 컴포넌트 : useRef()를 활용한 id값 추가
const id = useRef(Math.max(...todoList.map((todo) => todo.id)) + 1);
const addTodoList = (title) => {
console.log("새로 추가한 값 : id[" + id + "], title[" + title + "]");
const newTodo = {
id: id.current,
title: title,
};
setTodoList([...todoList, newTodo]);
id.current += 1;
};
- const newTodo = { id: id.current, title:title };
➡️새로운 값을 넣기위한 배열을 만든다.
➡️id(key), 값(value)의 구조로된 배열로 기존 배열과 동일한 구조를 가진다. - id.current 의미
➡️useRef의 현재 값을 가져온다. (현재 id값)
➡️useRef의 구조는 { current:0 } 처럼 "객체"로 만들어져있다.
id.current대신 id를 참조하면 { current : 0 } 처럼 객체 자체가 리턴되므로
id.current를 참조해주어야 id 객체의 current를 직접 참조하여 그 값을 가져올 수 있게되는 것
⚠️useRef사용시에는 current를 사용해야 useRef의 값을 사용 가능 - setTodoList([…todoList, newTodo]);
➡️useState의 set함수(setTodoList)를 사용하여 (기존 배열의 값들 + 새로운 배열)로 TodoList 배열을 업데이트
➡️(… 스프레드 연산자)를 사용하여 기존배열(todoList)의 값 요소만 가져와야한다.
👀 위 코드 대신 사용할 수 있는 코드로
setTodoList(toDoList.concat(newTodo));
➡️concat()를 활용해 기존 배열 toDoList에 concat을 통해서 newTodo라는 새로운 배열을 이어 붙여줄 수 있다.
Math.max(...todoList.map((todo) => todo.id))
- "데이터 배열"의 id값이 반드시 (1, 2, 3, 4) 이지 않을 수 있고, 특정 id값(ex.3번)이 삭제된 상태일 수도 있다.
- id는 primary key이므로 중간 id값이 비어있어도 오류로 발생되진 않아 문제를 찾기 힘들 수 있다.
- map()을 활용하여 현재 "데이터 배열"의 최대 id값을 구해야한다.
- Math.max()
➡️만약 todoList.map()을 사용하게되면 배열 자체를 가져오게되므로
max()함수가 객체에 대한 최대값을 반환하지 못한다.
➡️해결방법 : …배열(스프레드 문법)을 활용한다 (ex. 1, 2, 3, 4 중 최대값을 추출해낼 수 있다. ( "4" ))
▶️실습 - TodoInput 컴포넌트 : keyDownHandler() 수정
// TodoInput 컴포넌트 - keyDownHandler()에 추가
e.target.value("");
e.target.focus();
- e.target.value(""); ➡️할 일을 입력(Enter) 한 후 input 박스에 입력했던 값이 비워지도록 구현
- e.target.focus(); ➡️비워진 후에는 focus()를 통해 비워진 input박스에 커서가 위치할 수 있도록 구현
Todo리스트 - 할 일 "수정/삭제" 구현
- 수정, 삭제는 “데이터 배열"(=리스트)가 수정, 삭제 되어야하므로
"데이터 배열"(=리스트)가 담긴 TodoBox컴포넌트에서 수행
▶️실습 - TodoBox컴포넌트 : deleteTodoList() = 삭제 기능
// TodoBox 컴포넌트
// 2. 할 일 삭제 (추가)
const deleteTodoList = (id) => {
setTodoList(todoList.filter((todo) => todo.id !== id));
}
- const deleteTodoList = (id) => { }
➡️id값을 가져와서 그 id에 해당하는 할 일을 삭제할 수 있도록 구현 - todoList.filter()
⭐filter()함수를 활용
➡️조건을 만족하는 것만 뽑아내어 새로운 배열을 리턴해줌 (map()과의 차이점은 조건을 검사하는지 안하는지) - todo.id !== id
➡️filter()함수의 조건식을 의미
현재 TodoBox컴포넌트에서 useRef로 관리되고 있는 todo.id와 deleteTodoList 함수에서 전달받은 id를 비교
➡️filter()를 통해 id값이 일치하지 않는 것만 새로운 배열로 만들어냄
(즉 삭제할 할 일의 id값을 제외하고 새로운 배열로 만들어냄)
➡️삭제할 할 일을 제외하고 새 배열로 만들어지므로 “삭제하는 듯한 기능이 구현됨”
▶️실습 - TodoBox컴포넌트 : updateTodoList() = 수정 (리렌더링 X)
// TodoBox 컴포넌트
// 3. 할 일 수정 (리렌더링 X)
const updateTodoList = (todo) => {
todoList.map((item) => {
if (item.id === todo.id) {
item.title = todo.title;
}
});
}
- 할 일 수정 후 리렌더링이 필요하지 않을때의 코드
- "수정"의 기능을 하는 컴포넌트로부터 todo라는 객체를 받아와서 id와 title을 모두 관리할 수 있도록 함
💡todo 객체 { id: undefined, title : undefined} - map( (item) => { ... })
➡️TodoList에서 요소들을 모두 꺼내서 진행해야하므로 map() 사용 - item ➡️임의로 이름을 설정한 todo 객체
- item.id === todo.id
➡️item.id은 현재 TodoBox컴포넌트에서 관리되고 있는 리스트의 id를 가리킨다.
➡️todo.id는 updateTodoList 함수에서 전달받은 todo객체의 id를 가리킨다.
따라서 이 둘이 같으면 현재 TodoBox컴포넌트의 리스트의 title을
새롭게 받아온 todo객체의 title로 바꿔주는 코드를 의미
▶️실습 - TodoBox컴포넌트 : updateTodoList() = 수정 (리렌더링 O)
// TodoBox 컴포넌트
// 3. 할 일 수정 (리렌더링 O)
const updateTodoList = (todo) => {
const updating = todoList.map((item) => {
item.id === todo.id ? {...item, title:todo.title} : item;
});
setTodoList(updating);
}
- 할 일 수정 후 리렌더링이 필요할때의 코드
- item.id === todo.id ? {...item, title:todo.title} : item;
➡️삼항연산자를 활용해 item.id === todo.id 가 일치한다면 {...item, title:todo.title} 수행 - {…item, title:todo.title}
➡️조건식 (item.id === todo.id)가 true일때 실행하는 문장이다.
...item는 기존 배열의 요소들을 updating에 저장하고
title:todo.title에서 title은 todo.title로 갱신하는 기능을 한다. - item
➡️조건식 (item.id === todo.id)가 false일때 실행하는 문장이다.
수정할 id가 기존 배열에 존재하지 않을때 기존 배열만 그대로 updating에 저장 - setTodoList(updating);
➡️useState의 set함수를 이용해 위의 조건으로부터 수정되었던 배열(updating)을 업데이트 시킨다.
▶️실습 - 삭제 기능 적용
TodoBox컴포넌트
// TodoBox컴포넌트
return (
<div>
<TodoInput addTodo={addTodoList} />
<TodoList
todoList={todoList}
deleteHandler={deleteTodoList}
updateHandler={updateTodoList}
/>
</div>
);
- TodoBox 컴포넌트의 <TodoList>컴포넌트가 담긴 부분에서 deleteHandler와 updateHandler에 각각의 함수를 전달
➡️TodoList 컴포넌트에서 deleteHandler와 updateHandler를 props로 전달받아 사용 가능
TodoList컴포넌트
// TodoList컴포넌트
const TodoList = ({ todoList, deleteHandler, updateHandler}) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}>{toDo.title}</li>
))}
</ul>
);
};
export default TodoList;
- const TodoList = ( { todoList, deleteHandler, updateHandler } ) => { ... }
➡️props로 "데이터 배열"과 deleteHandler, updateHandler를 전달받음
하지만 이렇게되면 TodoList 컴포넌트에서 해야하는 기능이 많아질 수 있으므로 “컴포넌트 분리”를 고려
Todo 컴포넌트로 기능 분리
const Todo = () => {
return(
);
}
export default Todo;
- return은 <div>처럼 묶여있어야 사용할 수 있는데
이 경우 Todo컴포넌트를 다른 컴포넌트에서 사용하게될때 <div>가 묶인 상태로 전달받게되므로
<div>로 한번 더 감싸져 원치않는 결과가 나올 수 있다. - ➡️해결방법 : Fragment (<> </>) 사용
return문에서 묶는 기능을 하면서도 태그가 전달되진 않아 원하는 결과를 얻을 수 있다.
▶️실습 - Todo 컴포넌트 : 삭제 버튼 만들기
const Todo = ({todo, deleteHandler}) => {
return(
<>
{todo.title}
<button onClick={() => deleteHandler(todo.id)}>삭제</button>
</>
);
}
export default Todo;
- {todo.title}
➡️TodoList컴포넌트로부터 받아온 todo객체으로부터 title을 만들어줌 - onClick={ () => deleteHandler(todo.id) }
➡️props로 받아온 deleteHandler함수 안에 todo.id를 넘겨준다. (TodoBox컴포넌트까지 전달됨)
❓onClick = {deleteHandler(todo.id)}
onClick={ () ⇒ deleteHandler(todo.id)} 의 차이점
➡️실행 시점의 차이이다.
- deleteHandler(todo.id)
함수 호출 결과를 `onClick`에 전달하는 방식으로 "컴포넌트가 렌더링되는 시점에 함수가 즉시 실행"된다.
따라서 deleteHandler(todo.id)의 반환값이 onClick에 할당되므로 이벤트와는 상관없이 "렌더링 중에 함수가 실행"
- onClick={() => deleteHandler(todo.id)}
람다 함수를 사용해 deleteHandler(todo.id) 함수를 호출할 수 있다.
onClick이벤트가 발생했을때 "() 람다함수"가 실행되어 deleteHandler(todo.id)를 호출하는 것이기때문에
컴포넌트가 렌더링되는 시점에는 실행되지 않고, onClick 이벤트가 발생할때 deleteHandler()함수가 실행된다.
즉 람다 함수 ( () => )를 사용해야 이벤트가 발생할 때 원하는 함수가 실행된다.
▶️실습 - TodoList 컴포넌트 : 삭제 기능을 Todo컴포넌트에 적용
import Todo from "./Todo";
const TodoList = ({ todoList, deleteHandler, updateHandler}) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}><Todo todo={toDo} deleteHandler={deleteHandler}/></li>
))}
</ul>
);
};
export default TodoList;
- TodoList 컴포넌트에서는 {toDo.title}부분을 <Todo/>컴포넌트로 대체가능
➡️(Todo컴포넌트에서 <> 버튼 </>을 return하게 되므로) - 속성으로는 map()을 통해 관리한 값인 toDo가 담겨있는 todo 속성을 전달
- deleteHandler={deleteHandler};
➡️ToboBox컴포넌트로부터 받아온 deleteHandler를 다시 Todo컴포넌트로 넘겨주는 역할을 한다.


- 삭제 버튼을 클릭하면 deleteHandler가 발생하여
TodoBox컴포넌트의 리스트가 수정되는 것까지 구현
▶️실습 - Todo 컴포넌트 : 수정 버튼 만들기
- 요청이 2개일 것이다. “수정할 폼(틀)” 요청과 “수정한 값으로 전달”하는 요청을 수행해야한다.
ex. 마치 로그인에서 “로그인 폼(틀)” 요청과 “로그인 요청” 두개 인 것과 유사하다.

- 위의 그림에서 위에 있는 수정버튼은 “수정해달라”라는 요청이고
밑에 있는 수정버튼은 “수정 폼”을 요청하는 요청이다.
▶️실습 - TodoList 컴포넌트 : 수정 기능을 Todo컴포넌트에 적용
import { useState } from "react";
const Todo = ({ todo, deleteHandler }) => {
const [updateMode, setUpdateMode] = useState(false); // 기본값은 false
return (
<>
{todo.title}
<button onClick={() => deleteHandler(todo.id)}>삭제</button>
<button>수정</button>
</>
);
};
export default Todo;
- 수정폼 요청 부분과과 수정 요청 부분을 추가
- const [updateMode, setUpdateMode] = useState(false);
➡️updateMode를 useState로 상태를 관리하여 updateMode가 true바뀌면 수정폼이 보여지게끔 구현

- 수정버튼이 클릭되면 수정 폼이 요청되어 수정폼을 보여줄 수 있도록 하는 기능을 추가 구현해야한다.
➡️updateMode를 true로 바꿔주어 수정 폼이 보여지는 기능 필요
❓<button onClick={updateModeHanlder}>수정</button> 호출 방식에 대하여
삭제 기능을 구현할때 { () => deleteHandler(todo.id) } 처럼 람다를 활용한 방식과 호출방식이 똑같다.
그 이유는 updateModeHandler에 따로 매개변수를 전달하지 않기 때문에, 별도의 람다함수(() =>)가 필요없는 것이다.
따라서 <button onClick={updateModeHandler}>는 이벤트가 발생할때만 이벤트핸들러가 실행되므로
람다 함수 없이도 적절하게 동작한다.
onChange 속성
- 리액트에서 <input> 에 value속성을 설정했을때,
onChange 속성을 제공하지 않으면 "읽기 전용"으로 판단되기때문에 onChange속성을 적용해주어야한다. - 리액트에서 value 속성이 설정되면 "제어 컴포넌트"로 판단되어 입력 값이 부모 컴포넌트의 상태에 의해 제어된다.
- 💡따라서 onChange 속성을 추가하여 입력 값을 상태로 관리해야한다.
▶️실습 - TodoList컴포넌트 :
TodoBox컴포넌트에서 받은 updateHandler를 Todo컴포넌트에 다시 전달
import Todo from "./Todo";
const TodoList = ({ todoList, deleteHandler, updateHandler }) => {
return (
<ul>
{todoList.map((toDo) => (
<li key={toDo.id}>
<Todo
todo={toDo}
deleteHandler={deleteHandler}
updateHandler={updateHandler}
/>
</li>
))}
</ul>
);
};
export default TodoList;
- <Todo todo={toDo} deleteHandler={deleteHandler} updateHandler={updateHandler}
➡️TodoBox컴포넌트로부터 받아온 updateHandler를 Todo컴포넌트로 다시 전달
Todo컴포넌트
import { useState } from "react";
const Todo = ({ todo, deleteHandler, updateHandler }) => {
//...
const [title, setTitle] = useState(todo.title);
const updateModeHanlder = () => {
setUpdateMode(true);
console.log("updateMode가 " + updateMode + "입니다.");
};
// ...
export default Todo;
- const [title, setTitle] = useState(todo.title);
➡️초기값이 todo.title인 이유는 Todo컴포넌트가 TodoList컴포넌트로부터 todo 객체를 입력받아왔으므로
받아온 todo객체의 title을 수정하겠다고 명시해주어야하므로 todo객체의 title을 초기값으로 설정
▶️실습 - Todo컴포넌트 : onChange 적용
const changeHandler = (e) => {
setTitle(e.target.value);
};
if (updateMode) {
// 수정폼을 활성화시켰을때 (true)
return (
<>
<input type="text" value="수정할 값" onChange={changeHandler} />
<button>수정</button>
</>
);
}
- onChange를 이용하여 (e)이벤트가 발생했을때 수행할 changeHandler 이벤트를 요청
onChange의 코드를 onChange={(e) => setTitle(e.target.value)} 로 바꿔줄 수도 있다. - setTitle(e.target.value)
➡️changeHandler 함수에서 이벤트가 발생한 객체(e.target)의 값(.value)으로 현재 title을 바꿔주는 코드
⚠️setTitle()을 통해 Todo컴포넌트에서만 title이 수정되었기때문에
TodoBox컴포넌트의 "데이터 배열"까지는 수정된 title이 전달되지 않는다.
▶️실습 - Todo컴포넌트
import { useState } from "react";
const Todo = ({ todo, deleteHandler, updateHandler }) => {
// 수정폼, 수정
const [updateMode, setUpdateMode] = useState(false); // 기본값은 false
// updateMode가 true바뀌면 수정폼이 보여지게끔 구현 가능
const [title, setTitle] = useState(todo.title);
const updateModeHanlder = () => {
setUpdateMode(true);
console.log("updateMode가 " + updateMode + "입니다.");
};
const changeHandler = (e) => {
setTitle(e.target.value);
};
const updateValue = () => {
updateHandler({ id: todo.id, title: title });
setUpdateMode(false); // 다시 false로 바꿔주어 수정 폼을 비활성화 시킴
};
if (updateMode) {
// 수정폼을 활성화시켰을때 (true)
return (
<>
<input type="text" value="수정할 값" onChagne={changeHandler} />
<button onClick={updateValue}>수정</button>
</>
);
}
return (
<>
{todo.title}
<button onClick={() => deleteHandler(todo.id)}>삭제</button>
<button onClick={updateModeHanlder}>수정</button>
</>
);
};
export default Todo;
- const Todo = ({ todo, deleteHandler, updateHandler})
➡️TodoList 컴포넌트로부터 이 속성들을 전달받는다. (이 속성들은 TodoBox컴포넌트로부터 전달되었다) - const [title, setTitle] = useState(todo.title);
➡️todo객체의 title 속성으로 초기값을 설정해주는데
이 값은 TodoBox컴포넌트에서 관리되고 있는 리스트의 title속성이 가져와지는 것이다.
➡️id값은 primary key (기본키)로 고유한 값을 가지기때문에 title만 바꿔주어 데이터를 관리한다. - const updateValue = () ⇒ { ... }
➡️활성화된 수정폼 if문안에서 버튼 안에 onClick으로 작성된 이 이벤트핸들러는
다시 TodoBox컴포넌트 리스트를 수정할 수 있도록 TodoBox컴포넌트까지 수정된 값을 보내는 기능 수행updateHandler() 에 id와 title값을 전달한다. 이 updateHandler()는 TodoBox컴포넌트로부터 전달된 함수이다.
- setUpdateMode(false);
➡️다시 false로 바꿔주는 기능을 하여 수정이 끝나면 수정폼을 다시 닫아주는 기능
▶️실습 - TodoBox컴포넌트 적용
// 3. 할 일 수정
const updateTodoList = (todo) => {
todoList.map((item) => {
if (item.id === todo.id) {
item.title = todo.title;
}
});
};
- 함수의 인자로는 (todo)를 받고 있다. 이는 객체이므로 updateHandler()안에는 객체를 전달하게된다.



- 수정이 잘 되고 있는 것을 확인할 수 있다.
- 이 후 자바스크립트의 express 서버 등을 활용하여 백엔드와 프론트엔드를 연결하는 것도 가능하다.
Express회고는 별도의 회고로 작성해야겠다!
🚀 회고를 통해 TodoBox컴포넌트로부터 Todo컴포넌트까지의 프로젝트 흐름 등을 깨달을 수 있었고
구조적으로는 이해할 수 있었지만
Todo리스트를 처음부터 다시 설계해보려할때 이 과정을 생각해낼 수 있는지가 관건인것 같다.
비슷한 프로젝트를 진행하여서 익숙하게 만드는 방법 밖에 없을 것 같다!
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_31일차_"리액트 useEffect, Memo프로젝트" (1) | 2025.01.15 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_30일차_"리액트 Express" (0) | 2025.01.14 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_29일차_"useState, useRef" (0) | 2025.01.13 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_28일차_"리액트 React" (1) | 2025.01.10 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_27일차_"자바스크립트 이벤트" (0) | 2025.01.09 |