๊ด€๋ฆฌ ๋ฉ”๋‰ด

JiYoung Dev ๐Ÿ–ฅ

[React] ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ณธ๋ฌธ

full stack/React

[React] ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ๊ตฌํ˜„

Shinjio 2024. 11. 5. 20:19

์˜ค๋Š˜์€ ๋ฆฌ์•กํŠธ๋ฅผ ์ด์šฉํ•ด ๋กœ์ปฌ์— ์กด์žฌํ•˜๋Š” ์ฒจ๋ถ€ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์—ฌ API๋กœ ์ „์†กํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด ๋ณด์•˜๋‹ค. 

 

<input type="file"> ํƒœ๊ทธ

๋จผ์ €, ๋ฆฌ์•กํŠธ์—์„œ ์ฒจ๋ถ€ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” <input type="file"> ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. 

ํ•ด๋‹น ํƒœ๊ทธ๋Š” HTML ๊ธฐ๋ณธ ํƒœ๊ทธ๋กœ ๋กœ์ปฌ ์ปดํ“จํ„ฐ์— ์กด์žฌํ•˜๋Š” ํŒŒ์ผ์„ ๊ฐ€์ ธ ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ํƒœ๊ทธ์ด๋‹ค. 

๋งŒ์•ฝ ์•„๋ฌด ์„ค์ • ์—†์ด ํ•ด๋‹น ํƒœ๊ทธ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณด์—ฌ์ค€๋‹ค. 

 

ํŒŒ์ผ ์„ ํƒ์ด๋ผ๋Š” ๋ฒ„ํŠผ๊ณผ ํ…์ŠคํŠธ๊ฐ€ ๋œจ๊ณ , 'ํŒŒ์ผ ์„ ํƒ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์šฐ๋ฆฌ๊ฐ€ ์ž˜ ์•Œ๊ณ  ์žˆ๋Š” ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ•˜๋Š” ์ฐฝ์ด ๋œฌ๋‹ค. 

 

 

์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•˜๊ฒŒ input ํƒœ๊ทธ์˜ file ์†์„ฑ๋งŒ์œผ๋กœ๋„ ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์กฐ๊ธˆ ๋” ์‚ฌ์šฉ์ž๊ฐ€ ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก drag & drop ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

<label>์„ ์‚ฌ์šฉํ•œ drag & drop ๊ธฐ๋Šฅ ์ถ”๊ฐ€

๋“ค์–ด๊ฐ€๊ธฐ์— ์•ž์„œ label ํƒœ๊ทธ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

label ํƒœ๊ทธ๋Š” for ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค๋ฅธ ์š”์†Œ์™€ ๊ฒฐํ•ฉ์ด ๊ฐ€๋Šฅํ•œ ํƒœ๊ทธ์ด๋‹ค.

์˜ˆ๋ฅผ๋“ค์–ด, <input type="radio"> ํƒœ๊ทธ์˜ ๊ฒฝ์šฐ ๋ผ๋””์˜ค ๋ฒ„ํŠผ๋งŒ ํ‘œํ˜„๋˜์ง€ ์–ด๋– ํ•œ ํ…์ŠคํŠธ๋„ ํ‘œ์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค. 

์ด ๋•Œ๋Š” label ํƒœ๊ทธ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋ผ๋””์˜ค ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ ํ•ด๋‹น ๋ฒ„ํŠผ์ด ๊ฐ€์ง€๋Š” ํ…์ŠคํŠธ ์š”์†Œ๋ฅผ ํ•จ๊ป˜ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค.

 

<label> ํƒœ๊ทธ๋Š” for ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค๋ฅธ ์š”์†Œ์™€ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ๊ณ , ์ด ๋•Œ for ์†์„ฑ ๊ฐ’์€ ๊ฒฐํ•ฉํ•˜๊ณ ์ž ํ•˜๋Š” ์š”์†Œ์˜ id ์†์„ฑ ๊ฐ’๊ณผ ๊ฐ™์•„์•ผ ํ•œ๋‹ค. ๋˜ํ•œ, ๊ฒฐํ•ฉ์‹œํ‚ค๊ณ ์ž ํ•˜๋Š” ์š”์†Œ ๋‚ด์— <label> ํƒœ๊ทธ๋ฅผ ์œ„์น˜ ์‹œํ‚ค๋ฉด  for ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋”๋ผ๋„ ์š”์†Œ์™€ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

<label> ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž๊ฐ€ ๋ผ๋ฒจ ํ…์ŠคํŠธ๋ฅผ ํด๋ฆญํ•˜๋ฉด ๋ผ๋ฒจ๊ณผ ์—ฐ๊ฒฐ๋œ ์š”์†Œ๋ฅผ ๊ณง๋ฐ”๋กœ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด ์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค. <label> ํƒœ๊ทธ๋Š” <button>, <input>, <select>, <testarea>,<meter>,<output>,<progress>์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

<input type="file">์—์„œ๋„ <label> ํƒœ๊ทธ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ label ์š”์†Œ ํด๋ฆญ ์‹œ ํŒŒ์ผ ์—…๋กœ๋“œ ์ฐฝ์ด ๋œจ๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. 

 

<label for="input-file">
	<p>ํด๋ฆญ ํ˜น์€ ํŒŒ์ผ์„ ์ด๊ณณ์— ๋“œ๋กญํ•˜์„ธ์š”.</p>
    <p>ํŒŒ์ผ๋‹น ์ตœ๋Œ€ 3MB</p>
</label>
<input id="input-file" type="file"></input>

 

๊ทธ๋ฆฌ๊ณ  input์— ๋Œ€ํ•œ css๋Š” display="none"์„ ์ ์šฉ์‹œ์ผœ ๋ฒ„ํŠผ์€ ๋ณด์ด์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค๊ณ ,

label์— ๋Œ€ํ•œ css๋ฅผ ์ ์šฉํ•˜์—ฌ ๋ผ๋ฒจ์„ ํด๋ฆญ ํ–ˆ์„ ๋•Œ ํŒŒ์ผ ์—…๋กœ๋“œ ์ฐฝ์ด ๋œจ๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. 

 

๊ทธ๋ฆฌ๊ณ  drag & drop ๊ธฐ๋Šฅ์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ label์˜ drag์™€ drop์— ๋Œ€ํ•œ ์†์„ฑ์„ ์ด์šฉํ•œ๋‹ค. 

 

๋“œ๋ž˜๊ทธ & ๋“œ๋กญ ์ด๋ฒคํŠธ ์ข…๋ฅ˜

HTML ์š”์†Œ์—์„œ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์‹œ ์ผ์–ด๋‚˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋‚˜์—ดํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

dragstart 1. ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ์ฒด๋ฅผ ๋“œ๋ž˜๊ทธํ•˜๋ ค๊ณ  ์‹œ์ž‘ํ•  ๋•Œ ๋ฐœ์ƒ
drag 2. ๋Œ€์ƒ ๊ฐ์ฒด๋ฅผ ๋“œ๋ž˜๊ทธํ•˜๋ฉด์„œ ๋งˆ์šฐ์Šค๋ฅผ ์›€์ง์ผ ๋•Œ ๋ฐœ์ƒ
dragenter 3. ๋งˆ์šฐ์Šค๊ฐ€ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ์œ„๋กœ ์ฒ˜์Œ ์ง„์ž…ํ•  ๋•Œ ๋ฐœ์ƒ
dragover 4. ๋“œ๋ž˜๊ทธํ•˜๋ฉด์„œ ๋งˆ์šฐ์Šค๊ฐ€ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ์˜์—ญ ์œ„์— ์ž๋ฆฌ ์žก๊ณ  ์žˆ์„ ๋•Œ ๋ฐœ์ƒ
drop 5. ๋“œ๋ž˜๊ทธ๊ฐ€ ๋๋‚˜์„œ ๋“œ๋ž˜๊ทธํ•˜๋˜ ๊ฐ์ฒด๋ฅผ ๋†“๋Š” ์žฅ์†Œ์— ์œ„์น˜ํ•œ ๊ฐ์ฒด์—์„œ ๋ฐœ์ƒ. ๋ฆฌ์Šค๋„ˆ๋Š” ๋“œ๋ž˜๊ทธ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋“œ๋กญ ์œ„์น˜์— ๋†“๋Š” ์—ญํ• ์„ ํ•จ
dragleave 6. ๋“œ๋ž˜๊ทธ๊ฐ€ ๋๋‚˜์„œ ๋งˆ์šฐ์Šค๊ฐ€ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ์œ„์—์„œ ๋ฒ—์–ด๋‚  ๋•Œ ๋ฐœ์ƒ
dragend 7. ๋Œ€์ƒ ๊ฐ์ฒด๋ฅผ ๋“œ๋ž˜๊ทธํ•˜๋‹ค๊ฐ€ ๋งˆ์šฐ์Šค ๋ฒ„ํŠผ์„ ๋†“๋Š” ์ˆœ๊ฐ„ ๋ฐœ์ƒ

 

ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์— ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”

dragenter, dropover, drop, dragleave ์†์„ฑ์„ ์‚ฌ์šฉํ•œ๋‹ค. 

 

<label
  className={`preview${isActive ? " active" : ""}`}
  for="input-file"
  onDragEnter={handleDragStart}
  onDragLeave={handleDragEnd}
  onDragOver={handleDragOver}
  onDrop={handleDrop}
>
  {uploadedInfo && <FileInfo uploadedInfo={uploadedInfo} />}
  {!uploadedInfo && (
    <>
      <p className="preview-msg">ํด๋ฆญ ํ˜น์€ ํŒŒ์ผ์„ ์ด๊ณณ์— ๋“œ๋กญํ•˜์„ธ์š”.</p>
      <p className="preview-desc">ํŒŒ์ผ๋‹น ์ตœ๋Œ€ 3MB</p>
    </>
  )}
</label>

 

const [isActive, setActive] = useState(false);
const [uploadedInfo, setUploadedInfo] = useState(null);
const [file, setFile] = useState(null);

const handleDragStart = () => setActive(true);
const handleDragEnd = () => setActive(false);
const handleDrop = (event) => {
  event.preventDefault();
  const file = event.dataTransfer.files[0];
  setFileInfo(file);
  setActive(false);
};
const handleDragOver = (e) => {
  e.preventDefault();
};
const handleUpload = ({ target }) => {
  const file = target.files[0];
  setFileInfo(file);
};

 

dragstart๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋“œ๋ž˜๊ทธ ํ•˜์—ฌ label ํƒœ๊ทธ ์•ˆ์œผ๋กœ ๋งˆ์šฐ์Šค ์ปค์„œ๊ฐ€ ์ฒ˜์Œ ๋“ค์–ด์˜จ ์ˆœ๊ฐ„ ๋‚˜ํƒ€๋‚˜๋Š” ์ด๋ฒคํŠธ๋กœ ํ•ด๋‹น ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ์—๋Š” active ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ css ์š”์†Œ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค. 

 

dragover๋Š” ๋“œ๋ž˜๊ทธํ•˜๋ฉด์„œ ๋งˆ์šฐ์Šค๊ฐ€ label ๊ฐ์ฒด ์˜์—ญ ์œ„์— ์žˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋กœ ํ•ด๋‹น ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ์—๋Š” e.preventDefault()๋ฅผ ํ†ตํ•ด ๊ธฐ์กด์— ์กด์žฌํ•˜๋Š” ์ด๋ฒคํŠธ ๋ฐœ์ƒ์„ ๋ง‰๋Š”๋‹ค. 

๋งŒ์•ฝ ํ•ด๋‹น ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค๋ฉด ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์‹œ ์ž๋™์œผ๋กœ ํŒŒ์ผ ๋‹ค์šด์ด ์‹œ์ž‘๋œ๋‹ค. 

 

drop์€ ๋“œ๋ž˜๊ทธ๊ฐ€ ๋๋‚˜์„œ ๋“œ๋ž˜๊ทธํ•˜๋˜ ๊ฐ์ฒด๋ฅผ ํ•ด๋‹น ์˜์—ญ์—์„œ ๋†“์•˜์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋กœ ํ•ด๋‹น ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ์—๋Š” e.preventDefault()๋ฅผ ํ†ตํ•ด ๊ธฐ์กด ์ด๋ฒคํŠธ ๋ฐœ์ƒ์„ ๋ง‰๊ณ , drop ์ด๋ฒคํŠธ์—์„œ ๋ฐ˜ํ™˜๋˜๋Š” DataTransfer ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์„ ํƒํ•œ ํŒŒ์ผ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. 

 

dragleave๋Š” ๋“œ๋ž˜๊ทธํ•œ ์ƒํƒœ์—์„œ label ํƒœ๊ทธ์—์„œ ๋งˆ์šฐ์Šค๊ฐ€ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ ๋‚˜ํƒ€๋‚˜๋Š” ์ด๋ฒคํŠธ๋กœ dragstart์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ active ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ css ์š”์†Œ๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค.

 

๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ด๋ฒคํŠธ๋ฅผ ์œ„ํ•œ ๋ชจ๋“  ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ฉ”์†Œ๋“œ๋Š” DataTransfer ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. 
DataTransfer ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์ด๋ฆ„, ํฌ๊ธฐ, ํƒ€์ž… ๋“ฑ ํ•ด๋‹น ๊ฐ์ฒด์˜ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค. 

 

const setFileInfo = (file) => {
  const { name, size: byteSize, type } = file;
  const size = (byteSize / (1024 * 1024)).toFixed(2) + "mb";
  console.log(name);
  console.log(size);
  console.log(type);
  setUploadedInfo({ name, size, type });
  setFile(file);
};

 

  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>
  );

 

  <>
    <label
      className={`preview${isActive ? " active" : ""}`}
      for="input-file"
      onDragEnter={handleDragStart}
      onDragLeave={handleDragEnd}
      onDragOver={handleDragOver}
      onDrop={handleDrop}
    >
      {uploadedInfo && <FileInfo uploadedInfo={uploadedInfo} />}
      {!uploadedInfo && (
        <>
          <p className="preview-msg">ํด๋ฆญ ํ˜น์€ ํŒŒ์ผ์„ ์ด๊ณณ์— ๋“œ๋กญํ•˜์„ธ์š”.</p>
          <p className="preview-desc">ํŒŒ์ผ๋‹น ์ตœ๋Œ€ 3MB</p>
        </>
      )}
    </label>
    <input id="input-file" type="file" onChange={handleUpload}></input>
    <button onClick={uploadFile}>์ „์†กํ•˜๊ธฐ</button>
    <button onClick={() => setUploadedInfo(null)}>์ทจ์†Œํ•˜๊ธฐ</button>
  </>

 

 

Axios๋กœ ์—…๋กœ๋“œํ•œ file ๋ณด๋‚ด๊ธฐ

๋งˆ์ง€๋ง‰์œผ๋กœ ํŒŒ์ผ ์—…๋กœ๋“œ์—์„œ ์„ ํƒํ•œ ํŒŒ์ผ์„ axios๋ฅผ ํ†ตํ•ด ๋ฐฑ์—”๋“œ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค. 

ํŒŒ์ผ์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•ด์„œ๋Š” FormData๋ผ๋Š” ๊ฐ์ฒด ํ˜•ํƒœ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ณด๋‚ธ๋‹ค. 

 

formData ๊ฐ์ฒด๋Š” XMLHttpRequest ์ „์†ก์„ ์œ„ํ•˜์—ฌ ์„ค๊ณ„๋œ ํŠน์ˆ˜ํ•œ ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ํŠน์ •ํ•œ ์กฐ์ž‘์„ ๊ฐ€ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ฌธ์ž์—ดํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. formData ๊ฐ์ฒด๋Š” key, value ํ˜•์‹์œผ๋กœ ๋˜์–ด ์žˆ์œผ๋ฉฐ, multipart/form-data ํ˜•์‹์œผ๋กœ ์ „์†กํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

const uploadFile = (e) => {
  e.preventDefault();
  const formData = new FormData();

  formData.append("files", file);

  axios
    .post("http://localhost:8080/file/uploaded", formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
    })
    .then((res) => {
      console.log(res.data);
    })
    .catch((err) => {
      console.log(err);
    });
};

 


์ „์ฒด ์ฝ”๋“œ

import "./App.css";
import { useState } from "react";
import axios from "axios";

function App() {
  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 [isActive, setActive] = useState(false);
    const [uploadedInfo, setUploadedInfo] = useState(null);
    const [file, setFile] = useState(null);

    const setFileInfo = (file) => {
      const { name, size: byteSize, type } = file;
      const size = (byteSize / (1024 * 1024)).toFixed(2) + "mb";
      console.log(name);
      console.log(size);
      console.log(type);
      setUploadedInfo({ name, size, type });
      setFile(file);
    };

    const handleDragStart = () => setActive(true);
    const handleDragEnd = () => setActive(false);
    const handleDrop = (event) => {
      event.preventDefault();
      const file = event.dataTransfer.files[0];
      setFileInfo(file);
      setActive(false);
    };
    const handleDragOver = (e) => {
      e.preventDefault();
    };
    const handleUpload = ({ target }) => {
      const file = target.files[0];
      setFileInfo(file);
    };

    const uploadFile = (e) => {
      e.preventDefault();
      const formData = new FormData();

      formData.append("files", file);

      axios
        .post("http://localhost:8080/file/uploaded", formData, {
          headers: {
            "Content-Type": "multipart/form-data",
          },
        })
        .then((res) => {
          console.log(res.data);
        })
        .catch((err) => {
          console.log(err);
        });
    };

    return (
      <>
        <label
          className={`preview${isActive ? " active" : ""}`}
          for="input-file"
          onDragEnter={handleDragStart}
          onDragLeave={handleDragEnd}
          onDragOver={handleDragOver}
          onDrop={handleDrop}
        >
          {uploadedInfo && <FileInfo uploadedInfo={uploadedInfo} />}
          {!uploadedInfo && (
            <>
              <p className="preview-msg">ํด๋ฆญ ํ˜น์€ ํŒŒ์ผ์„ ์ด๊ณณ์— ๋“œ๋กญํ•˜์„ธ์š”.</p>
              <p className="preview-desc">ํŒŒ์ผ๋‹น ์ตœ๋Œ€ 3MB</p>
            </>
          )}
        </label>
        <input id="input-file" type="file" onChange={handleUpload}></input>
        <button onClick={uploadFile}>์ „์†กํ•˜๊ธฐ</button>
        <button onClick={() => setUploadedInfo(null)}>์ทจ์†Œํ•˜๊ธฐ</button>
      </>
    );
  };

  return (
    <div className="App">
      <UploadBox />
    </div>
  );
}

export default App;

 

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

#input-file {
  display: none;
}

#input-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.active {
  background-color: #efeef3;
  border-color: #111;
}

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

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

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

.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;
}

์ฐธ๊ณ ์ž๋ฃŒ

  1. https://guiyomi.tistory.com/148
  2. https://inpa.tistory.com/entry/%EB%93%9C%EB%9E%98%EA%B7%B8-%EC%95%A4-%EB%93%9C%EB%A1%AD-Drag-Drop-%EA%B8%B0%EB%8A%A5
  3. https://tcpschool.com/html-tags/label