본문 바로가기
개발/Node JS

Node JS - multer로 이미지 업로드하기 (Upload Images with multer)

by 피로물든딸기 2024. 1. 22.
반응형

Node JS 전체 링크

 

참고 

- FormData와 multer로 여러 파일 업로드하기

- 깃허브 API로 이미지 업로드하기

- 캡처한 이미지 여러 개 업로드하기

 

프론트엔드(리액트)에서 이미지를 업로드한 후, 서버(Node JS)에 저장해 보자.

 

먼저 링크를 참고하여 ReactImageList.js를 아래와 같이 수정하자.

불필요한 내용은 지우고 서버에 업로드만 하는 메서드인 serverUpload만 구현한다.

import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";

// upload button
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

const VisuallyHiddenInput = styled("input")({
  clip: "rect(0 0 0 0)",
  clipPath: "inset(50%)",
  height: 1,
  overflow: "hidden",
  position: "absolute",
  bottom: 0,
  left: 0,
  whiteSpace: "nowrap",
  width: 1,
});

const serverUpload = (image) => {
  
}

const ReactImageList = () => {
  const handleFileUpload = (e) => {
    const selectedImages = e.target.files;
  
    const uploadImageWithDelay = async () => {
      
      serverUpload(selectedImages[0]);
      for (let i = 1; i < selectedImages.length; i++) {
        await new Promise((resolve) => {
          setTimeout(() => {
            serverUpload(selectedImages[i]);
            resolve();
          }, 1000); // 1초 delay
        });
      }
    };
  
    uploadImageWithDelay();
  };
  
  return (
    <Box sx={{ m: 3 }}>
      <Button
        component="label"
        variant="contained"
        startIcon={<CloudUploadIcon />}
        onChange={handleFileUpload}
      >
        Upload file
        <VisuallyHiddenInput type="file" accept="image/*" multiple />
      </Button>
    </Box>
  );
};

export default ReactImageList;

 

Node JS에서 router(/multer)를 구현하였다면, serverUpload는 아래와 같이 구현할 수 있다.

image는 handleUpload에서 넘겨받으므로, formData를 만들어서 post로 전송하면 된다.

const serverUpload = (image) => {
  const formData = new FormData();
  formData.append("image", image);
  let server = `http://192.168.55.120:3002`;
  axios
    .post(`${server}/multer`, formData)
    .then((res) => console.log(res.data))
    .catch((error) => console.log(error));
};

 

multer에서 "D:\github\node-server\images" 경로에 저장하기 위해 path의 join 함수를 사용하였다.

const storage = multer.diskStorage({
  destination: (req, file, callback) => {
    callback(null, path.join("D:", "github", "node-server", "images")); // 업로드 파일의 저장 위치를 설정
  },
  filename: (req, file, callback) => {
    callback(null, `${file.originalname}`); // 파일이 저장될 때 이름 설정
  },
});

 

파일 크기 제한을 1G로 두고 multer를 만든다.

const limits = {
  files: 50,
  fileSize: 1024 * 1024 * 1024, // 1G
};

const upload = multer({ storage, limits });

 

post에서 upload.single("image")를 이용하면 이미지를 업로드할 수 있다.

router.post("/", upload.single("image"), (req, res) => {

 

전체 코드는 다음과 같다.

 

Node JS

 

server.js

const express = require("express");
const app = express();
const cors = require("cors");
app.use(cors());

const multer = require("./routes/multer");
app.use("/multer", multer);

app.listen(3002, () => {
  console.log("Node.js Server is running on port 3002...");
});

 

multer.js

const express = require("express");
const router = express.Router();
const multer = require("multer");
const path = require("path");

const storage = multer.diskStorage({
  destination: (req, file, callback) => {
    callback(null, path.join("D:", "github", "node-server", "images")); // 업로드 파일의 저장 위치를 설정
  },
  filename: (req, file, callback) => {
    callback(null, `${file.originalname}`); // 파일이 저장될 때 이름 설정
  },
});

const limits = {
  files: 50,
  fileSize: 1024 * 1024 * 1024, // 1G
};

const upload = multer({ storage, limits });

router.post("/", upload.single("image"), (req, res) => {
  res.json({ result: true });
  return;
});

module.exports = router;

 

리액트

 

ReactImageList.js

import React from "react";
import Box from "@mui/material/Box";

// upload button
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

import axios from "axios";

const VisuallyHiddenInput = styled("input")({
  clip: "rect(0 0 0 0)",
  clipPath: "inset(50%)",
  height: 1,
  overflow: "hidden",
  position: "absolute",
  bottom: 0,
  left: 0,
  whiteSpace: "nowrap",
  width: 1,
});

const serverUpload = (image) => {
  const formData = new FormData();
  formData.append("image", image);
  let server = `http://192.168.55.120:3002`;
  axios
    .post(`${server}/multer`, formData)
    .then((res) => console.log(res.data))
    .catch((error) => console.log(error));
};

const ReactImageList = () => {
  const handleFileUpload = (e) => {
    const selectedImages = e.target.files;

    const uploadImageWithDelay = async () => {
      serverUpload(selectedImages[0]);
      for (let i = 1; i < selectedImages.length; i++) {
        await new Promise((resolve) => {
          setTimeout(() => {
            serverUpload(selectedImages[i]);
            resolve();
          }, 1000); // 1초 delay
        });
      }
    };

    uploadImageWithDelay();
    e.target.value = ""; // 같은 파일 upload를 위한 처리
  };

  return (
    <Box sx={{ m: 3 }}>
      <Button
        component="label"
        variant="contained"
        startIcon={<CloudUploadIcon />}
        onChange={handleFileUpload}
      >
        Upload file
        <VisuallyHiddenInput type="file" accept="image/*" multiple />
      </Button>
    </Box>
  );
};

export default ReactImageList;

캡처한 이미지 업로드

 

이제 캡처한 이미지를 Node 서버에 업로드해 보자.

 

캡처한 이미지 여러 개 업로드하기를 참고하여 Capature.js를 수정하자.

handleUpload에 multer 코드를 추가하면 된다.

import React, { useState } from "react";

import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

import axios from "axios";

const Capture = () => {
  const [imageDataUrls, setImageDataUrls] = useState([]);

  const handleImagePaste = (event) => {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;

    for (let i = 0; i < items.length; i++) {
      if (items[i].type.indexOf("image") !== -1) {
        const blob = items[i].getAsFile();
        const reader = new FileReader();

        reader.onloadend = () => {
          const base64encoded = reader.result.split(",")[1];

          // 이미지 데이터 URL을 배열에 추가
          setImageDataUrls((prevUrls) => [...prevUrls, base64encoded]);
        };

        reader.readAsDataURL(blob);
      }
    }
  };

  const handleUpload = async () => {
    if (imageDataUrls.length === 0) {
      console.error("Please paste an image first.");
      return;
    }
    
    for (let index = 0; index < imageDataUrls.length; index++) {
      // axios multer 사용

      await new Promise((resolve) => setTimeout(resolve, 1000)); // 1초 딜레이
    }
  };

  return (
    <Box sx={{ m: 3 }}>
      <Button
        sx={{ m: 2 }}
        component="label"
        variant="contained"
        startIcon={<CloudUploadIcon />}
        onClick={handleUpload}
      >
        GitHub Upload
      </Button>
      <div
        onPaste={handleImagePaste}
        style={{
          border: "1px solid #ddd",
          padding: "20px",
          textAlign: "center",
        }}
      >
        <h1>이미지 붙여넣기</h1>
        {imageDataUrls.map((dataUrl, index) => (
          <img
            key={index}
            src={`data:image/png;base64,${dataUrl}`}            
          />
        ))}
      </div>
    </Box>
  );
};

export default Capture;

 

업로드를 하기 위해서는 encode된 이미지가 아니라 blob이 필요하다.

blobsuseState로 추가하고 handleImagePaste에서 추가한다.

  const [imageDataUrls, setImageDataUrls] = useState([]);
  const [blobs, setBlobs] = useState([]);

  const handleImagePaste = (event) => {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;

    for (let i = 0; i < items.length; i++) {
      if (items[i].type.indexOf("image") !== -1) {
        const blob = items[i].getAsFile();
        const reader = new FileReader();

        reader.onloadend = () => {
          const base64encoded = reader.result.split(",")[1];

          // 이미지 데이터 URL을 배열에 추가
          setImageDataUrls((prevUrls) => [...prevUrls, base64encoded]);

          // 이미지 데이터 URL을 배열에 추가
          setBlobs((prevUrls) => [...prevUrls, blob]);          
        };

        reader.readAsDataURL(blob);
      }
    }
  };

 

저장된 blobs를 handleUpload에서 formData로 전송하면 된다.

  const handleUpload = async () => {
    if (blobs.length === 0) {
      console.error("Please paste an image first.");
      return;
    }
    
    for (let index = 0; index < blobs.length; index++) {
      const image = blobs[index];
      const fileName = `capture_image_${index}.jpg`;
      
      try {
        const formData = new FormData();
        formData.append("image", image);
        formData.append("fileName", fileName);

        let server = `http://192.168.55.120:3002`;
        let result = await axios.post(`${server}/multer`, formData);
        
        console.log(result);
        console.log(`Image ${index + 1} uploaded successfully.`);
      } catch (error) {
        console.error(`Error uploading image ${index + 1}:`, error);
      }

      await new Promise((resolve) => setTimeout(resolve, 1000)); // 1초 딜레이
    }
  };

 

위와 같은 방식으로 하면 blob에 정해진 대로 파일 이름이 image.png로 고정된다.

따라서 multer.js에서 파일 이름에 저장한 시간을 추가하여 변경한다.

const storage = multer.diskStorage({
  destination: (req, file, callback) => {
    callback(null, path.join("D:", "github", "node-server", "images")); // 업로드 파일의 저장 위치를 설정
  },
  filename: (req, file, callback) => {
    const originalname = file.originalname;
    const extension = path.extname(originalname);
    const basename = path.basename(originalname, extension);
    
    callback(null, `${basename}_${Date.now()}${extension}`); // 파일이 저장될 때 이름 설정
  },
});

 

전체 코드는 다음과 같다.

 

Node JS - multer.js

const express = require("express");
const router = express.Router();
const multer = require("multer");
const path = require("path");

const storage = multer.diskStorage({
  destination: (req, file, callback) => {
    callback(null, path.join("D:", "github", "node-server", "images")); // 업로드 파일의 저장 위치를 설정
  },
  filename: (req, file, callback) => {
    const originalname = file.originalname;
    const extension = path.extname(originalname);
    const basename = path.basename(originalname, extension);

    callback(null, `${basename}_${Date.now()}${extension}`); // 파일이 저장될 때 이름 설정
  },
});

const limits = {
  files: 50,
  fileSize: 1024 * 1024 * 1024, // 1G
};

const upload = multer({ storage, limits });

router.post("/", (req, res) => {
  upload.single("image")(req, res, (err) => {
    if (err) {
      console.error("Multer error:", err);
      return res.status(500).json({ error: "Multer error" });
    }

    console.log("File uploaded successfully!");
    res.json({ result: true });
  });
});

module.exports = router;

 

리액트 - Capture.js

import React, { useState } from "react";

import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

import axios from "axios";

const Capture = () => {
  const [imageDataUrls, setImageDataUrls] = useState([]);
  const [blobs, setBlobs] = useState([]);

  const handleImagePaste = (event) => {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;

    for (let i = 0; i < items.length; i++) {
      if (items[i].type.indexOf("image") !== -1) {
        const blob = items[i].getAsFile();
        const reader = new FileReader();

        reader.onloadend = () => {
          const base64encoded = reader.result.split(",")[1];

          // 이미지 데이터 URL을 배열에 추가
          setImageDataUrls((prevUrls) => [...prevUrls, base64encoded]);

          // 이미지 데이터 URL을 배열에 추가
          setBlobs((prevUrls) => [...prevUrls, blob]);          
        };

        reader.readAsDataURL(blob);
      }
    }
  };

  const handleUpload = async () => {
    if (blobs.length === 0) {
      console.error("Please paste an image first.");
      return;
    }
    
    for (let index = 0; index < blobs.length; index++) {
      const image = blobs[index];
      const fileName = `capture_image_${index}.jpg`;
      
      try {
        const formData = new FormData();
        formData.append("image", image);
        formData.append("fileName", fileName);

        let server = `http://192.168.55.120:3002`;
        let result = await axios.post(`${server}/multer`, formData);

        console.log(result);
        console.log(`Image ${index + 1} uploaded successfully.`);
      } catch (error) {
        console.error(`Error uploading image ${index + 1}:`, error);
      }

      await new Promise((resolve) => setTimeout(resolve, 1000)); // 1초 딜레이
    }
  };

  return (
    <Box sx={{ m: 3 }}>
      <Button
        sx={{ m: 2 }}
        component="label"
        variant="contained"
        startIcon={<CloudUploadIcon />}
        onClick={handleUpload}
      >
        GitHub Upload
      </Button>
      <div
        onPaste={handleImagePaste}
        style={{
          border: "1px solid #ddd",
          padding: "20px",
          textAlign: "center",
        }}
      >
        <h1>이미지 붙여넣기</h1>
        {imageDataUrls.map((dataUrl, index) => (
          <img
            key={index}
            src={`data:image/png;base64,${dataUrl}`}            
          />
        ))}
      </div>
    </Box>
  );
};

export default Capture;
반응형

댓글