이번에는 파일 업로드와 관련된 부분에 대해 구현하도록 하자.
파일 업로드 기능 자체는 다른 부분에서도 필요할 수 있기 때문에 컴포넌트를 재사용할 수 있도록 Utils 폴더에 구현해둔다.
components/utils/FileUpload.js 생성
import React from "react";
function FileUpload(props) {
return (
<div>FileUpload</div>
);
}
export default FileUpload;
drop zone은 react에서 제공하는 react-dropzone 라이브러리를 사용해서 구현한다.
client 디렉토리에서 다음 명령어를 수행해주자.
npm install react-dropzone --save
파일 업로드 컴포넌트의 UI를 만들어준다. drop-zone 공식 라이브러리의 홈페이지에서 제공해주는 예제를 참고해서 만들어보았다.
components/utils/FileUpload.js 수정
import React from "react";
import Dropzone from "react-dropzone";
import { Icon } from "antd";
function FileUpload() {
return (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Dropzone onDrop={(acceptedFiles) => console.log(acceptedFiles)}>
{({ getRootProps, getInputProps }) => (
<section>
<div
style={{
width: 300,
height: 240,
border: "1px solid lightgray",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
{...getRootProps()}
>
<input {...getInputProps()} />
<Icon type="plus" style={{ fontSize: "3rem" }} />
</div>
</section>
)}
</Dropzone>
</div>
);
}
export default FileUpload;
우선 파일을 올리는 기능이 어떻게 작동하는지에 대해 알아보자.
유저가 파일을 올리면 올린 파일을 백엔드에서 저장해준 뒤, 저장해준 파일의 정보를 프론트에 띄워주기 위해 다시 프론트단으로 가져온다.
더 쉽게 정리하자면 아래의 순서로 진행된다.
1. 프론트엔드에서 백엔드로 파일을 전달해준다.
2. 백엔드에서 파일을 저장한다.
3. 백엔드에서 저장된 파일의 정보를 프론트로 전달해준다.
components/utils/FileUpload.js 수정
function FileUpload(props) {
// ...
const [Images, setImages] = useState([]);
const dropHandler = (files) => {
let formData = new FormData();
const config = {
header: { "content-type": "multipart/form-data" },
};
formData.append("file", files[0]);
axios.post("/api/product/image", formData, config).then((response) => {
if (response.data.success) {
console.log(response.data);
setImages([...Images, response.data.filePath]);
} else {
alert("파일을 저장하는데 실패했습니다.");
}
});
};
return (
<div>
<Dropzone onDrop={dropHandler}></Dropzone>
</div>
);
export default FileUpload;
Dropzone의 onDrop 이벤트가 일어났을 때 dropHandler 함수를 수행해준다.
해당 기능을 구현하기 위해 프론트에서 백엔드로 파일을 전해줄 때에는 axios를 사용해서 전달해준다. 이 때 formData와 config를 함께 보내주도록 한다. formData 안에는 올리는 파일의 정보가 append를 이용해 들어가고, header에 어떤 파일인지에 대한 content-type을 정의해주어서 백엔드에서 해당 request를 받을 때 별 다른 에러 없이 받을 수 있도록 처리해준다.
백엔드에서 drop 이벤트를 수행하기 위해서 product 관련 api를 생성해준다.
express.js 에서 제공하는 router를 사용해서 request를 나누어주기 위해 아래의 내용을 추가해준다.
server/index.js 수정
app.use("/api/product", require("./routes/product"));
멀티로 이미지를 업로드하기 위해서는 multer 미들웨어가 필요하기 때문에 해당 디펜던시를 다운받아준다.
해당 명령어는 root에서 실행해준다.
npm install multer --save
이후에 multer 공식 홈페이지에서 제공하는 내용을 바탕으로 routes 관련 product.js 작성해준다.
server/routes/product.js 생성
const express = require("express");
const router = express.Router();
const multer = require("multer");
//=================================
// Product
//=================================
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/");
},
filename: function (req, file, cb) {
cb(null, `${Date.now()}_${file.originalname}`);
},
});
const upload = multer({ storage: storage }).single("file");
router.post("/image", (req, res) => {
//가져온 이미지를 저장 해주면 된다.
upload(req, res, (err) => {
if (err) {
return req.json({ success: false, err });
}
return res.json({ success: true, filePath: res.req.file.path, fileName: res.req.file.filename });
});
});
module.exports = router;
server/routes/product.js 수정
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/");
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(null, `${Date.now()}_${file.originalname}`);
},
});
const upload = multer({ storage: storage });
요기에서 destination은 어디에 파일이 저장되는지에 대한 정보이고, filename은 uploads 폴더에 파일을 저장할 때 어떤 이름으로 저장할지에 대한 내용이다.
우리는 날짜와 랜덤 숫자를 기존의 파일 이름 앞에 붙여주는 처리를 통해 중복을 피해주자. 파일을 저장한 뒤 저장한 정보(어디에 저장했는지, 파일 이름은 무엇으로 저장했는지 등)를 front에 전달해준다.
delete는 쉽게 파일이 저장되어있는 배열의 index를 지워주는 방식으로 구현한다.
해당 index를 splice 해주기 위해서는 어떤 이미지가 클릭되고 있는지에 대한 파악이 중요하니 그 부분을 유념하면서 기능을 구현해보자.
components/utils/FileUpload.js 수정
function FileUpload() {
const deleteHandler = (image) => {
const currentIndex = Images.indexOf(image);
let newImages = [...Images];
newImages.splice(currentIndex, 1);
setImages(newImages);
};
}
return (
<div>
<div style={{ display: "flex", width: "350px", height: "240px", overflowX: "scroll" }}>
{Images.map((image, index) => (
<div onClick={() => deleteHandler(image)} key={index}>
<img
style={{ minWidth: "300px", width: "300px", height: "240px" }}
src={`http://localhost:5050/${image}`}
/>
</div>
))}
</div>
</div>
);
Image에서 map을 통해서 배열에 들어있는 이미지들이 쭉 나열될 수 있도록 한다.
deleteHandler 함수를 생성해서 div의 onClick 이벤트가 일어났을 시에 함수가 실행되도록 한다. 해당 함수가 실행되면 클릭된 이미지 인덱스를 파악해서 currentIndex 변수에 저장해주고 해당 변수 인덱스를 splice 해주므로써 해당 이미지가 제외된 새 배열을 setImages를 통해 넣어준다.
UploadProductPage에 컴포넌트가 하나 있고, UploadProductPage 안에 FileUpload라는 컴포넌트가 하나 들어있다.
업로드 페이지에서 확인 버튼을 누를 시 모든 정보들을 Server로 보내준 뒤 DB에 저장해야한다.
Image 정보는 FileUpload 컴포넌트에 있고, UploadProduct 페이지에는 없기 때문에 FileUpload 페이지에 있는 이미지 정보를 UploadProductPage에 먼저 전달을 해야한다. 이렇게 전달해준 이미지 정보를 확인 버튼을 누를 시에 같이 전달될 수 있도록 해준다.
components/views/UploadProductPage/UploadProductPage.js 수정
<FileUpload refreshFunction={updateImages} />
refreshFunction이라는 props를 전달해준다.
components/utils/FileUpload.js 수정
function FileUpload(props) {
const dropHandler = (files) => {
axios.post("/api/product/image", formData, config).then((response) => {
if (response.data.success) {
props.refreshFunction(setImages([...Images, response.data.filePath]));
} else {
alert("파일을 저장하는데 실패했습니다.");
}
});
};
const deleteHandler = (image) => {
props.refreshFunction(setImages(newImages));
};
}
props로 받아온 refreshFunction을 활용해준다.
따라하며 배우는 노드, 리액트 시리즈 - 쇼핑몰 사이트 만들기 를 공부하며 작성한 글입니다.