React에서 multer를 사용한 파일 업로드

✒️ 2025-05-28 09:42 내용 수정



서버 설정

  1. 파일 업로드에 사용할 미들웨어를 설정한다.
    • multer 미들웨어는 파일 업로드에 사용한다.
    • path 미들웨어는 랜덤 UUID를 생성하므로 파일 이름을 식별자로 사용하기 위해 사용되었다.
    • 파일을 서버 디스크에 저장하기 위해 경로를 지정할 pathfs 미들웨어를 사용했다.
      • 파일 시스템 fs를 사용하여 원하는 경로의 폴더를 확인하고, 폴더가 없는 경우엔 새로 생성한다.
    • DB에는 파일의 이름만 저장한다.
  2. RESTful API를 위해 Router를 사용하여 각 API를 설계했다.
// server/api/upload.js
const router = require('express').Router();
const pool = require("../db.js");

// 사진 업로드 위한 패키지 require
const path = require('path');
const multer = require('multer');
const uuid4 = require('uuid4');
const fs = require('fs');
  1. multer 미들웨어의 설정을 진행한다.
    • 사진의 저장 위치를 path로 지정하며, 아래 예시는 현재 폴더를 기준으로 ../../client/public/img/profile이라는 폴더로 지정한 것이다.
    • multer 미들웨어의 파일 이름, 저장 위치, 제한 등의 설정을 추가한다.
// server/api/upload.js

// 프로필 저장 위치
const profileImgPath = path.resolve(__dirname, '..', '..', 'client', 'public', 'img', 'profile');

// 프로필 저장 위치가 없으면 새로 생성
try {
  fs.readdirSync(profileImgPath); // 폴더 조회
} catch (error) {
  console.error('client/public/img/profile 폴더가 없어 새로 만듭니다');
  fs.mkdirSync(profileImgPath); // 폴더 생성
}

// 미들웨어 설정
const upload = multer({
  storage: multer.diskStorage({
    filename(req, file, done) { // 파일이름
      const randomID = uuid4(); // UUID 생성
      const ext = path.extname(file.originalname);
      const filename = randomID + ext; // 파일 이름은 (UUID + 파일이름)로 저장
      done(null, filename);
    },
    destination(req, file, done) { // 프로필 이미지 저장 위치
      done(null, profileImgPath);
    },
  }),
  limits: { fileSize: 1024 * 1024 }, // 용량 제한 1MB
});
  1. 미들웨어 설정이 끝나면 라우트에 post API를 설정한다.
    • 업로드하는 파일이 1개 일 때 upload.single("field_name"), 여러 개 일 때 upload.array("field_name")을 사용한다.
    • 파일이 클라이언트로부터 넘어오면 req.file (1개)또는 req.files(여러개)에 저장된다.
    • 이후 DB connection pool을 통해 파일의 이름을 DB에 저장한다.
// server/api/upload.js

// 파일 업로드
router.post('/upload', upload.single('profile_image'), async (req, res) => {

  const filename = req.file?.filename;
  
  let sql = 'INSERT INTO image_files (profile_image) VALUES (?)';

  //console.log(req.body)
    try {
      let result = await pool.query(sql, [ filename ]);
      res.send(result);
    } catch (error) {
      console.error(error);
      res.send('error');
    }
});

module.exports = router;

multer_react 1.png


클라이언트

  1. 서버와 연결하기 위한 axios 설정을 먼저 진행한 후, api 파일을 만들어 파일을 보낼 함수를 작성한다.
    • Axios 참고.
    • axios 사용 시 쿠키 관련 에러가 발생할 수도 있으므로 CORS(Cross-Origin Resource Sharing)를 참고해서 설정을 미리 해둔다.
    • 파일을 보내기 위해 request 설정에서 header에 "Content-Type": "multipart/form-data"를 추가한다.
export async function upload(formData) {

    const { profile_image } = formData;

     const res = await axios.post('/upload', { profile_image }, 
     { headers: {"Content-Type": "multipart/form-data"} }
     );

    if (res.statusText != "OK") {
        throw new Error("파일등록 실패");
    }

    const body = res.data;
    return body;
}
  1. 이제 React Component에서 파일을 입력받을 input 태그 설정과 데이터 전송을 위한 설정을 진행한다.
    • FormData를 사용하거나, 예시처럼 객체를 사용하여 보낼 수 있다.
    • input 태그#input file 참고.
    • 클라이언트에서는 inputevent.target.files를 사용하여 입력받은 파일들을 확인할 수 있다.
import 'bootstrap/dist/css/bootstrap.min.css';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { upload } from '../api/upload.js';
import { Button, Container } from 'react-bootstrap';
import { Camera, XCircleFill } from 'react-bootstrap-icons';

function UploadImage() {

    const [formData, setFormData] = useState({ profile_image : '' });

    // 이미지 미리보기 URL 담는 useState
    const [uploadImgUrl, setUploadImgUrl] = useState("");

    // 이미지 미리보기 함수
    function onchangeImageUpload(event) {
        const newImage = event.target.files[0]; // event.target.file는 배열
        const fileType = event.target.files[0]?.type; // 파일 타입 확인 가능
        
        if (fileType.includes('image')) {
            // 미리보기 이미지 url
            setUploadImgUrl(URL.createObjectURL(newImage));
            // 이미지 파일
            setFormData((formData)=>({...formData, profile_image : newImage}));
        } else {
            alert("이미지 파일만 업로드할 수 있습니다!");
            return;
        }
    }

    // X 버튼 클릭 시 이미지 파일 삭제
    function handleDeleteImage () {
        setUploadImgUrl('');
        setFormData((formData)=>({...formData, profile_image : ''}));
    }

    // 파일 업로드
    async function check() {
		const res = await upload(formData);
		if (res) {
			navigate("/");
		}
    }

    return (
        <Container>
        <div className="inner text-center">
            <form onSubmit={(e)=>{e.preventDefault()}}>
                <div>
                    <Camera/>
                    <label htmlFor="file"/>
                    <input type="file" name='profile_image' id='file' accept="image/*" onChange={onchangeImageUpload}/>
                </div>
                { // uploadImgUrl이 존재할 때 요소 생성
	                uploadImgUrl &&
	                <div>
	                    <img alt='preview' src={uploadImgUrl}/>
	                    <button type='button' onClick={handleDeleteImage}><XCircleFill/></button>
	                </div>
                }
                <div>
                    <Button variant='primary' type="submit" onClick={()=>{check()}}>확인</Button>
                </div>
            </form>
        </div>
        </Container>
    )
}

export default Join;

multer_react 2.png