리액트에서 useState의 setter 함수를 어느 위치에 있는 컴포넌트에서 사용할지 항상 헷갈리는 것 같다. 최근 투두리스트를 만들면서도 헷갈렸는데, 오늘 코드를 리팩토링하며 useState의 setter 함수 선언 위치를 바꾼 내용을 포스팅해보려고 한다.
투두리스트의 구조는 아래와 같다. 각 투두는 TodoItem이라는 컴포넌트 안에 있고, TodoItem 컴포넌트들을 TodoList 컴포넌트가 감싸고 있다. 즉 TodoList 컴포넌트의 하위에 TodoItem 컴포넌트가 존재한다.
상위 컴포넌트인 TodoList에는 아래와 같이 서버에서 가져온 투두리스트 정보를 저장하는 상태인 todoList와 setter 함수 setTodoList가 있다. 이 setter 함수가 사용되는 곳은 총 세군데로, 투두를 등록/수정/삭제하는 함수에서 선언된다.
const [todoList, setTodoList] = useState<TodoList | null>(null);
리팩토링 전에는 TodoList 컴포넌트에 handleEdit, handleSubmit 함수가 있었고 하위의 TodoItem 컴포넌트에 handleDelete 함수가 있었다.
handleDelete 함수는 아래와 같으며 내부에서 setter 함수가 사용된다.
const handleDelete = async (id: Todo["id"]) => {
await deleteTodo(id);
const response = await getTodos();
setTodoList(response);
setIsDeleteModalOpen(false);
};
하지만 생각해보면 하위 컴포넌트에서 setter 함수를 사용할 이유가 없었다. 왜냐하면 투두 하나를 나타내는 TodoItem 컴포넌트는 본인이 맡은 투두만 알면 되기 때문이다.
a라는 투두가 삭제된다면 a를 포함하는 전체 투두리스트에서 a를 삭제하는 것이 맞지, a 안에서 전체 투두리스트를 변경하는 것은 맞지 않다는 생각이 들었다. 그래서 전체 투두를 가진 TodoList 컴포넌트에서 TodoItem을 업데이트해주도록 수정했다.
사실 투두리스트에 대한 모든 변경을 상위 컴포넌트에서 해주고 있는데, 삭제에 관한 부분만 하위 컴포넌트에서 하는게 이상하다고 느껴지긴 했다.
하지만 그럼에도 왜 그렇게 했는지를 생각해보면, 모달 컴포넌트 때문인 것 같다. 위에서는 간단하게 설명하기 위해 TodoList와 TodoItem 컴포넌트만 설명했지만, DeleteModal이라는 컴포넌트가 더 있었다.
DeleteModal은 TodoItem에서 삭제 버튼 클릭시에 띄워주는 삭제 확인 모달이다. 여기서 확인 버튼을 눌러야 실제로 삭제가 되는데, 모달 컴포넌트 안에서 확인 버튼의 onClick 이벤트 핸들러로 바로 handleDelete 함수를 넣어줬었다. 직관적으로는 이게 맞는데, 생각해보니 id를 상위 컴포넌트로 올려서 TodoList에서 처리하는 게 더 적절한 것 같아 리팩토링하게 되었다.
오늘 리팩토링을 하고 아래의 궁금증이 생겼다.
useState를 어느 컴포넌트에서 선언하고 setter 함수를 어떤 컴포넌트에서 사용할지는 항상 고민이 되는 것 같다. 리액트를 사용하는 오픈소스 코드를 많이 읽어서 익혀야겠다는 생각을 했다!
😎 피드백:
- 미성숙한 최적화는 안하는 것보다 못하기 때문에 렌더링 적게 되는 걸 너무 의식하지 않는 게 좋다.
- 직관적으로 읽기 좋고, 의미에 맞는 코드를 짜고, 그게 실제로 성능에 영향을 끼친다는 걸 프로파일링을 통해 깨달아 냈을 때에야 최적화 하는게 맞다.
공감하며 읽었습니다. 좋은 글 감사드립니다.