[ 인턴 3주차 ] pdf 파일 업로드 기능 구현 (버튼, 드래그 앤 드롭)

Innes·2025년 1월 11일
0

인턴

목록 보기
8/14

✨ pdf 파일 업로드 기능

1️⃣ 버튼으로 업로드

1) input 옵션을 활용하기

type=“file”

accept="application/pdf"
-> pdf파일로 제한

hidden
-> 파일을 받는 input을 안보이게 만든다. (버튼으로 보여줄것이기 때문)

onChange={handleFileUpload}
-> 버튼 클릭시 실행됨, input으로 받은 file은 event.target.files?.[0]으로 받아오기 때문에 onClick이 아닌 onChange이다!

-> onChange는 기본적으로 첫번째 인자에 event가 자동으로 들어오기 때문에 굳이 (e) => handleFileUpload(e) 할 필요 없다.
하지만! 추가적인 데이터를 전달해야 한다면 그때는 감싸줘야한다.
ex) <input type="file" onChange={(e) => handleFileUpload(e, "추가 데이터")} />

But!! 그냥 input, button을 연달아 쓴다고 되는게 아님!
둘을

<input id=“—“>, 
<label htmlFor=“—“>
<button/>
</label>

이렇게 id, htmlFor로 연결해줘야함

-> 나는 여기서 계속 오류가 나서
input을 강제로 열도록 button에 onClick을 부여함
(input에 id 부여 + button onClick에 document.getEl…. )

  const handleButtonClick = () => {
    document.getElementById("pdfFileUpload")?.click();
  };

2) onChange함수 handleFileUpload
file을 정의해준 다음, file이 존재하면 ‘파일 검증 함수’에 file넣고 실행!

3) 파일 검증 함수 (인자로 file을 받음)

  • pdf파일이 아니라면 alert ( file.type 으로 확인한다. )
  • 파일 용량이 30mb미만이 아니라면 alert ( file.size로 확인 )

2️⃣ 드래그 앤 드롭

✅ 1. 드래그 앤 드롭
(pdf만, 30mb 미만만)

onDragOver, onDragLeave, onDrop는 기본 JavaScript 이벤트 리스너야! (JavaScript의 Drag & Drop API의 일부)
이 이벤트들은 특정 태그에 종속된 것이 아니라 모든 HTML 요소에서 사용할 수 있는 기본 JavaScript 이벤트 리스너야.
onDragOver: 사용자가 파일을 드래그하여 해당 요소 위로 가져올 때 발생
onDragLeave: 드래그 중이던 파일이 해당 요소의 범위를 벗어날 때 발생
onDrop: 사용자가 드래그한 파일을 해당 요소에 드롭할 때 발생

onDragOver에서 event.preventDefault()가 필요한 이유
HTML에서는 기본적으로 파일을 드래그하면 브라우저가 이를 새 창에 열려고 시도해
onDragOver에서 event.preventDefault()를 호출하지 않으면 파일을 드래그해도 onDrop 이벤트가 실행되지 않음!

이 기본 동작을 막기 위해 event.preventDefault()를 꼭 사용해야 해.

onDragLeave에서는 event.preventDefault()가 필요 없음
onDragLeave는 드래그 중이던 파일이 해당 영역을 벗어날 때 실행되는 이벤트.
이 이벤트는 단순히 스타일 변경 등에 사용되며, 브라우저의 기본 동작을 막아야 할 필요가 없음.

onDrop에서도 event.preventDefault()가 필수

onDrop 이벤트에서 preventDefault()를 호출하지 않으면, 브라우저가 파일을 새 탭에서 열려고 시도함.
파일을 열지 않고 개발자가 직접 처리하려면 반드시 preventDefault()를 호출해야 함.

🛠️ 트러블슈팅

  1. dragOver시 한개의 파일을 한번 드래그 over한것 뿐인데 드래그오버 이벤트가 무한정 호출되는 이슈

    // pdf파일 드래그 해제
    // (기본)
    const handleDragLeave = () => {
    setIsDragging(false);
    console.log("dragLeave");
    };

-> 드래그 over도 마찬가지로 단순하게 setIsDragging(true)로 관리

MDN문서 왈 : drag 이벤트는 사용자가 요소 또는 텍스트를 드래그하는 동안 매 수백 밀리초마다 발생합니다.

한 동작임에도 isDragging을 true로 바꾸는게 계속 실행되는것 같아서 useCallback, debounce, throttling의 방법을 고안하게 됨.

시도 1) useCallback
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();

setIsDragging((prev) => {
  if (!prev) {
    console.log("dragOver");
    return true;
  }
  return prev;
});

}, []);

-> 드래그 over 이벤트가 무한 호출되는건 막아진다.
But 드래그앤드롭 영역 안에서 드래그over 상태로 계속 커서 움직이면 drag over, drag leave가 계속 반복되어 호출되었다.

시도 2) debounce w.lodash
const handleDragOver = useCallback(
_.debounce((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
console.log("dragOver");
}, 300), // 300ms 동안 연속 호출 방지
[],
);

-> 사용자가 빠르게 움직일 경우 debounce때문에 동작에 지연이 생길 수 있다.
지연이 생겨도 괜찮은 경우는 debounce로 무한 호출을 막아도 괜찮지만, UX를 고려했을때 지연이 생기면 곤란한 경우라면 debounce는 없이 useCallback만 쓰는것도 방법이 되겠다.

시도 3) throttling w.lodash
일정 간격을 두고 함수가 계속 실행되다보니
이벤트가 실행되는 간격만 길어졌을뿐, 무한 실행되는 이슈를 막지는 못했다.
const handleDragOver = useCallback(
_.throttle((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
console.log("dragOver");
}, 500), // 500ms마다 한 번만 실행
[],
);

시도 4) useState는 값이 변경되면 리렌더링이 일어나므로, isDragging상태를 useRef boolean값으로 리팩토링
(debounce + useRef로 리팩토링)
(하지만 useState로 관리하는 isDragging도 필요했다.
useRef로만 관리하면 UI 업데이트가 일어나지 않기 때문.
그래도 리렌더링을 완전히 없애는 것이 목적이 아니라, 불필요한 리렌더링을 줄이면서도 UI 업데이트는 정상적으로 이루어지도록 하는 것이 목표이므로 괜찮다?!

const isDraggingRef = useRef(false);
const handleDragOver = useCallback( _.debounce((e: React.DragEvent) => { e.preventDefault(); if (!isDraggingRef.current) { isDraggingRef.current = true; setIsDragging(true); console.log("dragOver"); } }, 300), // 300ms 동안 연속 호출 방지 [] );

-> e.preventDefault(); 가 제대로 동작을 안해서 자꾸 파일이 그냥 열려버림.
드래그 앤 드롭에는 동작 지연이 있으면 안되는 듯? 하여
-> useCallback만 쓰던 로직으로 작성 완료

왜 useCallback을 사용해야 할까?
handleDragOver 함수가 컴포넌트가 리렌더링될 때마다 새로 생성되지 않도록 하기 위해.
onDragOver={handleDragOver} 처럼 이벤트 핸들러로 전달할 때, 매번 새로운 함수가 생성되면 불필요한 리렌더링이 발생할 수 있음.
이를 방지하기 위해 useCallback을 사용하면 함수가 메모이제이션되어, 동일한 함수를 계속 재사용할 수 있음.

dropLeave에 debounce 사용하는 이유?
debounce 적용 및 useCallback 최적화
🔹 debounce란?
• 이벤트가 연속적으로 발생하는 경우, 마지막 이벤트만 실행하도록 지연시키는 기법.
• 300ms 동안 추가 호출이 없으면 실행되므로, 불필요한 setState 호출을 줄일 수 있음.
🔹 왜 useCallback을 사용해야 할까?
• debounce는 내부적으로 새로운 함수를 생성하기 때문에, useCallback으로 감싸지 않으면 컴포넌트가 리렌더링될 때마다 새로운 debounce 함수가 생성됨.
• useCallback을 사용하여 debounce 함수도 메모이제이션하여, 리렌더링될 때마다 새로 생성되지 않도록 함.

[isDragging] (의존성 배열)
→ isDragging 값이 변경될 때만 새로운 debounce 함수를 생성하여 최적화.

  1. 드롭시 파일 형식 안맞는데도 pdfFile이 set되고있음
    검증 함수에서도 setFile을 하고있고,
    handlDrop에서도 setFile을 하고있어서

검증 함수에서 setFile을 안하고 alert을 띄웠더라도
handleDrop함수에서 setFile이 되어버려서

alert도 뜨고 UI도 바뀌는 기현상이 일어난것!

->
검증 함수에서 boolean값을 Return하게 만들면 됐다.
하나의 함수에서는 해당 함수에서 하고자 하는 기능만 잘 담도록 하자.
목표로 하는 기능에 추가로 다른 기능까지 넣지 않도록 하자.
ex) 검증 함수에서는 검증만! setFile은 다른곳에서!

기존)
const validateAndSetFile = (file: File) => {
if (file.type !== "application/pdf") {
alert("PDF 파일만 업로드 가능합니다.");
return;
}

if (file.size > MAX_FILE_SIZE) {
  alert("파일 크기가 30MB를 초과했습니다.");
  return;
}

setPdfFile(file);

};

변경 후)
const validateAndSetFile = (file: File): boolean => {
if (file.type !== "application/pdf") {
alert("PDF 파일만 업로드 가능합니다.");
return false;
}

if (file.size > MAX_FILE_SIZE) {
  alert("파일 크기가 30MB를 초과했습니다.");
  return false;
}

return true;

};

  1. 드래그앤드롭 영역 바깥에서 파일 드롭시 파일이 열림
    useEffect에서 window전체 영역의 드래그 드롭 기본 동작을 막아버림!
    -> window 전체 영역에서는 막혔지만,
    드롭 동작하는 div에 onDrop={} 지정되어있는 곳에서는 handleDrop 함수가 정상 동작하기때문에
    지정한 영역에서만 드롭이 실행되고, 나머지 영역에서는 useEffect때문에 드롭이 실행 안되는것!
  1. pdf 파일 이름에서 확장자 있으면 빼기
    1차 시도 ) 파일명에서 마지막 4글자 제거

-> 이렇게하면 파일명에 확장자가 포함되지 않은 파일명에서도 맨 뒷 글자가 잘리는 현상 발생할 것을 생각 못함..

2차 시도 ) 대소문자 무시하고 파일명에서 .pdf 있으면 제거하고 표시
const getStandardizedPdfFileName = (fileName: string) => {
return fileName.replace(/.pdf$/i, ""); // 대소문자 무시하고 .pdf 제거
};

const modifiedFileName = pdfFile?.name ? getStandardizedPdfFileName(pdfFile.name) : "";

profile
무서운 속도로 흡수하는 스펀지 개발자 🧽

0개의 댓글