반응형
클라이언트에서 최적화, 서버에서 효율적인 저장
이미지 업로드는 대부분의 웹 서비스에서 빈번하게 발생하는 작업입니다. 특히 모바일 기기와 고해상도 카메라의 보급으로 인해, 사용자들이 업로드하는 이미지의 해상도와 파일 용량은 지속적으로 증가하고 있습니다. 이러한 고용량 이미지들은 서버 부하를 가중시키고, 전송 속도를 저하시켜 사용자 경험을 악화시키는 원인이 될 수 있습니다.
이를 해결하기 위한 방법 중 하나는 클라이언트 측에서 이미지를 사전 리사이즈하는 것입니다. 사용자가 업로드하기 전에 브라우저에서 이미지의 크기와 용량을 줄이면, 다음과 같은 장점이 있습니다:
- 서버 저장 공간 절약
- 네트워크 전송 속도 향상
- 업로드 대기 시간 단축
- 사용자 불만 감소
JavaScript에서 이미지 리사이즈 처리
JavaScript에서는 FileReader
와 Canvas API
를 이용해 이미지를 로드하고, 가로·세로 크기를 제한하여 리사이즈할 수 있습니다.
function resizeImage(file, maxWidth, maxHeight, callback) {
// FileReader 객체 생성: 파일을 읽어 base64로 변환
const reader = new FileReader();
// 파일 읽기 완료 시 호출되는 이벤트 핸들러 설정
reader.onload = function (event) {
// 이미지 객체 생성
const img = new Image();
// 이미지 로딩 완료 시 처리
img.onload = function () {
// 원본 이미지의 너비와 높이 저장
let width = img.width;
let height = img.height;
// 최대 크기에 맞게 비율 계산 (가로 또는 세로 중 더 작은 비율을 기준으로 축소)
const ratio = Math.min(maxWidth / width, maxHeight / height);
// 비율을 적용하여 새로운 너비와 높이 계산
width *= ratio;
height *= ratio;
// canvas 요소 생성 및 크기 설정
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
// canvas에 2D 컨텍스트 가져오기
const ctx = canvas.getContext('2d');
// canvas에 리사이즈된 이미지 그리기
ctx.drawImage(img, 0, 0, width, height);
// canvas의 내용을 JPEG Blob 형태로 추출하여 콜백 함수에 전달
// 두 번째 인자는 출력 형식, 세 번째 인자는 품질 (0.8은 80% 품질)
canvas.toBlob((blob) => callback(blob), 'image/jpeg', 0.8);
};
// img.src에 base64 데이터 설정 → 이미지 로딩 트리거
img.src = event.target.result;
};
// FileReader로 파일을 base64(DataURL)로 읽기 시작
reader.readAsDataURL(file);
}
canvas.toBlob()
은 품질 조절 및 포맷 지정이 가능해 유용합니다.- 원본 비율을 유지하며 최대 크기 내로 축소됩니다.
리사이즈된 이미지 전송 방식
리사이즈된 이미지는 FormData
를 통해 Blob 형태로 전송하거나, Base64로 인코딩하여 서버에 전달할 수 있습니다.
Blob 전송 예제
resizeImage(file, 800, 800, (blob) => {
// FormData 객체 생성: multipart/form-data 형태로 서버에 전송하기 위해 사용
const formData = new FormData();
// Blob 객체를 'image'라는 필드명으로 추가 (파일명은 'resized.jpg'로 지정)
formData.append('image', blob, 'resized.jpg');
// fetch API를 사용하여 서버에 POST 요청 전송
fetch('/upload.php', {
method: 'POST', // HTTP 메서드 설정
body: formData // 전송할 데이터로 FormData 객체 사용
});
// 주의: 성공/실패에 대한 처리는 생략되어 있음
});
Base64 전송 시 유의사항
- Base64는 원본보다 약 30% 정도 용량이 증가합니다.
- PHP에서는 헤더(
data:image/jpeg;base64,...
) 제거 후 디코딩 필요합니다.
PHP에서 리사이즈 이미지 저장 처리
Blob 업로드 처리 예제
// 업로드된 'image' 파일이 존재하고, 오류 없이 업로드되었는지 확인
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
// 임시 업로드 파일 경로 (서버가 임시로 저장한 위치)
$tmpName = $_FILES['image']['tmp_name'];
// 클라이언트가 업로드한 원래의 파일 이름 (디렉토리명 제거)
$originalName = basename($_FILES['image']['name']);
// 업로드 파일이 저장될 디렉토리 설정 (현재 파일 기준 'uploads' 폴더)
$uploadDir = __DIR__ . '/uploads/';
// 업로드 디렉토리가 존재하지 않으면 생성
if (!is_dir($uploadDir)) mkdir($uploadDir);
// 저장할 최종 경로 생성: 현재 시간 기반으로 파일명에 prefix 추가
$targetPath = $uploadDir . time() . '_' . $originalName;
// 임시 파일을 지정한 경로로 이동 (실제 파일 저장)
move_uploaded_file($tmpName, $targetPath);
}
Base64 업로드 처리 예제
// 업로드된 파일이 존재하고 오류 없이 업로드되었는지 확인
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
// 임시 업로드 파일 경로 (서버가 임시로 저장한 파일 위치)
$tmpName = $_FILES['image']['tmp_name'];
// 클라이언트가 업로드한 원본 파일 이름에서 디렉토리 경로를 제거
$originalName = basename($_FILES['image']['name']);
// 업로드할 최종 디렉토리 경로 설정 (현재 파일 위치 기준 'uploads/' 폴더)
$uploadDir = __DIR__ . '/uploads/';
// 업로드 디렉토리가 없으면 생성
if (!is_dir($uploadDir)) mkdir($uploadDir);
// 저장할 최종 경로: 파일명 앞에 현재 시간 타임스탬프를 붙여 중복 방지
$targetPath = $uploadDir . time() . '_' . $originalName;
// 임시 파일을 최종 경로로 이동 (실제 업로드 처리)
move_uploaded_file($tmpName, $targetPath);
}
보안 고려 사항
- 확장자는 서버에서 검사하거나 강제로 지정합니다.
- 업로드된 파일은
imagecreatefromstring()
등을 통해 유효성 검사를 수행해야 합니다.
$img = imagecreatefromstring(file_get_contents($targetPath));
if (!$img) unlink($targetPath); // 유효하지 않으면 삭제
클라이언트 리사이즈 vs 서버 리사이즈
방식 | 장점 | 단점 |
---|---|---|
JavaScript 리사이즈 | 빠른 미리보기, 사용자 반응성 향상 | 브라우저 지원 제한, 품질 제어 어려움 |
PHP 리사이즈 | 서버 일관성 확보, 품질 제어 쉬움 | 서버 자원 소모 증가 |
권장 선택 기준:
- 미리보기와 UX를 중시하면 JavaScript 리사이즈
- 통일된 품질 관리가 필요하면 PHP 리사이즈
HTML + JavaScript + PHP 예제 정리
HTML
<!-- 미리보기 -->
<div class="pic_preview"><img src="noimage.png"></div>
<!-- 파일찾기 -->
<input type="file" class="file" accept='image/*'>
<!-- 서버에 전송할 이미지 데이터(base64) -->
<input type='hidden' name='pic' value=''/>
JavaScript: 리사이즈 및 미리보기 처리
// 파일 입력 필드 변경 이벤트 감지 (.file 클래스 대상)
$(document).on('change', '.file', function(e) {
// 선택된 첫 번째 파일 객체 추출
const file = e.target.files[0];
if (!file) return; // 파일이 없을 경우 중단
// FileReader를 사용하여 파일을 읽음
const reader = new FileReader();
// 파일 읽기 완료 시 실행되는 콜백
reader.onload = function(event) {
// 이미지 객체 생성
const img = new Image();
// 이미지가 로딩되었을 때 실행
img.onload = function() {
// 최대 너비 설정
const maxWidth = 200;
// 가로 기준으로 비율 계산 (세로 자동 조정)
const scale = maxWidth / img.width;
// canvas 요소 생성 및 크기 설정
const canvas = document.createElement('canvas');
canvas.width = maxWidth;
canvas.height = img.height * scale;
// canvas에 2D context 가져오기
const ctx = canvas.getContext('2d');
// canvas에 이미지 그리기 (리사이즈된 크기로)
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// canvas를 JPEG 포맷 base64 데이터로 추출 (품질 90%)
const dataURL = canvas.toDataURL('image/jpeg', 0.9);
// 숨겨진 input[name="pic"]에 base64 데이터 저장
$('input[name="pic"]').val(dataURL);
// 미리보기 이미지 src 속성 변경
$('.pic_preview img').attr('src', dataURL);
};
// 이미지 객체의 src에 base64 데이터를 설정 → 이미지 로딩 유도
img.src = event.target.result;
};
// 파일을 base64 (data URL) 형식으로 읽기 시작
reader.readAsDataURL(file);
});
PHP: 파일 저장 처리
$pic = $_POST['pic'];
$UPLOAD_DIR = $_SERVER['DOCUMENT_ROOT'] . '/data/board/';
if ($pic) {
// base64 헤더 분석: 확장자 추출
if (preg_match('/^data:image\/(\w+);base64,/', $pic, $matches)) {
$ext = strtolower($matches[1]); // jpg, jpeg, png, gif 등
// 허용 확장자만 필터링
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
if (!in_array($ext, $allowed)) {
exit('허용되지 않는 이미지 형식입니다.');
}
// base64 데이터만 추출
$data = substr($pic, strpos($pic, ',') + 1);
$data = base64_decode($data);
if ($data === false) {
exit('base64 디코딩에 실패했습니다.');
}
// 저장 디렉토리가 없다면 생성
if (!is_dir($UPLOAD_DIR)) {
mkdir($UPLOAD_DIR, 0755, true);
}
// 고유 파일명 생성
$filename = 'base64_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$filepath = $UPLOAD_DIR . $filename;
// 파일 저장
if (file_put_contents($filepath, $data) === false) {
exit('파일 저장에 실패했습니다.');
}
// 상대 경로 또는 URL 경로로 결과 저장
$pic = '/data/board/' . $filename;
// 이후 사용 예:
// echo json_encode(['status' => 'success', 'url' => $pic]);
} else {
exit('잘못된 이미지 데이터 형식입니다.');
}
}
참고 링크
- FileReader: https://developer.mozilla.org/en-US/docs/Web/API/FileReader
- Canvas API: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
- imagecreatefromstring: https://www.php.net/manual/en/function.imagecreatefromstring.php
JavaScript와 PHP를 조합한 이 방식은 이미지 용량을 효과적으로 줄이고, 사용자 경험을 향상시키며, 서버 자원도 절약하는 실용적인 업로드 전략이 될 수 있습니다.
반응형
'Programing > javascript' 카테고리의 다른 글
JavaScript로 구현하는 게임 데미지 단위 축약 (A ~ ZZZZ) (0) | 2025.05.27 |
---|---|
JavaScript로 구현하는 금액 단축 표기 (K / M / B 표기법) (0) | 2025.05.27 |
JavaScript로 구현하는 금액의 영어 단위 변환 (Number to Words) (0) | 2025.05.27 |
기수 정렬 (Radix Sort) 설명과 JavaScript 예제 (1) | 2025.05.22 |
힙 정렬 (Heap Sort) 설명과 JavaScript 예제 (0) | 2025.05.22 |
JavaScript 비교 연산자 (1) | 2025.05.18 |
JavaScript 조건문 (0) | 2025.05.18 |
JavaScript 반복문 (0) | 2025.05.18 |