html에서 이미지를 업로드하기 위해서는 input 태그의 type="file"을 사용하면 된다.
그리고 addEventListener로 이벤트 변경이 일어나는걸 감지해서 img 태그의 profileImage.src에 해당 파일을 삽입한다.
<form id="mypageForm" class="mypage-form">
<div class="form-image">
<img src="/images/profile.jpg" alt="프로필 이미지" id="profileImage" name="profileImage">
<input type="file" id="input-file" accept="image/*">
<label for="input-file">수정</label>
</div>
</form>
<script>
let profileImage = document.getElementById('profileImage');
let inputFile = document.getElementById('input-file');
inputFile.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
profileImage.src = URL.createObjectURL(file);
}
})
</script>
그럼 이미지 파일을 선택하고 업로드가 된다. 그런데 이 파일을 서버에 POST 요청을 하면 문제가 생기고 DB에 저장할 수도 없다! 파일을 무작정 body에 담아서 POST 요청을 보내면 서버는 파일을 가져오지 못한다.
일반적으로 클라이언트에서 서버로 폼 데이터(form data)가 전송될 때 해당 데이터는 인코딩되어 전송된다. 이때 파일을 전송하려면 form 태그의 enctype 속성을 "multipart/form-data"로 바꿔줘야 한다. enctype 속성은 입력된 데이터가 서버에 전송될 때 인코딩되는 방식을 결정한다.
디폴트는 application/x-www-form-urlencoded인데, 이 경우 모든 문자를 인코딩하여 전송하게 되며, 위에서 언급한 multipart/form-data는 모든 문자를 인코딩하지 않고 전송한다.
mutler가 바로 이 multipart/form-data를 다루기 위한 node.js의 미들웨어다.
<form id="mypageForm" class="login-form" enctype="multipart/form-data">
multer 모듈 설치
`npm i multer'
라우터에서 multer 미들웨어를 추가해 파일을 public/uploads에 저장
const express = require('express')
const router = express.Router()
const multer = require('multer')
const path = require('path')
const fs = require('fs')
// 업로드 스토리지 설정 (public/uploads)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadPath = path.join(__dirname, '..', 'public', 'uploads')
fs.mkdir(uploadPath, { recursive: true }, (err) => cb(err, uploadPath))
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase()
const unique = (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex'))
cb(null, `${unique}${ext}`)
},
})
// 이미지 파일만 허용
const fileFilter = (req, file, cb) => {
if (file.mimetype && file.mimetype.startsWith('image/')) return cb(null, true)
cb(new Error('이미지 파일만 업로드할 수 있습니다'))
}
const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 } })
// POST /mypage
const postMypage = async (req, res) => {
try {
const { nickname, password } = req.body
const username = res.locals.user.username
const fieldsToUpdate = {}
// 파일 업로드가 있는 경우 public 상대 경로로 저장
if (req.file) {
const relativePath = path.posix.join('/uploads', req.file.filename)
fieldsToUpdate.profileImage = relativePath
}
}
}
inputFile.value = ''처리를 해준다.이미지를 제거하면 그 결과를 서버에게 알려줘야한다. 그런데 직접적으로 알릴 방법이 없다. 그래서 삭제 버튼을 누르면 변경되는 값(0,1)을 통해 서버가 감지해서 기존 파일을 삭제하고 DB에서 삭제가 이뤄지도록 구현했다.
removeProfileImage를 추가해, 삭제 버튼 클릭 시 서버로 값이 1로 전송되도록 한다. removeProfileImage === '1'이면 기존 파일을 삭제하고(fs.unlink), DB의 profileImage를 null로 업데이트한다.<div class="form-image-buttons">
<label for="input-file">수정</label>
<label id="delete-image">삭제</label>
</div>
<input type="hidden" id="removeProfileImage" name="removeProfileImage" value="0">
<script>
let profileImage = document.getElementById('profileImage');
let inputFile = document.getElementById('input-file');
inputFile.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
profileImage.src = URL.createObjectURL(file);
// 파일을 새로 선택하면 removeProfileImage를 0으로 되돌린다.
document.getElementById('removeProfileImage').value = '0'
}
})
let deleteImage = document.getElementById('delete-image');
deleteImage.addEventListener('click', function(e) {
profileImage.src = '/images/profile.jpg';
inputFile.value = ''
document.getElementById('removeProfileImage').value = '1'
})
</script>
// POST /mypage
const postMypage = async (req, res) => {
try {
const { nickname, password, removeProfileImage } = req.body
const username = res.locals.user.username
const fieldsToUpdate = {}
// 삭제 플래그가 있으면 이미지 제거
if (removeProfileImage === '1') {
// 기존 파일 삭제 시도
const current = await findUserByUsername(username)
if (current && current.profileImage) {
const absolute = path.join(__dirname, '..', 'public', current.profileImage)
fs.unlink(absolute, () => {})
}
fieldsToUpdate.profileImage = null
}
// 새 파일 업로드가 있는 경우 설정 (삭제 플래그보다 우선)
if (req.file) {
const relativePath = path.posix.join('/uploads', req.file.filename)
fieldsToUpdate.profileImage = relativePath
}
}
}
