Frontend/React

React.js - 드래그 앤 드롭 파일 업로드 만들기

둉이 2023. 7. 31. 17:15

드래그 앤 드롭으로 파일을 업로드하는 기능을 만들어 보자.

 

이미지프레소 같은 이미지/동영상 용량 줄이는 사이트를 사용하다 보면 클릭 혹은 드래그 앤 드롭으로 파일을 업로드하는 영역을 볼 수 있다.

클릭 혹은 드래그 앤 드롭이 가능한 파일 업로드 영역

 

파일을 드래그해서 영역 위로 가져가면 아래 사진처럼 스타일도 변경되기 때문에 드래그가 잘 되고 있다는(?) 것도 확인이 가능하다.

드래그 시 스타일 변경

 

오늘은 이 드래그 파일 업로드 기능을 만드는 방법에 대해 한 번 알아보자.

 

 


 

 

클릭하여 파일 업로드 기능 구현

일단 드래그 업로드 기능을 지원하기 전에, 기본적으로 클릭하여 파일을 업로드 하는 기능 동작이 가능해야 한다.

 

HTML에는 파일을 업로드할 수 있는 태그인 <input type="file">이 존재한다.

input type="file"

이 태그를 클릭하면 파일을 업로드할 수 있는 창이 뜬다.

 

 

하지만 문제는 태그의 기본 제공 스타일이 너무 안이쁘다는 점이다.

 

이럴 때는 <input type="file"> 태그를 display: none 속성으로 숨김 처리하고, 상위에 <label> 태그로 감싼 후 label 태그 내에 여러 가지 요소를 추가하여 파일 업로드 영역을 꾸며주면 된다.

 

input을 label로 감쌌기 때문에 label 영역을 클릭하기만 해도 파일 업로드가 가능하다.

 

나는 이미지프레소를 참고하여 아래처럼 업로드 영역을 꾸며줬다.

업로드 영역 꾸미기

 

 

코드는 다음과 같다.

const Logo = () => (
  <svg className="icon" x="0px" y="0px" viewBox="0 0 24 24">
    <path fill="transparent" d="M0,0h24v24H0V0z" />
    <path
      fill="#000"
      d="M20.5,5.2l-1.4-1.7C18.9,3.2,18.5,3,18,3H6C5.5,3,5.1,3.2,4.8,3.5L3.5,5.2C3.2,5.6,3,6,3,6.5V19  c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V6.5C21,6,20.8,5.6,20.5,5.2z M12,17.5L6.5,12H10v-2h4v2h3.5L12,17.5z M5.1,5l0.8-1h12l0.9,1  H5.1z"
    />
  </svg>
);

const UploadBox = () => {
  return (
    <label className="preview">
      <input type="file" className="file" />
      <Logo />
      <p className="preview_msg">클릭 혹은 파일을 이곳에 드롭하세요.</p>
      <p className="preview_desc">파일당 최대 3MB</p>
    </label>
  );
};
#root {
  align-items: center;
  display: flex;
  width: 100vw;
  height: 100vh;
}

.icon {
  width: 100px;
  height: 100px;
  pointer-events: none;
}

.file {
  display: none;
}

.file::file-selector-button {
  font-size: 14px;
  background: #fff;
  border: 1px solid #111;
  border-radius: 12px;
  padding: 4px 32px;
  cursor: pointer;
}

.preview {
  width: 300px;
  height: 150px;
  margin: auto;
  background-color: #fff;
  border-radius: 5px;
  border: 3px dashed #eee;
  padding: 70px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  cursor: pointer;
}

.preview:hover {
  border-color: #111;
}

.preview_msg {
  font-weight: 500;
  font-size: 18px;
  margin: 20px 0 10px;
}

.preview_desc {
  margin: 0;
  font-size: 14px;
}

 

 

 

드래그 앤 드롭 파일 업로드 기능 구현

이제 앞에서 만든 코드에 이어서 드래그 앤 드롭시 파일이 업로드되는 기능을 구현해 보자.

 

먼저, 파일을 드래그해서 영역 위로 이동했을 때 스타일이 변경되는 부분부터 만들어 보자.

 

드래그 active 여부를 저장하기 위한 state를 하나 선언하고, dragoverdragend 이벤트 핸들러에 해당 state를 변경하는 코드를 추가하면 된다.

  const [isActive, setActive] = useState(false);
  const handleDragStart = () => setActive(true);
  const handleDragEnd = () => setActive(false);
  
  return (
    <label
      className={`preview${isActive ? ' active' : ''}`}  // isActive 값에 따라 className 제어
      onDragEnter={handleDragStart}  // dragstart 핸들러 추가
      onDragLeave={handleDragEnd}  // dragend 핸들러 추가
      onDrop={handleDrop}
    >
      ...
    </label>
  );
.preview.active {
  background-color: #efeef3;
  border-color: #111;
}

 

 

위 코드를 적용하면 아래처럼 드래그 동작에 따라 변경된 스타일을 확인할 수 있다.

직접 만든 드래그 스타일

 

 

 

이제 드래그해서 파일을 업로드하는 기능을 만들어 보자.

 

영역 내에 파일이 드롭되는 순간의 이벤트에 접근하기 위해 drop 이벤트 핸들러에 다음과 같이 코드를 작성한다.

  const handleDrop = (event) => {
    event.preventDefault();

    const file = event.dataTransfer.files[0];
    readImage(file);
    setActive(false);

    // 드롭된 파일 핸들링
    // ...
  };
  
  return (
    <label
      className={`preview${isActive ? ' active' : ''}`}
      onDragEnter={handleDragStart}
      onDragLeave={handleDragEnd}
      onDrop={handleDrop}  // drop 핸들러 추가
    >
      ...
    </label>
  );

 

 

여기서 주의해야 할 점!!!!

 

일반적으로 브라우저 화면에 파일을 드롭하게 되면, 브라우저 기본 동작에 의해 새 창이 뜨는 이슈가 발생한다.

 

따라서, 해당 동작이 일어나지 않게 하기 위해서는 dragover와 drop 이벤트에서 기본 동작을 막아줘야 한다.

  const handleDragOver = (event) => {
    event.preventDefault();  // 필수 1
  };
  
  const handleDrop = (event) => {
    event.preventDefault();  // 필수 2
    // ...
  };
  
  return (
    <label
      className={`preview${isActive ? ' active' : ''}`}
      onDragEnter={handleDragStart}
      onDragOver={handleDragOver}  // dragover 핸들러 추가
      onDragLeave={handleDragEnd}
      onDrop={handleDrop}
    >
      ...
    </label>
  );

 

 

여기까지 작업을 완료하면 드래그하여 파일을 업로드할 수 있다.

 

좀 더 완성도를 높이기 위해 업로드한 파일에 대한 정보와 미리보기 기능도 구현해 보자.

 

 

 

업로드한 파일 미리보기 기능 구현

업로드된 파일의 정보를 가져와서 화면에 뿌려주는 기능을 만들어 보자.

 

업로드된 파일의 정보를 저장하기 위헤 별도의 state를 선언한 후, 해당 state의 값 유무에 따라 파일의 정보를 보여주면 된다.

 

drop / change 핸들러에 state 값을 업데이트하는 코드를 추가하고, size는 mb 단위로 계산하여 저장했다.

const FileInfo = ({ uploadedInfo }) => (
  <ul className="preview_info">
    {Object.entries(uploadedInfo).map(([key, value]) => (
      <li key={key}>
        <span className="info_key">{key}</span>
        <span className="info_value">{value}</span>
      </li>
    ))}
  </ul>
);

const UploadBox = () => {
  const [uploadedInfo, setUploadedInfo] = useState(null);
  
  // ...생략

  const setFileInfo = (file) => {
    const { name, size: byteSize, type } = file;
    const size = (byteSize / (1024 * 1024)).toFixed(2) + 'mb';
    setUploadedInfo({ name, size, type });  // name, size, type 정보를 uploadedInfo에 저장
  };

  const handleDrop = (event) => {
    event.preventDefault();
    setActive(false);

    const file = event.dataTransfer.files[0];
    setFileInfo(file);  // 코드 추가
  };

  const handleUpload = ({ target }) => {
    const file = target.files[0];
    setFileInfo(file);  // 코드 추가
  };

  return (
    <label
      className={`preview${isActive ? ' active' : ''}`}
      onDragEnter={handleDragStart}
      onDragOver={handleDragOver}
      onDragLeave={handleDragEnd}
      onDrop={handleDrop}
    >
      <input type="file" className="file" onChange={handleUpload} />  // change 핸들러 추가
      {uploadedInfo && <FileInfo {...uploadedInfo} />}  // uploadedInfo 값 유무에 따른 분기
      {!uploadedInfo && (
        <>
          <Logo />
          <p className="preview_msg">클릭 혹은 파일을 이곳에 드롭하세요.</p>
          <p className="preview_desc">파일당 최대 3MB</p>
        </>
      )}
    </label>
  );
};
.preview_info {
  width: 100%;
  list-style: none;
  padding: 0;
  gap: 16px;
  display: flex;
  flex-direction: column;
}

.preview_info .info_key {
  display: block;
  font-weight: 500;
  font-size: 12px;
  margin-bottom: 4px;
}

.preview_info .info_value {
  font-size: 14px;
}

 

 

여기까지 완료하면 파일을 업로드했을 때 업로드된 파일의 정보를 확인할 수 있다.

파일 정보 미리보기

 

 

만약 업로드한 이미지 파일에 대해 미리보기를 구현하고 싶다면, 업로드한 파일의 type으로 이미지 여부를 확인하고 FileReader를 사용하여 해당 파일을 읽어 이미지 인코딩 문자열로 변환하여 imageUrl로 저장하도록 하면 된다.

  const setFileInfo = (file) => {
    const { name, type } = file;
    const isImage = type.includes('image');
    const size = (file.size / (1024 * 1024)).toFixed(2) + 'mb';

    if (!isImage) {
      setUploadedInfo({ name, size, type });
      return;
    }
    const reader = new FileReader();
    reader.onload = () => {
      setUploadedInfo({ name, size, type, imageUrl: String(reader.result) });
    };
    reader.readAsDataURL(file);
  };

 

 

미리보기 기능에 대한 더 자세한 구현 방법은 아래 글을 참고하자!

 

Javascript - 업로드한 이미지 미리보기 구현

자바스크립트에서는 input type="file" 태그를 이용하여 이미지를 포함한 다양한 확장자의 파일을 업로드할 수 있다. 만약 이미지만 업로드하게끔 제한하고 싶다면 accept="image/*" 프로퍼티를 추가하

guiyomi.tistory.com

 

 

 

완성

완성본은 다음과 같다.

See the Pen drag and drop file upload by MiJeong Kim (@sap03110) on CodePen.