본문 바로가기

개발일지/디스턴스

이미지 전송하기 (FormData, Blob)

반응형

위치 기반 랜덤채팅 어플, 디스턴스에서는

대학생인지를 인증하는 학생 인증 절차가 있다.

학생 인증 절차에는 3가지 옵션이 있는데 그 중 실물 학생증을 사진을 찍어 보내는 학생증 인증 방법

모바일 학생증을 캡처해서 보내는 모바일 학생증 인증 방법이 있다.

이에 사진을 FormData 객체로 관리자에게 전송하는 기능을 구현해야 했다.

학생증 인증 화면 UI

목차

  1. File객체와 Blob
  2. FormData란 무엇인가
  3. 이슈1 - 비디오 전송이 가능한 문제
  4. 이슈2 - 사진 포맷이 크면 413 에러가 발생하는 문제

1.  File 객체와 Blob

먼저 파일을 업로드하기 위해서는 type이 file인 input태그를 통해 파일 객체를 입력받아야 한다.

const VerifyMobileIdPage = () => {

const fileInputRef = useRef();

    const handleButtonClick = () => {
        fileInputRef.current.click();
    };

    //생략 ...

    return (
        <img
            src={uploadedImage} //아래서 설명
            alt="profile"
            onClick={handleButtonClick}
        />
        <input
            ref={fileInputRef}
            id="fileInput"
            type="file"
            onChange={onChangeImage}
            hidden
        />
    );
}

UI 상으로 봤을 때 input이 아니라 이미지 태그를 입력했을 때 사진 업로드가 가능해야 했기 때문에

input 태그에 hidden 속성을 넣어 안 보이게 하고 ref로 사용하여 img태그 클릭 시 input태그를 참조하도록 만들었다.

 

img 태그를 클릭하고 파일을 선택하여 input태그에 넣으면 File 객체를 받게 된다.

const [uploadedImage, setUploadedImage] = useState(null);
const [file, setFile] = useState(null);

const onChangeImage = (e) => {
    const file = e.target.files[0];
    
    const imageUrl = URL.createObjectURL(file);
    setUploadedImage(imageUrl);
    setFile(file);
}

e.target.files로 input에 입력받은 파일 객체에 접근할 수 있다.

files를 리스트 형태이기 때문에 0번째 index를 명시하여 이미지 파일 객체를 file 변수에 저장한다.

file과 uploadedImage 2개의 상태값을 만들고 file state를 입력받은 파일 객체로 초기화한다.

입력받은 파일을 이미지 태그 내에서 보여주기 위해 url이 필요하다.

때문에 createObjectURL을 사용하여 브라우저가 읽을 수 있는 Blob 객체의 URL을 생성한다.

 

createObjectURL 사용하여 Blob URL을 생성할 때에는 특정 브라우저 세션과 연결하여 URL을 생성한다.

따라서 생성된 window의 document에서만 유효하고 외부 탭이나 컴포넌트에서의 무단 접근을 방지한다.

생성된 URL을 콘솔로 찍어보면 blob:http://localhost:3000/{주소} 이런 식으로 나온다.

더보기

[ 잠깐 정리 ]

 

Blob(Binary Large Object)은 이미지, 사운드, 비디오와 같은 멀티미디어 데이터를 다룰 때 사용하는 객체로

데이터의 크기, MIME 타입을 알아내거나 데이터 송수신을 위한 작은 Blob 객체로 나누는 등의 작업에 사용된다.

*File 객체는 Blob을 상속받기 때문에 Blob의 모든 특성을 가지고 있다.

 

MIME(Multipurpose Internet Mail Extentions)는 웹을 통해 여러 형태의 파일을 전달하는데 사용되는 인코딩 방법이다.

원래 인터넷 이메일을 통해 텍스트 외에 이미지, 비디오, 오디오 등의 다양한 데이터를 보낼 수 있도록 설계된 것이었는데

현재는 웹 기술에서 파일의 형식과 내용을 정의하는데 사용되는 표준이 되었다.

 

 

2.  FormData란 무엇인가

위와 같이 input에 File을 입력 받고 file state에 해당 Blob객체를 저장해 놓은 상태이다.

그렇다면 이제 서버로 보내기만 하면 된다.

서버로 보낼 때는 FormData를 사용하여 보내야 한다.

 

FormData 객체란, 폼 데이터를 생성하여 손 쉽게 조작하고 서버에 전송할 수 있도록 해주는 객체이다.

다양한 유형의 데이터(멀티파트폼 데이터)를 <form>태그로 묶어서 FormData 객체를 생성하면

보다 쉽게 전송할 수 있게 만들어준다.

특히 파일 또는 대량 데이터 전송 시에 용이하다.

 

<form>태그를 꼭 사용하지 않아도 FormData 객체를 선언해서 사용할 수 있고

자바스크립트로 동적 제어를 하여 보다 유연하게 사용할 수도 있다.

const sendStudentId = async () => {
    if (!file) {
      alert("이미지를 먼저 업로드해주세요.");
      return;
    }
    const formData = new FormData();
    formData.append("studentcard", file);

    try {
      await axios.post("/studentcard/send", formData);
      window.confirm(
        "인증되었습니다. 식별 불가능한 사진일 경우 사용이 제한됩니다."
      ) && navigate("/");
    } catch (error) {
      console.log(error);
    }
};

//위에 적은 코드
//img태그와 input 태그 ...

<button onClick={sendStudentId}>이미지 전송하기</button>

formData.append(name, value) 에서 name 자리에는 서버에서 참조할 데이터 키(필드명)을 적으면 되고

value 값에는 파일 객체를 넣으면 된다.

 

 

3.  이슈1 - 비디오 전송이 가능한 문제

 여기까지 파일을 전송하는 기능을 모두 구현했다.

그런데 테스트를 해보다 보니 문제가 생겼다.

학생증 인증은 사진만 가능한데 비디오도 선택이 가능하도록 보인다는 것이다.

 

이 문제를 해결하기 위해 input 포맷 지정이 필요했다.

input태그의 file type이 가진 속성 중에 accept는 사용자가 선택할 수 있는 파일의 타입을 제한한다.

그래서 input 태그에 해당 속성값을 추가하였다.

<input
    ref={fileInputRef}
    id="fileInput"
    type="file"
    accept="image/*" //여기 추가
    onChange={onChangeImage}
    hidden
/>

 

또한 onChangeImage에서 입력받은 file 객체의 값이 이미지인지 확인하고 아니면 alert을 띄우는 확인 작업도 추가하였다.

const onChangeImage = (e) => {
    const file = e.target.files[0];
    //여기 추가
    if (!file.type.startsWith('image/')) {
      alert('이미지 파일만 업로드 가능합니다.');
      return;
    }
    const imageUrl = URL.createObjectURL(file);
    setUploadedImage(imageUrl);
    setFile(file);
}

 

이로서 이미지만 업로드가 가능하도록 만들고 이슈를 해결하였다.

 

 

4.  이슈2 - 사진 포맷이 크면 413에러가 발생하는 문제

사진을 여러 장 보내며 테스트를 하다 보니 몇몇 사진은 전송이 안되고 413에러가 발생했다.

원인을 찾아보니 File 객체의 size가 너무 큰 것이 문제였다.

이에 onChangeImage에서 전송 전에 파일의 사이즈를 리사이징하는 과정을 추가하였다.

 

size를 줄이는 방법은 2가지인데 해상도를 낮추거나 압축률을 조정하는 것이다.

먼저 input태그 아래에 canvas 태그를 추가한다.

canvas 태그를 추가하는 이유는 이미지 크기를 조작하기 위해서이다.

img 태그가 아닌 canvas를 사용하는 이유는 img는 정적 이미지 표시를 위해 사용되는 태그이고

canvas는 img보다 동적 조작이 좀 더 용이하기 때문이다.

<input
    ref={fileInputRef}
    id="fileInput"
    type="file"
    accept="image/*"
    onChange={onChangeImage}
    hidden
/>
<canvas ref={canvasRef} style={{ display: 'none' }}></canvas> //여기 추가
const onChangeImage = (e) => {

    const file = e.target.files[0];
    if (!file.type.startsWith('image/')) {
      alert('이미지 파일만 업로드 가능합니다.');
      return;
    }

    const imageUrl = URL.createObjectURL(file);
    setUploadedImage(imageUrl);

    const reader = new FileReader();
    reader.onload = function (e) {
      const img = new Image();
      img.onload = function () {
        const canvas = canvasRef.current;
        const ctx = canvas.getContext('2d');

        // 이미지 크기를 조절
        const scaleFactor = 0.3;
        canvas.width = img.width * scaleFactor;
        canvas.height = img.height * scaleFactor;

        // 축소된 크기로 이미지 그리기
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

        // canvas의 내용을 이미지 파일로 변환 (포맷, 품질)
        canvas.toBlob(function (blob) {
          console.log('Resized image size:', blob.size);
          setFile(blob)
        }, 'image/jpeg', 0.7);
      };
      img.src = e.target.result; // 2번. 파일 리더 결과를 이미지 소스로 설정
    };
    reader.readAsDataURL(file); // 1번. 파일을 Data URL로 읽기

    console.log(e.target.files)
    console.log(imageUrl);
  };

위의 코드는 이미지를 리사이징하는 로직을 추가한 onChangeImage 함수이다.

이미지를 조정하기 위해서 먼저 FileReader()객체를 생성하여 reader에 저장하고

input에 입력받은 파일 데이터를 DataURL로 읽어왔다. (1번)

해당 코드를 통해 파일이 잘 읽어져왔으면 reader.onload가 자동으로 실행된다.

 

onload를 실행하기 위해 함수 블록 안으로 들어오면 Image() 객체를 생성하여 img에 저장하고 있다.

1번에서 읽어 온 파일 데이터의 결과(URL)를 img 객체의 src 속성에 연결하여 저장하였다.

여기서도 마찬가지로 e.target.result가 잘 읽어져 오면 img.onload가 자동으로 실행된다.

 

canvas에 연결했던 ref 참조값을 이용하여 태그를 가져오고

getContext를 사용하여 이미지를 새로 그린다.

width와 height를 일정 크기 만큼 줄이고

축소된 크기로 이미지(img)를 새롭게 그려서 canvas에 저장한다.

(ctx.drawImage는 그려진 이미지가 자동으로 canvas로 저장된다)

 

마지막으로 canvas의 포맷(jpeg)과 품질(0.7)을 설정하여

Blob 객체로 내보냈다.

내보낸 blob객체는 setFile을 이용하여 file state에 저장하였다.

 

이제 이 file을 서버에 보내기만 하면 된다.

서버에 보내는 것은 위와 동일하게 FormData로 보내면 된다.

이로써 두 번째 이슈도 해결되었다.

 

 

 

[ 참고 자료 ]

https://juyoung-1008.tistory.com/4

https://heropy.blog/2019/02/28/blob/