string
타입으로, Base64로 인코딩된 데이터 URL 형태입니다. → newImages 에 저장
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA..."
File
객체 형태로 저장됩니다. → newFiles 에 저장
// hooks/useImageDragDrop.ts
// 파일 객체 자체는 별도 배열에 저장
newFiles.push(file);
// 미리보기용으로는 FileReader를 사용해 데이터 URL로 변환해 저장
const reader = new FileReader();
reader.onloadend = () => {
newImages.push(reader.result as string); // Base64 데이터 URL
// ...
};
reader.readAsDataURL(file);
browser-image-compression 설치
nnpm install browser-image-compression
이미지 최적화 - file, Base64 두가지 변환 가능
// utils/imageCompression.ts
// 이미지 최적화 관련 유틸리티 함수
import imageCompression from 'browser-image-compression'
// 이미지 최적화 옵션
const defaultOptions = {
maxSizeMB: 1, // 최대 파일 크기 (1MB)
maxWidthOrHeight: 1920, // 최대 너비 또는 높이 (1920px)
useWebWorker: true, // WebWorker 사용 (백그라운드 처리)
fileType: 'image/jpeg', // 출력 파일 형식
initialQuality: 0.8, // 초기 품질 (0.8 = 80%)
}
/**
* 이미지 파일 압축 함수
* @param file 원본 이미지 파일
* @param options 압축 옵션 (옵션)
* @returns 압축된 이미지 파일
*/
export const compressImage = async (
file: File,
options?: Partial<typeof defaultOptions>,
): Promise<File> => {
try {
console.log(`압축 전 이미지 크기: ${file.size / 1024 / 1024} MB`)
// 옵션 합치기
const compressionOptions = {
...defaultOptions,
...options,
}
// 이미지 압축 (browser-image-compression 라이브러리 사용)
const compressedFile = await imageCompression(file, compressionOptions)
console.log(`압축 후 이미지 크기: ${compressedFile.size / 1024 / 1024} MB`)
return compressedFile
} catch (error) {
console.error('이미지 압축 중 오류 발생:', error)
// 압축 실패 시 원본 파일 반환
return file
}
}
/**
* 이미지 Base64 변환 함수
* @param file 이미지 파일
* @returns Base64 인코딩된 문자열
*/
export const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(file)
})
}
/**
* 여러 이미지 파일 압축 함수
* @param files 원본 이미지 파일 배열
* @param options 압축 옵션 (옵션)
* @returns 압축된 이미지 파일 배열
*/
export const compressImageFiles = async (
files: File[],
options?: Partial<typeof defaultOptions>,
): Promise<File[]> => {
try {
const compressPromises = files.map((file) => compressImage(file, options))
return await Promise.all(compressPromises)
} catch (error) {
console.error('이미지 일괄 압축 중 오류 발생:', error)
return files // 압축 실패 시 원본 파일 배열 반환
}
}
https://react-dnd.github.io/react-dnd/about
npm install react-dnd react-dnd-html5-backend
// 핸드폰 적용시 추가 설치
npm install react-dnd-touch-backend react-dnd-multi-backend rdndmb-html5-to-touch
import { useDrag, useDrop } from 'react-dnd'
// 썸네일 이미지 컴포넌트
const ImagePreviewContainer = styled.div<{ isDragging: boolean }>`
${tw`relative w-[4.5rem] h-[4.5rem] rounded-md object-cover shrink-0 cursor-move`}
opacity: ${(props) => (props.isDragging ? 0.4 : 1)};
border: ${(props) => (props.isDragging ? '2px dashed #9ca3af' : 'none')};
touch-action: none; /* 이 설정이 모바일에서 중요합니다 */
-webkit-touch-callout: none; /* iOS에서 길게 터치시 메뉴 방지 */
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version */
`
// 모바일 환경 감지
const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
)
}
// 사용할 백엔드 결정 (HTML5 또는 Touch)
const getBackend = () => {
return isMobile() ? TouchBackend : HTML5Backend
}
interface DragCollectedProps {
isDragging: boolean
}
const [{ isDragging }, drag] = useDrag<DragItem, unknown, DragCollectedProps>(
{
type: 'IMAGE_ITEM',
item: () => {
return { index, id: `image-${index}`, type: 'IMAGE_ITEM' }
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
},
)
const [, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
accept: 'IMAGE_ITEM',
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
}
},
hover(item) {
if (!ref.current) return
const dragIndex = item.index
const hoverIndex = index
// 같은 아이템 위에 드랍하는 경우 무시
if (dragIndex === hoverIndex) return
// 위치 변경 실행
moveImage(dragIndex, hoverIndex)
// 드래그 인덱스 업데이트
item.index = hoverIndex
},
})
// ref를 드래그와 드롭 모두에 연결
drag(drop(ref))
// html에서 이런식으로 사용
return (
<ImagePreviewContainer ref={ref} isDragging={isDragging}>
<img
src={image}
alt={`thumbnail-${index}`}
className="w-full h-full object-cover rounded-md"
/>
<RemoveImageBtn
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
removeImage(index)
}}
>
<XmarkCircleSolid
width="1.5rem"
height="1.5rem"
color={colors.darkGray}
/>
</RemoveImageBtn>
</ImagePreviewContainer>
)