[React] 리액트 이미지 미리보기(업로드) 구현 / File API
file
타입의<input>
태그와 File API를 이용해 컴퓨터에 저장된 이미지를 업로드할 수 있다. 이미지 태그 자체를 “파일선택” 버튼으로 기능하도록 할 수 있고, 라벨의 스타일을 수정하는 방식으로 “파일 선택”(파일 필드) 스타일을 변경할 수도 있다.
기본 구조
업로드한 이미지는 컴포넌트 내부 상태(image)로 관리하고, 업로드 하기 전엔 기본 프로필 사진(fallbackUrl)을 표시하도록 한다.
<input>
태그의 type
을 file
로 명시하면 “파일 선택” 버튼이 표시된다. accept
속성엔 허용할 파일 유형을 .확장자
형태로 입력한다. 확장자는 대소문자를 구분하지 않는다. 여러 값을 입력할 땐 콤마 ,
로 구분한다.
특정 타입(MIME 유형)의 모든 확장자를 허용하고 싶으면 타입/*
을 입력한다. 허용하지 않은 파일 유형은 파일 선택 창에서 선택할 수 없다. (파일 유형 참고글)
- 확장자 입력 예시 :
.jpg, .png, .pdf
(jpg
png
pdf
파일 허용) - 특정 타입의 모든 확장자 허용 :
image/*
video/*
audio/*
기본적으로 1개 파일만 선택해서 업로드할 수 있으며 multiple
속성을 true
로 설정하면 여러 파일을 업로드할 수 있다(multiple
속성을 명시하지 않으면 1개 파일만 업로드할 수 있다).
const fallbackUrl =
'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png';
const [image, setImage] = useState('');
const uploadHandler = () => {};
return (
<>
// 생략
<img
src={image || fallbackUrl} // 이미지를 업로드하기 전엔 fallback 이미지 표시
alt="profile"
className="w-28 h-28 rounded-full" // Tailwind CSS 스타일
/>
<input
accept="image/jpg,image/png,image/jpeg"
type="file"
onChange={uploadHandler}
/>
</>
);
업로드 핸들러 — File 객체 / FileReader API
// DataURL 사용 예시
const uploadHandler = ({ target }) => {
const file = target.files[0]; // File 객체에 선택한 이미지 파일 정보가 담김
const reader = new FileReader(); // FileReader 인스턴스 생성
// 에러없이 읽기를 마쳤을 때 onload 이벤트 호출
reader.onload = () => {
setImage(reader.result); // FileReader를 통해 DataURL로 읽은 결과(DataURL) 업데이트
};
reader.readAsDataURL(file); // FileReader API로 File 객체 읽기
};
return (
// 생략
<img
src={image || fallbackUrl}
alt="profile"
className="w-28 h-28 rounded-full" // Tailwind CSS 스타일
/>
);
File 객체
💡 File 객체는 Blob 기능을 상속받아 확장된 것으로, Blob을 사용할 수 있는 곳이면 File도 사용할 수 있다.
“파일 선택”을 클릭해서 업로드할 파일을 선택하면 onChange
에 할당한 핸들러(uploadHandler
)가 호출된다. 선택한 파일 정보는 event.target.files
(FileList)객체에 저장되며, 파일명, 사이즈(byte), 파일 유형 등의 정보를 볼 수 있다.
1개 파일만 업로드 할 수 있으므로(multiple
속성을 안줘서) 첫번째 인덱스의 files[0]
파일 정보를 불러온다. File
객체엔 선택한 파일에 대한 정보만 있을 뿐 파일 데이터는 없다. 파일 데이터를 읽으려면 FileReader API를 이용해야 한다.
const file = event.target.files[0];
FileReader API
new FileReader()
로 새로운 FileReader 인스턴스를 생성한다.
const reader = new FileReader();
FileReader는 File 객체와 Blob 형식의 데이터를 “비동기적”으로 읽을 수 있다. 파일을 읽는 방법은 4가지가 있다. 이미지 파일을 읽어서 src
에 사용해야 하므로 readAsDataURL()
메서드를 이용한다.
❶ readAsText(blob, [encoding])
: 지정한 인코딩(기본값 utf-8)의 텍스트 문자열로 데이터 읽기
# 이미지 파일을 텍스트로 읽으면 아래처럼 깨진 문자로 나온다
�PNG
IHDR��Ͷz|iCCPkCGColorSpaceGenericRGB8��U]hU>���+$Ԧ���5��lRф���e�m�,�l�A���ݝi&3���i)>A������[�'!j��-��P��(���G�� �3����k������~��s����,[��%,�-�������:t�}�}�
❷ readAsDataURL(blob)
: base64로 인코딩한 DataURL로 데이터 읽기
# DataURL 예시
data:image/png;base64,iVBORw0KGgoAAAANSU...
- base64로 인코딩된 문자열은 브라우저가 파싱해서 원래 데이터로 만들 수 있다
- 주소창에 DataURL을 쳐보면 파일 내용이 표시된다
- 즉 DataURL은 파일 정보를 주소처럼 활용하는 것
<img>
태그의src
값에도 사용할 수 있다URL.createObjectURL(blob)
을 대신 사용할 수도 있다
❸ readAsArrayBuffer(blob)
: ArrayBuffer로 데이터 읽기
readAsArrayBuffer
메서드로 데이터를 읽으면 ArrayBuffer 객체를 반환하며, 버퍼링 처럼 데이터를 일정한 크기로 잘라서 서버로 보낼 때 사용한다.
❹ readAsBinaryString(blob)
: 바이너리(이진) 형식으로 데이터 읽기
# readAsBinaryString 메서드로 데이터를 읽으면 아래처럼 이진 데이터를 반환한다
PNG
IHDRØØͶz|iCCPkCGColorSpaceGenericRGB8U]hU>¹³+$ÎÔ¦¦þ5´lRÑÚèþe³mÜ,l´AÉìÝi&3ãü¤i)>AÁ¨ààÿ[Á'!j«í-¢´P¢(øÐúG¡Ò ë¹3³»¸k½ËÜùæï~çÞsîÞ¸,[Þ%,®-åÓâ³ÇæÄÄ:tÁ}Ð}Ð-+*&ã¿Úíï ÆÞ×ö·÷ÿgë®PGÝ
ج8Ê"âeþŲ]AûÈ ×bø Ä;l âõWð²Ï2_E,(ªþÄÛç#öZsðÛ<5¨)"ËEÉ6«N#Ó½û¶EÝkÄÛO³0}߸ö*ráUäÜt¯.i³Åÿe¹i ñ#]»¼
r
❺ abort()
: 데이터 읽기 취소(onloadstart
이벤트가 발생했을 때 사용할 수 있음)
데이터 읽기 이벤트 (Progress Event)
readAsDataURL()
메서드 인자에 파일 객체를 추가하면 파일을 읽기 시작한다.
reader.readAsDataURL(file);
데이터를 성공적으로 읽었다면 onload
이벤트가 호출(fire)된다. 따라서 onload
이벤트의 핸들러를 작성해야 한다. onload
외에도 읽기 과정에서 발생하는 다양한 이벤트(Progress Event)를 사용할 수 있다.
onloadstart
: 읽기 시작onprogress
: 읽기 도중onloadend
: 읽기 완료(성공/실패 여부 관계없이)onload
: 에러 없이 읽기 성공 — 자주 사용함 ⭐️onerror
: 읽기 에러 — 자주 사용함 ⭐️onabort
: 읽기 동작 중단
onloadstart
이벤트가 발생했을 때 abort()
메서드로 데이터 읽기를 취소할 수 있다.
// 데이터를 읽기 시작할 때 호출
reader.onloadstart = () => {
reader.abort(); // 데이터 읽기 취소
};
// 성공 여부에 관계없이 데이터 읽기를 완료했을 때 호출
reader.onloadend = () => {
console.log(reader.error); // 데이터 읽기를 취소했으므로 에러 발생
// reader.error.code -> 에러 코드 확인
// reader.error.name -> 에러 이름 확인
// reader.error.message -> 에러 메시지 확인
};
데이터 읽기 결과 조회
데이터 읽기를 마친 결과는 reader.result
에서 확인할 수 있다. 이벤트가 발생한 대상이 FileReader 이므로 event.target.result
에서 확인해도 동일하다. 데이터 읽기 도중 문제가 발생했을 땐 reader.error
에서 에러 내용을 확인할 수 있다.
reader.onload = (e) => {
setImage(reader.result); // e.target.result === reader.result
};
reader.readAsDataURL(file);
참고로 reader.readyState
에서 읽기 작업에 대한 현재 상태를 확인할 수 있다.
0
(EMPTY) : 리더 생성 — FileReader 인스턴스 생성 직후1
(LOADING) : 읽기 메서드 호출2
(DONE) : 작업 완료 (읽기 성공/오류/중단 포함)
URL.createObjectURL 활용
💡 업로드한 파일을 서버로 보내지 않고 브라우저 내에서만 사용할 때 활용(미리 보기 등). 서버로 보낼땐
DataURL을 FormData 객체로 만들어서 보내면 된다(참고 링크).
URL.createObjectURL(blob)
은 인자에 명시한 Blob(File) 객체를 가리키는 URL(DOMString)을 “동기적”으로 생성하며, DOM에서 참조할 수 있다. 아래 같은 문자열 형태로 되어 있다.
# Blob URL 예시
blob:http://localhost:3000/90e56ed1-ba85-4e19-b40c-65a4495383a0
Blob URL은 자신을 생성한 window
의 document
(브라우저) 에서만 유효하기 때문에 다른 window
에서 재활용할 수 없다. window
창이 사라지면 Blob URL도 없어진다.
FileReader.readAsDataURL(blob)
로 생성한 DataURL 처럼, URL.createObjectURL
로 만든 Blob URL(DOMString)도 주소창에 치면 파일 내용이 표시된다.
DataURL은 <img>
태그의 src
속성에 적용할 때마다 문자열을 파싱해서 이미지로 만들기 때문에 속도가 느리다. 반면 Blob 객체의 URL 주소는 메모리에 등록해서 사용하기 때문에 더 빠르다.
메모리에 추가된 Blob URL은 가비지 콜렉터가 따로 청소하지 않는다. 따라서 <img>
태그의 src
속성에 적용해서 DOM과 바인딩했다면, revokeObjectURL
메서드로 URL을 해제(폐기) 해줘야 메모리 누수를 방지할 수 있다. URL을 해제한 후에 Blob URL을 주소창에 쳐보면 아무것도 나오지 않는다.
// Blob URL 사용 예시
const [image, setImage] = useState('');
const imageRef = useRef(null);
const uploadHandler = ({ target }) => {
const file = target.files[0]; // File 객체에 선택한 이미지 파일 정보가 담김
const imageUrl = URL.createObjectURL(file); // File 객체에 대한 Blob URL 생성
imageRef.current.onload = () => {
URL.revokeObjectURL(imageUrl); // 이미지 로드를 완료하면 Blob URL 폐기
};
setImage(imageUrl);
// imageRef.current.src = imageUrl -> 이미지 태그 src에 직접 할당할 수도 있음
};
return (
// 생략
<img
src={image || fallbackUrl}
ref={imageRef}
alt="profile"
className="w-28 h-28 rounded-full" // Tailwind CSS 스타일
/>
);
이미지 클릭 시 파일 선택창 표시
ref
객체를 만들어서 <input>
태그 ref
속성 값에 할당하고, <img>
태그를 <button>
으로 감싼다. 버튼을 클릭했을 때 <input>
엘리먼트에 접근하여 click()
이벤트가 실행되도록 핸들러를 작성하면 된다.
const inputRef = useRef(null);
// ...생략
return (
<>
// ...생략
<button
className="w-fit" // Tailwind CSS 스타일
type="button"
onClick={() => inputRef.current.click()}
>
<img
src={image || fallbackUrl}
alt="profile"
className="w-28 h-28 rounded-full" // Tailwind CSS 스타일
/>
</button>
<input
accept="image/jpg,image/png,image/jpeg"
type="file"
ref={inputRef}
onChange={uploadHandler}
/>
</>
);
이런 방식으로 다른 엘리먼트에서 file
타입의 <input>
을 핸들링할 수 있다. ref
객체를 사용하지 않고 아래처럼 Input 태그를 직접 선택 하는 방식으로 작성할 수도 있다.
document.getElementById('inputId').click(); // === inputRef.current.click()
파일 Input 스타일 수정
file
타입의 <input>
태그는 브라우저마다 조금씩 다른 기본 UI를 가진다. 아쉽게도 이 UI는 CSS 스타일로 변경할 수 없다. 스타일을 수정하고 싶다면 <input>
태그를 화면에서 숨기고 <label>
태그에 원하는 스타일을 정의해야 된다.
💡 radio 타입의 <input>
은 1개만 선택 할 수 있고, checkbox 타입의 <input>
은 여러 개 선택 할 수 있다. name
속성은 체크박스의 이름을 나타내며 같은 분류의 체크박스를 그룹으로 묶을 때 사용한다. React / Vue에서 특정 <input>
태그를 식별할 때 name
속성을 활용하기도 한다. form 태그로 데이터를 전송하면 <input>
의 name
, value
속성 값이 ...?name=value
형태로 전송된다(참고)
💡 <label>
태그와 <input>
태그를 연결하면 <label>
의 textContent
영역만 클릭해도 <input>
체크박스를 핸들링할 수 있다. <label>
태그의 for
속성과 <input>
태그의 id
속성 값을 동일하게 입력하면 두 태그를 연결할 수 있다. 접근성을 고려해 <input>
과 <label>
태그는 같이 쓰는게 좋다. <input>
태그가 <label>
안쪽에 있다면 연결된 상태가 되므로 for
, id
를 입력하지 않아도 된다.
- Label ⇄ Input 태그 연결
<label>
태그의htmlFor
속성과<input>
태그의id
속성을 동일하게 작성하면 두 태그가 연결된다. 그럼<label>
의textContent
콘텐츠 영역을 클릭해도 input 박스를 핸들링 할 수 있다. - Input 태그 숨기기
<input>
태그에hidden
속성을 추가해서 숨겨진 필드로 정의한다. 그럼 화면에서 보이지 않는다. - Label 태그에 원하는 스타일 지정
Input 태그를 숨김 필드로 만들었으므로 "파일찾기" 버튼이 더이상 표시되지 않는다. 이제<label>
태그에 원하는 스타일을 지정해주면 된다.
Tailwind Components를 살펴보면 다른 사용자가 Tailwind CSS로 이미 작성해놓은 다양한 버튼 스타일이 있어서 참고하기 좋다. 버튼과 비슷한 느낌을 줘야하므로 cursor: pointer;
스타일도 추가한다.
// input type=”file” 태그 스타일 수정 예시
// ...생략
const buttonDesign =
'p-2 pl-5 pr-5 bg-transparent border-2 border-blue-500 text-blue-500 text-lg rounded-lg hover:bg-blue-500 hover:text-gray-100 active:translate-y-px';
return (
// 생략
<label
htmlFor="input-file"
className={classNames(buttonDesign, 'cursor-pointer')}
>
{image === '' ? 'UPLOAD IMAGE' : 'CHANAGE IMAGE'}{' '}
{/* <label>의 콘텐츠 영역 */}
<input
hidden // 파일 필드("파일 찾기" 버튼) 숨기기
accept="image/jpg,image/png,image/jpeg" // 모든 이미지 타입을 허용할 땐 image/*
id="input-file"
type="file"
onChange={dataURLHandler}
/>
</label>
);
🔍️ classNames 라이브러리를 사용해서 클래스를 더 쉽게 추가할 수 있다. 특정 변수/상태의 true
false
에 따라 클래스를 추가하거나, 추가하지 않을 수 있다(값이 true
면 추가 / false
면 추가 안함)
import classNames from 'classnames';
const btnClass = classNames(
'p-2 pl-5 pr-5', // 기본 지정 클래스
{ 'bg-gray-100': isDarkMode }, // isDarkMode가 true로 평가되면 'bg-gray-100' 클래스 추가
);
return <button className={btnClass} />;
같은 파일 다시 올리기
파일 선택 → 확인 버튼을 누른 후, Input 태그 onChange
핸들러에서 받은 event.target.value
값엔 선택한 파일에 대한 가짜경로.파일명
문자열이 할당되어 있다.
console.log(event.target.value); // C:\fakepath\Profile_OpenPeeps.png
<img>
태그 src
속성에 할당된 이미지 state를 삭제(빈 문자열로 변경)하는 “삭제 버튼”을 구현했다고 가정해본다. ➊image1.png
업로드 → ➋삭제 → ➌image1.png
업로드를 다시 해보면 아무런 일도 일어나지 않는다. Input의 onChange
이벤트는 데이터(input.value
)가 변경됐을 때만 동작하기 때문이다.
image1.png
를 처음 업로드했을 때 C:\fakepath\image1.png
값이 input.value
에 할당된 상태이므로, 이와 동일한 파일을 업로드해서 onChange
이벤트가 발생하지 않은 것이다.
같은 파일을 다시 올렸을 때도 onChange
이벤트가 발생하도록 하려면, onChange
핸들러에 input.value
(event.target.value
) 값을 비워주는 코드를 추가하면 된다.
const [image, setImage] = useState('');
// input 태그의 onChange 핸들러
const uploadHandler = ({ target }) => {
const file = target.files[0];
const reader = new FileReader();
reader.onload = () => {
setImage(reader.result);
};
reader.readAsDataURL(file);
target.value = ''; // 같은 파일을 다시 올려도 이벤트가 발생할 수 있도록 input.value 값 초기화
};
// 이미지 삭제 버튼 onClick 핸들러
const deleteImageHandler = () => {
setImage('');
};
완성 코드 — Codepen
See the Pen Preview selected image using input type="file" by ColorFilter (@colorfilter) on CodePen.
레퍼런스
- File 和 FileReader
- (HTML&DOM) File API - 이미지 미리보기
- [React] 프로필 사진 업로더 만들기
- input type="file" 커스터마이징 하는 방법
- Blob(블랍) 이해하기
- image Blob 객체를 url로 바꾸어 img 띄우기, javascript, JavaScript, blob, createObjectUrl, revokeObjectUrl, react, vue, window, document
글 수정사항은 노션 페이지에 가장 빠르게 반영됩니다. 링크를 참고해 주세요
'🪄 Programming' 카테고리의 다른 글
[HTML/CSS] CSS 인접하는 엘리먼트들의 border 겹침 문제 해결 (0) | 2024.05.03 |
---|---|
[HTML/CSS] 이메일 템플릿 작성법 (feat. 테이블 코딩) (0) | 2024.05.03 |
[TS] 타입스크립트 - 느낌표 연산자 (0) | 2024.05.02 |
[Algorithm] 자바스크립트 쌍(pairs)을 포함하는 배열에서 유니크 넘버 찾기 (0) | 2024.05.02 |
[Git] git revert, git reset 차이점 및 HEAD 분리 (0) | 2024.05.01 |
댓글
이 글 공유하기
다른 글
-
[HTML/CSS] CSS 인접하는 엘리먼트들의 border 겹침 문제 해결
[HTML/CSS] CSS 인접하는 엘리먼트들의 border 겹침 문제 해결
2024.05.03 -
[HTML/CSS] 이메일 템플릿 작성법 (feat. 테이블 코딩)
[HTML/CSS] 이메일 템플릿 작성법 (feat. 테이블 코딩)
2024.05.03 -
[TS] 타입스크립트 - 느낌표 연산자
[TS] 타입스크립트 - 느낌표 연산자
2024.05.02 -
[Algorithm] 자바스크립트 쌍(pairs)을 포함하는 배열에서 유니크 넘버 찾기
[Algorithm] 자바스크립트 쌍(pairs)을 포함하는 배열에서 유니크 넘버 찾기
2024.05.02