[React] ๋ฆฌ์กํธ ๋๋๊ทธ์ค๋๋กญ ํ์ผ ์ ๋ก๋ ๊ตฌํ
์์ฆ ๋๋ถ๋ถ ์น์ฌ์ดํธ์์ ํ์ผ์ ์ ๋ก๋ํ ๋ ๋ง์ฐ์ค๋ก ์ํ๋ ํ์ผ์ ๋์ด ๋๋ ๋๋๊ทธ&๋๋กญ ๊ธฐ๋ฅ์ ์ง์ํ๋ค. ๋ฆฌ์กํธ์์ React DnD ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ์๋ ์์ง๋ง HTML5์์ ์ ๊ณตํ๋ ๋๋๊ทธ ๋๋กญ API๋ฅผ ์ด์ฉํด์ ์ง์ ๊ตฌํํ ์ ์๋ค. ์๊ฐ๋ณด๋ค ์ด๋ ต์ง๋ ์๋ค.
๊ธฐ๋ณธ ๊ตฌ์กฐ
export interface IFileTypes {
id: number;
object: File; // File ๊ฐ์ฒด
}
const DragDrop = (
{
/* ... */
},
) => {
const [isDragging, setIsDragging] = useState<boolean>(false);
const [files, setFiles] = useState<IFileTypes[]>([]);
const fileId = useRef<number>(0);
const onChangeFiles = useCallback(/* onChange ๋ก์ง */);
const handleFilterFile = useCallback(/* ํ์ผ ์ญ์ ๋ก์ง */);
const handleDragOut = useCallback(/* onDragLeave ํธ๋ค๋ฌ */);
const handleDragOver = useCallback(/* onDragOver ํธ๋ค๋ฌ */);
const handleDrop = useCallback(/* onDrop ํธ๋ค๋ฌ(onChangeFiles ํธ์ถ) */);
return (
<div>
<section>
<label htmlFor="fileUpload" onDragLeave={} onDragOver={} onDrop={}>
<input
hidden
id="fileUpload"
type="file"
accept={}
multiple={}
onChange={}
/>
ํ์ผ์ ๋์ด์ ์
๋ก๋ํด์ฃผ์ธ์
</label>
</section>
{files.length > 0 &&
files.map(({ id, object: { name } }: IFileTypes) => {
return (
<section>
<span>{`๐ ${name}`}</span> {/* ์
๋ก๋ํ ํ์ผ๋ช
*/}
<button>×</button> {/* ํ์ผ ์ญ์ ๋ฒํผ */}
</section>
);
})}
</div>
);
};
export default DragDrop;
- ํ์ผ์ ์ ๋ก๋ํ๋ฉด ๋๋กญ ์์ญ ํ๋จ์ ์ ๋ก๋ํ๋ ํ์ผ๋ช ๋ฆฌ์คํธ๋ฅผ ๋ณด์ฌ์ค๋ค.
- ๊ฐ ํ์ผ๋ช
์ฐ์ธก์ ์๋
×
๋ฒํผ์ ๋๋ฅด๋ฉด ์ ๋ก๋ํ ํ์ผ์ด ์ญ์ ๋๋ค. - (์ฐธ๊ณ ) ์ ๋ก๋ํ ํ์ผ์ด ์ด๋ฏธ์ง ํ์์ด๋ผ๋ฉด FileReader API๋ฅผ ์ด์ฉํด ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๊ตฌํํ ์๋ ์๋ค.
์ํ
// Dragdrop.tsx
export interface IFileTypes {
id: number;
object: File; // File ๊ฐ์ฒด
}
const [isDragging, setIsDragging] = useState<boolean>(false);
const [files, setFiles] = useState<IFileTypes[]>([]);
const fileId = useRef<number>(0); // ์ํ๊ฐ ๋ณํด๋ ๋ฆฌ๋ ๋ํ ํ์ ์์ผ๋ฏ๋ก useRef๋ก ๊ด๋ฆฌ
- isDragging : ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ ์์ ์์นํ๋์ง ์ฌ๋ถ
- ๐
onDragOver
,onDrop
์ด๋ฒคํธ๊ฐ ๊ฑธ๋ฆฐ ์์๋ ๋๋กญ ์์ญ์ผ๋ก ์ง์ ๋๋ค - files : ์ ๋ก๋ํ ํ์ผ์ด ๋ด๊ธธ ๋ฐฐ์ด. ๊ฐ ์์๋ ํ๋์ File ๊ฐ์ฒด
- fileId : ์ ๋ก๋ํ ํ์ผ ๋ฒํธ (1๊ฐ ํ์ผ์ ์ ๋ก๋ํ ๋๋ง๋ค 1์ฉ ์ฆ๊ฐ)
onChange ํธ๋ค๋ฌ
๐ก File ํ์ Input ์์์ onChange ์ด๋ฒคํธ๋ ๋ง์ฐ์ค ํด๋ฆญ → ํ์ผ ์ ํ → ์ ๋ก๋ ํ์ ๋ ํธ๋ฆฌ๊ฑฐ๋๋ค.
const onChangeFiles = useCallback(
(e): void => {
const selectFiles: File[] =
e.type === 'drop'
? e.dataTransfer.files // ๋๋๊ทธ์ค๋๋กญ์ผ๋ก ์
๋ก๋ํ์ ๋
: e.target.files; // ํด๋ฆญํด์ ํ์ผ์ ์
๋ก๋ํ์ ๋(e.type === 'change')
// ํ์์ ํ์ผ ์ ํ, ํฌ๊ธฐ ๋ฑ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง ์ถ๊ฐ(https://dev-gorany.tistory.com/254)
// ์ ํจ์ฑ์ ํต๊ณผํ์ง ์๋๋ค๋ฉด return
// FileList ๊ฐ์ฒด๋ length ํ๋กํผํฐ๋ฅผ ๊ฐ์ง ์ ์ฌ ๋ฐฐ์ด์ด์ Symbol.iterator๊ฐ ๊ตฌํ๋ ์ดํฐ๋ฌ๋ธ
// ๋ฐ๋ผ์ map ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์ ๊ฐ์ฐ์ฐ์ ํน์ Array.from์ผ๋ก ์ง์ง ๋ฐฐ์ด๋ก ๋ง๋ค์ด์ค๋ค
const tempFiles: IFileTypes[] = [...selectFiles].map((f) => ({
id: fileId.current++,
object: f,
}));
setFiles([...files, ...tempFiles]);
e.target.value = ''; // ๋์ผ ํ์ผ ์ญ์ /์
๋ก๋ ๊ฐ๋ฅํ๋๋ก ํ์ฉ(ํด๋ฆญ ์
๋ก๋ ์ input.value ๊ฐ์ด ํ์ผ๋ช
์ผ๋ก ํ ๋น๋จ)
},
[acceptFileFormat, files, maxFileLength],
);
์ด๋ฒคํธ ํ์
File ํ์
Input ์์์ onChange
์์ฑ์ ํ ๋น๋ onChangeFiles
ํธ๋ค๋ฌ๋ ์๋ 2๊ฐ์ง ์ํฉ์์ ์คํ๋๋ค. โ๋๋๊ทธ ๋๋กญ โํด๋ฆญ - ํ์ผ ์ ํ ์
๋ก๋ ๋ฐฉ์์ ๋ฐ๋ผ ์ด๋ฒคํธ ํ์
์ด ๋ค๋ฅด๋ฏ๋ก ํ์ผ ๊ฐ์ฒด๊ฐ ๋ด๊ธฐ๋ ๊ณณ๋ ์์ดํ๋ค.
- ๋๋กญ ์์ญ ๋ง์ฐ์ค ํด๋ฆญ → ํ์ผ ์ ํ ํ ์
๋ก๋ (
onChangeFiles
ํธ๋ค๋ฌ ํธ์ถ)- ์ด๋ฒคํธ ํ์
(
e.type
) :change
- File ๊ฐ์ฒด ์์น :
event.dataTransfer.files
- ์ด๋ฒคํธ ํ์
(
- ํ์ผ์ ๋๋กญ ์์ญ์ผ๋ก ๋๋๊ทธ / ๋๋กญํด์ ์
๋ก๋ (onDrop ์ด๋ฒคํธ ํธ๋ค๋ฌ์์
onChangeFiles
ํธ์ถ)- ์ด๋ฒคํธ ํ์
(
e.type
) :drop
- File ๊ฐ์ฒด ์์น :
event.target.files
- ์ด๋ฒคํธ ํ์
(
FileList
์
๋ก๋ํ ํ์ผ์ FileList
๊ฐ์ฒด์ ๋ด๊ธฐ๊ณ , FileList
๋ Symbol.iterator
๋ฉ์๋๊ฐ ๊ตฌํ๋ ์ดํฐ๋ฌ๋ธ์ด์ length
ํ๋กํผํฐ๋ฅผ ๊ฐ์ง ์ ์ฌ๋ฐฐ์ด์ด๋ค. ๋ฐ๋ผ์ map
๊ฐ์ ๋ฐฐ์ด ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด Array.from
์ด๋ ์ ๊ฐ ์ฐ์ฐ์๋ฅผ ์ด์ฉํด ์ง์ง ๋ฐฐ์ด๋ก ๋ง๋ค์ด์ฃผ๋ ์์
์ด ํ์ํ๋ค.
๊ฐ์ ํ์ผ ๋ค์ ์ฌ๋ฆฌ๊ธฐ
๐ก ๋๋๊ทธ ๋๋กญ์ผ๋ก ์
๋ก๋ํ ํ์ผ์ <label>
์์์ ๊ฑธ๋ฆฐ ๋๋๊ทธ ์ด๋ฒคํธ๋ฅผ ํตํด ํธ๋ค๋งํ๋ฏ๋ก ์๋ ๋ด์ฉ์ ํด๋น๋์ง ์๋๋ค.
๋๋กญ ์์ญ ๋ง์ฐ์ค ํด๋ฆญ → ํ์ผ ์ ํ ํ ์
๋ก๋ํ๋ฉด event.target.value
๊ฐ์ ๊ฒฝ๋ก.ํ์ผ๋ช
์ด ํ ๋น๋๋ค. ์ํ์ ์ถ๊ฐ๋ ๊ธฐ์กด ํ์ผ์ ์ญ์ ํ๊ณ ๋์ผํ ํ์ผ์ ๋ค์ ์ฌ๋ฆฌ๋ฉด ์๋ฌด์ผ๋ ์ผ์ด๋์ง ์๋๋ค. input.value
์ ํ ๋น๋ ํ์ผ๋ช
๊ณผ ๋์ผํ๊ธฐ ๋๋ฌธ์ onChange
์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์ง ์์์ ๊ทธ๋ฐ๊ฒ.
// onChange ํธ๋ค๋ฌ ๋ด๋ถ์์ ์ฝ์ ์ถ๋ ฅ ์
// C:\fakepath\Profile_OpenPeeps.png
console.log(event.target.value);
๊ฐ์ ํ์ผ์ ๋ค์ ์ฌ๋ ธ์ ๋๋ onChange
์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋๋ก ํ๋ ค๋ฉด, onChange
ํธ๋ค๋ฌ์ input.value
(event.target.value
) ๊ฐ์ ๋น์์ฃผ๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ฉด ๋๋ค.
๋๋๊ทธ ๋๋กญ ์ด๋ฒคํธ ํธ๋ค๋ฌ
๐ก onDragOver
์ onDrop
์ด๋ฒคํธ๊ฐ ๊ฑธ๋ ค์๋ DOM์ด ๋๋กญ ๊ฐ๋ฅํ ์์ญ(์ ํจํ ๋๋กญ ๋์)์ด ๋๋ค.
handleDragOver — ๋๋กญ ์์ญ ์
const handleDragOver = useCallback((e: DragEvent<HTMLLabelElement>): void => {
e.preventDefault(); // ๋๋กญ ํ์ฉ(dragover, dragenter ์ด๋ฒคํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋กญ ์ด๋ฒคํธ๋ฅผ ์ทจ์ ์ํด)
setIsDragging(true);
}, []);
- ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ(
onDragOver
,onDrop
์ด๋ฒคํธ๊ฐ ๊ฑธ๋ฆฐ ์์) ์์ ์์ ๋ ํธ๋ฆฌ๊ฑฐ - ํธ๋ค๋ฌ๋ ๋๋กญ ์์์
onDragOver
์์ฑ์ ํ ๋น
๐๏ธ ์ฐธ๊ณ ๋กonDragEnter
์ด๋ฒคํธ๋ ๋์ ๊ฐ์ฒด ์์ ์ฒ์ ์ง์ ํ์ ๋ 1ํ๋ง ํธ๋ฆฌ๊ฑฐ ๋๊ณ ,onDragOver
์ด๋ฒคํธ๋ ๋์ ๊ฐ์ฒด ์์ ์๋ ๋์ ์๋ฐฑ๋ฐ๋ฆฌ์ด ๊ฐ๊ฒฉ์ผ๋ก ๊ณ์ ํธ๋ฆฌ๊ฑฐ๋๋ค. - ํ์ฌ ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ ์์ ์์นํ์ผ๋ฏ๋ก
isDragging
์ํ๋ฅผtrue
๋ก ๋ณ๊ฒฝ onDragOver
,onDragEnter
์ด๋ฒคํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋กญ ์ด๋ฒคํธ๋ฅผ ์ทจ์ํ๋ ๊ธฐ๋ณธ ๋์์ ๊ฐ์ง. ํ์ผ์ ๋๋กญ ํ ์ ์๋๋ก ํ๊ธฐ ์ํดpreventDefault
๋ฉ์๋ ํธ์ถ
handleDragOut — ๋๋กญ ์์ญ ๋ฐ
// Dragdrop.tsx
const handleDragOut = useCallback((): void => {
setIsDragging(false);
}, []);
- ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ(
onDragOver
,onDrop
์ด๋ฒคํธ๊ฐ ๊ฑธ๋ฆฐ ์์)์ ๋ฒ์ด๋ฌ์ ๋ ํธ๋ฆฌ๊ฑฐ - ํธ๋ค๋ฌ๋ ๋๋กญ ์์์
onDragLeave
์์ฑ์ ํ ๋น - ํ์ฌ ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ์ ๋ฒ์ด๋ฌ์ผ๋ฏ๋ก
isDragging
์ํ๋ฅผfalse
๋ก ๋ณ๊ฒฝ
handleDrop — ๋๋กญ ์์ญ์์ ๋๋กญ
// Dragdrop.tsx
const handleDrop = useCallback(
(e: DragEvent<HTMLLabelElement>): void => {
e.preventDefault(); // ๋ธ๋ผ์ฐ์ ์ ํญ์์ ํ์ผ์ด ์ด๋ฆฌ๋ ๊ธฐ๋ณธ ์ด๋ฒคํธ ๋ฐฉ์ง
e.stopPropagation(); // ์ด๋ฒคํธ ์ ํ ๋ฐฉ์ง(๋ง์ฐ์ค ๊ด๋ จ ํฌ๋กฌ ํ์ฅ ํ๋ก๊ทธ๋จ์ด ํธ๋ฆฌ๊ฑฐ ๋๋ ํ์ ๋ฐฉ์ง)
onChangeFiles(e); // Drop ์์ ์ ๋ง์ฐ์ค๋ก ๋๋๊ทธํ๋ ํ์ผ ์ ๋ณด๊ฐ e.dataTransfer.files ๊ฐ์ฒด์ ๋ด๊น
setIsDragging(false);
},
[onChangeFiles],
);
- ๋๋กญ ์์ญ์์(
onDragOver
,onDrop
์ด๋ฒคํธ๊ฐ ๊ฑธ๋ฆฐ ์์) ๋ง์ฐ์ค ํด๋ฆญ์ ํด์ ํ์ฌ ๋๋กญํ์ ๋ ํธ๋ฆฌ๊ฑฐ - ํธ๋ค๋ฌ๋ ๋๋กญ ์์์
onDrop
์์ฑ์ ํ ๋น - ๋๋กญ ์์ ์ ๋ง์ฐ์ค๋ก ๋๋๊ทธํ๋ ํ์ผ ์ ๋ณด๋
event.dataTransfer.files
๊ฐ์ฒด์ ๋ด๊น๋๋กญ ์์ ์event.dataTransfer.files
์ฝ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ
- ๋๋กญํ ํ์ผ์ ์ฒ๋ฆฌํ๊ธฐ(์ํ๋ก ์ ์ฅ) ์ํด ์ด๋ฒคํธ ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ๋๊ฒจ
onChangeFiles
ํธ๋ค๋ฌ ํธ์ถ - ํ์ผ ๋๋กญ ํ์ ๋์ด์ ๋๋๊ทธ ์ํ๊ฐ ์๋๋ฏ๋ก
isDragging
์ํ๋ฅผfalse
๋ก ๋ณ๊ฒฝ onDrop
์ด๋ฒคํธ๋ ๋ธ๋ผ์ฐ์ ์ํญ์ ๋๋กญํ ํ์ผ์ ์ฌ๋(Open) ๊ธฐ๋ณธ ๋์์ ๊ฐ์ง. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํดpreventDefault
๋ฉ์๋ ํธ์ถ- crxMouse ๊ฐ์ ๋ง์ฐ์ค ์ ์ค์ฒ ๋ถ๊ฐ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ฉด ๋๋กญ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ํ ์ด๋ฒคํธ๊ฐ ์ ํ๋ผ์ ์ฝ์์ ์๋ฌ๋ฅผ ์ฐ๋๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด
stopPropagation
๋ฉ์๋๋ฅผ ํธ์ถํด์ ์ด๋ฒคํธ ์ ํ๋ฅผ ๋ง๋๋ค.
๋๋กญ ์์ญ
ํ์ผ Input ์คํ์ผ ์์
<input>
ํ๊ทธ์ type
์ file
๋ก ๋ช
์ํ๋ฉด “ํ์ผ ์ ํ” ๋ฒํผ์ด ํ์๋๋ค. accept
์์ฑ์ ํ์ฉํ ํ์ผ ์ ํ์ .ํ์ฅ์
ํํ๋ก ์
๋ ฅํ๋ค. ํ์ฅ์๋ ๋์๋ฌธ์๋ฅผ ๊ตฌ๋ถํ์ง ์๋๋ค. ์ฌ๋ฌ ๊ฐ์ ์
๋ ฅํ ๋ ์ฝค๋ง ,
๋ก ๊ตฌ๋ถํ๋ค.
ํน์ ํ์
(MIME ์ ํ)์ ๋ชจ๋ ํ์ฅ์๋ฅผ ํ์ฉํ๊ณ ์ถ์ผ๋ฉด ํ์
/*
์ ์
๋ ฅํ๋ค. ํ์ฉํ์ง ์์ ํ์ผ ์ ํ์ ํ์ผ ์ ํ ์ฐฝ์์ ์ ํํ ์ ์๋ค. (ํ์ผ ์ ํ ์ฐธ๊ณ ๊ธ)
- ํ์ฅ์ ์
๋ ฅ ์์ :
.jpg, .png, .pdf
(jpg
,png
,pdf
ํ์ผ ํ์ฉ) - ํน์ ํ์
์ ๋ชจ๋ ํ์ฅ์ ํ์ฉ :
image/*
,video/*
,audio/*
๊ธฐ๋ณธ์ ์ผ๋ก 1๊ฐ ํ์ผ๋ง ์ ํํด์ ์
๋ก๋ํ ์ ์์ผ๋ฉฐ multiple
์์ฑ์ true
๋ก ์ค์ ํ๋ฉด ์ฌ๋ฌ ํ์ผ์ ์
๋ก๋ํ ์ ์๋ค(multiple
์์ฑ์ ๋ช
์ํ์ง ์์ผ๋ฉด 1๊ฐ ํ์ผ๋ง ์
๋ก๋ํ ์ ์๋ค).
file
ํ์
์ <input>
ํ๊ทธ๋ ๋ธ๋ผ์ฐ์ ๋ง๋ค ์กฐ๊ธ์ฉ ๋ค๋ฅธ ๊ธฐ๋ณธ UI๋ฅผ ๊ฐ์ง๋ค. ์์ฝ๊ฒ๋ ์ด UI๋ CSS ์คํ์ผ๋ก ๋ณ๊ฒฝํ ์ ์๋ค. ์คํ์ผ์ ์์ ํ๊ณ ์ถ๋ค๋ฉด <input>
ํ๊ทธ๋ฅผ ํ๋ฉด์์ ์จ๊ธฐ๊ณ <label>
ํ๊ทธ์ ์ํ๋ ์คํ์ผ์ ์ ์ํด์ผ ๋๋ค.
- Label โ Input ํ๊ทธ ์ฐ๊ฒฐ
<label>
ํ๊ทธ์htmlFor
์์ฑ๊ณผ<input>
ํ๊ทธ์id
์์ฑ์ ๋์ผํ๊ฒ ์์ฑํ๋ฉด ๋ ํ๊ทธ๊ฐ ์ฐ๊ฒฐ๋๋ค. ๊ทธ๋ผ<label>
์textContent
์ฝํ ์ธ ์์ญ์ ํด๋ฆญํด๋ input ๋ฐ์ค๋ฅผ ํธ๋ค๋ง ํ ์ ์๋ค. - Input ํ๊ทธ ์จ๊ธฐ๊ธฐ
<input>
ํ๊ทธ์hidden
์์ฑ์ ์ถ๊ฐํด์ ์จ๊ฒจ์ง ํ๋๋ก ์ ์ํ๋ค. ๊ทธ๋ผ ํ๋ฉด์์ ๋ณด์ด์ง ์๋๋ค. - Label ํ๊ทธ์ ์ํ๋ ์คํ์ผ ์ง์
Input ํ๊ทธ๋ฅผ ์จ๊น ํ๋๋ก ๋ง๋ค์์ผ๋ฏ๋ก “ํ์ผ์ฐพ๊ธฐ” ๋ฒํผ์ด ๋์ด์ ํ์๋์ง ์๋๋ค. ์ด์ <label>
ํ๊ทธ์ ์ํ๋ ์คํ์ผ์ ์ง์ ํด์ฃผ๋ฉด ๋๋ค.
Tailwind CSS๋ก ๋๋กญ ์์ญ ์คํ์ผ ์ ์ํ ์์ โผ
<div className={/*...*/}>
<section
className={classnames(
'w-full h-[138px] border-2 border-dashed rounded-lg active:border-blue-400',
{ 'bg-blue-100 border-blue-400': isDragging }, // ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ์ ์์ ๋ ๋ฐฐ๊ฒฝ์ ๋ณ๊ฒฝ
)}
>
<label
className="cursor-pointer h-full grid place-content-center text-[#4E5968]"
htmlFor="fileUpload" // label โ input ํ๊ทธ ์ฐ๊ฒฐ (input์ด ์์ชฝ์ ์์ด์ ์์ ์ด๋ ๋์ง๋ง ์ฐธ๊ณ ์ฉ์ผ๋ก ๊ธฐ์ฌ)
// ...
>
<input
type="file"
hidden // ์จ๊ฒจ์ง ํ๋๋ก ์ ์(ํ๋ฉด์์ ๊ฐ์ถค)
id="fileUpload"
// ...
/>
ํ์ผ์ ๋์ด์ ์
๋ก๋ํด์ฃผ์ธ์
</label>
</section>
{/* ํ์ผ ๋ฆฌ์คํธ ์๋ต... */}
</div>;
๐ก radio ํ์
์ <input>
์ 1๊ฐ๋ง ์ ํ ํ ์ ์๊ณ , checkbox ํ์
์ <input>
์ ์ฌ๋ฌ ๊ฐ ์ ํ ํ ์ ์๋ค. name
์์ฑ์ ์ฒดํฌ๋ฐ์ค์ ์ด๋ฆ์ ๋ํ๋ด๋ฉฐ ๊ฐ์ ๋ถ๋ฅ์ ์ฒดํฌ๋ฐ์ค๋ฅผ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ ๋ ์ฌ์ฉํ๋ค. React / Vue์์ ํน์ <input>
ํ๊ทธ๋ฅผ ์๋ณํ ๋ name
์์ฑ์ ํ์ฉํ๊ธฐ๋ ํ๋ค. form ํ๊ทธ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ฉด <input>
์ name
, value
์์ฑ ๊ฐ์ด ...?name=value
ํํ๋ก ์ ์ก๋๋ค(์ฐธ๊ณ ).
๐ก <label>
ํ๊ทธ์ <input>
ํ๊ทธ๋ฅผ ์ฐ๊ฒฐํ๋ฉด <label>
์ textContent
์์ญ๋ง ํด๋ฆญํด๋ input
์ฒดํฌ๋ฐ์ค๋ฅผ ํธ๋ค๋งํ ์ ์๋ค. <label>
ํ๊ทธ์ for
์์ฑ๊ณผ <input>
ํ๊ทธ์ id
์์ฑ ๊ฐ์ ๋์ผํ๊ฒ ์
๋ ฅํ๋ฉด ๋ ํ๊ทธ๋ฅผ ์ฐ๊ฒฐํ ์ ์๋ค. ์ ๊ทผ์ฑ์ ๊ณ ๋ คํด <input>
๊ณผ <label>
ํ๊ทธ๋ ๊ฐ์ด ์ฐ๋๊ฒ ์ข๋ค. <input>
ํ๊ทธ๊ฐ <label>
์์ชฝ์ ์๋ค๋ฉด ์ฐ๊ฒฐ๋ ์ํ๊ฐ ๋๋ฏ๋ก for
, id
๋ฅผ ์
๋ ฅํ์ง ์์๋ ๋๋ค.
ํธ๋ค๋ฌ ์ฐ๊ฒฐ / accet, multiple ์์ฑ ์ง์
Input ํ๊ทธ๋ ์จ๊ฒจ์ง ํ๋๋ก ์ ์ํด์ ํ๋ฉด์ ๋ณด์ด์ง ์์ผ๋ฏ๋ก, Input ํ๊ทธ์ ์ฐ๊ฒฐ๋ Label ํ๊ทธ์ ๋๋๊ทธ ๋๋กญ ๊ด๋ จ ํธ๋ค๋ฌ๋ฅผ ํ ๋นํ๋ค. ๋ง์ฐ์ค๋ก ํด๋ฆญํ์ ๋๋ ํ์ผ ์
๋ก๋๋ฅผ ์ฒ๋ฆฌ ํ๊ธฐ ์ํด Input ํ๊ทธ์ onChange
์์ฑ์ onChangeFiles
ํธ๋ค๋ฌ๋ฅผ ํ ๋นํ๋ค.
๐ก accept
, multiple
์์ฑ์ Input ํ๊ทธ์ ์ง์ ํ์ผ๋ฏ๋ก Label ํ๊ทธ์์ ์ด๋ค์ง๋ ๋๋๊ทธ ๋๋กญ ์ก์
์ ์ ์ฉ๋์ง ์๋๋ค. ๋ฐ๋ผ์ onChangeFiles
ํธ๋ค๋ฌ์์ ํด๋น ์กฐ๊ฑด์ ๋ฐ๋ก ์ถ๊ฐํด์ค์ผ ํ๋ค.
// ์๋ ๋ ๋ณ์๋ DragDrop.ts ์ปดํฌ๋ํธ๊ฐ props๋ก ๋ฐ์
// acceptFileFormat: Array<'jpg'|'png'|'...'>
// maxFileLength: number
<label
// ...
onDragLeave={handleDragOut} // ๋๋กญ ์์ญ์ ๋ฒ์ด๋ฌ์ ๋
onDragOver={handleDragOver} // ๋๋กญ ์์ญ ์์ ์์ ๋
onDrop={handleDrop} // ๋๋กญ ์์ญ์์ ๋๋กญํ์ ๋
>
<input
// ...
accept={acceptFileFormat.map((t) => `.${t}`).join()} // ํ์ฉํ ํ์ผ ํ์ฅ์ *ex) 'jpg,png'*
multiple={maxFileLength > 1} // ์ฌ๋ฌ ํ์ผ ์
๋ก๋ ํ์ฉ ์ฌ๋ถ. ๊ธฐ๋ณธ๊ฐ false
onChange={onChangeFiles} // ๋ง์ฐ์ค๋ก ํด๋ฆญํด์ ํ์ผ ์
๋ก๋ ์ ํธ๋ค๋ฌ ํธ์ถ
/>
ํ์ผ์ ๋์ด์ ์
๋ก๋ํด์ฃผ์ธ์
</label>
ํ์ผ ๋ฆฌ์คํธ
ํ์ผ ์ญ์ ํธ๋ค๋ฌ
id
๋ฅผ ์ธ์๋ก ๋ฐ์, filter
๋ฉ์๋๋ก ํด๋น id
๋ฅผ ๊ฐ์ง ํ์ผ ๊ฐ์ฒด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค(์ญ์ ).
const handleFilterFile = useCallback(
(id: number): void => {
setFiles(files.filter((file: IFileTypes) => file.id !== id));
},
[files],
);
ํ์ผ ๋ฆฌ์คํธ ๋ ๋๋ง
์
๋ก๋ํ ํ์ผ์ ๋ด์๋๋ files
์ํ ๋ฆฌ์คํธ๋ฅผ ๋ ๋๋งํ๋ค. ํ์ผ๋ช
์ ๊ฐ ํ์ผ ๊ฐ์ฒด์ name
์์ฑ์์ ์ฐธ์กฐํ ์ ์๋ค. ×
๋ฒํผ์ ๋๋ฅด๋ฉด ํด๋น ํ์ผ์ id
๋ฅผ ์ธ์๋ก ๋๊ฒจ ํ์ผ ์ญ์ ํธ๋ค๋ฌ๋ฅผ ํธ์ถํด์ ์ญ์ ํ๋ค.
<div className={`text-sm ${classNameProps}`}>
{/* ๋๋กญ ์์ญ(input/label ์๋ต)... */}
{files.length > 0 &&
files.map(({ id, object: { name } }: IFileTypes) => {
return (
<section
key={id}
className="bg-[#E7F1FD] rounded-lg mt-3 p-4 flex items-center justify-between text-[#327ADF]"
>
<span>{`๐ ${name}`}</span>
<button type="button" onClick={() => handleFilterFile(id)}>
×
</button>
</section>
);
})}
</div>
Custom Hook ์ผ๋ก ๋ถ๋ฆฌ
๋๋๊ทธ ๋๋กญ ์ด๋ฒคํธ ๋ก์ง๋ฅผ ์ปค์คํ
ํ
์ผ๋ก ๋ฐ๋ก ๋ถ๋ฆฌํ ์๋ ์๋ค. ์ปค์คํ
ํ
์ โref
๊ฐ์ฒด์ โ๋๋กญ ์์ญ ์์ ์์นํ๋์ง ์ฌ๋ถ์ ๋ํ isDragging
์ํ๋ฅผ ๊ด๋ฆฌํ๋ค. ์ด ๋์ ์ปค์คํ
ํ
์ ํธ์ถํ ๊ณณ์์ ๋ฐํ๋ฐ์ ์ฌ์ฉํ๋ค.
Hook์ด ๋ง์ดํธ ๋๋ฉด ์ธ์๋ก ๋ฐ์ ref
๊ฐ์ฒด์ current
์์ฑ์ onDragOver
๊ฐ์ ๋๋๊ทธ ๊ด๋ จ ์ด๋ฒคํธ๋ฅผ ๋ฑ๋กํ๋ค. ๋ฐ๋๋ก Hook์ด ์ธ๋ง์ดํธ๋์ ๋ ๋ฑ๋กํ ์ด๋ฒคํธ๋ฅผ ํด์ ํ๋ค.
๋๋๊ทธ ๊ด๋ จ ์ด๋ฒคํธ๋ฅผ ๋ฑ๋กํ ๋ค onDrop
์ด๋ฒคํธ๊ฐ ํธ๋ฆฌ๊ฑฐ ๋๋ฉด ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ onChange
ํธ๋ค๋ฌ๋ฅผ ํธ์ถํ๋ค. ํธ๋ค๋ฌ๋ฅผ ํธ์ถํ ๋ ์ด๋ฒคํธ ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ์ ๋ฌํ๋ค.
// hooks/useDragDrop.tsx
export default function useDragrop<T extends HTMLElement>(
onChangeHandler: UseDragDropProps,
) {
const [isDragging, setIsDragging] = useState(false); // ๋๋๊ทธ ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ ์์ ์์นํ๋์ง ์ฌ๋ถ
const dragRef = useRef<T | null>(null);
const handleDragOut = useCallback(/* onDragLeave ํธ๋ค๋ฌ */);
const handleDragOver = useCallback(/* onDragOver ํธ๋ค๋ฌ */);
const handleDrop = useCallback(/* onDrop ํธ๋ค๋ฌ(onChangeHandler ํธ์ถ) */);
const initDragEvents = useCallback(() => {
if (dragRef.current) {
dragRef.current.addEventListener('dragover', handleDragOut);
// dragleave(onDragLeave), drop(onDrop) ์ด๋ฒคํธ ๋ฑ๋ก ์๋ต...
}
}, [dragRef, handleDragOut, handleDragOver, handleDrop]);
const resetDragEvents = useCallback(() => {
if (dragRef.current) {
dragRef.current.removeEventListener('dragover', handleDragOut);
// dragleave(onDragLeave), drop(onDrop) ์ด๋ฒคํธ ๋ฑ๋ก ํด์ ์๋ต...
}
}, [dragRef, handleDragOut, handleDragOver, handleDrop]);
useEffect(() => {
initDragEvents(); // Hook ๋ง์ดํธ ์ ์ด๋ฒคํธ ๋ฑ๋ก
return () => resetDragEvents(); // Hook ์ธ๋ง์ดํธ์ ์ด๋ฒคํธ ๋ฑ๋ก ํด์
}, [initDragEvents, resetDragEvents]);
return [isDragging, dragRef] as const; // onDragOver ์ฌ๋ถ, ref ๊ฐ์ฒด ๋ฐํ
}
import { useCallback, useEffect, useRef, useState } from 'react';
interface UseDragDropProps {
(e: DragEvent): void;
}
export default function useDragDrop<T extends HTMLElement>(
onChangeHandler: UseDragDropProps,
) {
const [isDragging, setIsDragging] = useState(false);
const dragRef = useRef<T | null>(null);
// ๋์ ๊ฐ์ฒด ์์ญ์ ๋ฒ์ด๋ฌ์ ๋
const handleDragOut = useCallback((_e: DragEvent) => {
setIsDragging(false);
}, []);
// ๋์ ๊ฐ์ฒด ์์ ์์ ๋(์์ ์๋ ๋์ ๊ณ์ ํธ์ถ)
const handleDragOver = useCallback((e: DragEvent) => {
e.preventDefault(); // ๋๋กญ ํ์ฉ(dragover, dragenter ์ด๋ฒคํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋กญ ์ด๋ฒคํธ๋ฅผ ์ทจ์ ์ํด)
setIsDragging(true);
}, []);
// ๋ง์ฐ์ค ํด๋ฆญ์ ํด์ ํ์ฌ ๋๋กญํ์ ๋
const handleDrop = useCallback(
(e: DragEvent) => {
e.preventDefault(); // ๋ธ๋ผ์ฐ์ ์ ์ ํญ์์ ํ์ผ์ด ์ด๋ฆฌ๋ ๊ธฐ๋ณธ ์ด๋ฒคํธ ๋ฐฉ์ง
e.stopPropagation(); // ์ด๋ฒคํธ ์ ํ ๋ฐฉ์ง(๋ง์ฐ์ค ๊ด๋ จ ํฌ๋กฌ ํ์ฅ ํ๋ก๊ทธ๋จ์ด ํธ๋ฆฌ๊ฑฐ ๋๋ ํ์ ๋ฐฉ์ง)
onChangeHandler(e); // Drop ์์ ์ ๋ง์ฐ์ค๋ก ๋๋๊ทธํ๋ ํ์ผ ์ ๋ณด๊ฐ e.dataTransfer.files ๊ฐ์ฒด์ ๋ด๊น
setIsDragging(false);
},
[onChangeHandler],
);
const initDragEvents = useCallback(() => {
if (dragRef.current) {
dragRef.current.addEventListener('dragleave', handleDragOut);
dragRef.current.addEventListener('dragover', handleDragOver);
dragRef.current.addEventListener('drop', handleDrop);
}
}, [dragRef, handleDragOut, handleDragOver, handleDrop]);
const resetDragEvents = useCallback(() => {
if (dragRef.current) {
dragRef.current.removeEventListener('dragleave', handleDragOut);
dragRef.current.removeEventListener('dragover', handleDragOver);
dragRef.current.removeEventListener('drop', handleDrop);
}
}, [dragRef, handleDragOut, handleDragOver, handleDrop]);
useEffect(() => {
initDragEvents();
return () => resetDragEvents();
}, [initDragEvents, resetDragEvents]);
return [isDragging, dragRef] as const;
}
DragDrop.ts
์ปดํฌ๋ํธ์์ onChange
ํธ๋ค๋ฌ๋ฅผ ์ธ์๋ก ๋๊ฒจ Hook์ ํธ์ถํ ๋ค onDragOver
์ฌ๋ถ๋ฅผ ๋ํ๋ด๋ isDragging
์ํ์ dragRef
๊ฐ์ฒด๋ฅผ ๋ฐํ ๋ฐ๋๋ค. dragRef
๊ฐ์ฒด๋ ๋๋กญ ์์ญ์ผ๋ก ์ง์ ๋ ํ๊ทธ์ ref
์์ฑ์ ํ ๋นํ๋ค. isDragging
์ ๋๋๊ทธ ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ ์์ ์์น ํ๋์ง์ ๋ฐ๋ผ ๋ฐฐ๊ฒฝ์์ ๋ณ๊ฒฝํ ๋ ์ฌ์ฉํ๋ค.
// DragDrop.tsx
const DragDrop = (
{
/* ... */
},
) => {
const onChangeFiles = useCallback(/* onChange ๋ก์ง */);
const [isDragging, dragRef] = useDragDrop<HTMLLabelElement>(onChangeFiles);
// ...์๋ต
return (
<div className={/* ... */}>
<section
className={classnames(
'w-full h-[138px] border-2 border-dashed rounded-lg active:border-blue-400',
{ 'bg-blue-100 border-blue-400': isDragging }, // ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ ์์ ์์ ๋ ๋ฐฐ๊ฒฝ์ ๋ณ๊ฒฝ
)}
>
<label
className="cursor-pointer h-full grid place-content-center text-[#4E5968]"
htmlFor="fileUpload"
ref={dragRef}
>
{/* <input onChange={onChangeFiles} ... /> ์๋ต */}
ํ์ผ์ ๋์ด์ ์
๋ก๋ํด์ฃผ์ธ์
</label>
</section>
{/* ํ์ผ ๋ฆฌ์คํธ ์๋ต... */}
</div>
);
};
export default DragDrop;
์ ์ฒด ์ฝ๋
// lib/utils.ts
import { ERR_FILE_NUM_EXCEEDED, ERR_NOT_ALLOWED_EXTENSION } from './constants';
import { AcceptFormat, IFileTypes } from '../types';
// AcceptFormat: 'jpg' | 'jpeg' | 'png' | 'gif' | 'webp' | 'tiff';
export const checkAcceptFormat = (files: File[], acceptList: string[]) => {
return [...files].every(
({ type }) => acceptList.includes(type.split('/')[1]), // image/png → ['image', 'png']
);
};
interface ValidCheckParams {
files: IFileTypes[]; // ์ด๋ฏธ ์
๋ก๋ํ ํ์ผ์ด ๋ด๊ธด ๋ฆฌ์คํธ (DragDrop ์ปดํฌ๋ํธ์์ ๊ด๋ฆฌ)
selectFiles: File[]; // ์
๋ก๋ํ๊ธฐ ์ํด ์ฌ์ฉ์๊ฐ ํ์ฌ ์ ํํ(ํน์ ๋๋๊ทธ ๋๋กญ) ํ์ผ ๋ฆฌ์คํธ
maxFileLength: number; // ์ต๋ ์
๋ก๋ ํ์ผ ๊ฐ์ (DragDrop ์ปดํฌ๋ํธ๊ฐ props๋ก ๋ฐ์)
acceptFileFormat: AcceptFormat[]; // ์
๋ก๋ ํ ์ ์๋ ํ์ผ ํฌ๋งท (DragDrop ์ปดํฌ๋ํธ๊ฐ props๋ก ๋ฐ์)
}
export const validCheck = ({
files,
selectFiles,
maxFileLength,
acceptFileFormat,
}: ValidCheckParams) => {
const result = { isValid: true, errMsg: '' };
if (files.length >= maxFileLength || selectFiles.length > maxFileLength) {
result.isValid = false;
result.errMsg = `${ERR_FILE_NUM_EXCEEDED} \n`; // '์ต๋ ํ์ผ ๊ฐฏ์๋ฅผ ์ด๊ณผ ํ์์ต๋๋ค'
}
if (!checkAcceptFormat(selectFiles, acceptFileFormat)) {
result.isValid = false;
result.errMsg += ERR_NOT_ALLOWED_EXTENSION; // 'ํ์ฉ๋์ง ์์ ํ์ฅ์์
๋๋ค'
}
return result;
};
// DragDrop.tsx
import classnames from 'classnames';
import React, {
DragEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { IFileTypes, SignContent } from 'types';
import {
DD_UPLOAD_GUIDE,
MULTIPLICATION_SIGN,
UPLOAD_COMPLETE,
} from 'lib/constants';
import { validCheck } from 'lib/utils';
interface DragDropProps {
classNameProps: string;
handler: (arg: boolean) => void;
acceptFileInfo: SignContent['file'];
}
const DragDrop = ({
classNameProps,
handler,
acceptFileInfo: { maxFileLength, acceptFileFormat },
}: DragDropProps) => {
const [isDragging, setIsDragging] = useState<boolean>(false);
const [files, setFiles] = useState<IFileTypes[]>([]);
const fileId = useRef<number>(0); // ์ํ๊ฐ ๋ณํด๋ ๋ฆฌ๋ ๋ํ ํ์ ์์ผ๋ฏ๋ก useRef ๋ก ๊ด๋ฆฌ
const onChangeFiles = useCallback(
(e): void => {
const selectFiles: File[] =
e.type === 'drop'
? e.dataTransfer.files // ๋๋๊ทธ์ค๋๋กญ์ผ๋ก ์
๋ก๋ํ์ ๋
: e.target.files; // ํด๋ฆญํด์ ํ์ผ์ ์
๋ก๋ํ์ ๋(e.type === 'change')
// ํ์์ ํ์ผ ํฌ๊ธฐ ๋ฑ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง ์ถ๊ฐ(https://dev-gorany.tistory.com/254)
const { isValid, errMsg } = validCheck({
files,
selectFiles,
maxFileLength,
acceptFileFormat,
});
if (!isValid) return alert(errMsg);
// FileList ๊ฐ์ฒด๋ length ํ๋กํผํฐ๋ฅผ ๊ฐ์ง๋ ์ ์ฌ ๋ฐฐ์ด์ด์ Symbol.iterator ๊ฐ ๊ตฌํ๋ ์ดํฐ๋ฌ๋ธ
// ๋ฐ๋ผ์ map ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์ ๊ฐ์ฐ์ฐ์ ํน์ Array.from ์ผ๋ก ์ง์ง ๋ฐฐ์ด๋ก ๋ง๋ค์ด์ค๋ค
const tempFiles: IFileTypes[] = [...selectFiles].map((f) => ({
id: fileId.current++,
object: f,
}));
setFiles([...files, ...tempFiles]);
e.target.value = ''; // ๋์ผ ํ์ผ ์ญ์ /์
๋ก๋ ๊ฐ๋ฅํ๋๋ก ํ์ฉ(ํด๋ฆญ ์
๋ก๋ ์ value ๊ฐ์ด ํ์ผ๋ช
์ผ๋ก ํ ๋น๋จ)
alert(UPLOAD_COMPLETE);
},
[acceptFileFormat, files, maxFileLength],
);
const handleFilterFile = useCallback(
(id: number): void => {
setFiles(files.filter((file: IFileTypes) => file.id !== id));
},
[files],
);
// ๋์ ๊ฐ์ฒด ์์ญ์ ๋ฒ์ด๋ฌ์ ๋
const handleDragOut = useCallback((): void => {
setIsDragging(false);
}, []);
// ๋์ ๊ฐ์ฒด ์์ ์์ ๋(์์ ์๋ ๋์ ๊ณ์ ํธ์ถ)
const handleDragOver = useCallback((e: DragEvent<HTMLLabelElement>): void => {
e.preventDefault(); // ๋๋กญ ํ์ฉ(dragover, dragenter ์ด๋ฒคํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋กญ ์ด๋ฒคํธ๋ฅผ ์ทจ์ ์ํด)
setIsDragging(true);
}, []);
// ๋ง์ฐ์ค ํด๋ฆญ์ ํด์ ํ์ฌ ๋๋กญํ์ ๋
const handleDrop = useCallback(
(e: DragEvent<HTMLLabelElement>): void => {
e.preventDefault(); // ๋ธ๋ผ์ฐ์ ์ ์ ํญ์์ ํ์ผ์ด ์ด๋ฆฌ๋ ๊ธฐ๋ณธ ์ด๋ฒคํธ ๋ฐฉ์ง
e.stopPropagation(); // ์ด๋ฒคํธ ์ ํ ๋ฐฉ์ง(๋ง์ฐ์ค ๊ด๋ จ ํฌ๋กฌ ํ์ฅ ํ๋ก๊ทธ๋จ์ด ํธ๋ฆฌ๊ฑฐ ๋๋ ํ์ ๋ฐฉ์ง)
onChangeFiles(e); // Drop ์์ ์ ๋ง์ฐ์ค๋ก ๋๋๊ทธํ๋ ํ์ผ ์ ๋ณด๊ฐ e.dataTransfer.files ๊ฐ์ฒด์ ๋ด๊น
setIsDragging(false);
},
[onChangeFiles],
);
useEffect(() => {
const len = files.length;
handler(len > 0 && len <= maxFileLength);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [files]);
return (
<div className={`text-sm ${classNameProps}`}>
<section
className={classnames(
'w-full h-[138px] border-2 border-dashed rounded-lg active:border-blue-400',
{ 'bg-blue-100 border-blue-400': isDragging }, // ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ์ ์์ ๋ ๋ฐฐ๊ฒฝ์ ๋ณ๊ฒฝ
)}
>
<label
className="cursor-pointer h-full grid place-content-center text-[#4E5968]"
htmlFor="fileUpload"
onDragLeave={handleDragOut}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<input
hidden // ์จ๊ฒจ์ง ํ๋๋ก ์ ์
type="file"
id="fileUpload"
accept={acceptFileFormat.map((t) => `.${t}`).join()} // ๋ง์ฐ์ค๋ก ํด๋ฆญํด์ ํ์ผ ์
๋ก๋ ์ ํธ๋ค๋ฌ ํธ์ถ
multiple={maxFileLength > 1} // ์ฌ๋ฌ ํ์ผ ์
๋ก๋ ํ์ฉ ์ฌ๋ถ. ๊ธฐ๋ณธ๊ฐ false
onChange={onChangeFiles} // ๋ง์ฐ์ค๋ก ํด๋ฆญํด์ ํ์ผ ์
๋ก๋ ์ ํธ๋ค๋ฌ ํธ์ถ
/>
{DD_UPLOAD_GUIDE}
</label>
</section>
{files.length > 0 &&
files.map(({ id, object: { name } }: IFileTypes) => {
return (
<section
key={id}
className="bg-[#E7F1FD] rounded-lg mt-3 p-4 flex items-center justify-between text-[#327ADF]"
>
<span>{`๐ ${name}`}</span>
<button type="button" onClick={() => handleFilterFile(id)}>
{MULTIPLICATION_SIGN}
</button>
</section>
);
})}
</div>
);
};
export default DragDrop;
// DragDrop.tsx
import classnames from 'classnames';
import React, {
ChangeEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { IFileTypes, SignContent } from 'types';
import {
DD_UPLOAD_GUIDE,
MULTIPLICATION_SIGN,
UPLOAD_COMPLETE,
} from 'lib/constants';
import { validCheck } from 'lib/utils';
import useDragDrop from 'hooks/useDragDrop';
interface DragDropProps {
classNameProps: string;
handler: (arg: boolean) => void;
acceptFileInfo: SignContent['file'];
}
const DragDrop = ({
classNameProps,
handler,
acceptFileInfo: { maxFileLength, acceptFileFormat },
}: DragDropProps) => {
const [files, setFiles] = useState<IFileTypes[]>([]);
const fileId = useRef<number>(0); // ์ํ๊ฐ ๋ณํด๋ ๋ฆฌ๋ ๋ํ ํ์ ์์ผ๋ฏ๋ก useRef ๋ก ๊ด๋ฆฌ
const onChangeFiles = useCallback(
(e: DragEvent | ChangeEvent<HTMLInputElement>): void => {
const selectFiles: File[] = [];
if ('dataTransfer' in e) {
// FileList ๊ฐ์ฒด๋ length ํ๋กํผํฐ๋ฅผ ๊ฐ์ง๋ ์ ์ฌ ๋ฐฐ์ด์ด์ Symbol.iterator ๊ฐ ๊ตฌํ๋ ์ดํฐ๋ฌ๋ธ
// ๋ฐ๋ผ์ map ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์ ๊ฐ์ฐ์ฐ์ ํน์ Array.from ์ผ๋ก ์ง์ง ๋ฐฐ์ด๋ก ๋ง๋ค์ด์ค๋ค
selectFiles.push(...Array.from(e.dataTransfer?.files!)); // ๋๋๊ทธ์ค๋๋กญ์ผ๋ก ์
๋ก๋ํ์ ๋
} else {
selectFiles.push(...Array.from(e.target.files!)); // ํด๋ฆญํด์ ํ์ผ์ ์
๋ก๋ํ์ ๋(e.type === 'change')
e.target.value = ''; // ๋์ผ ํ์ผ ์ญ์ /์
๋ก๋ ๊ฐ๋ฅํ๋๋ก ํ์ฉ(ํด๋ฆญ ์
๋ก๋ ์ value ๊ฐ์ด ํ์ผ๋ช
์ผ๋ก ํ ๋น๋จ)
}
// ํ์์ ํ์ผ ํฌ๊ธฐ ๋ฑ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง ์ถ๊ฐ(https://dev-gorany.tistory.com/254)
const { isValid, errMsg } = validCheck({
files,
selectFiles,
maxFileLength,
acceptFileFormat,
});
if (!isValid) return alert(errMsg);
const tempFiles: IFileTypes[] = selectFiles.map((f) => ({
id: fileId.current++,
object: f,
}));
setFiles([...files, ...tempFiles]);
alert(UPLOAD_COMPLETE);
},
[acceptFileFormat, files, maxFileLength],
);
const handleFilterFile = useCallback(
(id: number): void => {
setFiles(files.filter((file: IFileTypes) => file.id !== id));
},
[files],
);
const [isDragging, dragRef] = useDragDrop<HTMLLabelElement>(onChangeFiles);
useEffect(() => {
const len = files.length;
handler(len > 0 && len <= maxFileLength);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [files]);
return (
<div className={`text-sm ${classNameProps}`}>
<section
className={classnames(
'w-full h-[138px] border-2 border-dashed rounded-lg active:border-blue-400',
{ 'bg-blue-100 border-blue-400': isDragging }, // ๋๋๊ทธ์ค์ธ ๋ง์ฐ์ค๊ฐ ๋๋กญ ์์ญ์ ์์ ๋ ๋ฐฐ๊ฒฝ์ ๋ณ๊ฒฝ
)}
>
<label
className="cursor-pointer h-full grid place-content-center text-[#4E5968]"
htmlFor="fileUpload"
ref={dragRef}
>
<input
hidden // ์จ๊ฒจ์ง ํ๋๋ก ์ ์
type="file"
id="fileUpload"
accept={acceptFileFormat.map((t) => `.${t}`).join()} // ํ์ฉํ ํ์ผ ํ์ฅ์ ex) 'jpg,png'
multiple={maxFileLength > 1} // ์ฌ๋ฌ ํ์ผ ์
๋ก๋ ํ์ฉ ์ฌ๋ถ. ๊ธฐ๋ณธ๊ฐ false
onChange={onChangeFiles} // ๋ง์ฐ์ค๋ก ํด๋ฆญํด์ ํ์ผ ์
๋ก๋ ์ ํธ๋ค๋ฌ ํธ์ถ
/>
{DD_UPLOAD_GUIDE}
</label>
</section>
{files.length > 0 &&
files.map(({ id, object: { name } }: IFileTypes) => {
return (
<section
key={id}
className="bg-[#E7F1FD] rounded-lg mt-3 p-4 flex items-center justify-between text-[#327ADF]"
>
<span>{`๐ ${name}`}</span>
<button type="button" onClick={() => handleFilterFile(id)}>
{MULTIPLICATION_SIGN}
</button>
</section>
);
})}
</div>
);
};
export default DragDrop;
๊ด๋ จ ํฌ์คํ / ๋ ํผ๋ฐ์ค
- ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ(์ ๋ก๋) ๊ตฌํ / File API
- ๋ฆฌ์กํธ ์์ ๋๋๊ทธ์ค๋๋กญ ๊ตฌํ
- React์์ Drag & Drop์ ์ด์ฉํ ํ์ผ ์ ๋ก๋ ํ๊ธฐ
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[React] ํค๋ณด๋๋ก ์กฐ์ํ ์ ์๋ ๋๋กญ๋ค์ด ์๋์์ฑ ๊ฒ์์ฐฝ ๊ตฌํํ๊ธฐ (0) | 2024.05.05 |
---|---|
[HTML/CSS] Tailwind CSS ํด๋์ค ํจํด ์ฌํ์ฉ / ๊ธฐ๋ณธ ํ ๋ง ์์ &ํ์ฅ (0) | 2024.05.05 |
[HTML/CSS] width ์์ฑ ์๋ ๋งค์ปค๋์ฆ (0) | 2024.05.05 |
[TS] ํ์ ์คํฌ๋ฆฝํธ ๋งต๋ ํ์ / ์ ํธ๋ฆฌํฐ ํ์ / Enum (0) | 2024.05.05 |
[TS] ํ์ ์คํฌ๋ฆฝํธ - ํ์ ํธํ (0) | 2024.05.04 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[React] ํค๋ณด๋๋ก ์กฐ์ํ ์ ์๋ ๋๋กญ๋ค์ด ์๋์์ฑ ๊ฒ์์ฐฝ ๊ตฌํํ๊ธฐ
[React] ํค๋ณด๋๋ก ์กฐ์ํ ์ ์๋ ๋๋กญ๋ค์ด ์๋์์ฑ ๊ฒ์์ฐฝ ๊ตฌํํ๊ธฐ
2024.05.05 -
[HTML/CSS] Tailwind CSS ํด๋์ค ํจํด ์ฌํ์ฉ / ๊ธฐ๋ณธ ํ ๋ง ์์ &ํ์ฅ
[HTML/CSS] Tailwind CSS ํด๋์ค ํจํด ์ฌํ์ฉ / ๊ธฐ๋ณธ ํ ๋ง ์์ &ํ์ฅ
2024.05.05 -
[HTML/CSS] width ์์ฑ ์๋ ๋งค์ปค๋์ฆ
[HTML/CSS] width ์์ฑ ์๋ ๋งค์ปค๋์ฆ
2024.05.05 -
[TS] ํ์ ์คํฌ๋ฆฝํธ ๋งต๋ ํ์ / ์ ํธ๋ฆฌํฐ ํ์ / Enum
[TS] ํ์ ์คํฌ๋ฆฝํธ ๋งต๋ ํ์ / ์ ํธ๋ฆฌํฐ ํ์ / Enum
2024.05.05