지난 글에서는 fetch의 PATCH 메서드를 활용해 유저들의 데이터를 수정하는 기능을 구현했다.
다음 단계는 삭제하기(DELETE)로, CRUD 중 마지막 작업이다.
삭제 기능도 수정하기와 마찬가지로 선택된 여러 유저를 한 번에 삭제하기와 개별 유저만 삭제하기 두 가지로 나눠져 있다.
fetch의 DELETE 메서드는 삭제 대상 리소스의 식별자(id)를 URL에 포함해 호출하는 것만으로 충분한 경우가 많다는 점에서, 이전에 구현했던 POST나 PATCH 작업보다 상대적으로 단순한 편이다.
reqres.in API 문서를 보면, DELETE /api/users/{id} 요청이 성공했는지 여부는 응답 status 코드가 204인지로 판단하면 된다고 나와 있다.

우리 프로젝트에서는 앞서 구현한 수정 기능과 동일하게,
두 가지 기능으로 나눠서 처리한다.
따라서 API 함수도 각각 별도로 만들고, 다수 삭제의 경우 Promise.all을 사용해 여러 DELETE 요청을 동시에 보낼 계획이다.
// src/api/users.api.ts
export const deleteUserApi = async (id: User['id']) => {
const response = await fetch(`https://reqres.in/api/users/${id}`, {
method: 'DELETE',
headers: {
'x-api-key': 'MY_API_KEY',
},
})
if (!response.ok) throw Error('유저 데이터를 삭제할 수 없습니다.')
const isSuccess = response.status === 204 ? true : false
return isSuccess
}
export const deleteSelectedUsersApi = async (ids: User['id'][]) => {
const responses = await Promise.all(
ids.map((id) =>
fetch(`https://reqres.in/api/users/${id}`, {
method: 'DELETE',
headers: {
'x-api-key': 'MY_API_KEY',
},
}),
),
)
const isError = responses.some((res) => !res.ok)
if (isError) throw Error('유저 데이터를 삭제할 수 없습니다.')
const isAllSuccess = !(
await Promise.all(
responses.map((res) => (res.status === 204 ? true : false)),
)
).includes(false)
return isAllSuccess
}
deleteUserApi는 단일 유저의 id를 받아 DELETE /users/{id} 요청을 보낸다.ok가 아니면 에러를 던지고, status === 204이면 삭제 성공으로 판단해 true를 반환한다.deleteSelectedUsersApi는 여러 id를 받아 Promise.all을 이용해 여러 삭제 요청을 동시에 실행하고, 모든 응답이 204일 때만 전체 성공으로 처리한다.// src/hooks/useUsersQuery.ts
export function useUsersQuery() {
const [users, setUsers] = useState<User[]>([])
const [error, setError] = useState<string>('')
const deleteUser = useCallback(
async (id: User['id']) => {
setError('')
try {
const isSuccess = await deleteUserApi(id)
if (isSuccess) {
setUsers((prev) => prev.filter((user) => user.id !== id))
}
} catch (err) {
setErrorMessage(err, '유저 데이터를 삭제할 수 없습니다.')
}
},
[setErrorMessage],
)
const deleteSelectedUsers = useCallback(
async (ids: User['id'][]) => {
setError('')
try {
const isAllSuccess = await deleteSelectedUsersApi(ids)
if (isAllSuccess) {
setUsers((prev) => prev.filter((user) => !ids.includes(user.id)))
}
} catch (err) {
setErrorMessage(err, '유저 데이터를 삭제할 수 없습니다.')
}
},
[setErrorMessage],
)
return {
users,
deleteUser,
deleteSelectedUsers,
error,
}
}
deleteUser는 단일 유저 삭제에 성공한 경우, 클라이언트 상태에서 해당 유저를 제거한다.deleteSelectedUsers는 선택된 유저 여러 명이 모두 삭제에 성공한 경우에만, 해당 id들을 가진 유저들을 한 번에 제거한다.둘 다 reqres가 mock API라 실제 DB는 존재하지 않고, 클라이언트에서 users 상태를 직접 조정해 리스트를 갱신한다.
이렇게 만든 삭제용 훅도 이전과 동일하게 UsersContainer로 넘겨 사용한다.
// src/features/users/containers/UsersContainer.tsx
export default function UsersContainer() {
const { users, deleteUser, deleteSelectedUsers } = useUsersQuery()
return (
<UsersProvider
users={users}
onDeleteUser={deleteUser}
onDeleteSelectedUsers={deleteSelectedUsers}
>
{/* ... */}
</UsersProvider>
)
}
API 함수를 설계할 때, 이미 다음과 같은 기준으로 역할을 나눠 두었다.
User['id'][] 형태의 id 배열을 받아 일괄 삭제id만 받아 해당 유저만 삭제UI를 만들면서도 이미 삭제 기능에 필요한 준비는 해 둔 상태다.
id가 모인 배열id를 인자로 넘기는 로직따라서 이제 해야 할 일은, 이미 준비된 이 값들을 우리가 만든 삭제 함수에 연결해 주는 것뿐이다.
// src/features/users/context/UsersProvider.tsx
const onClickDeleteSelectedItems = useCallback(async () => {
if (checkedDeleteItemsRef.current.length === 0) {
// 선택된 데이터가 없을 때
alert('선택한 데이터가 없습니다.')
} else {
if (isCheckedDeletingRef.current) return
const names = checkedDeleteItemsRef.current
.map((id) => initialBuiltAllUsersValue[id])
.filter(Boolean)
.map((u) => `${u[`first_name_${u.id}`]} ${u[`last_name_${u.id}`]}`)
.join(', ')
const confirmMsg = `${names} 유저들을 삭제하시겠습니까?`
if (!confirm(confirmMsg)) return
try {
setisCheckedDeleting(true)
await onDeleteSelectedUsers(checkedDeleteItemsRef.current)
resetChecked()
alert('삭제를 완료하였습니다.')
} catch (err) {
console.error(err)
alert('삭제에 실패했습니다. 다시 시도해주세요.')
} finally {
setisCheckedDeleting(false)
setIsShowDeleteCheckbox(false)
}
}
}, [initialBuiltAllUsersValue, onDeleteSelectedUsers, resetChecked])
isCheckedDeletingRef.current) 중복 실행을 막는다.const onClickDeleteItem = useCallback(
async (id: User['id']) => {
if (isDeletingRef.current !== null) return
const target = initialBuiltAllUsersValue[id]
const confirmMsg = `${target[`first_name_${id}`]} ${target[`last_name_${id}`]}님의 데이터를 삭제하시겠습니까?`
if (!confirm(confirmMsg)) return
try {
setIsDeleting(id)
await onDeleteUser(id)
alert('삭제를 완료하였습니다.')
} catch (err) {
console.error(err)
alert('삭제에 실패했습니다. 다시 시도해주세요.')
} finally {
setIsDeleting(null)
}
},
[initialBuiltAllUsersValue, onDeleteUser],
)
isDeletingRef.current) 중복 실행을 막는다.DELETE 초기 작업은 아래 Git Branch 링크에서 확인할 수 있으며,
현재는 Main Branch에서 코드 리펙토링으로 많이 수정되었다.
feature/fetchGet Branch 바로가기
여기까지 GET, POST, PATCH, DELETE까지 모든 CRUD 기능을 구현해 보았다.
퍼블리셔로서 주로 UI 위주 작업만 해 왔지만, 이번 프로젝트를 통해 CRUD 전 과정을 직접 구현하면서 코드 흐름을 더 넓은 시야에서 이해하게 된 것 같다.
여기까지 모든 CRUD 기능을 구현했다. 테스트 결과 실제 기능면에서는 문제 없이 잘 구현이 된다.
하지만 코드에서는 여러 문제점이 발견되었는데, 다음 게시물에서는 지금까지 작업한 코드의 문제점을 분석하고 해결하기 위한 리펙토링 과정을 담을 예정이다.