이미지를 미리 보여 주는 기능을 만들려고 생각했을 때 아래 두 가지 방법을 생각했다.
두 번째 이미지를 서버에 올리기 전 이미지를 미리 보여주는 방법
을 선택했는데 이유는 사용자가 잘못 이미지를 올렸을 경우 서버에 불필요한 이미지가 저장되는 것을 막고, 사용자가 여러 번 이미지를 교체할 경우 서버에 올리는 시간이 걸리기 때문에 더 나은 사용자 경험을 주기 위해서다.
limit을 주었을 때 이미지 업로드 개수를 제한할 수 있는 컴포넌트로 구현하였다.
<input />
태그로 파일을 불러오기 때문에 <input ref={fileRef}/>
태그에 fileRef
를 넣고, +
를 클릭했을 때(ex: fileRef.current?.click())
onFileChange
함수가 실행될 수 있도록 했다.
import React, { useRef, useState } from 'react';
interface Props {
limit?: number;
}
const FileInput: React.FC<Props> = ({
limit,
}) => {
const fileRef = useRef<HTMLInputElement>(null);
/*...*/
return (
<div className="flex w-full overflow-auto scrollbar-hide">
{/*...*/}
<div>
<input
accept="image/*"
ref={fileRef}
onChange={onFileChange}
type="file"
className="hidden"
/>
<div
onClick={() => {
fileRef && fileRef.current?.click();
}}
className="cursor-pointer border-dashed text-[40px] w-[200px] h-[200px] border border-gray-200 mt-5 flex items-center justify-center font-jua text-gray-300"
>
+
</div>
</div>
{/*...*/}
</div>
);
};
export default FileInput;
이미지를 불러올 때 이벤트 함수 onFileChange
함수가 작동되면서 이미지 파일을 불러온다.
e.target.files
배열에 파일의 정보가 담겨있다. const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
};
new FileReader()
의 readAsDataURL
에 이미지 파일을 넣어준다.
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
};
new FileReader()
의 onload
를 사용해서 파일을 읽는다.
e.target.result
에 담겨있는 url을 이미지 태그 src에 넣어준다.
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = (e) => {
const result = e?.target?.result as string;
setImgSrcList([...imgSrcList, result]);
};
};
import React, { useRef, useState } from 'react';
import { useEffect } from 'react';
import { CgClose } from 'react-icons/cg';
import imageCompression from 'browser-image-compression';
interface Props {
data?: string[];
limit?: number;
handleFile: (val: File) => void;
handleImgSrcList?: (val: string[]) => void;
}
const FileInput: React.FC<Props> = ({
data,
limit,
handleFile,
handleImgSrcList,
}) => {
const fileRef = useRef<HTMLInputElement>(null);
const [imgSrcList, setImgSrcList] = useState<string[]>([]);
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
const options = {
maxSizeMB: 2,
maxWidthOrHeight: 1920,
};
if (file) {
const compressedFile = await imageCompression(file, options);
const formData = new FormData();
formData.append('image', compressedFile);
handleFile(compressedFile);
const fileReader = new FileReader();
fileReader.readAsDataURL(compressedFile);
fileReader.onload = (e) => {
const result = e?.target?.result as string;
setImgSrcList([...imgSrcList, result]);
handleImgSrcList && handleImgSrcList([...imgSrcList, result]);
};
}
};
const handleRemoveImage = (index: number) => {
const result = JSON.parse(JSON.stringify(imgSrcList));
result.splice(index, 1);
setImgSrcList(result);
handleImgSrcList && handleImgSrcList(result);
};
useEffect(() => {
if (data?.length) {
setImgSrcList(data);
handleImgSrcList && handleImgSrcList(data);
}
}, [data?.length]);
return (
<div className="flex w-full overflow-auto scrollbar-hide">
{imgSrcList?.map((el, i) => {
return (
<div
key={i}
className="relative min-w-[200px] max-w-[200px] min-h-[200px] max-h-[200px] bg-black mt-5 mr-5 flex items-center justify-center"
>
<button
className="absolute top-0 right-0 bg-[#444040b8] text-white w-8 h-8 flex justify-center items-center"
onClick={() => handleRemoveImage(i)}
>
<CgClose />
</button>
<img
alt={'이미지'}
src={el}
className="w-full h-full object-contain"
/>
</div>
);
})}
{limit !== imgSrcList?.length ? (
<div>
<input
accept="image/*"
ref={fileRef}
onChange={onFileChange}
type="file"
className="hidden"
/>
<div
onClick={() => {
fileRef && fileRef.current?.click();
}}
className="cursor-pointer border-dashed text-[40px] w-[200px] h-[200px] border border-gray-200 mt-5 flex items-center justify-center font-jua text-gray-300"
>
+
</div>
</div>
) : null}
</div>
);
};
export default FileInput;