이미지 업로드, 미리보기Frontend/UI&UX2021. 11. 19. 13:01
Table of Contents
What to do?
- 페이지에서 클라이언트가 로컬 컴퓨터로 이미지를 업로드하고 서버에 전송하기 전에 미리 사용자가 업로드할 이미지를 보여주고 싶을 경우가 있을 것이다.
- 아래처럼 input 태그에 type을 file로 주고 accept라는 키워드를 사용해서 이미지 형식의 파일만 받게 태그를 변경하면 이미지 업로더를 구현할 수 있다.
<input type="file" accept="image/*"/>
- 클라이언트로 부터 받은 이미지 파일은 state에 저장하고 삭제 버튼을 클릭하면 state를 초기화 시키자.
- 클라이언트가 이미지를 업로드하기 전에는 디폴트 이미지를 보여주고 클라이언트가 이미지를 업로드하면 해당 이미지를 보여주도록 하자.
- 클라이언트가 업로드 버튼을 클릭하면 서버에 이미지가 등록되고 이를 postman에서 미리 구현한 api로 확인할 수 있다!
- 여기를 클릭해서 DB에 이미지를 등록하고 가져오는 API를 확인해보자!
소스코드
App.js
import Uploader from "./components/Uploader";
const App = () => {
return (
<div className="App">
<Uploader/>
</div>
);
}
export default App;
FileLeader와 readAsDataURL을 사용하는 방법
components/Uploader.js
- image state에 image_file과 priview_URL 두개의 상태 값을 줬는데 image_file은 서버에 보낼 실제 이미지 파일이고 preview_URL은 이미지 파일을 readAsDataURL로 읽어서(base64로 인코딩한 string 데이터) img 태그에 src에 넣어서 클라이언트에게 미리 보여줄 이미지의 경로이다!
- readAsDataURL은 비동기로 실행된다!
- file을 비동기로 읽기 위한 FileReader 객체를 생성하고, onload 이벤트를 달아준다. 이후 readAsDataURL(file)을 통해 onload를 트리거 시킨다.
- onload 함수는 FileReader가 이미지를 잘 인코딩하고 난 후의 결과를 처리하는 함수이다!
- 하나 주의할 점이 있는데 만약 이미지를 업로드하고 삭제버튼을 클릭하지 않은 후에 업로드 버튼을 클릭하고 취소 버튼을 클릭하면 오류가 발생하는데 이를 방지하기 위해서 if(e.target.files[0])으로 조건문을 걸어두었다!
- 이미지와 같은 파일을 서버에 업로드할 때는 FormData를 사용해야 한다. append 메소드를 이용해서 key, value를 보낼 수 있는데 key에는 'file'을 value에는 실제 이미지 파일을 넣고 axios를 이용해서 api 호출을 하면 서버에 이미지가 등록된다!
- 기존의 <input> 태그는 이쁘지 않기 때문에 display: none으로 가려두고 ant design의 버튼에 ref를 사용해서 연결해두었다.
import { useState } from 'react';
import "./uploader.scss";
import { Button } from "@mui/material";
import axios from 'axios';
const Uploader = () => {
const [image, setImage] = useState({
image_file: "",
preview_URL: "img/default_image.png",
});
let inputRef;
const saveImage = (e) => {
e.preventDefault();
const fileReader = new FileReader();
if(e.target.files[0]){
fileReader.readAsDataURL(e.target.files[0])
}
fileReader.onload = () => {
setImage(
{
image_file: e.target.files[0],
preview_URL: fileReader.result
}
)
}
}
const deleteImage = () => {
setImage({
image_file: "",
preview_URL: "img/default_image.png",
});
}
const sendImageToServer = async () => {
if(image.image_file){
const formData = new FormData()
formData.append('file', image.image_file);
await axios.post('/api/image/upload', formData);
alert("서버에 등록이 완료되었습니다!");
setImage({
image_file: "",
preview_URL: "img/default_image.png",
});
}
else{
alert("사진을 등록하세요!")
}
}
return (
<div className="uploader-wrapper">
<input type="file" accept="image/*"
onChange={saveImage}
// 클릭할 때 마다 file input의 value를 초기화 하지 않으면 버그가 발생할 수 있다
// 사진 등록을 두개 띄우고 첫번째에 사진을 올리고 지우고 두번째에 같은 사진을 올리면 그 값이 남아있음!
onClick={(e)=>e.target.value = null}
ref={refParam => inputRef = refParam}
style={{ display: "none" }}
/>
<div className="img-wrapper">
<img src={image.preview_URL} />
</div>
<div className="upload-button">
<Button type="primary" variant="contained" onClick={() => inputRef.click()}>
Preview
</Button>
<Button color="error" variant="contained" onClick={deleteImage}>
Delete
</Button>
<Button color="success" variant="contained" onClick={sendImageToServer}>
Upload
</Button>
</div>
</div>
);
}
export default Uploader;
createObjectURL과 revokeObjectURL을 사용하는 방법 (추천)
- readAsDataURL과 다르게 동기적으로 실행되고 주어진 객체를 가리키는 URL을 DOMString으로 반환한다. 창을 닫을 때 까지 유지되며, 그 전에 해제하기 위해서는 메모리 누수 방지를 위해 앵간하면 revokeObjectURL()을 호출해야한다.
- FileLeader와 달리 시간이 필요하지 않고 revoke만 잘해준다면 속도가 많이 빠르다
import {useEffect, useState} from 'react';
import "./uploader.scss";
import {Button} from "@mui/material";
import axios from 'axios';
const Uploader = () => {
const [image, setImage] = useState({
image_file: "",
preview_URL: "img/default_image.png",
});
let inputRef;
const saveImage = (e) => {
e.preventDefault();
if(e.target.files[0]){
// 새로운 이미지를 올리면 createObjectURL()을 통해 생성한 기존 URL을 폐기
URL.revokeObjectURL(image.preview_URL);
const preview_URL = URL.createObjectURL(e.target.files[0]);
setImage(() => (
{
image_file: e.target.files[0],
preview_URL: preview_URL
}
))
}
}
const deleteImage = () => {
// createObjectURL()을 통해 생성한 기존 URL을 폐기
URL.revokeObjectURL(image.preview_URL);
setImage({
image_file: "",
preview_URL: "img/default_image.png",
});
}
useEffect(()=> {
// 컴포넌트가 언마운트되면 createObjectURL()을 통해 생성한 기존 URL을 폐기
return () => {
URL.revokeObjectURL(image.preview_URL)
}
}, [])
const sendImageToServer = async () => {
if (image.image_file) {
const formData = new FormData()
formData.append('file', image.image_file);
await axios.post('/api/image/upload', formData);
alert("서버에 등록이 완료되었습니다!");
setImage({
image_file: "",
preview_URL: "img/default_image.png",
});
} else {
alert("사진을 등록하세요!")
}
}
return (
<div className="uploader-wrapper">
<input type="file" accept="image/*"
onChange={saveImage}
// 클릭할 때 마다 file input의 value를 초기화 하지 않으면 버그가 발생할 수 있다
// 사진 등록을 두개 띄우고 첫번째에 사진을 올리고 지우고 두번째에 같은 사진을 올리면 그 값이 남아있음!
onClick={(e) => e.target.value = null}
ref={refParam => inputRef = refParam}
style={{display: "none"}}
/>
<div className="img-wrapper">
<img src={image.preview_URL}/>
</div>
<div className="upload-button">
<Button type="primary" variant="contained" onClick={() => inputRef.click()}>
Preview
</Button>
<Button color="error" variant="contained" onClick={deleteImage}>
Delete
</Button>
<Button color="success" variant="contained" onClick={sendImageToServer}>
Upload
</Button>
</div>
</div>
);
}
export default Uploader;
components/uploader.scss
.uploader-wrapper{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.img-wrapper{
margin: 50px 0 20px 0;
img{
width: 200px;
height: 200px;
}
}
.upload-button{
button{
margin: 0 5px;
}
}
}
'Frontend > UI&UX' 카테고리의 다른 글
tailwind-css (0) | 2022.06.11 |
---|---|
무한 스크롤 - infinite scroll (0) | 2022.06.11 |
react-toastify (0) | 2021.09.29 |
Swiper 만들기 (0) | 2021.08.30 |
window.scrollTo() + ref (0) | 2021.08.18 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!