이미지 업로드 전 미리보기 기능 구현하기
이미지를 미리 보여 주는 기능을 만들려고 생각했을 때 아래 두 가지 방법을 생각했다.
- 이미지를 서버에 올리고 반환된 이미지 주소로 이미지를 미리 보여주는 방법
- 이미지를 서버에 올리기 전 이미지를 미리 보여주는 방법
두 번째 이미지를 서버에 올리기 전 이미지를 미리 보여주는 방법
을 선택했는데 이유는 사용자가 잘못 이미지를 올렸을 경우 서버에 불필요한 이미지가 저장되는 것을 막고, 사용자가 여러 번 이미지를 교체할 경우 서버에 올리는 시간이 걸리기 때문에 더 나은 사용자 경험을 주기 위해서다.
구현 방법
- limit을 주었을 때 이미지 업로드 개수를 제한할 수 있는 컴포넌트로 구현하였다.
<input />
태그로 파일을 불러오기 때문에<input ref={fileRef}/>
태그에fileRef
를 넣고,+
를 클릭했을 때(ex: fileRef.current?.click())
onFileChange
함수가 실행될 수 있도록 했다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
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
배열에 파일의 정보가 담겨있다.1 2 3
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; };
new FileReader()
의readAsDataURL
에 이미지 파일을 넣어준다.- FileReader.readAsDataURL
1 2 3 4 5 6
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에 넣어준다.1 2 3 4 5 6 7 8 9 10
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]); }; };
전체 구현 소스
- 아래는 이미지 생성 및 수정을 할 수 있도록 구현한 전체 소스이다.
- 이미지는 이미지 크기 줄여 업로드하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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;