어제 투두를 클릭하면 삭제되는 기능을 구현했었다! 그리고 오늘 ChatGPT를 이용하던 중, 눈에 들어오는 기능을 발견했다. 채팅방을 삭제하는데, 삭제 버튼을 누르면 따로 모달창을 띄우는 것이 아니라 삭제 버튼이 위치했던 요소 자체에 삭제 확인/삭제 취소 버튼이 생긴다. 굉장히 예뻐보였고, 그래서 만들고 싶어졌다. 만들어보기로 하자!
일단 기능을 만들기 전에 삭제 아이콘부터 삽입해보자.
아이콘은 Font Awesome
(https://fontawesome.com/)이라는 곳에서 찾았다.
리액트에서 Font Awesome 아이콘을 사용하는 방법은 다음과 같다.
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
faTrashCan
은 선택한 아이콘의 코드 부분에 쓰여있는 icon=""
안의 fa-trash-can
을 변형한 것이다. fa는 그대로 써주고, 그 다음부터 - 뒤의 소문자만 대문자로 바꾸어 써준다."@fortawesome/free-regular-svg-icons"
은 코드에 있는 "fa-regular"
를 의미하는 것이다. 만약에 "fa-regular"
가 아니라 "fa-solid"
라고 쓰여있다면 "@fortawesome/free-solid-svg-icons"
라고 써줘야 한다. solid인지, regular인지, light인지, thin인지 등은 동일한 아이콘의 스타일에 따라 달라진다.아이콘을 적용한 코드는 아래와 같다.
// TodoItem.js
import styles from './TodoItem.module.css'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
const TodoItem = (props) => {
const onDelete = (id) => props.onDelete(id);
return (
<div>
{props.item.map((todo) => (
<div className={styles.textAndDeteteBtn} key={todo.id}>
<div
className={styles.text}
onClick={() => onDelete(todo.id)}>{todo.text}
</div>
<button><FontAwesomeIcon icon={faTrashCan} size="2x"color="white"/></button>
</div>
))}
</div>
);
};
export default TodoItem;
잘 적용되었다!
이제 삭제 버튼을 누르면 삭제 버튼이 삭제확인/삭제취소 버튼으로 변경되도록 해봤다. 그리고 삭제확인 버튼을 누르면 눌린 요소가 삭제되고, 삭제취소 버튼을 누르면 다시 삭제 버튼으로 돌아가게 했다.
그러기 위해서 useState로 isDeleteClicked와 이를 업데이트하는 setIsDeleteClicked를 만들어 삭제 버튼이 눌렸는지 확인했다.
만약 눌렸으면 isDeleteClicked를 true로 만들어 삭제확인/삭제취소 버튼이 나오게 했고, 여기서 삭제취소 버튼을 누르면 isDeleteClicked를 false로 만들어 다시 삭제 버튼이 나오게 했다.
조건부 렌더링을 위해 삼항 연산자를 사용했다.
// import React from 'react'
import styles from './TodoItem.module.css'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useState } from 'react';
const TodoItem = (props) => {
const [isDeleteClicked, setIsDeleteClicked] = useState(false);
const onDelete = (id) => {
props.onDelete(id);
}
const openDelete = () => {
setIsDeleteClicked(true)
}
const cancelDelete = () => {
setIsDeleteClicked(false)
}
return (
<div>
{props.item.map((todo) => (
<div className={styles.textAndDeteteBtn} key={todo.id}>
<div className={styles.text}>{todo.text}
</div>
{isDeleteClicked ?
<div>
<button className={styles.delteOkIcon} onClick={() => onDelete(todo.id)}><FontAwesomeIcon icon={faCheck} size="2x" color="white"/></button>
<button className={styles.delteNoIcon} onClick={cancelDelete}><FontAwesomeIcon icon={faXmark} size="2x" color="white"/></button>
</div> :
<button className={styles.delteIcon} onClick={openDelete}><FontAwesomeIcon icon={faTrashCan} size="2x" color="white"/></button> }
</div>
))}
</div>
);
};
export default TodoItem;
완성되었다! 그런데 무언가 이상하다. 삭제 버튼을 누르면 다른 요소까지 동일하게 변경이 된다😂 이제 이걸 고쳐보자.
삭제 기능 자체는 컴포넌트 하나하나마다 잘 되는데 왜 삭제버튼을 누르면 모든 요소가 다 삭제확인/삭제취소 버튼으로 변경되는 걸까?
개발자 도구를 열어봤다. 일단 각 투두마다 state가 있어야 하는데, state가 하나뿐이다! TodoItem
컴포넌트에 state가 단 하나만 있다고 나오기 때문이었다!! TodoItem
컴포넌트 전체 범위에서가 아니라 각 버튼마다 state가 따로 있어야 하는데 그렇지 않은 것이 원인인 것을 알게 되었다.
map() 안에서 state를 변경했으니 map 안의 각 요소마다 각각 적용되어야 하는 것 아닌가? 하고 생각했으나, state의 범위는 state가 선언된 컴포넌트 전체라는 것이 떠올랐다. 그래서 삭제 버튼 위치에 해당하는 하위 컴포넌트 Buttons
를 하나 더 만들어 TodoItem
컴포넌트에서 분리했다.
// TodoItem.js
import Buttons from "./Buttons";
import styles from './TodoItem.module.css';
const TodoItem = (props) => {
const onDelete = (id) => {
props.onDelete(id);
}
return (
<div>
{props.item.map((todo) => (
<div className={styles.textAndDeteteBtn} key={todo.id}>
<div className={styles.text}>{todo.text}</div>
<Buttons onDelete={onDelete} todo={todo} />
</div>
))}
</div>
);
};
export default TodoItem;
// Buttons.js
import styles from './Buttons.module.css';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useState } from 'react';
const Buttons = (props) => {
const [isDeleteClicked, setIsDeleteClicked] = useState(false);
const openDelete = () => {
setIsDeleteClicked(true)
}
const cancelDelete = () => {
setIsDeleteClicked(false)
}
const onDelete = (id) => props.onDelete(id);
return (
<div>
{isDeleteClicked ?
<div>
<button className={styles.delteOkIcon} onClick={() => onDelete(props.todo.id)}><FontAwesomeIcon icon={faCheck} size="2x" color="white"/></button>
<button className={styles.delteNoIcon} onClick={cancelDelete}><FontAwesomeIcon icon={faXmark} size="2x" color="white"/></button>
</div> :
<button className={styles.delteIcon} onClick={openDelete}><FontAwesomeIcon icon={faTrashCan} size="2x" color="white"/></button>
}
</div>
);
};
export default Buttons;
그 결과, 각 요소마다 다른 상태가 적용되는 것을 알 수 있다!
개발자 도구로 봤을 때 Buttons 컴포넌트마다 state가 다르게 적용된 것을 확인했다.
그런데!! 생각해보면 컴포넌트 이름이 TodoItem인데 여기서 map을 하고 있었다. 그러니까 상위 컴포넌트인 TodoList에서 map을 하여 TodoItem에는 map 안에 있는 내용만 오는 것이 컴포넌트 이름에 더 부합할 것이라고 생각하여 코드를 수정했다.
// 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}>
{props.item.map((todo) => (
<TodoItem todo={todo} onDelete={onDelete}/>
))}
</div>
);
};
export default TodoList;
import styles from './TodoItem.module.css';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useState } from 'react';
const TodoItem = (props) => {
const [isDeleteClicked, setIsDeleteClicked] = useState(false);
const openDelete = () => {
setIsDeleteClicked(true)
}
const cancelDelete = () => {
setIsDeleteClicked(false)
}
const onDelete = (id) => {
props.onDelete(id);
}
return (
<div className={styles.textAndDeteteBtn} key={props.todo.id}>
<div className={styles.text}>{props.todo.text}</div>
{isDeleteClicked ?
<div>
<button className={styles.delteOkIcon} onClick={() => onDelete(props.todo.id)}><FontAwesomeIcon icon={faCheck} size="2x" color="white"/></button>
<button className={styles.delteNoIcon} onClick={cancelDelete}><FontAwesomeIcon icon={faXmark} size="2x" color="white"/></button>
</div>
: <button className={styles.delteIcon} onClick={openDelete}><FontAwesomeIcon icon={faTrashCan} size="2x" color="white"/></button>
}
</div>
);
};
export default TodoItem;
이 컴포넌트는 없앴다!
의도한대로 완성되었다~!!
일상 생활에서 마음에 든 요소를 직접 만들어보는 것은 정말 즐거운 것 같다!! 마지막에 오류가 있어서 고민하긴 했지만 state의 범위가 컴포넌트 전체라는 것을 며칠 전에 알아서(setState로 state를 업데이트하면 state가 속한 컴포넌트 전체가 리렌더링된다는 것) 비교적 쉽게 해결할 수 있어 뿌듯했다😊