저번 포스팅에 이어 이번에는 삭제 기능을 구현해보기로 했다.
맨 아래 컴포넌트부터 위로 올라가보자.
TodoItem 컴포넌트는 입력값 데이터(입력한 문자열과 id가 들어있음)를 map으로 하나씩 화면에 표시하는 컴포넌트이다. 아래와 같이 id와 text로 이루어져 있다.
TodoInput 컴포넌트에서 선언했고 상위의 Todo 컴포넌트로 올려보내서 다시 TodoList 컴포넌트로 내려보냈다. 그리고 여기서 다시 TodoItem 컴포넌트로 내려보냈다.
const enteredGoalArray = {
id: Math.random(),
text: enteredGoal
}
다른 얘기지만 참고로 리액트 베타 문서를 보면 map()으로 루프를 돌 때 즉석에서 key를 랜덤하게 생성하지 말라고 한다.
- 위에서 한 것처럼 미리 항목을 삽입할 때 id를 생성하는 용도로 Math.random()을 쓰는 게 나쁘지는 않다고 함
- 하지만 결과값의 분포가 고르게 퍼져 있지 못하여 실용적인 케이스에서의 id 생성용으로는 추천되지 않는다고 함
하나의 투두를 클릭하면 onDelete 함수에 해당 투두의 id를 인자로 준다. 그럼 props로 받은 onDelete 함수에 매개변수로 id를 전달하여 실행한다.
// TodoItem.js
import styles from './TodoItem.module.css'
const TodoItem = (props) => {
const onDelete = (id) => props.onDelete(id);
return (
<div className={styles.items}>
{props.item.map((todo) => (
<div
className={styles.item}
key={todo.id}
onClick={() => onDelete(todo.id)}>{todo.text}
</div>
))}
</div>
);
};
export default TodoItem;
props.onDelete(id)를 실행하기 위해 상위 컴포넌트 TodoList에 있는 함수 onDelete가 실행된다. 여기서도 다시 상위에 있는 컴포넌트 Todo의 onDelete 함수에 id를 인자로 전달한다.
// TodoList.js
import TodoItem from '../TodoItem/TodoItem';
import styles from './TodoList.module.css';
const TodoList = (props) => {
const onDelete = (id) => props.onDelete(id);
return (
<div className={styles.container}>
<TodoItem item={props.item} onDelete={onDelete}/>
</div>
);
};
export default TodoList;
드디어 Todo 컴포넌트에 도달했다! 이곳의 onDelete 함수를 실행하기 위해 이렇게 데이터를 끌어올린 것이다.
왜 굳이 Todo에서 onDelete 함수를 실행시키려고 했냐면 Todo 컴포넌트에 TodoInput 컴포넌트와 TodoList 컴포넌트가 같이 있기 때문이다.
TodoInput에서 입력받은 데이터들을 이곳에서 state인 displayInputs로 관리하고 있고, 이를 업데이트하는 함수 setDisplayInputs 또한 이곳에서 관리하고 있다.
onDelete 함수에서 setDisplayInputs를 써서 현재 투두리스트 목록을 업데이트하고 있기 때문에, 투두리스트 목록을 관리하는 이곳에서 onDelete 함수를 선언하는 것이 좋을 것 같다고 생각했다.
// Todo.js
import { useState } from 'react';
import TodoInput from '../TodoInput/TodoInput';
import TodoList from '../TodoList/TodoList';
import styles from './Todo.module.css';
const Todo = () => {
const [displayInputs, setDisplayInputs] = useState([]);
const onSaveGoal = (goal) => {
setDisplayInputs([...displayInputs, goal]);
}
const onDelete = (id) => {
setDisplayInputs(displayInputs.filter((todo) => todo.id !== id));
}
return (
<div className={styles.container}>
<h1 className={styles.title}>목표를 이루기 위해 <br/>해야 할 것들을 적어주세요!</h1>
<TodoInput onSaveGoal={onSaveGoal} />
<TodoList item={displayInputs} onDelete={onDelete} />
</div>
);
};
export default Todo;
여기서 onDelete 함수 부분만 자세히 봐보자. 가장 하위에 있는 컴포넌트였던 TodoItem 컴포넌트에서부터 클릭한 투두의 id를 onDelete 함수에 인자로 보냈었다.
그래서 현재의 모든 투두리스트 목록을 가지고 있는 displayInputs를 filter 함수로 필터링하여 setDisplayInputs 함수로 투두리스트 목록을 업데이트한다.
투두리스트 하나의 id가 onDelete 함수에 인자로 전달된 id(클릭된 투두의 id)와 같지 않은 것만 남긴다. 즉, 같은 것은 삭제된다. 이 상태가 setDisplayInputs로 업데이트되어 결론적으로 displayInputs라는 투두리스트 목록이 클릭된 투두 데이터가 삭제된 상태로 업데이트된다.
const onDelete = (id) => {
setDisplayInputs(displayInputs.filter((todo) => todo.id !== id));
}
TodoItem 컴포넌트의 onClick 부분에서 꽤 오랜 시행착오를 겪었다.
// TodoItem.js
import styles from './TodoItem.module.css'
const TodoItem = (props) => {
const onDelete = (id) => props.onDelete(id);
return (
<div className={styles.items}>
{props.item.map((todo) => (
<div
className={styles.item}
key={todo.id}
onClick={() => onDelete(todo.id)}>{todo.text}
</div>
))}
</div>
);
};
export default TodoItem;
여기서 onClick={() => onDelete(todo.id)}
이 부분을 처음에 onClick={onDelete(todo.id)}
라고 썼었다.
그랬더니 Warning: Cannot update a component (Todo) while rendering a different component (TodoItem). To locate the bad setState() call inside TodoItem, follow the stack trace as described in https://reactjs.org/link/setstate-in-render at TodoItem ~
이라는 오류가 났다.
다른 컴포넌트를 렌더링하는 동안 해당 컴포넌트를 렌더링할 수 없다고 한다. 이게 무슨 뜻일까?
일단 오류메시지를 검색해보니까 useEffect를 사용하라고 한다. 그런데 아직 배운 적이 없고.. 강의에서 삭제 기능 구현했을 때는 useEffect를 안쓰고 만들었어서 더 찾아봤다.
컴포넌트를 렌더링 하는 동안 함수를 호출했기 때문에 에러가 난 것이라고 한다. 그래서 onClick={onDelete(todo.id)}
대신 onClick={() => onDelete(todo.id)}
이런식으로 쓰라고 한다.
하지만 왜 이렇게 해야 하는지 전혀 이해가 가지 않았고, 이렇게 고쳤었는데도 다른 오류와 합쳐져서 또다른 오류가 났었기 때문에 이게 해결책인줄 몰랐다😂 그리고 지인에게 질문을 한 뒤에야 이것이 문제였다는 걸 알게되었다!
저 둘은 무엇이 다를까? 일단 이렇게 설명을 들었다.
onClick={onDelete(todo.id)}
: 함수를 호출하는 것onClick={() => onDelete(todo.id)}
: 함수 표현식을 넘기는 것onClick={onDelete(todo.id)}를 하면 onClick에 onDelete를 호출한 결과가 담기는 것이니까 onClick이 실행되지 않아도 onDelete가 실행되는 것일까? 그래서 의도대로 클릭될 때마다 onDelete가 실행되는 것이 아니라 컴포넌트가 렌더링될 때(아직 클릭되지 않았을 때) onDelete가 실행되는 것일까?
그런데 컴포넌트를 렌더링하는 동안 함수를 호출하면 왜 에러가 날까?
onClick={() => onDelete(todo.id)}와 같이 함수 표현식을 넘기는 방법이 어떻게 동작하는지 잘 이해가 가지 않는다.
커뮤니티에 다시 질문을 했고 자세한 답변을 얻을 수 있었다.
onClick={onDelete(todo.id)}를 하면 onClick에 onDelete를 호출한 결과가 담기는 것이니까 onClick이 실행되지 않아도 onDelete가 실행되는 것일까? 그래서 의도대로 클릭될 때마다 onDelete가 실행되는 것이 아니라 컴포넌트가 렌더링될 때(아직 클릭되지 않았을 때) onDelete가 실행되는 것일까?
👉 내가 이해한 것이 맞다!
그런데 컴포넌트를 렌더링하는 동안 함수를 호출하면 왜 에러가 날까?
👉 컴포넌트를 렌더링하는 동안 함수를 호출한다고 에러가 나는 게 아님! 컴포넌트를 렌더링하는 동안 setState를 호출하면 에러가 나는 것이다. 왜냐하면 setState를 호출하면 렌더링을 다시 하는데(해당 setState가 포함되어 있는 컴포넌트를 다시 그린다는 뜻. 즉 컴포넌트에 있는 코드를 처음부터 다시 실행한다.) 렌더링 중에 setState를 다시 만나면 또다시 렌더링을 하게 되고, 무한으로 setState를 호출하게 되기 때문에 리액트가 에러를 내는 것!
👉 나의 경우 컴포넌트를 렌더링하는 과정에서 onDelete가 호출되게 되고, 이는 상위 컴포넌트로 올라가 아래처럼 setState를 호출하게 된다. 그럼 setState을 통해 렌더링을 다시 하고, 그럼 컴포넌트가 처음부터 실행이 되고, 그럼 또 setState를 만나 다시 렌더링되고.. 를 무한 반복하여 에러가 났던 것이다.
👉 그래서 컴포넌트를 렌더링하는 동안 setState를 호출하면 안 된다.
const onDelete = (id) => {
setDisplayInputs(displayInputs.filter((todo) => todo.id !== id));
}
onClick={() => onDelete(todo.id)}와 같이 함수 표현식을 넘기는 방법이 어떻게 동작하는지 잘 이해가 가지 않는다.
👉 () => onDelete(todo.id)는 그저 함수이다. (화살표 함수에 대한 개념이 잘 안 잡혀 있어서 생소했고 몰랐음. 화살표 함수 기본 에서 개념을 다시 공부하자)
그저 () => onDelete(todo.id)는 함수였던 것이다. 그리고 내가 여태껏 이벤트 속성에 넘겼던 값은 모두 함수였다. onClick={titleChangeHandler}와 같이! (함수 외의 다른 걸 넘기면 작동을 안 한다고 함)
👉 onClick={() => onDelete(todo.id)}
는 다르게 말하면 아래와 같다.
const itemClickHandler = () => {
onDelete(todo.id)
}
// ...
onClick={itemClickHandler}
👉 그렇다면 왜 지금껏 하던대로 onClick={onDelete}라고 하면 안되는가?
왜냐하면 todo.id도 함께 넘겨야 하기 때문이다. 위에서 itemClickHandler와 같은 함수를 따로 만들지 않았으니 화살표 함수로 그냥 {} 안에 함수를 넣어버린 것! 그럼 따로 함수를 만들지 않고도 todo.id를 넘길 수 있다.
삭제 기능 구현에 성공했다!!
화살표 함수를 몰라서 엄청 헤맸던 시간이었다... 예전에도 화살표 함수를 이해 못하겠어서 자바스크립트를 포기하려고 했던 것 같은데🤣 문서로 공부한다고는 했지만 이렇게 실제로 만나보지 못하면 제대로 알 수 없나보다. 당시에 공부도 하고 정리도 했지만 이론과 실제는 다르다는 걸(?) 절실하게 알 수 있었다.. 그리고 이벤트 속성에 항상 함수를 넘기고 있었다는 것도 이전까지 인지하지 못했다. 그래서 알고 굉장히 신기했다. 지금껏 그걸 모르고 함수만을 계속 넘기고 있었다니.. 삽질하고 질문하고 답변받은 것에 굉장히 감사하다! 많은 것을 알아갈 수 있었다.