파일 업로드를 할 때, Input태그를 활용하여 기존의 선택 방식이 아닌
영역을 지정해 drag & drop로 업로드하는 방법을 알아보겠습니다.
Input과 Label 연결시키기
일단 파일을 업로드시킬 영역을 지정합니다.
input
대신 사용할 커스텀 버튼을 만들 때 처럼 label
을 활용하여 영역을 설정합니다.
// Upload.tsx
const Upload = () => {
return (
<div className="UploadContainer">
<div className={`${isDragging ? 'UploadWrapperActive' : 'UploadWrapper'}`}>
<label className="UploadArea" htmlFor="fileUpload">
<div className="PlaceHolderText">Drag&Drop File Here</div>
</label>
</div>
<div className="FileContainer">
// 첨부된 파일 리스트
</div>
<input
type="file"
id="fileUpload"
style={{ display: 'none' }}
multiple={true}
/>
</div>
);
};
export default Upload;
input
의 id
와 label
의 htmlfor
을 연결시켜 주고 입맛에 맞게 css처리합니다.
이벤트 등록
useEffect를 활용해 이벤트 처리를 합니다.
const handleDragIn = useCallback((e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDragOut = useCallback((e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDragOver = useCallback((e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = useCallback(
(e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
onChangeFiles(e);
},
[onChangeFiles]
);
const addDragEvents = useCallback((): void => {
if (dragRef.current !== null) {
dragRef.current.addEventListener('dragenter', handleDragIn);
dragRef.current.addEventListener('dragleave', handleDragOut);
dragRef.current.addEventListener('dragover', handleDragOver);
dragRef.current.addEventListener('drop', handleDrop);
}
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop]);
const removeDragEvents = useCallback((): void => {
if (dragRef.current !== null) {
dragRef.current.removeEventListener('dragenter', handleDragIn);
dragRef.current.removeEventListener('dragleave', handleDragOut);
dragRef.current.removeEventListener('dragover', handleDragOver);
dragRef.current.removeEventListener('drop', handleDrop);
}
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop]);
useEffect(() => {
addDragEvents();
return () => removeDragEvents();
}, [addDragEvents, removeDragEvents]);
모든 이벤트에는 preventDefault()
stopPropagation()
메서드 처리를 해줘야 합니다.
해주지 않으면 파일 드래그 드롭 시 브라우저 기본 동작에 의해 브라우저에서 열리게 됩니다.
파일 업로드 이벤트 처리
이제 드래그 이벤트를 등록했으니, 원하는 영역에 파일을 드래그 했을 때,
State에 원하는 형태로 저장하는 단계입니다.
드래그 이벤트를 동작시킬 영역에 useRef
를 활용합니다.
const dragRef = useRef<HTMLLabelElement | null>(null);
const handleLabelClick = (e: any) => {
e.preventDefault();
};
return (
<div className="UploadContainer">
<div className={`${isDragging ? 'UploadWrapperActive' : 'UploadWrapper'}`}>
// label에 dragRef 연결, onClick 방지
<label className="UploadArea" htmlFor="fileUpload" ref={dragRef} onClick={handleLabelClick}>
<UploadFileIcon></UploadFileIcon>
<div className="PlaceHolderText">Drag&Drop File Here</div>
</label>
</div>
<input
type="file"
id="fileUpload"
style={{ display: 'none' }}
multiple={true}
/>
</div>
);
};
또한 드래그로 업로드만 가능하게 하기 위해 저는 Click 이벤트를 막았습니다.
먼저 file의 구분을 위해 interface를 설정합니다.
import { ChangeEvent, useCallback, useRef, useState, useEffect } from 'react';
...
// 관리하고 싶은 포맷 설정
interface FileFormat {
id: number;
content: File;
}
const Upload = () => {
...
}
이제 드롭한 데이터를 저장할 State를 만들어 줍니다.
이전에 작성한 handleDrop
을 타고 onChangeFiles
함수를 실행시켜 파일을 files에 담아줍니다.
const [files, setFiles] = useState<FileFormat[]>([]);
const onChangeFiles = useCallback(
(e: ChangeEvent<HTMLInputElement> | any): void => {
let selectFiles: File[] = [];
// 드래그로 올린 파일 구분
e.type === 'drop' ? (selectFiles = e.dataTransfer.files) : (selectFiles = e.target.files);
//원하는 형태로 files에 담기 위한 반복문
let tempFiles: FileFormat[] = files;
for (const file of selectFiles) {
tempFiles = [
...tempFiles,
{
id: fileId.current++,
content: file,
},
];
}
setFiles(tempFiles);
},
[files]
);
🔥한 가지 주의할 점
드래그로 받아오는 파일은 e.target.files
로 읽지 않습니다.
드래그에 잡혀있는 파일을 e.dataTransfer.files
에서 읽어옵니다.
물론 지금은 클릭으로 파일을 첨부하는 것을 막아두었지만, 기존 코드와 비교해서 무엇이 다른지 알아보기 위해
onChangeFiles
콜백 함수에서 두 가지 방식을 모두 다루었습니다.
그리고 e.dataTransfer.files
이나 e.target.files
는 모두 FileList로 받아집니다.
그렇기 때문에 임의로 만든 FileFormat에 맞춰 파일들을 담아주기 위해
for문을 돌리려면 FileList를 배열로 바꾸어야합니다.
그렇기 때문에 위 코드와 같이
selectFiles: File[] = []; 처럼 File을 담을 빈 배열을 생성해 담거나,
FileList를 Array()에 담아 배열로 바꾸어 활용해야 합니다.
파일 보여주기 및 삭제
state에 저장한 값을 보여주기만 하면 됩니다.
<div className="FileContainer">
{files.length > 0 &&
files.map((file: FileFormat) => {
const {
id,
content: { name },
} = file;
return (
<div className="UploadFile" key={id}>
<div className="Filename">{name} </div>
<div className="DeleteBtn" onClick={() => handleDeleteFile(id)}>
<DeleteOutlineIcon></DeleteOutlineIcon>
</div>
</div>
);
})}
</div>
삭제는 Icon을 클릭했을 때 처리했습니다.
const handleDeleteFile = useCallback(
(id: number): void => {
setFiles(files.filter((file: FileFormat) => file.id !== id));
},
[files]
);
완성
전체 코드
// Upload.tsx
import { ChangeEvent, useCallback, useRef, useState, useEffect } from 'react';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import './Upload.css';
interface FileFormat {
id: number;
content: File;
}
const Upload = () => {
const [files, setFiles] = useState<FileFormat[]>([]);
const [isDragging, setIsDragging] = useState<boolean>(false);
const dragRef = useRef<HTMLLabelElement | null>(null);
const fileId = useRef<number>(0);
const onChangeFiles = useCallback(
(e: ChangeEvent<HTMLInputElement> | any): void => {
let selectFiles: File[] = [];
let tempFiles: FileFormat[] = files;
e.type === 'drop' ? (selectFiles = e.dataTransfer.files) : (selectFiles = e.target.files);
for (const file of selectFiles) {
tempFiles = [
...tempFiles,
{
id: fileId.current++,
content: file,
},
];
}
setFiles(tempFiles);
},
[files]
);
const handleLabelClick = (e: any) => {
e.preventDefault();
};
const handleDeleteFile = useCallback(
(id: number): void => {
setFiles(files.filter((file: FileFormat) => file.id !== id));
},
[files]
);
const handleDragIn = useCallback((e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDragOut = useCallback((e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
}, []);
const handleDragOver = useCallback((e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer!.files) {
setIsDragging(true);
}
}, []);
const handleDrop = useCallback(
(e: DragEvent): void => {
e.preventDefault();
e.stopPropagation();
onChangeFiles(e);
setIsDragging(false);
},
[onChangeFiles]
);
const initDragEvents = useCallback((): void => {
if (dragRef.current !== null) {
dragRef.current.addEventListener('dragenter', handleDragIn);
dragRef.current.addEventListener('dragleave', handleDragOut);
dragRef.current.addEventListener('dragover', handleDragOver);
dragRef.current.addEventListener('drop', handleDrop);
}
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop]);
const resetDragEvents = useCallback((): void => {
if (dragRef.current !== null) {
dragRef.current.removeEventListener('dragenter', handleDragIn);
dragRef.current.removeEventListener('dragleave', handleDragOut);
dragRef.current.removeEventListener('dragover', handleDragOver);
dragRef.current.removeEventListener('drop', handleDrop);
}
}, [handleDragIn, handleDragOut, handleDragOver, handleDrop]);
useEffect(() => {
initDragEvents();
return () => resetDragEvents();
}, [initDragEvents, resetDragEvents]);
return (
<div className="UploadContainer">
<div className={`${isDragging ? 'UploadWrapperActive' : 'UploadWrapper'}`}>
<label className="UploadArea" htmlFor="fileUpload" ref={dragRef} onClick={handleLabelClick}>
<UploadFileIcon></UploadFileIcon>
<div className="PlaceHolderText">Drag&Drop File Here</div>
</label>
</div>
<div className="FileContainer">
{files.length > 0 &&
files.map((file: FileFormat) => {
const {
id,
content: { name },
} = file;
return (
<div className="UploadFile" key={id}>
<div className="Filename">{name} </div>
<div className="DeleteBtn" onClick={() => handleDeleteFile(id)}>
<DeleteOutlineIcon></DeleteOutlineIcon>
</div>
</div>
);
})}
</div>
<input type="file" id="fileUpload" style={{ display: 'none' }} multiple={true} onChange={onChangeFiles} />
</div>
);
};
export default Upload;
// Upload.css
.UploadWrapper {
width: 400px;
height: 200px;
border: 2px dotted black;
}
.UploadWrapperActive {
background: #0000000f;
width: 400px;
height: 200px;
border: 2px dotted black;
opacity: 0.7;
}
.UploadArea {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.FileContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
margin-top: 10px;
gap: 2px;
}
.UploadFile {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 5px;
background-color: #0000000f;
border-radius: 5px;
}
.Filename {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.DeleteBtn {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
text-align: center;
vertical-align: center;
}
.DeleteBtn:hover {
cursor: pointer;
}
전체 코드에서 아이콘은 mui에서 가져왔고 isDragging State를 활용해서 Drag시 hoverEffect를 나타냈습니다.
만약 업로드 시킨 이미지를 미리보기 하고 싶다면?
Reference
'개발일기 > Web' 카테고리의 다른 글
[React] 함수형 컴포넌트에서 componentDidUpdate() 구현하기 (0) | 2023.08.28 |
---|---|
[React] 검색 요청 최적화하기 with Debounce (1) | 2023.08.25 |
[React] React Hooks 이해하기 (0) | 2023.06.23 |
[React] 리액트 클래스형 컴포넌트의 생명 주기 메서드(Life Cycle) (0) | 2023.06.20 |
[npm vs yarn] yarn은 무엇이 다를까? (0) | 2023.06.12 |