글을 쓸 때 이미지도 다중으로 업로드가 가능하고, slack
처럼 내가 업로드 할 이미지가 미리보기로 보여야 했다.
처음 구현해본 기능이라, 어떻게 구현했는지 정리할 겸 기록해보려 한다.
<input
id="file-selector"
type="file"
className="hidden"
accept=".png, .jpg, .jpeg, .gif"
onChange={handleFileChange}
multiple
/>
input 태그의 accept
속성을 이용하면, 원하는 파일 형식을 지정해 잘못된 파일 타입을 선택하는 것을 방지할 수 있다. (images/*
와 같은 형태로도 사용 가능)
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
MDN (input - accept)
그리고 다중 이미지 선택을 허용하려면 multiple
속성만 추가해주면 된다.
기본 input 태그의 업로드 버튼 UI가 예쁘지 않기 때문에, 아마 대다수가 나처럼 기본 input은 display: none
으로 숨겨놓고 업로드 UI를 따로 만들어서 사용할 것이다.
const clickFileSeletor = () => {
const fileSelector = document.getElementById('file-selector');
if (fileSelector) {
fileSelector.click();
}
};
여러개의 파일을 선택하고 업로드할 경우, handleFileChange
함수로 선택한 파일들이 전달될 것이다.
이 때 event.target.files
는 이렇게 객체
// FileList 예시
{
0: File {name: "image1.jpg", size: 102400, type: "image/jpeg", ...},
1: File {name: "image2.png", size: 204800, type: "image/png", ...},
length: 2
}
이 FileList들을 일반 배열로 변환해주고,
uploadImage라는 File[]
타입의 useState 배열 안에 담아준다.
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
const newFiles = Array.from(files).slice(0, 10); // 최대 10개 파일만 선택
setUploadImage((prev) => {
const updatedFiles = prev ? [...prev, ...newFiles] : newFiles;
return updatedFiles.slice(0, 10);
});
....
생략
}
이제, 선택한 파일들의 리스트가 배열에 담겼으니 미리보기 구현을 위해 img 태그 src 속성에 할당할 이미지 경로가 필요한데, 이 때 FileReader
를 사용할 수 있다.
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
const newFiles = Array.from(files).slice(0, 10); // 최대 10개 파일만 선택
setUploadImage((prev) => {
const updatedFiles = prev ? [...prev, ...newFiles] : newFiles;
return updatedFiles.slice(0, 10);
});
// ** FileReader로 File 읽어 URL 생성
newFiles.forEach((file) => {
const reader = new FileReader();
reader.onloadend = () => {
const imageUrl = reader.result as string;
setPreviewUrl((prev) => {
const updatedUrls = prev ? [...prev, imageUrl] : [imageUrl];
return updatedUrls.slice(0, 10);
});
};
reader.readAsDataURL(file);
});
}
setImgUploadModal(false);
};
FileReader는 newFiles (File들이 담긴 배열) 배열을 순회하며 reader.readAsDataUrl(file)
메서드로 File을 읽어 Base64로 인코딩된 데이터 URL을 생성한다.
이 URL을 previewUrl이라는 배열에 담아주고, 섹션에 이미지들을 렌더링해주면 내가 파일을 업로드할때마다 미리보기 이미지들이 생성된다.
https://developer.mozilla.org/en-US/docs/Web/API/FileReader
MDN - FileReader
미리보기 구현시 URL.createObjectURL 을 이용하는 방법도 있지만, 이때 생성된 URL이 사용하지 않아도 브라우저 메모리에 계속 남아있어 메모리 누수 문제가 발생할 수 있다고 한다. (URL.revokeObjectURL(objectURL)
을 통해 clear 해줘야 한다고 함)
모든 브라우저에서 지원하는 것을 확인한 뒤 비동기로 동작하는 FileReader를 이용해 주었다.
const [previewUrl, setPreviewUrl] = useState<string[] | null>(null);
const [uploadImage, setUploadImage] = useState<File[] | null>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
const newFiles = Array.from(files).slice(0, 10); // 최대 10개 파일만 선택
setUploadImage((prev) => {
const updatedFiles = prev ? [...prev, ...newFiles] : newFiles;
return updatedFiles.slice(0, 10);
});
newFiles.forEach((file) => {
const reader = new FileReader();
reader.onloadend = () => {
const imageUrl = reader.result as string;
setPreviewUrl((prev) => {
const updatedUrls = prev ? [...prev, imageUrl] : [imageUrl];
return updatedUrls.slice(0, 10);
});
};
reader.readAsDataURL(file);
});
}
setImgUploadModal(false);
};
// 이미지 렌더링 코드는 많이 생략되었음
{previewUrl?.map((image, idx) => {
return (
<img
src={image}
alt={`upload-preview-${idx}`}
/>
);
})}