본문 바로가기

비개발자의 개발 일지

현장 팀장에서 수출 ERP까지 — AI와 함께 시스템을 만든 기록

시리즈 보기
php

PHP PDO LIKE 검색 — fetch(FETCH_ASSOC) + htmlspecialchars 완전 정리

by 왕진 2026. 7. 8.
반응형

 

 

Web Development · PHP PDO

PHP PDO LIKE 검색 — fetch(FETCH_ASSOC) + htmlspecialchars 완전 정리

검색어에 % 와일드카드를 붙여 부분 일치 검색하고, while + fetch로 결과를 한 행씩 꺼내 HTML에 안전하게 출력하는 패턴
시작 오늘 분석할 코드

검색 폼(search.html)에서 이름을 POST로 전송하면, list.php가 PDO로 MySQL에 LIKE 검색을 실행하고 결과를 표로 출력한다. 핵심 포인트는 %검색어% 와일드카드, fetch(PDO::FETCH_ASSOC)의 while 루프, htmlspecialchars() XSS 방어다.

search.html — 검색 폼
<FORM name="form1" method="post" action="list.php"> 이름:<BR> <INPUT type="text" name="search_key"><BR> <INPUT type="submit" value="검색하기"> </FORM>
list.php — PDO LIKE 검색 + 결과 출력
$search_key = '%' . $_POST['search_key'] . '%'; // 앞뒤에 % 추가 → 부분 일치 검색 try { $sql = "SELECT * FROM member WHERE last_name LIKE :last_name OR first_name LIKE :first_name"; $stmh = $pdo->prepare($sql); $stmh->bindValue(':last_name', $search_key, PDO::PARAM_STR); $stmh->bindValue(':first_name', $search_key, PDO::PARAM_STR); $stmh->execute(); $count = $stmh->rowCount(); print "검색결과는 " . $count . "건입니다."; } catch (PDOException $e) { print "오류: " . $e->getMessage(); } if ($count < 1) { print "검색결과가 없습니다."; } else { while ($row = $stmh->fetch(PDO::FETCH_ASSOC)) { // 한 행씩 꺼내 연관 배열 $row에 저장 echo htmlspecialchars($row['last_name']); // XSS 방어: < > " ' & → HTML 엔티티 변환 } }
실제 출력 미리보기
search.html
이름:

list.php 출력
검색결과는 2건입니다.
번호 이름 연령
1 길동 30
3 석천 45
용어 핵심 용어 정리
LIKESQL의 패턴 매칭 연산자. % 와일드카드와 함께 부분 일치 검색에 사용
%0개 이상의 임의 문자를 대체하는 와일드카드. %홍%은 "홍이 포함된 모든 값"
fetch결과셋에서 한 행씩 꺼내는 메서드. while 루프와 함께 사용해 전체 결과를 순회
FETCH_ASSOC컬럼명을 키로 한 연관 배열 반환. $row['last_name']처럼 이름으로 접근
rowCountexecute() 이후 처리된 행 수. SELECT에서 결과 건수를 미리 확인할 때 사용
htmlspecialcharsHTML 특수문자를 엔티티로 변환. DB 값에 악성 코드가 있어도 태그로 해석 안 됨
XSSCross-Site Scripting. DB 값에 <script> 등을 삽입해 사용자 브라우저에서 실행되는 공격
자리표시자SQL의 :last_name처럼 실제 값 자리를 표시하는 플레이스홀더. bindValue로 값 주입

1 LIKE + % — 왜 검색어 앞뒤에 %를 붙이는가

LIKE 연산자는 패턴 매칭을 지원한다. %를 붙이지 않으면 완전 일치 검색이 된다. 사용자가 "홍"을 입력했을 때 "홍길동", "홍석천" 모두 찾으려면 '%홍%'처럼 앞뒤에 % 와일드카드를 붙여야 한다.

패턴 의미 예시 매칭
'홍' 완전 일치 "홍"만 해당 — "홍길동" 불일치
'홍%' 홍으로 시작 홍길동, 홍석천 O / 김홍도 X
'%홍' 홍으로 끝남 김홍 O / 홍길동 X
'%홍%' 홍 포함 (부분 일치) 홍길동, 김홍도, 홍 모두 O
$search_key = '%' . $_POST['search_key'] . '%'; // 사용자가 "홍" 입력 → $search_key = "%홍%" // bindValue(':last_name', $search_key) → WHERE last_name LIKE '%홍%'
※ % 와일드카드를 PHP에서 미리 붙이고 그 결과를 bindValue로 넘긴다. SQL 문자열 안에서 '%:last_name%' 처럼 쓰면 자리표시자가 인식되지 않아 에러가 난다.

2 OR 조건 — 성(last_name)과 이름(first_name)을 동시에 검색하는 이유

회원 이름이 성(last_name)과 이름(first_name)으로 분리 저장되어 있다. "홍"을 검색할 때 성이 "홍"인 경우와 이름에 "홍"이 포함된 경우 모두 찾으려면 OR로 두 컬럼을 함께 비교해야 한다. 두 자리표시자 :last_name, :first_name에 같은 $search_key 값을 각각 바인딩한다.

$sql = "SELECT * FROM member WHERE last_name LIKE :last_name OR first_name LIKE :first_name"; $stmh->bindValue(':last_name', $search_key, PDO::PARAM_STR); $stmh->bindValue(':first_name', $search_key, PDO::PARAM_STR);
⚠ 같은 값이라도 자리표시자 이름이 다르면 bindValue를 따로 호출해야 한다. 이름이 같은 자리표시자(:name)를 반복 사용하면 드라이버에 따라 동작이 다를 수 있다.

3 fetch(FETCH_ASSOC) — 왜 while 루프로 한 행씩 꺼내는가

SELECT 결과는 여러 행이 될 수 있다. fetch()는 한 번 호출할 때마다 결과셋에서 한 행을 꺼내고 커서를 다음으로 이동시킨다. 더 이상 행이 없으면 false를 반환해 while 루프가 자동으로 종료된다.

execute()
결과셋(RS) 생성
fetch() × N번
false → 루프 종료
FETCH_ASSOC — 왜 연관 배열인가
FETCH_NUM (기본)
$row[0], $row[1], $row[2] · 숫자 인덱스로 접근
· 컬럼 순서가 바뀌면 코드도 수정해야 함
· 의미 파악이 어려움
FETCH_ASSOC (권장)
$row['id'], $row['last_name'] · 컬럼명을 키로 접근
· 컬럼 순서가 바뀌어도 코드 그대로
· 가독성 높고 실수가 줄어듦
※ fetchAll(PDO::FETCH_ASSOC)은 결과 전체를 배열로 한 번에 가져온다. 건수가 적을 때는 편리하지만, 결과가 수만 건이면 while+fetch가 메모리 효율적이다.

 

반응형

 

4 htmlspecialchars — 왜 DB 값을 그냥 echo하면 안 되는가

DB에 저장된 값을 그대로 echo하면 XSS(Cross-Site Scripting) 공격에 노출된다. 누군가 이름 칸에 <script>alert('hack')</script>를 저장해 두었다면, 이 값이 화면에 그대로 출력될 때 브라우저가 스크립트를 실행한다. htmlspecialchars()는 HTML 특수문자를 엔티티로 변환해 태그로 해석되지 않도록 차단한다.

원본 문자 변환 후 (HTML 엔티티) 브라우저 출력
< &lt; 화면에 < 텍스트로 표시
> &gt; 화면에 > 텍스트로 표시
" &quot; 속성 안에서 따옴표 깨짐 방지
' &#039; 싱글쿼트 이스케이프
& &amp; & 문자 안전하게 표시
// 위험: $row 값에 <script>가 있으면 브라우저가 실행 echo $row['last_name']; // 안전: HTML 특수문자를 엔티티로 변환 후 출력 echo htmlspecialchars($row['last_name']);

5 rowCount로 건수 먼저 확인 — 왜 결과 출력 전에 세는가

결과가 0건일 때 while+fetch 루프는 아무것도 출력하지 않는다. 하지만 사용자는 빈 화면만 보게 된다. rowCount()로 건수를 먼저 확인해 "검색결과가 없습니다"를 출력하면 UX가 훨씬 나아진다. 또한 건수가 0이면 테이블 구조 자체도 출력하지 않아 빈 표가 그려지는 것을 방지한다.

1
execute() 실행SELECT 쿼리를 DB에 전송, 결과셋(ResultSet) 생성
2
rowCount()로 건수 확인0건이면 "검색결과 없음" 출력하고 종료
3
while($row = $stmh->fetch(FETCH_ASSOC))1건 이상이면 한 행씩 꺼내 <TR> 출력
4
htmlspecialchars()로 안전하게 출력각 컬럼 값을 XSS 방어 처리 후 <TD>에 삽입
⚠ rowCount()는 드라이버에 따라 SELECT 결과 행 수를 정확히 반환하지 않을 수 있다. PDO::ATTR_EMULATE_PREPARES를 false로 설정하면 MySQL 드라이버가 직접 처리해 정확한 값을 반환한다.

6 결과 테이블 출력 — PHP와 HTML을 어떻게 섞는가

이 코드의 독특한 점은 PHP 코드 중간에 HTML 테이블 구조를 삽입하는 방식이다. ?>로 PHP를 끊고 HTML을 직접 쓴 다음 <?php로 다시 PHP를 이어간다. while 루프 안에서도 마찬가지로 <TR>을 직접 작성한다.

if ($count >= 1) { // PHP 종료 → HTML 직접 작성 ?> <TABLE><TBODY> <TR> <TH>번호</TH><TH></TH> <TH>이름</TH><TH>연령</TH> </TR> <?php // PHP 재개 → while 루프 while($row = $stmh->fetch(PDO::FETCH_ASSOC)){ ?> <TR> <TD><?=htmlspecialchars($row['id'])?></TD> <TD><?=htmlspecialchars($row['last_name'])?></TD> </TR> <?php } ?> </TBODY></TABLE> <?php }
<?=<?php echo의 단축 표현이다. 짧은 출력을 HTML 안에 삽입할 때 가독성이 좋다.

7 fetch vs fetchAll — 언제 어떤 걸 쓰는가
메서드 방식 적합한 상황
fetch() 한 번에 1행, while 루프 결과가 많을 때, 조건에 따라 도중 중단할 때
fetchAll() 전체 결과를 배열로 한 번에 결과가 적고, 전체 배열을 함수로 넘길 때
fetchColumn() 한 컬럼 값 1개 COUNT(*) 등 단일 값만 필요할 때
// fetch() — while 루프 패턴 while ($row = $stmh->fetch(PDO::FETCH_ASSOC)) { echo $row['last_name']; } // fetchAll() — 전체 배열 반환 $rows = $stmh->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { echo $row['last_name']; }

8 자주 하는 실수
실수 증상 해결
%를 PHP가 아닌 SQL 안에 붙임 LIKE '%:last_name%' → 자리표시자 인식 실패, 에러 또는 전체 검색 PHP에서 미리 '%'.$_POST['key'].'%' 조립 후 bindValue
htmlspecialchars 생략 DB에 <script> 저장 시 XSS 공격 성공, 사용자 브라우저에서 스크립트 실행 echo 전에 반드시 htmlspecialchars($row['컬럼']) 적용
fetch() 결과를 if로 확인 안 함 결과가 0건이면 빈 화면 — 사용자가 오류인지 정상인지 알 수 없음 rowCount()로 건수 확인 후 "결과 없음" 분기 처리
OR 대신 AND 사용 성과 이름 모두 일치해야 검색됨 — 의도와 다른 좁은 결과 last_name LIKE ... OR first_name LIKE ... 로 변경
같은 자리표시자명 중복 사용 일부 드라이버에서 두 번째 바인딩이 무시되거나 에러 :last_name, :first_name처럼 서로 다른 이름 사용, 값이 같아도 따로 bindValue

핵심 한 줄 요약

LIKE + %검색어 앞뒤에 % — '%홍%'은 "홍이 포함된 모든 값" 부분 일치
% 붙이는 위치SQL 안이 아니라 PHP에서 미리 조립 후 bindValue로 넘긴다
fetch(FETCH_ASSOC)while 루프로 한 행씩 — $row['컬럼명']으로 접근, false 반환 시 루프 종료
rowCount()결과 건수 먼저 확인 — 0건이면 "없음" 출력 후 테이블 생략
htmlspecialchars()DB 값을 echo하기 전 반드시 적용 — XSS를 구조적으로 차단
OR 검색성·이름 두 컬럼을 동시에 검색 — 같은 값을 두 자리표시자에 각각 bindValue

Tags

#PDO #PHP #MySQL #LIKE검색 #fetch #FETCH_ASSOC #prepare #bindValue #htmlspecialchars #rowCount #XSS방어 #PHP학습 #웹개발 #티스토리
▼ 티스토리 태그 입력란 복사용
PDO, PHP, MySQL, LIKE검색, fetch, FETCH_ASSOC, htmlspecialchars, rowCount, XSS방어, PHP학습
반응형

댓글