회원 프로필 사진이나 상품 목록 썸네일처럼 작은 이미지를 다룰 때, 파일을 디스크에 따로 두지 않고 DB 한 곳에 몰아넣고 싶을 때가 있습니다. 백업할 때 DB 덤프 하나로 끝나고, 서버를 옮길 때 업로드 폴더 권한이나 경로 때문에 깨지는 일도 없으니까요. 원본 크기 그대로 넣으면 DB가 금방 무거워지지만, 몇십 픽셀짜리 썸네일로 줄여서 넣으면 부담이 적고 조회도 빠르게 끝납니다.
흐름은 단순합니다. 업로드된 파일 내용을 문자열로 읽고, GD로 크기를 줄인 PNG 바이너리를 만든 다음, 그 바이너리를 BLOB 컬럼에 넣습니다. 보여 줄 때는 꺼낸 바이너리를 base64로 바꿔 <img src="data:...">에 바로 꽂고요.
테이블 준비
CREATE TABLE `images` (
`id` int(10) NOT NULL,
`type` char(4) NOT NULL,
`thumbs` blob NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
blob은 최대 64KB까지 들어갑니다. 썸네일 용도면 충분하지만, 크기를 넉넉히 잡을 거라면 mediumblob으로 바꿔 두는 게 안전합니다. 새로 만든다면 엔진도 요즘 기본인 InnoDB를 쓰는 편이 낫고요.
원본 코드
썸네일 변환 함수와 저장·조회 부분입니다.
function Thumbnail_String($string, $user_width=86, $user_height=null)
{
ob_start();
ob_flush();
flush();
$im = imagecreatefromstring( $string );
$orig_width = imagesx($im);
$orig_height = imagesy($im);
if($orig_width >= $user_width)
{
if(strlen($user_height) === 0)
{
$user_height = @round($orig_height*($user_width/$orig_width));
}
}
else
{
$user_width = $orig_width;
$user_height = $orig_height;
}
$im_new = imagecreatetruecolor( $user_width, $user_height );
imagecopyresampled($im_new, $im, 0, 0, 0, 0,
$user_width, $user_height, $orig_width, $orig_height);
imagepng($im_new);
imagedestroy($im);
imagedestroy($im_new);
$data = ob_get_contents();
ob_end_clean();
return $data;
}
// 이미지를 sql에 저장하기
$file = file_get_contents($_FILES['userfile']['tmp_name']);
unlink($_FILES['userfile']['tmp_name']);
$data = Thumbnail_String($file, 50);
mysqli_query($link,
"insert into from tbname VALUES('1', 'png', '".$data."');");
// sql에 저장된 이미지를 불러오기
$result = mysqli_query($link, "select thumbs from tbname WHERE id=1;");
$row = mysqli_fetch_array($result, MYSQLI_NUM);
echo "<img src='data:image/gif;base64, ".base64_encode($row[0])."'>";
Thumbnail_String()의 요령은 출력 버퍼입니다. imagepng()는 파일 경로를 안 주면 결과를 바로 화면으로 내보내는데, 그걸 ob_start()로 가로채서 ob_get_contents()로 문자열로 받아 냅니다. 높이를 안 주면 원본 비율대로 계산하고, 원본이 지정한 폭보다 작으면 굳이 키우지 않고 원래 크기를 그대로 씁니다. imagecreatefromstring()이 JPEG, PNG, GIF 등을 알아서 판별해 주니 입력 형식은 신경 쓸 필요가 없고, 결과는 항상 PNG로 나옵니다.
그대로 돌리면 안 되는 부분
이 코드는 개념을 보여 주는 용도로 보고, 실제로 쓸 때는 몇 군데를 손봐야 합니다.
먼저 INSERT 문이 insert into from tbname으로 돼 있어서 문법 오류로 실패합니다. from이 들어가면 안 되고, 테이블 이름도 위에서 만든 images와 다릅니다. 더 큰 문제는 PNG 바이너리를 따옴표 안에 그냥 이어 붙인다는 점이에요. 바이너리 안에 '나 \ 바이트가 섞이는 순간 쿼리가 깨집니다. 바이너리는 prepared statement로 넘기는 게 정석입니다.
출력할 때 MIME 타입이 image/gif로 돼 있는데 실제 데이터는 PNG라서 image/png가 맞습니다. 브라우저가 대충 알아서 그려 주긴 하지만 굳이 틀리게 둘 이유는 없죠. base64, 뒤의 공백도 빼는 게 깔끔합니다.
함수 첫머리의 ob_flush(), flush()는 바깥 버퍼를 비워 버리는 동작이라, 이 함수를 호출하기 전에 출력해 둔 내용이 있으면 예상보다 일찍 브라우저로 나갑니다. 버퍼를 열어 PNG만 받아 내는 데는 필요 없는 줄입니다. 그리고 PHP 8.1부터는 strlen(null)이 deprecated 경고를 내니 $user_height === null로 비교하는 쪽이 낫습니다.
업로드 쪽도 $_FILES['userfile']['error'] 확인 없이 바로 읽고 있고, imagecreatefromstring()이 이미지가 아닌 파일이면 false를 돌려주는데 그 경우를 처리하지 않습니다. 임시 파일은 요청이 끝나면 PHP가 지워 주므로 unlink()는 없어도 됩니다.
위 내용을 반영해서 고친 버전입니다.
function Thumbnail_String($string, $user_width = 86, $user_height = null)
{
$im = @imagecreatefromstring($string);
if ($im === false) {
return false; // 이미지가 아니거나 지원하지 않는 형식
}
$orig_width = imagesx($im);
$orig_height = imagesy($im);
if ($orig_width >= $user_width) {
if ($user_height === null) {
$user_height = (int) round($orig_height * ($user_width / $orig_width));
}
} else {
$user_width = $orig_width;
$user_height = $orig_height;
}
$im_new = imagecreatetruecolor($user_width, $user_height);
imagecopyresampled($im_new, $im, 0, 0, 0, 0,
$user_width, $user_height, $orig_width, $orig_height);
ob_start();
imagepng($im_new);
$data = ob_get_clean();
imagedestroy($im);
imagedestroy($im_new);
return $data;
}
// 저장 (수정: 문법 오류 제거, prepared statement 사용)
if ($_FILES['userfile']['error'] === UPLOAD_ERR_OK) {
$file = file_get_contents($_FILES['userfile']['tmp_name']);
$data = Thumbnail_String($file, 50);
if ($data !== false) {
$id = 1;
$type = 'png';
$stmt = mysqli_prepare($link, "INSERT INTO images (id, type, thumbs) VALUES (?, ?, ?)");
mysqli_stmt_bind_param($stmt, 'iss', $id, $type, $data);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
}
// 불러오기 (수정: MIME 타입 png)
$result = mysqli_query($link, "SELECT thumbs FROM images WHERE id = 1");
$row = mysqli_fetch_array($result, MYSQLI_NUM);
echo "<img src='data:image/png;base64," . base64_encode($row[0]) . "'>";
data URI 방식은 이미지가 HTML 안에 박히기 때문에 브라우저 캐시를 못 탑니다. 목록에 썸네일이 수십 개씩 뜨는 페이지라면, 이미지를 내보내는 PHP 파일을 따로 두고(thumb.php?id=1처럼) Content-Type: image/png 헤더와 함께 바이너리를 출력하게 하면 캐시도 되고 HTML도 가벼워집니다. 그리고 원본 크기 이미지까지 DB에 넣는 건 이 방식으로는 권하지 않습니다. 원본은 파일로 두고 DB엔 경로만 저장하는 쪽이 관리가 편합니다.
참고: 하보니 PHP - 작은 이미지로 변환하여 MySQL에 저장하기