React에서 multer를 사용한 파일 업로드
✒️ 2025-05-28 09:42 내용 수정
- 팀 프로젝트 중 이미지 파일을 업로드하는 기능을 추가할 필요가 있었다.
- 팀원의 원본 코드를 보고 1개의 이미지 파일만 업로드하는 것으로 수정하였다.
- 서버는 Node와 express.js로, 클라이언트는 React를 사용하였다.
- 중간의 몇몇 Component는 React-bootstrap의 Component다.
서버 설정
- 파일 업로드에 사용할 미들웨어를 설정한다.
multer미들웨어는 파일 업로드에 사용한다.- multer 참고.
path미들웨어는 랜덤 UUID를 생성하므로 파일 이름을 식별자로 사용하기 위해 사용되었다.- 파일을 서버 디스크에 저장하기 위해 경로를 지정할
path와fs미들웨어를 사용했다.- 파일 시스템
fs를 사용하여 원하는 경로의 폴더를 확인하고, 폴더가 없는 경우엔 새로 생성한다.
- 파일 시스템
- DB에는 파일의 이름만 저장한다.
- 아래의
pool은 DB connection pool이다.
- 아래의
- 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');
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
});
- 미들웨어 설정이 끝나면 라우트에
postAPI를 설정한다.- 업로드하는 파일이 1개 일 때
upload.single("field_name"), 여러 개 일 때upload.array("field_name")을 사용한다. - 파일이 클라이언트로부터 넘어오면
req.file(1개)또는req.files(여러개)에 저장된다. - 이후 DB connection pool을 통해 파일의 이름을 DB에 저장한다.
- 업로드하는 파일이 1개 일 때
// 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;
- 서버 api 파일에서
console.log(req.file)(1개) 또는console.log(req.files)(여러개)로 터미널에 로그를 찍어보면 파일의 정보를 확인할 수 있다.- 테스트를 위해 다른 코드를 사용해서 fieldname이 'files'로 나왔는데, 예시 코드대로 한다면 'profile_name'이 나온다.
클라이언트
- 서버와 연결하기 위한
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;
}
- 이제 React Component에서 파일을 입력받을
input태그 설정과 데이터 전송을 위한 설정을 진행한다.FormData를 사용하거나, 예시처럼 객체를 사용하여 보낼 수 있다.- input 태그#input file 참고.
- 클라이언트에서는
input의event.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;
- input의
event.target.file을 적용하여console.log를 찍어보면 파일에 대한 정보가 나오므로 이를 응용해서 사용할 수도 있다.