서론
백엔드에서 이미지를 url로 보내주세요~라고 요청을 했는데, url?? 도대체 무슨 소리지...파일을 어떻게 보내지? 라는 생각이 들었다.
Blob과 File 그리고 url 사이에서 무슨 소리지? 싶어서 찾아보았다.
Blob
Blob은 Binary Large Object의 약자로 주로 이미지, 오디오, 비디오와 같은 멀티미디어 파일 바이너리를 객체 형태로 저장한 것을 의미한다. 멀티미디어 파일들은 대다수 용량이 큰 경우가 많기 때문에, 이를 데이터베이스에 효과적으로 저장하기 위해 고안된 자료형이다.
이 Blob 객체는 blobpart와 optional type로 나누어져 있다.
먼저, blobpart는 Blob | BufferSource | String을 속성으로 지닌 값의 배열이다.
optional type는 Blob의 type를 의미하는데, 보통 MIME type를 가진다.(i.e. image/png)
MIME type(media type)
MIME type는 인터넷에 전달되는 파일 포맷 및 포맷 컨텐츠를 위한 식별자로 기본적으로 type/subtype 구조를 갖고 있다.
여기서 type는 데이터 타입이 속한 general category를 표현하는데, video
나 text
가 이에 속한다.
subtype는 지정된 MIME type의 정확한 타입을 나타낸다.
참고로 MIME type는 Content-Type과 비슷한 면이 있는데, 이는 Content-Type가 표준 MIME type 중의 하나이기 때문이다.
그래서 blob을 업로드/다운로드할 때, 네트워크 request에서 blob의 MIME type이 자연스럽게 Content-Type이 된다.
File
File 객체는 Blob의 한 종류로, Blob을 사용할 수 있는 모든 맥락에서 사용할 수 있다.
그런데, 이 File 객체를 잘 모르고 사용할려 한다면, File 객체에 접근했다고 생각했을 때, 사실은 FileList 객체에 접근한 것과 같은 불상사가 일어날 수 있다.
예를 들어 간단하게 DOM에서 <input />
의 type에 file을 지정했다고 생각해보자.
event.target을 통해서 files에 접근을 할 수 있지만, 이때 files는 FileList 객체이다.
<input type="file" accept="image/*" onChange={(event) => inputTheImage(event)} />
function inputTheImage(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault();
console.log(event.target.files);
}
File 객체가 아니라 FileList 객체인 이유는 <input />
이 여러 파일을 담을 수 있기 때문이다.
단순하게 multiple 속성을 true로 만들어주면 여러 개의 파일을 담을 수 있다.
<input type="file" accept="image/*" multiple onChange={(event) => inputTheImage(event)} />
FileList 객체는 인덱스와 length로 파일을 접근할 수 있는데 multiple 속성을 가져서 파일을 여러 개 가진 FileList 객체는 다음과 같이 나타난다.
그래서 보통 한 이미지만을 다룰 때, FileList 객체의 첫 번째 인덱스를 참조하는 방식(event.target.files[0])으로 File 객체를 접근한다.
function inputTheImage(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault();
const file = event.target.files?.[0];
console.log(file);
}
Blob과 URL
다시 Blob 객체로 돌아와서 이 Blob을 웹 페이지에서 사용할려면 다음과 같은 방법이 존재한다.
- URL.creaetObjectURL()
- FileReader 객체 활용하기
브라우저에서는 Blob의 내용을 보여주기 위해서(i.g. 이미지 미리보기) url로 사용할 수 있는데, 먼저 URL.createObject를 사용하여 url를 활용해보자.
URL.createObjectURL
Blob URL(as Object URL)은 Blob 및 File 객체를 이미지의 URL 소스나 바이너리 데이터를 다운로드하기 위한 링크 등으로 사용할 수 있도록 하는 의사 프로토콜(pseudo-protocol)이다.
브라우저에서 Blob URL을 생성하기 위해 URL.createObjectURL 을 사용한다.
const objectURL = URL.createObjectURL(object);
이 메서드는 Blob 객체를 가져와서 blob:<origin>/<uuid>
형태의 고유한 URL을 생성한다.
브라우저는 내부적으로 URL.createObjectURL을 통해 생성한 URL을 Blob과 매핑한다(URL → Blob). 하지만 Blob은 그 자체로 메모리 내에 남아있으며 브라우저는 이를 해제할 수 없다.
Document가 unloaded되면 매핑이 자동으로 정리되면서 Blob 객체도 정리되지만, 만약 웹 애플리케이션이 오랫동안 실행중이면 메모리 해제가 발생하지 않는 상황이 나타난다. 이에 의해 메모리 누수가 일어날 가능성이 생긴다.
그래서 URL.revokeObjectURL를 호출하여 내부 맵의 참조를 제거하여 blob을 삭제하고, 메모리를 해제해야 한다.
URL.revokeObjectURL(objectUrl)
URL의 예시는 다음과 같이 생성한다.
import { useEffect, useState } from "react";
function ImageUrlLoader() {
const [imageURL, setImageURL] = useState<string>();
useEffect(() => {
console.log(imageURL);
return () => {
if (imageURL) URL.revokeObjectURL(imageURL);
};
}, [imageURL]);
function inputTheImage(e: React.ChangeEvent<HTMLInputElement>) {
e.preventDefault();
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setImageURL(url);
}
}
const blob = new Blob(["이건 블롭"], { type: "text/plain" });
console.log(blob);
return (
<>
<p>URL.createObjectURL</p>
<input type="file" accept="image/*" onChange={(e) => inputTheImage(e)} />
{imageURL && <img src={imageURL} alt="URL createObject" />}
</>
);
}
export default ImageUrlLoader;
useEffect의 cleanup 함수를 통해서 URL.revokeObjectURL를 실행하여 쉽게 메모리 누수를 방지할 수 있다.
그러나 이 Blob url은 클라이언트 사이드 측에서만 접근이 가능하고, 백엔드로 이 url을 보내는 것은 일반적으로 통용되지 않는다.
FileReader
FileReader 객체는 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여 읽을 파일을 가리키는 File 객체 또는 Blob 객체를 이용해 파일의 내용을 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 해준다.
FileReader로 데이터를 읽을 때, 3개의 함수를 통해 파일을 읽을 수 있다(FileReader.ReadAsBinaryString은 deprecated).
- FileReader.ReadAsArrayBuffer : result속성에 파일의 ArrayBuffer 형태를 저장한다.
- FileReader.ReadAsDataURL : result속성에 파일을 나타내는 URL을 저장(보통 base64로 암호화되어서 저장됨)한다.
- FileReader.ReadAsText : result속성에 파일의 텍스트 문자열 형태를 저장한다.
이 FileReader는 비동기적으로 데이터를 읽기 때문에, 다음과 같이 6개의 이벤트 핸들러가 존재하고, 이를 통해 파일의 실제 데이터에 접근할 수도 있다.
- FileReader.onabort : 읽기 중단 시
- FileReader.onerror : 읽는 도중 오류 발생
- FileReader.onload : 읽기 완료 시(성공만)
- FileReader.onloadstart : 읽기 시작 시
- FileReader.onloadend : 읽기 완료 시(성공,실패)
- FileReader.onprogress : 읽는 도중
FileReader.onload 속성을 이용해서 DataURL을 사용하면 blob을 성공적으로 저장할 수 있고, 그 url를 활용하여 이미지를 렌더링할 수도 있다. 그러나 as string을 통해서 타입을 string으로 지정해주지 않으면 <img />
에서 사용할 수 없으니 유의하자.
<input
type="file"
accept="image/*"
onChange={getImage}
ref={inputRef}
style={{ display: "none" }}
/>
<button type="button" color="primary" onClick={handleClick}>
사진 고르기
</button>
const getImage = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const file = e.target.files?.[0];
if (!file) {
alert("파일이 없습니다.");
return;
}
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
console.log(fileReader.result);
};
},
[]
);
실행화면 및 완성 코드
상위컴포넌트
import { useEffect, useState } from "react";
import "./App.css";
import ImageUploader from "./file-loader/FileLoader";
import ImageUrlLoader from "./ImageUrlLoader";
export interface imageInterface {
image_file: string;
file_obj: File | null;
}
function App() {
const [image, setImage] = useState<imageInterface>({
image_file: "",
file_obj: null,
});
useEffect(() => {
console.log(image);
}, [image]);
return (
<div className="App">
<img
src={image.image_file}
alt="image_file"
style={{ width: "200px", height: "auto" }}
/>
<ImageUploader image={image} setImage={setImage} />
<ImageUrlLoader />
{image.file_obj && image.file_obj.toString()}
</div>
);
}
export default App;
FileReader
import {
useState,
ChangeEvent,
useCallback,
useEffect,
useRef,
Dispatch,
SetStateAction,
} from "react";
import { imageInterface } from "../App";
function ImageUploader({
image,
setImage,
}: {
image: imageInterface;
setImage: Dispatch<SetStateAction<imageInterface>>;
}) {
const [fileName, setFileName] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const getImage = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const file = e.target.files?.[0];
if (!file) {
alert("파일이 없습니다.");
return;
}
setFileName(file.name);
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
console.log(fileReader.result);
setImage({
image_file: fileReader.result as string,
file_obj: file,
});
};
},
[setImage]
);
function getDisplayFileName(name: string) {
if (name.length > 15) {
return `${name.substring(0, 10)}...`;
}
return name;
}
function handleClick() {
if (inputRef?.current) {
inputRef.current.click();
}
}
useEffect(() => {
console.log(fileName);
}, [fileName]);
return (
<div>
<p>FileReader</p>
<input
type="file"
accept="image/*"
onChange={getImage}
ref={inputRef}
style={{ display: "none" }}
/>
<button type="button" color="primary" onClick={handleClick}>
사진 고르기
</button>
{fileName && <div>{getDisplayFileName(fileName)}</div>}
</div>
);
}
export default ImageUploader;
URL.createObjectURL
import { useEffect, useState } from "react";
function ImageUrlLoader() {
const [imageURL, setImageURL] = useState<string>();
useEffect(() => {
console.log(imageURL);
return () => {
if (imageURL) URL.revokeObjectURL(imageURL);
};
}, [imageURL]);
function inputTheImage(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault();
const file = event.target.files?.[0];
if (file) {
console.log(event.target.files);
console.log(file);
const url = URL.createObjectURL(file);
setImageURL(url);
}
}
const blob = new Blob(["이건 블롭"], { type: "text/plain" });
console.log(blob);
return (
<>
<p>URL.createObjectURL</p>
<input
type="file"
accept="image/*"
onChange={(event) => inputTheImage(event)}
/>
{imageURL && <img src={imageURL} alt="URL createObject" />}
</>
);
}
export default ImageUrlLoader;
결론
Blob과 FileReader를 React로 잘 활용할 수 있게 되었지만, 프로젝트를 할 당시에 백엔드 팀원이 url로 보내달라했지만, url로 axios 요청을 하였지만 오류가 떠서 그냥 File 객체를 보냈더니 성공하였었다...내 잘못이 아닌데 내가 고생한 케이스...
참고
https://ko.javascript.info/blob
https://developer.mozilla.org/ko/docs/Web/API/File
https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
https://velog.io/@sehyunny/intro-to-blob-api-and-its-use-cases#4-blob-url-%EC%83%9D%EC%84%B1
https://www.taeny.dev/file-object
'React 관련 > 기초' 카테고리의 다른 글
React 렌더링 사이클과 useEffect, 그리고 useRef (1) | 2024.04.25 |
---|