목적
아래 프로필 수정 페이지에서 프로필 사진을 수정하려 한다.
가입할 때 부여되는 기본 이미지 파일은 app/public/img 폴더 내에 존재하기 때문에 파일을 넣어주지 않아도 되지만, 유저가 임의의 이미지로 변경하고자 할 때는 유저가 업로드한 이미지를 로컬이나 서버에 저장해주어야 한다.
서버에 이미지 파일을 저장하려면 s3를 이용하는게 일반적인데, 이번 차수에서는 s3는 접근하지 않고 로컬에 파일을 생성하는 것이 목표.
필요 라이브러리(munetic_express)
multer
$ npm install --save multer
$ npm install --save @types/multer
파일을 디스크에 저장하기 위한 storage 생성
//imgCreateMiddleware.ts
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '../munetic_app/public/img');
},
filename: function (req, file, cb) {
const ext = path.extname(file.originalname);
cb(null, path.basename(file.originalname, ext) + '-' + Date.now() + ext);
},
});
storage 를 생성하였다면 이 storage 를 사용하고 파일 형식 및 크기 제한 등을 설정해 줄 multer 세팅을 해준다.
//imgCreateMiddleware.ts
const storageConfig = multer({
storage: storage,
limits: { fileSize: 1000000 },
fileFilter: function (req, file, callback) {
const ext = path.extname(file.originalname);
if (
ext !== '.png' &&
ext !== '.jpg' &&
ext !== '.jpeg' &&
ext !== '.gif' &&
ext !== '.svg'
) {
return callback(new Error('이미지 형식이 잘못됐습니다.'));
}
callback(null, true);
},
});
multer 세팅이 완료되었으면 api에 미들웨어와 컨트롤러 연결해준다.
axios.post('/image', storageConfig.single('img'), UserAPI.createProfileImg);
컨트롤러에서는 req.file로 해당 파일의 정보를 받아올 수 있다. 우리는 생성된 파일명만 필요하므로 req.file.filename 만 res.data 에 넣어주면 BE는 끝!
// user.controller.ts
export const createProfileImg: RequestHandler = async (req, res, next) => {
try {
if (req.user) {
let result: ResJSON;
result = new ResJSON(
'프로필 사진 교체를 성공하였습니다.',
req.file?.filename,
);
res.status(Status.OK).json(result);
} else {
next(new ErrorResponse(Status.UNAUTHORIZED, '로그인이 필요합니다.'));
}
} catch (err) {
next(err);
}
};
파일 업로드는 input 태그의 type을 ‘file’로 지정해주면 못생긴 업로드 요소가 생긴다.
못생겼으니 display: none
해서 안보이게 하고 useRef 를 이용해 기억해주고, label 태그로 업로드 버튼을 만들어줘서 onClick 이벤트를 통해 useRef 로 연결된 input 태그에 click 이벤트를 만들어준다.
// EditProfile.tsx
<div className="imgWrapper">
<img className="img" src={imageValue} alt="" />
<label
className="imgLabel"
onClick={() => (inputRef.current ? inputRef.current.click() : '')}
>
<AddPhotoAlternateOutlinedIcon />
</label>
<input
type="file"
onChange={onChangeProfileImg}
ref={inputRef}
style={{ display: 'none' }}
/>
</div>
유저가 업로드를 하면?!
유저가 업로드를 하면 input 태그의 값이 바뀌어 onChange 이벤트가 발동된다.
업로드 된 파일 객체는 e.target.files 에 배열 형태로 저장되고 단일 파일이므로 e.target.files[0] 해서 가져온다.
FormData
는 ajax로 form 태그를 대신해서 form 전송을 할 수 있게 해주는 객체이고 Key-Value 구조로 데이터를 전송하게 한다.
따라서 formData.append(’img’, e.target.files[0]) 를 해주고 req.body로 보내면, BE의 multer 가 single 파일을 가져올 때 지정해준 ‘img’ 키 값을 통해 파일을 가져온다.
api 요청 후 받은 res 내에 파일명이 있으므로 ‘/img/’를 앞에 붙여주어 유저 image_url 에 저장해주면 끝!
// EditProfile.tsx
const onChangeProfileImg = async (e: any) => {
try {
const formData = new FormData();
formData.append('img', e.target.files[0]);
const res = await ProfileAPI.createProfileImg(formData);
const newImageUrl = `/img/${res.data.data}`;
setImageValue(newImageUrl);
} catch (e) {
console.log(e, '프로필 사진 변경에 실패하였습니다.');
}
};