Image preview, delete, and drag-to-reorder in React
tsx
const [dragStartIndex, setDragStartIndex] = useState<null | number>(null);
const [dragEnterIndex, setDragEnterIndex] = useState<null | number>(null);
const handleAddShowImg = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) return;
if (e.target.files.length + showImg.length > 8) {
alert('등록할 수 있는 이미지의 최대 갯수는 8장입니다.');
return;
}
const imgList = e.target.files;
const imgUrlList = [...showImg];
for (let i = 0; i < imgList.length; i++) {
const imgSrc = URL.createObjectURL(imgList[i]);
const imgName = imgList[i].name;
imgUrlList.push({ imgSrc, imgName });
}
setShowImg(imgUrlList);
};
const handleDeleteShowImg = (index: number) => {
const newShowImgs = showImg.filter((img, i) => i !== index);
setShowImg(newShowImgs);
};
const handleDrag = () => {
if (dragStartIndex === null || dragEnterIndex === null) return;
const newShowImgs = [...showImg];
const spliceImg = newShowImgs.splice(dragStartIndex, 1)[0];
newShowImgs.splice(dragEnterIndex, 0, spliceImg);
setShowImg(newShowImgs);
};
<tr>
<td className='text-sm font-medium border bg-gray-200 border-gray-300 text-center'>
Photo
</td>
<td className='border border-gray-300 p-2'>
<div className='flex flex-col gap-1 w-full'>
<label
htmlFor='photo'
className='w-20 border border-gray-400 text-gray-700 hover:bg-gray-50 rounded-md flex justify-center items-center py-0.5 cursor-pointer'
>
파일 선택
<input
id='photo'
type='file'
accept='image/png, image/jpeg'
multiple
onChange={(e) => handleAddShowImg(e)}
className='hidden'
/>
</label>
{showImg.length > 0 && (
<div className='flex w-full gap-1 py-2 flex-wrap'>
{showImg.map((img, i) => {
return (
<div key={`${img.imgName} + ${i}`}>
<img
className='w-32 h-32 border border-gray-300 cursor-move'
src={img.imgSrc}
alt={`${img.imgName} + ${i}`}
onDragStart={() => setDragStartIndex(i)}
onDragEnter={() => setDragEnterIndex(i)}
onDragEnd={() => handleDrag()}
/>
<div className='flex items-center justify-between mt-1'>
<span className='truncate max-w-[5rem] text-'>
{img.imgName}
</span>
<button
className='border px-1 rounded-md'
onClick={() => handleDeleteShowImg(i)}
>
삭제
</button>
</div>
</div>
);
})}
</div>
)}
<span className='text-xs text-red-600'>
※ 등록한 사진 순서대로 게시글에 보여집니다.
</span>
<span className='text-xs text-red-600'>
※ 한 게시물당 최대 5개의 사진을 등록할 수 있습니다.
</span>
</div>
</td>
</tr>The key part was handling this through onDragStart and onDragEnter events on each image.
With onDragStart, I kept the index of the image picked up with the mouse in the dragStartIndex state. When the picked-up image entered, that is, moved over another image, I updated the dragEnterIndex state through the onDragEnter event.
I called the logic that reorders the images with splice() from the onDragEnd event. When onDragEnd fires, the image has finished moving, so that's when I ran the function.

- #ondragend
- #onDragEnter
- #ondragstart
- #image preview
- #delete image
- #reorder images