비디오&이미지 업로더 - 미리보기/시간 제한Frontend/UI&UX2022. 7. 1. 04:30
Table of Contents
What to do?
- 페이지에서 클라이언트가 로컬 컴퓨터로 이미지나 비디오 파일을 업로드하고 서버에 전송하기 전에 미리 사용자가 업로드할 이미지를 보여주고 싶을 경우가 있을 것이다.
- 아래처럼 input 태그에 type을 file로 주고 accept라는 키워드를 사용해서 비디오와 이미지 형식의 파일만 받게 태그를 변경하면 비디오&이미지 업로더를 구현할 수 있다.
<input type="file" accept="video/*, image/*"/>
- 클라이언트로 부터 받은 비니오와 이미지 파일은 state에 저장하고 삭제 버튼을 클릭하면 state를 초기화 시키자.
- 클라이언트가 이미지를 업로드하기 전에는 디폴트 이미지를 보여주고 클라이언트가 비디오나 이미지를 업로드하면 해당 이미지를 보여주도록 하자.
- 그리고 사용자가 업로드하는 비디오의 시간을 제한하는 기능도 추가해보자!
- 예를들어 서버의 자원을 많이 차지하지 않기 위해 백엔드에서 16초 미만의 비디오만 요청할 수 있음!
App.js
import Uploader from "./components/Uploader";
const App = () => {
return (
<div className="App">
<Uploader/>
</div>
);
}
export default App;
FileLeader와 readAsDataURL을 사용하는 방법
components/Uploader.js
- file state에 fileObject와 priview_URL, type 세개의 상태 값을 줬는데 fileObject은 서버에 보낼 실제 이미지 파일 객체이고 preview_URL은 이미지 파일을 readAsDataURL로 읽어서(base64로 인코딩한 string 데이터) img 태그와 video 태그 src에 넣어서 클라이언트에게 미리 보여줄 이미지의 경로이다. type은 이미지와 비디오는 각각 img와 video 태그를 사용하기 때문에 type에 따라서 다른 미리보기를 보여주기 위한 값이다.
- readAsDataURL은 비동기로 실행된다!
- file을 비동기로 읽기 위한 FileReader 객체를 생성하고, onload 이벤트를 달아준다. 이후 readAsDataURL(file)을 통해 onload를 트리거 시킨다.
- onload 함수는 FileReader가 이미지를 잘 인코딩하고 난 후의 결과를 처리하는 함수이다!
- 하나 주의할 점이 있는데 만약 이미지를 업로드하고 삭제버튼을 클릭하지 않은 후에 업로드 버튼을 클릭하고 취소 버튼을 클릭하면 오류가 발생하는데 이를 방지하기 위해서 if(e.target.files[0])으로 조건문을 걸어두었다!
- onload 함수 안에서 file의 타입이 video이고 길이가 16초보다 길다면 상태를 업데이트하지 않고 alert를 띄우는 부분을 추가했다
- setInterval로 비동기 처리를 꼭 해야한다!
- 주석을 꼭 잘보길 바람!
- 기존의 <input> 태그는 이쁘지 않기 때문에 display: none으로 가려두고 mui의 버튼에 ref를 사용해서 연결해두었다.
import {useState} from 'react';
import "./uploader.scss";
import {Button} from "@mui/material";
const Uploader = () => {
const [file, setFile] = useState({
fileObject: "",
preview_URL: "img/default_image.png",
type: "image"
});
let inputRef;
const saveImage = (e) => {
e.preventDefault();
// 미리보기 url 만들기
const fileReader = new FileReader();
// 파일이 존재하면 file 읽기
if (e.target.files[0]) {
fileReader.readAsDataURL(e.target.files[0])
}
// 읽기 동작이 성공적으로 완료되었을 때
fileReader.onload = () => {
const fileType = e.target.files[0].type.split("/")[0]
// video일 때 시간 제한 16초
if (fileType === "video") {
let videoElement = document.createElement("video");
videoElement.src = fileReader.result
/*
video 길이 제한!
videoElement의 readyState가 4면 비디오가 로딩이 된 것이므로 길이를 판별할 수 있다
video가 재생할 수 있는 상태로 만드는 과정이 비동기적으로 실행되기 때문에
setInterval로 비디오가 로딩된 상태가 될 때까지 계속 확인하면서 기다려준다
*/
const timer = setInterval(() => {
if (videoElement.readyState == 4) {
if (videoElement.duration > 16) {
alert("동영상의 길이가 16초보다 길면 안됩니다")
} else {
setFile(
{
fileObject: e.target.files[0],
preview_URL: fileReader.result,
type: fileType
}
)
}
clearInterval(timer);
}
}, 500);
} else { // image일 땐 시간제한이 없으므로 그냥 상태에 넣어줌
setFile(
{
fileObject: e.target.files[0],
preview_URL: fileReader.result,
type: fileType
}
)
}
}
}
// 상태 초기화하기
const deleteImage = () => {
setFile({
fileObject: "",
preview_URL: "img/default_image.png",
type: "image"
});
}
return (
<div className="uploader-wrapper">
<input
type="file" accept="video/*, image/*"
onChange={saveImage}
// 클릭할 때 마다 file input의 value를 초기화 하지 않으면 버그가 발생할 수 있다
// 사진 등록을 두개 띄우고 첫번째에 사진을 올리고 지우고 두번째에 같은 사진을 올리면 그 값이 남아있음!
onClick={(e) => e.target.value = null}
ref={refParam => inputRef = refParam}
style={{display: "none"}}
/>
<div className="file-wrapper">
{file.type === "image" ?
<img src={file.preview_URL}/> :
<video controls={true} autoPlay={true} src={file.preview_URL}/>
}
</div>
<div className="upload-button">
<Button variant="contained" onClick={() => inputRef.click()}>
Preview
</Button>
<Button variant="contained" color="error" onClick={deleteImage}>
Delete
</Button>
</div>
</div>
);
}
export default Uploader;
createObjectURL과 revokeObjectURL을 사용하는 방법 (추천)
components/Uploader.js
- readAsDataURL과 다르게 동기적으로 실행되고 주어진 객체를 가리키는 URL을 DOMString으로 반환한다. 창을 닫을 때 까지 유지되며, 그 전에 해제하기 위해서는 메모리 누수 방지를 위해 앵간하면 revokeObjectURL()을 호출해야한다.
- FileLeader와 달리 시간이 필요하지 않고 revoke만 잘해준다면 속도가 많이 빠르다
import {useState} from 'react';
import "./uploader.scss";
import {Button} from "@mui/material";
const Uploader = () => {
const [file, setFile] = useState({
fileObject: "",
preview_URL: "img/default_image.png",
type: "image"
});
let inputRef;
const saveImage = (e) => {
e.preventDefault();
// 미리보기 url 만들기
// 파일이 존재하면 file 읽기
if (e.target.files[0]) {
// 새로운 파일 올리면 createObjectURL()을 통해 생성한 기존 URL을 폐기
URL.revokeObjectURL(file.preview_URL);
// 새로운 미리보기 URL 생성
const preview_URL = URL.createObjectURL(e.target.files[0]);
const fileType = e.target.files[0].type.split("/")[0];
// video일 때 시간 제한 15초
if (fileType === "video") {
let videoElement = document.createElement("video");
videoElement.src = preview_URL;
/*
video 길이 제한!
videoElement의 readyState가 4면 비디오가 로딩이 된 것이므로 길이를 판별할 수 있다
video가 재생할 수 있는 상태로 만드는 과정이 비동기적으로 실행되기 때문에
setInterval로 비디오가 로딩된 상태가 될 때까지 계속 확인하면서 기다려준다
*/
const timer = setInterval(() => {
if (videoElement.readyState == 4) {
if (videoElement.duration > 16) {
alert("동영상의 길이가 16초보다 길면 안됩니다");
// src에 넣지 않을 것이므로 미리보기 URL 제거
URL.revokeObjectURL(preview_URL);
} else {
setFile(
{
fileObject: e.target.files[0],
preview_URL: preview_URL,
type: fileType
}
)
}
clearInterval(timer);
}
}, 500);
} else { // image일 땐 시간제한이 없으므로 그냥 상태에 넣어줌
setFile(
{
fileObject: e.target.files[0],
preview_URL: preview_URL,
type: fileType
}
)
}
}
}
// 상태 초기화하기
const deleteImage = () => {
// createObjectURL()을 통해 생성한 기존 URL을 폐기
URL.revokeObjectURL(file.preview_URL);
setFile({
fileObject: "",
preview_URL: "img/default_image.png",
type: "image"
});
}
return (
<div className="uploader-wrapper">
<input
type="file" accept="video/*, image/*"
onChange={saveImage}
// 클릭할 때 마다 file input의 value를 초기화 하지 않으면 버그가 발생할 수 있다
// 사진 등록을 두개 띄우고 첫번째에 사진을 올리고 지우고 두번째에 같은 사진을 올리면 그 값이 남아있음!
onClick={(e) => e.target.value = null}
ref={refParam => inputRef = refParam}
style={{display: "none"}}
/>
<div className="file-wrapper">
{file.type === "image" ?
<img src={file.preview_URL}/> :
<video controls={true} autoPlay={true} src={file.preview_URL}/>
}
</div>
<div className="upload-button">
<Button variant="contained" onClick={() => inputRef.click()}>
Preview
</Button>
<Button variant="contained" color="error" onClick={deleteImage}>
Delete
</Button>
</div>
</div>
);
}
export default Uploader;
components/uploader.scss
.uploader-wrapper{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.file-wrapper{
margin: 50px 0 20px 0;
img, video{
width: 300px;
height: 300px;
object-fit: contain;
}
}
.upload-button{
button{
margin: 0 5px;
}
}
}
'Frontend > UI&UX' 카테고리의 다른 글
snap-scroll & scroll된 동영상 재생 (youtube shorts) (0) | 2022.07.07 |
---|---|
이미지&비디오 여러개 업로드 - 미리보기/시간제한/삭제 (0) | 2022.07.05 |
react-dnd → Drag & Drop (1) | 2022.06.30 |
tailwind-css (0) | 2022.06.11 |
무한 스크롤 - infinite scroll (0) | 2022.06.11 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!