[React] 이미지 업로드 미리보기(FileReader / createObjectURL)
미음제
·2022. 9. 5. 17:44
지난 최종 프로젝트에서 이미지 업로드를 다뤄야 했었다. 사용자로부터 이미지를 받고, 이미지 검증 후 미리보기를 보여주고 Form 입력이 완료되면 최종적으로 이미지가 포함된 Form Data를 백엔드 서버로 전송해야 했다. 내가 맡은 스터디 생성/수정 페이지에서는 FileReader를 사용했고, 다른 분이 작업한 커뮤니티 포스트에서는 createObjectURL 방식을 사용했다. 업로드한 이미지를 미리 보여주기 위해 사용한 방법에 차이가 있어 정리를 하게 되었다.
업로드 한 이미지 미리보기
업로드한 이미지 미리보기를 구현하기 위한 방법으로 2가지가 있다. 첫 번째는 FileReader 객체를 사용하는 것, 두 번째는 URL 객체를 사용하는 방법이다. 두 방법 모두 클라이언트 단에서 미리보기를 위해 사용하는 것으로 서버에 전송할 땐 FormData를 통해 전송한다.
프로젝트 당시 이미지 업로드에 관해 base64 인코딩 방식을 생각했었는데, 인코딩을 거치고 백엔드 서버로 전송하면 이미지가 깨지고, 디코딩 과정도 거쳐야 해 백엔드 쪽에서 File 자체를 넘겨달라고 요청했었다. 이미지 자체는 FormData로 전송하고, FileReader, URL 객체를 사용한 방법으로 미리보기를 구현했다.
이미지 미리보기 구현
이미지 미리보기를 테스트하기 위해 코드를 간단하게 작성한다.
우선 이미지를 사용자로부터 받기 위한 Input 컴포넌트를 생성한다.
// src/components/fileInput.js
import { useRef } from "react";
export default function FileInput({ label, onChange }) {
const ref = useRef(null);
const onClick = () => {
ref.current?.click();
};
return (
<button onClick={onClick}>
{label}
<input
hidden
type="file"
accept="image/jpg,image/png,image/jpeg,image/gif"
name="image-input"
onChange={onChange}
ref={ref}
/>
</button>
);
}
Input은 hidden으로 가려주고 button 클릭을 통해 이미지를 입력받을 수 있도록 onClick 메서드를 button에 달아준다. ref를 통해 Input을 가리키고, button 클릭 시 ref에 연결된 Input을 클릭하는 메서드를 실행한다. 그리고 사용자에게서 이미지를 받으면 onChange함수를 통해 이미지를 다룬다. onChange함수는 상위 컴포넌트에서 내려준다.
// src/App.js
import React, { useState } from "react";
import "./index.css";
import FileInput from "./components/fileInput";
function App() {
const [fileReaderThumbnail, setFileReaderThumbnail] = useState();
const [URLThumbnail, setURLThumbnail] = useState();
const encodeFile = (fileBlob) => { // FileReader 방식
const reader = new FileReader();
if (!fileBlob) return;
reader.readAsDataURL(fileBlob);
return new Promise((resolve) => {
reader.onload = () => {
const result = reader.result;
setFileReaderThumbnail(result);
resolve();
};
});
};
const createImageURL = (fileBlob) => { // createObjectURL 방식
if (URLThumbnail) URL.revokeObjectURL(URLThumbnail);
const url = URL.createObjectURL(fileBlob);
setURLThumbnail(url);
};
const onFileReaderChange = (e) => {
const { files } = e.target;
if (!files || !files[0]) return;
const uploadImage = files[0];
encodeFile(uploadImage);
};
const onImageChange = (e) => {
const { files } = e.target;
if (!files || !files[0]) return;
const uploadImage = files[0];
createImageURL(uploadImage);
};
return (
<div className="main">
<div className="section">
<h1>File Input by FileReader</h1>
<div className="image-wrapper">
{fileReaderThumbnail ? (
<img src={fileReaderThumbnail} alt="thumbnail" />
) : (
"이미지 미리보기"
)}
</div>
<FileInput label="File Reader Upload" onChange={onFileReaderChange} />
</div>
<div className="section">
<h1>File Input by createObjectURL</h1>
<div className="image-wrapper">
{URLThumbnail ? (
<img src={URLThumbnail} alt="thumbnail" />
) : (
"이미지 미리보기"
)}
</div>
<FileInput label="create object URL Upload" onChange={onImageChange} />
</div>
</div>
);
}
export default App;
fileInput 컴포넌트에 prop으로 내려주는 onChange 함수가 onFileReaderChange(FileReader 방식), onImageChange(createObjectURL)이다. 이미지를 업로드하게 되면, 해당 target에서 file을 가져오고 입력받은 file blob을 각각 방식에 맞는 함수로 넘겨준다.
Blob 이란?
특정 MIME Type의 바이너리 데이터를 저장하는 객체로 Binary Large Object의 약자이다. 텍스트, 오디오, 이미지, 비디오 등을 다룰 수 있다. Blob 객체의 size를 통해 파일 크기를 알아낼 수 있고, slice() 메서드를 통해 데이터 송/수신 간 파일을 쪼개는 일도 가능하다.
Blob 객체는 마이크 소리, 화면, 영상 등을 다루고 오디오(mp3 파일 같은)나 이미지(jpg, png, ...) 파일은 Blob을 상속받는 File 객체를 통해 다룬다. File 객체는 Blob 객체를 상속받아 모든 프로퍼티가 동일하고 추가로 파일명과 최종 수정일을 확인할 수 있는 프로퍼티가 추가되어 있다.
FileReader 방식
FileReader는 File, Blob 객체가 저장하고 있는 바이너리 데이터를 '비동기적'으로 읽어주는 객체이다. 기본 동작은 다음 과정을 거친다.
- 1. read
- 2. load event
- 3. result
File 객체는 new File() 생성자 함수로 생성할 수 있는데, Input element가 이 역할을 한다. 따라서 Input element의 onChange 메서드를 통해 target의 file에 접근할 수 있다.
const encodeFile = (fileBlob) => {
const reader = new FileReader();
if (!fileBlob) return;
reader.readAsDataURL(fileBlob);
return new Promise((resolve) => {
reader.onload = () => {
const result = reader.result;
setFileReaderThumbnail(result);
resolve();
};
});
};
파일을 비동기로 읽기 위해 FileReader 객체를 생성한다. 객체를 생성한 뒤 넘겨받은 파일을 readAsDataURL()로 읽는다. 읽기가 완료되면 onload() 실행하는 Promise 객체를 return 하도록 한다. onload()가 실행되면 base64 인코딩 string이 저장되고 해당 string을 통해 미리보기를 보여 줄 수 있다.
createObjectURL 방식
const createImageURL = (fileBlob) => {
if (URLThumbnail) URL.revokeObjectURL(URLThumbnail);
const url = URL.createObjectURL(fileBlob);
setURLThumbnail(url);
};
URL 객체의 createObjectURL() 메서드는 File 또는 Blob 객체를 가리키는 URL을 생성할 수 있다. createObjectURL()를 통해 File, Blob 객체의 고유한 URL을 생성하게 된다. 생성된 URL string을 통해 미리보기를 보여줄 수 있다. 고유한 URL 객체는 Document가 닫히기 전까지 유지되고, 그전에 해제하고 싶다면 revokeObjectURL()를 통해 해제할 수 있다.
위 함수에서 revokeObjectURL()를 먼저 실행하는 이유이다. 한 화면에서 이미지를 계속 바꾸면 고유한 URL 객체가 매번 생성되고 revokeObjectURL()를 해주지 않는 이상 화면이 닫히기 전까지 계속해서 유지되기 때문이다. 따라서 이전에 저장된 미리보기용 string에 대한 URL 객체를 revoke 하기 위함이다.
두 방법 모두 사용자에게서 받은 이미지를 미리 보여주기 위해 사용한 것이다. Input을 받고 해당 이미지를 보여주기 위해 string을 생성하고 보여주는 과정은 동일하다. 그러나 메모리 효율, 속도, 수용 가능 용량의 차이가 있어 비교를 하고자 정리를 하게 되었다.
FileReader vs createObjectURL
메모리
파란 표시가 된 것이 FileReader 방식의 string, 빨간 밑 줄이 createObjectURL 방식의 string이다. 단순 문자열만 비교해봐도 FileReader 방식이 긴 것을 확인할 수 있다.
FileReader 방식은 base64 인코딩 string을 createObjectURL 방식은 포인터를 사용한다. FileReader 방식은 사용되지 않을 때 가비지 콜렉터에 의해 자동으로 수거되지만, createObjectURL 방식은 Document가 닫히거나 revoke하지 않으면 메모리에 계속 남아 있게 된다. 가비지 콜렉터에 의해 자동으로 수거되긴 하지만, 그럼에도 createObjectURL 방식이 더 효율 적이다. 화면 이동 간에는 자동으로 revoke가 되니까 revoke를 해야 하는 시점에 적절하게 revoke를 해주면 된다.
속도 및 편의성
FileReader 방식은 File, Blob 객체를 읽고 base64 인코딩 string으로 변환하는데 까지 많은 작업을 거치고(read, load, result) 비동기적으로 처리되어야 하는 불편함이 있다. 반면에 createObjectURL 방식은 동기적으로 동작하며 File, Blob 객체의 고유한 URL를 즉시 생성한다(읽기 과정을 거치지 않아도 된다).
수용 가능 용량
FileReader 방식의 경우 대략 10mb 정도의 용량을 수용할 수 있고, createObjectURL 방식은 Blob의 최대치에 가까운 800mb 정도의 용량까지 수용할 수 있다고 한다.
프로젝트를 하면서 PR을 보고 미리 비교해 봤다면 나도 FileReader 방식에서 createObjectURL 방식을 사용했을 것 같다. 이미지 업로드 및 전송을 구상하면서 초기에 base64 인코딩 방식을 생각하고 진행하다 보니 자연스럽게 FileReader 방식을 서칭하고 적용했다. 백엔드 쪽에서 base64 인코딩 방식 말고 FormData를 그대로 전송해달라고 했을 때 미리 찾아보고 알아봤으면 하는 아쉬움이 있지만 이 기회에 둘의 차이를 알게 되어 좋았다. 하나의 이미지만 업로드하고 보여주어(백엔드 요청에 따라 크기도 최대 1MB로 제한했다) 큰 차이는 없었겠지만 훨씬 사용하기 간단하고 성능적으로도 좋기 때문에 createObjectURL 방식을 미리 알았더라면 하는 아쉬움이 있다.
참고
'Developer > React' 카테고리의 다른 글
Redux-persist (0) | 2022.12.02 |
---|