PHP / MariaDB / 데이터베이스에서 데이터 삭제하기
PHP에서 MariaDB 행을 삭제할 때는 삭제 대상을 기본 키로 제한한 DELETE를 준비된 문장으로 실행합니다. 폼의 ID를 서버에서 검증하고 실제 삭제된 행 수를 확인해야 합니다. 아래 예제는 확인 체크를 한 POST 요청만 처리하며, 단순한 목록 조회나 GET 링크로 데이터를 삭제하지 않습니다.
별도의 삭제 실습 데이터 준비
PHP의 mysqli 확장과 MariaDB 서버, learning 데이터베이스 및 계정이 필요합니다. 연결 준비는 mysqli_connect() 예제를 참고하세요. 다음은 실제 직원과 무관한 삭제 연습용 데이터입니다. 운영 테이블이나 보존할 자료가 있는 테이블에서 실행하지 않습니다.
USE learning;
CREATE TABLE employees_delete_demo (
id INT NOT NULL PRIMARY KEY,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO employees_delete_demo VALUES
(1, '삭제 연습용'), (2, '보존 확인용');
계정에는 테이블 준비 단계의 CREATE·INSERT와 실습의 SELECT·DELETE 권한이 필요합니다. 실제 서비스에서는 필요한 최소 범위로 나누세요. 이미 같은 이름의 테이블이 있으면 지워서 덮어쓰지 말고 별도 실습 DB를 준비합니다. 다른 테이블의 외래 키로 연결된 기존 직원을 학습용으로 삭제하는 방식은 피해야 합니다.
실습 폴더에 database-config.php를 만들고 그 안에 public 하위 폴더를 만듭니다. 설정 파일은 public 밖에 둡니다. 아래 비밀번호 자리표시자와 계정·주소·포트를 실제 실습 값으로 바꾸고, 설정 파일을 공개하거나 버전 관리에 넣지 않도록 합니다.
<?php
return [
'host' => '127.0.0.1',
'username' => 'learning_user',
'password' => 'REPLACE_WITH_YOUR_PRIVATE_PASSWORD',
'database' => 'learning',
'port' => 3306,
];
목록과 삭제 폼의 전체 코드
다음 전체 코드를 public/employee-delete.php로 저장합니다. 실습 폴더에서 php -S 127.0.0.1:8000 -t public을 실행한 뒤 http://127.0.0.1:8000/employee-delete.php를 엽니다. 세션 쿠키를 허용하고 PHP 시작 태그 앞에는 출력이 없도록 하세요.
<?php
session_start(['cookie_httponly' => true, 'cookie_samesite' => 'Lax']);
header('Content-Type: text/html; charset=UTF-8');
if (!extension_loaded('mysqli')) {
http_response_code(500);
exit('mysqli 확장이 필요합니다.');
}
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
$message = $_SESSION['flash'] ?? '';
unset($_SESSION['flash']);
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
$id = null;
if ($isPost) {
$token = $_POST['csrf'] ?? null;
if (!is_string($token) || !hash_equals($_SESSION['csrf'], $token)) {
http_response_code(403);
exit('폼을 다시 열고 제출하세요.');
}
$rawId = $_POST['id'] ?? null;
$id = is_string($rawId) ? filter_var($rawId, FILTER_VALIDATE_INT,
['options' => ['min_range' => 1, 'max_range' => 2147483647]]) : false;
if ($id === false || ($_POST['confirm'] ?? null) !== '1') {
http_response_code(400);
exit('유효한 ID와 삭제 확인이 필요합니다.');
}
}
function escapeText($value): string {
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$rows = [];
$error = '';
$redirect = false;
$link = $stmt = null;
try {
$c = require dirname(__DIR__) . '/database-config.php';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$link = mysqli_connect($c['host'], $c['username'], $c['password'], $c['database'], $c['port']);
mysqli_set_charset($link, 'utf8mb4');
if ($isPost) {
$stmt = mysqli_prepare($link, 'DELETE FROM employees_delete_demo WHERE id = ?');
mysqli_stmt_bind_param($stmt, 'i', $id);
mysqli_stmt_execute($stmt);
$deleted = mysqli_stmt_affected_rows($stmt);
$_SESSION['flash'] = $deleted === 1
? "ID $id 행을 삭제했습니다."
: '삭제할 행이 없습니다. 이미 삭제되었을 수 있습니다.';
$_SESSION['csrf'] = bin2hex(random_bytes(32));
$redirect = true;
} else {
$result = mysqli_query($link, 'SELECT id, name FROM employees_delete_demo ORDER BY id LIMIT 20');
while ($row = mysqli_fetch_assoc($result)) $rows[] = $row;
mysqli_free_result($result);
}
} catch (Throwable $e) {
http_response_code(500);
error_log('Employee operation failed. Code: ' . $e->getCode());
$error = '처리하지 못했습니다. 서버 로그를 확인하세요.';
} finally {
if ($stmt instanceof mysqli_stmt) mysqli_stmt_close($stmt);
if ($link instanceof mysqli) mysqli_close($link);
}
if ($redirect) {
header('Location: employee-delete.php', true, 303);
exit;
}
?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>직원 삭제 실습</title>
</head>
<body>
<h1>직원 삭제 실습</h1>
<?php if ($message !== ''): ?><p><?= escapeText($message) ?></p><?php endif; ?>
<?php if ($error !== ''): ?>
<p><?= escapeText($error) ?></p>
<?php elseif ($rows === []): ?>
<p>표시할 직원이 없습니다.</p>
<?php else: ?>
<table>
<thead><tr><th>ID</th><th>이름</th><th>삭제</th></tr></thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<td><?= escapeText($row['id']) ?></td>
<td><?= escapeText($row['name']) ?></td>
<td><form action="employee-delete.php" method="post">
<input type="hidden" name="csrf" value="<?= escapeText($_SESSION['csrf']) ?>">
<input type="hidden" name="id" value="<?= escapeText($row['id']) ?>">
<label><input type="checkbox" name="confirm" value="1" required>이 행 삭제 확인</label>
<button type="submit">삭제</button>
</form></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</body>
</html>
목록의 ID 1은 삭제 연습용, ID 2는 보존 확인용입니다. ID 1 행의 확인 체크를 선택하고 그 행의 삭제 버튼을 누르면 완료 메시지와 함께 목록으로 돌아옵니다. ID 2는 그대로 있어야 합니다. 체크박스는 브라우저 기능에만 맡기지 않고 서버에서도 confirm 값을 검사합니다.
이 예제는 로컬 학습용입니다. 운영의 관리 화면에는 실제 사용자 로그인과 해당 행에 대한 삭제 권한 검사가 반드시 별도로 필요합니다. 숨겨진 id 입력은 사용자가 바꿀 수 있으므로, 운영에서는 사용자·조직 등 권한 범위도 서버의 조회·DELETE 조건에 반영해야 합니다. 양의 정수 검사만으로 다른 사람의 데이터를 삭제할 권한이 생기는 것은 아닙니다.
정확한 대상과 실제 삭제 건수 확인
DELETE에는 WHERE id = ?가 있고 id는 기본 키이므로 조건에 맞는 행은 최대 한 건입니다. WHERE를 빼면 의도하지 않은 여러 행을 삭제할 수 있습니다. filter_var()로 정수 범위를 검사한 뒤 bind_param()의 i 형식으로 전달합니다. 문자열로 받은 ID를 SQL에 직접 덧붙이지 않습니다.
mysqli_stmt_affected_rows()가 1이면 한 행이 삭제되었고, 0이면 그 시점에 조건과 일치하는 행이 없었다는 뜻입니다. 이미 다른 요청이 지운 경우도 있으므로 실행에 오류가 없었다는 이유만으로 삭제했습니다라고 표시하지 않습니다. 구문과 조건의 의미는 MariaDB DELETE 문서에서 확인할 수 있습니다.
코드는 SQL 오류를 잡아 HTTP 500과 일반적인 안내를 표시합니다. 이를 삭제할 행이 없음과 구분하고 실제 원인은 제한된 서버 로그에서 확인합니다. 운영 환경에서는 PHP의 상세 오류 표시를 끄고 SQL이나 접속 정보를 응답에 노출하지 않도록 설정해야 합니다.
POST·CSRF·리디렉션의 역할 구분
POST라는 이유만으로 안전한 요청이 되는 것은 아닙니다. 코드는 세션에 저장한 임의의 토큰을 폼에 넣고 제출값과 비교해 교차 사이트 요청 위조(CSRF)에 대응합니다. 토큰이 없거나 다르면 403, ID나 확인 값이 잘못되면 400으로 종료하고 DB 삭제를 실행하지 않습니다. hash_equals()는 토큰 문자열 비교에 사용합니다.
처리 후에는 토큰을 바꾸고 303 리디렉션으로 GET 목록을 엽니다. 완료 화면을 새로고침해도 같은 POST를 재전송하지 않도록 하는 흐름입니다. 이미 열린 다른 탭의 폼은 이전 토큰을 사용할 수 있으므로 이 예제에서는 목록을 다시 열어야 합니다. 리디렉션이나 토큰 교체는 이미 확정한 삭제를 되돌리는 기능이 아닙니다.
삭제 결과 확인과 다시 실습하기
SQL 클라이언트에서 다음을 실행합니다. ID 1을 삭제한 뒤에는 ID 2의 보존 확인용 한 행만 나와야 합니다. 선택한 행과 남겨야 할 행을 모두 확인해야 삭제 범위가 맞았는지 판단할 수 있습니다.
SELECT id, name FROM learning.employees_delete_demo ORDER BY id;
이 예제의 단일 DELETE는 기본 자동 커밋 상태에서 실행하므로 완료 후 취소 버튼이나 ROLLBACK으로 복원할 수 없습니다. 다시 연습하려면 ID 1이 없는 것을 확인한 뒤 아래의 알려진 예제 행만 재생성할 수 있습니다. 이것은 실제로 삭제된 임의 데이터를 복구하는 방법이 아닙니다.
INSERT INTO learning.employees_delete_demo (id, name) VALUES (1, '삭제 연습용');
여러 변경을 하나로 묶어 확정 전 검증하려면 InnoDB 등 트랜잭션을 지원하는 테이블에서 트랜잭션 시작·커밋·롤백을 설계해야 합니다. 커밋 후에도 되돌려야 하는 업무라면 삭제 표시를 남기는 방식이나 적절한 백업·복구 정책을 검토하세요.
실제 테이블이 다른 자료와 연결되어 있으면 외래 키 제약 때문에 삭제가 거절되거나 연쇄 삭제 규칙이 작동할 수 있습니다. 오류를 피하려고 외래 키 검사를 무조건 끄지 말고 관계와 영향 범위를 먼저 확인해야 합니다. 실습 뒤에는 로컬 서버와 불필요한 공개 파일을 정리하세요.




