-
빠르게 시작하는 드래그 앤 드롭React.js & Next.js 2023. 3. 26. 20:15
prerequisites
- typescript
- interface
- react.js
- useRef
- useState
- Props
- inline-styling
- conditional render
전체 코드
import React, { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'; interface IFileTypes { id: number; object: File; } const basicInputStyle = { width: '100%', height: '200px', border: '2px solid', borderRadius: '10px', cursor: 'pointer', transition: '0.12s ease-in', display: 'flex', justifyContent: 'center', alignItems: 'center', }; const Test = () => { const [files, setFiles] = useState<IFileTypes[]>([]); const [isDragging, setIsDragging] = useState<boolean>(false); const fileId = useRef<number>(0); const dragRef = useRef<HTMLLabelElement>(null); const onSubmit = useCallback(() => { console.log('-- onsubmit files:', files); }, [files]); useEffect(() => { console.log('-- files changed:', files); }, [files]); const onChangeFiles = useCallback( (e: ChangeEvent<HTMLInputElement> | any): void => { let selectFiles: File[] = []; let tempFiles: IFileTypes[] = [...files]; if (e.type === 'drop') { selectFiles = e.dataTransfer.files; } else { selectFiles = e.target.files; } for (const file of selectFiles) { tempFiles = [ ...tempFiles, { id: fileId.current++, object: file, }, ]; } setFiles(tempFiles); }, [files, setFiles] ); const handleFilterFile = useCallback( (id: number): void => { setFiles(files.filter((file: IFileTypes) => file.id !== id)); }, [files, setFiles] ); const handleDragIn = useCallback((e: DragEvent): void => { e.preventDefault(); }, []); const handleDragOut = useCallback((e: DragEvent): void => { e.preventDefault(); setIsDragging(false); }, []); const handleDragOver = useCallback((e: DragEvent): void => { e.preventDefault(); if (e.dataTransfer!.files) { setIsDragging(true); } }, []); const handleDrop = useCallback( (e: DragEvent): void => { e.preventDefault(); if (dragRef.current) onChangeFiles(e); setIsDragging(false); }, [dragRef, 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); } }, [dragRef, 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); } }, [dragRef, handleDragIn, handleDragOut, handleDragOver, handleDrop]); useEffect(() => { initDragEvents(); return () => resetDragEvents(); }, [initDragEvents, resetDragEvents]); return ( <div> <label ref={dragRef} style={ isDragging ? { ...basicInputStyle, background: '#8cff69', } : { ...basicInputStyle, background: '#ffb7b7', } } > <input type='file' id='fileUpload' onChange={onChangeFiles} style={{ width: '100%', border: 'none' }} aria-label='file upload' /> </label> <div style={{ marginTop: '3%' }}> {files.length > 0 && files.map((file: IFileTypes) => { return ( <div key={file.id} style={{ display: 'flex', justifyContent: 'space-between', width: '100%', border: '1px solid', marginBottom: 2, padding: 2, }} > <span style={{ marginRight: 10 }}>{file.object.name}</span> <span onClick={() => handleFilterFile(file.id)} style={{ cursor: 'pointer' }}> X </span> </div> ); })} </div> <button type='button' onClick={onSubmit}> 제출 </button> </div> ); }; export default Test;
설명
IFileTypes
interface IFileTypes { id: number; object: File; }
- 인터페이스로 선언하여 사용할 파일 인터페이스입니다
- 파일은 HTML에 의해 제공되는 파일을 사용합니다
/** Provides information about files and allows JavaScript in a web page to access their content. */ interface File extends Blob { readonly lastModified: number; readonly name: string; readonly webkitRelativePath: string; }
basicInputStyle
const basicInputStyle = { width: '100%', height: '200px', border: '2px solid', borderRadius: '10px', cursor: 'pointer', transition: '0.12s ease-in', display: 'flex', justifyContent: 'center', alignItems: 'center', };
- Input 스타일링 코드입니다
- 스타일드 컴포넌트 등 추가적인 CSS-in-JS를 사용하는 것 대신, 인라인 스타일을 사용하여 코드 이용에 편의를 높였습니다.
useState
const [files, setFiles] = useState<IFileTypes[]>([]); const [isDragging, setIsDragging] = useState<boolean>(false); const fileId = useRef<number>(0); const dragRef = useRef<HTMLLabelElement>(null);
files:
전체 파일을 관리할 객체입니다
isDragging:
드래그 중인 상황을 캐치할 boolean 타입의 객체입니다
fileId:
IFileTypes 인터페이스를 통해 파일을 관리하게 되는데, fileId를 동적으로 추가하기 위한 useRef입니다
dragRef:
드래그 앤 드롭 이벤트를 캐치하여 처리할 useRef입니다. DOM 조작을 위해 사용합니다
Handler
onChangeFiles
const onChangeFiles = useCallback( (e: ChangeEvent<HTMLInputElement> | any): void => { let selectFiles: File[] = []; let tempFiles: IFileTypes[] = [...files]; if (e.type === 'drop') { selectFiles = e.dataTransfer.files; } else { selectFiles = e.target.files; } for (const file of selectFiles) { tempFiles = [ ...tempFiles, { id: fileId.current++, object: file, }, ]; } setFiles(tempFiles); }, [files, setFiles] );
- 파일 변화를 감지하여 files state에 삽입 삭제할 실제 비즈니스 로직입니다
- 드래그 앤 드롭의 드롭 이벤트 또는 file input 클릭으로 파일을 삽입할 수 있습니다
- 따라서 이벤트 타입은 drop인 경우와 그 외의 경우를 나눠서 처리하였습니다
- 이후 for of 문을 통해 순회하며 선택된 각 파일에 IFileTypes 인터페이스에 맞게 id 값을 추가합니다
handleFilterFile
const handleFilterFile = useCallback( (id: number): void => { setFiles(files.filter((file: IFileTypes) => file.id !== id)); }, [files, setFiles] );
- 클릭 이벤트를 바탕으로 인덱싱 된 id 값에 해당하는 파일을 기존 배열에서 제거합니다
const handleDragIn = useCallback((e: DragEvent): void => { e.preventDefault(); }, []); const handleDragOut = useCallback((e: DragEvent): void => { e.preventDefault(); setIsDragging(false); }, []); const handleDragOver = useCallback((e: DragEvent): void => { e.preventDefault(); if (e.dataTransfer!.files) { setIsDragging(true); } }, []); const handleDrop = useCallback( (e: DragEvent): void => { e.preventDefault(); if (dragRef.current) onChangeFiles(e); setIsDragging(false); }, [dragRef, 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); } }, [dragRef, 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); } }, [dragRef, handleDragIn, handleDragOut, handleDragOver, handleDrop]); useEffect(() => { initDragEvents(); return () => resetDragEvents(); }, [initDragEvents, resetDragEvents]);
event handler
- 우리는 dragRef를 통해 총 4가지의 이벤트를 핸들링하게 됩니다
dragenter :
선택된 영역 안에 드래그가 들어온 경우
dragleave:
선택된 영역 밖으로 드래그가 나간 경우
const handleDragOut = useCallback((e: DragEvent): void => { e.preventDefault(); setIsDragging(false); }, []);
드래그가 유효하지 않은 상황임을 나타내기 위해 isDragging useState를 false로 변경합니다.
dragover:
드래그가 유효한 영역(선택한 영역) 안에 들어온 경우
const handleDragOver = useCallback((e: DragEvent): void => { e.preventDefault(); if (e.dataTransfer!.files) { setIsDragging(true); } }, []);
드래그가 유효한 상황임을 나타내기 위해 isDragging useState를 true로 변경합니다.
drop:
선택된 영역 안으로 드래그를 드롭한 경우
const handleDrop = useCallback( (e: DragEvent): void => { e.preventDefault(); if (dragRef.current) { onChangeFiles(e); } setIsDragging(false); }, [dragRef, onChangeFiles] );
dragRef.current 에 유효할 경우 onChangeFiles 이벤트를 호출합니다.
dragenter 이벤트와 dragover 이벤트의 차이는 무엇인가요?
The dragover and dragenter events are both part of the HTML Drag and Drop API and are fired when an element is being dragged over another element. However, there are some differences between them:
- Triggering: The dragover event is fired continuously as long as the dragged element is over the target element, while the dragenter event is only fired once when the dragged element first enters the target element.
- Default behavior: By default, the dragover event allows the dragged element to be dropped on the target element, while the dragenter event does not allow it. To enable dropping on the target element, you need to call event.preventDefault() on the dragover event.
- Use case: The dragenter event is typically used to apply some visual feedback to the target element, such as changing the background color or adding a border, to indicate that the element can be dropped on it. The dragover event is typically used to determine whether the dragged element can be dropped on the target element, and to update the visual feedback accordingly.
전체적으로 드래그 앤터 이벤트는 끌린 요소가 대상 요소에 들어갈 때 사용자에게 시각적 피드백을 제공하는 데 사용되는 반면, 드래그 오버 이벤트는 대상에 요소를 떨어뜨릴 수 있는지 여부를 결정하고 그에 따라 시각적 피드백을 제공하는 데 사용됩니다. 따라서 두 이벤트 모두에 대한 컨트롤이 필요합니다.
useEffect
useEffect(() => { initDragEvents(); return () => resetDragEvents(); }, [initDragEvents, resetDragEvents]);
마운트 시에 initDragEvents 함수를 호출합니다. 이후 언마운트 시에 클린업 함수를 모아놓은 resetDragEvents 함수를 호출합니다.
render
return ( <div> <label ref={dragRef} style={ isDragging ? { ...basicInputStyle, background: '#8cff69', } : { ...basicInputStyle, background: '#ffb7b7', } } > <input type='file' id='fileUpload' onChange={onChangeFiles} style={{ width: '100%', border: 'none' }} aria-label='file upload' /> </label> <div style={{ marginTop: '3%' }}> {files.length > 0 && files.map((file: IFileTypes) => { return ( <div key={file.id} style={{ display: 'flex', justifyContent: 'space-between', width: '100%', border: '1px solid', marginBottom: 2, padding: 2, }} > <span style={{ marginRight: 10 }}>{file.object.name}</span> <span onClick={() => handleFilterFile(file.id)} style={{ cursor: 'pointer' }}> X </span> </div> ); })} </div> <button type='button' onClick={onSubmit}> 제출 </button> </div> );
결과 보기
'React.js & Next.js' 카테고리의 다른 글
다양한 파일 확장자 파일 미리보기 지원하기 (0) 2023.04.05 input image 미리보기 구현하기 (0) 2023.03.31 Next Image load super slow (1) 2023.01.04 냅다 시작하는 리액트 쿼리 (SSR편) (0) 2022.12.31 냅다 시작하는 리액트 쿼리 (개념편) (1) 2022.12.31 - typescript