[CRUD 실습 - React + Vite] DELETE (데이터 삭제하기)

Chan의 기술 블로그·2025년 12월 10일

산출물 링크
- GitHub
- 배포 페이지

지난 글에서는 fetchPATCH 메서드를 활용해 유저들의 데이터를 수정하는 기능을 구현했다.

다음 단계는 삭제하기(DELETE)로, CRUD 중 마지막 작업이다.
삭제 기능도 수정하기와 마찬가지로 선택된 여러 유저를 한 번에 삭제하기와 개별 유저만 삭제하기 두 가지로 나눠져 있다.


DELETE (데이터 삭제하기)

fetchDELETE 메서드는 삭제 대상 리소스의 식별자(id)를 URL에 포함해 호출하는 것만으로 충분한 경우가 많다는 점에서, 이전에 구현했던 POSTPATCH 작업보다 상대적으로 단순한 편이다.

reqres.in API 문서를 보면, DELETE /api/users/{id} 요청이 성공했는지 여부는 응답 status 코드가 204인지로 판단하면 된다고 나와 있다.

우리 프로젝트에서는 앞서 구현한 수정 기능과 동일하게,

  • 선택된 여러 유저를 한 번에 삭제하기
  • 개별 유저만 삭제하기

두 가지 기능으로 나눠서 처리한다.
따라서 API 함수도 각각 별도로 만들고, 다수 삭제의 경우 Promise.all을 사용해 여러 DELETE 요청을 동시에 보낼 계획이다.

API 함수 만들기

// 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일 때만 전체 성공으로 처리한다.

DELETE 요청을 수행하는 Custom Hook 만들기

// 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 기능을 구현했다. 테스트 결과 실제 기능면에서는 문제 없이 잘 구현이 된다.

하지만 코드에서는 여러 문제점이 발견되었는데, 다음 게시물에서는 지금까지 작업한 코드의 문제점을 분석하고 해결하기 위한 리펙토링 과정을 담을 예정이다.

profile
퍼블리셔에서 프론트앤드로 전향하기

0개의 댓글