Multer는 파일 업로드를 위해 사용되는 multipart/form-data
를 다루기 위한 node.js 의 미들웨어 입니다. 효율성을 최대화 하기 위해 busboy 를 기반으로 하고 있습니다.
npm i multer
storage
디스크 스토리지 엔진은 파일을 디스크에 저장하기 위한 모든 제어 기능을 제공합니다.
limits
파일의 크기 제한에 사용됩니다.
const multer = require("multer");
const path = require("path");
const upload = multer({
storage: multer.diskStorage({
destination(req, file, done) {
done(null, "uploads");
},
filename(req, file, done) {
// file.originalname 파일 이름 (이재훈.png)
const ext = path.extname(file.originalname); //확장자 추출(.png)
const basename = path.basename(file.originalname); // 이름 추출 (이재훈)
done(null, basename + new Date().getTime() + ext); // (이재훈213123123.png)
},
}),
limits: { fieldSize: 20 * 1024 * 1024 }, // 20MB
});
destination
과 filename
의 두가지 옵션이 가능합니다. 두 옵션 모두 파일을 어디에 저장할 지를 정하는 함수입니다.
destination
옵션은 어느 폴더안에 업로드 한 파일을 저장할 지를 결정합니다. 이는 string
형태로 주어질 수 있습니다 (예. '/tmp/uploads'
). 만일 destination
옵션이 주어지지 않으면, 운영체제 시스템에서 임시 파일을 저장하는 기본 디렉토리를 사용합니다.
filename
은 폴더안에 저장되는 파일 명을 결정하는데 사용됩니다. 만일 filename
이 주어지지 않는다면, 각각의 파일은 파일 확장자를 제외한 랜덤한 이름으로 지어질 것입니다.
router.post("/images", isLoggedIn, upload.array("image"), postImages);
req.files 는 image
라는 파일정보를 배열로 가지고 있습니다.
exports.postImages = (req, res, next) => {
return res.json(req.files.map(v => v.filename));
};
프론트로 파일명을 보내줍니다.
Uploads폴더에 이미지를 넣었으니 프론트에서 해당 파일에 접근할 수 있게 해줘야합니다.
app.use("/", express.static(path.join(__dirname, "uploads")));
.none()
오직 텍스트 필드만 허용합니다. 만일 파일이 업로드 되었을 경우, "LIMIT_UNEXPECTED_FILE" 와 같은 에러 코드가 발생할 것입니다. 이는 upload.fields([])
와 같은 동작을 합니다.
router.post("/", isLoggedIn, upload.none(), createPost);
exports.createPost = async (req, res, next) => {
try {
const { content, image } = req.body;
const { id: UserId } = req.user;
const post = await Post.create({
content,
UserId,
});
if (image) {
if (Array.isArray(image)) {
// 이미지를 여러개 올리면 image: ["이.png", "재.png"]
const dbImages = await Promise.all(
image.map(imagePath => Image.create({ src: imagePath }))
);
await post.addImages(dbImages);
} else {
// 이미지를 하나만 올리면 image: "이.png"
const dbImage = await Image.create({ src: image });
await post.addImages(dbImage);
}
}
...
프론트로부터 Post의 content와 image경로를 받고 Post를 create해줍니다.
만약 image가 있고, image가 배열이라면 Promise.all을 통해 Image를 만들어주고 addImages를 통해 post와 image를 연결해줍니다.
추가적으로 사용자가 content에 #과 같은 해쉬태그를 넣었을 떄 HashTag또한 만들어야 합니다.
exports.createPost = async (req, res, next) => {
try {
const { content, image } = req.body;
const { id: UserId } = req.user;
const hashtags = content.match(/#[^\s#]+/g);
const post = await Post.create({
content,
UserId,
});
if (hashtags) {
const result = await Promise.all(
hashtags.map(hashtag =>
Hashtag.findOrCreate({
where: { name: hashtag.slice(1).toLowerCase() },
})
)
); // [[노드,true],[리액트,true]]
await post.addHashtags(result.map(v => v[0]));
}
...
먼저 hashtags가 있는지 찾습니다. match메서드의 인자로 정규표현식을 쓰면 해당 정규표현식에 해당하는 것만 배열로 만들어 줍니다.
배열로 만든 hashtags를 findOrCreate메서드를 사용하여 db에 저장해줍니다.
findOrCreate
findOrCreate 메서드는 쿼리 옵션을 충족하는 항목을 찾을 수 없는 경우 테이블에 항목을 생성합니다
반환값이 배열인데 첫번째 인덱스
에는 해당 인스턴스를
, 두번째 인덱스
로는 해당 인스턴스가 이미 존재했는지 여부를 Boolean
으로 표시해줍니다.
마지막으로는 addHashtag메서드를 사용하여 post와 hashtag를 연결해줍니다.
FromData
란 ajax로 폼 전송을 가능하게 해주는 FormData 객체입니다.
페이지 전환 없이 폼 데이터를 제출 하고 싶을 때 바로 FormData 객체를 사용합니다.
append() 메소드로 key-value 값을 하나씩 추가해주면 됩니다.
만든 FromData
를 dispatch
해줍니다.
const PostForm = () => {
//...
const onChangeImages = useCallback(e => {
console.log("images", e.target.files);
const imageFormData = new FormData();
[].forEach.call(e.target.files, f => {
imageFormData.append("image", f);
});
dispatch({
type: UPLOAD_IMAGES_REQUEST,
data: imageFormData,
});
});
return (
<Form
style={{ margin: "10px 0 20px" }}
encType="multipart/form-data"
onFinish={onSubmitBtn}
>
{/* ... */
<div>
<input
type="file"
name="image"
multiple
hidden
ref={imageInput}
onChange={onChangeImages}
/>
{/* ... */
</Form>
);
};
export default PostForm;
saga
를 통해 formData
백엔드로 보내고 백엔드에서 받은 파일명을 담아 다시 dispatch
해줍니다
function uploadImagesAPI(data) {
return axios.post(`/post/images`, data);
}
function* uploadImages(action) {
try {
const result = yield call(uploadImagesAPI, action.data);
yield put({
type: UPLOAD_IMAGES_SUCCESS,
data: result.data,
});
} catch (err) {
console.error(err);
yield put({
type: UPLOAD_IMAGES_FAILURE,
error: err.response.data,
});
}
}
받은 파일명을 state
에 넣어줍니다.
case UPLOAD_IMAGES_SUCCESS: {
draft.imagePaths = action.data;
draft.uploadImagesLoading = false;
draft.uploadImagesDone = true;
break;
}
현재 text가 비어 있으면 경고창이뜨게 해줍니다.
FormData
로 현재 state에 있는 imagePaths
를 append
해주고 content
또한 append
해줍니다.
그렇게 완성된 formData
를 action data에 넣어 dispatch
해줍니다.
const onSubmitBtn = useCallback(() => {
if (!text || !text.trim()) {
return alert("게시글을 작성하세요.");
}
const formData = new FormData();
imagePaths.forEach(image => {
formData.append("image", image);
});
formData.append("content", text);
dispatch({
type: ADD_POST_REQUEST,
data: formData,
});
}, [text, imagePaths]);