multer 란?
Nodejs와 Express의 인기가 높아짐에 따라 파일 업로드와 관련된 복잡한 작업을 쉽게 처리하기 위한 라이브러리나 미들웨어의 필요성이 증가했습니다.
multipart/form-data는 파일 업로드에 사용되는 복잡한 인코딩 유형 중 하나로 이를 처리하기 위해서는 별도의 라이브러리나 미들웨어가 필요한데 이러한 필요성 때문에 multer가 등장하게 됬습니다.
주요 기능
스토리지 엔진 : multer는 다양한 스토리지 엔진을 제공하여 파일을 어떻게 저장할지 정의할 수 있으며 일반적인 두 가지 스토리지 엔진은 DiskStorage, MemoryStorage가 있습니다. DiskStorage는 파일을 서버 디스크에 저장하며 MemoryStorage는 파일을 메모리에 저자한 후 다른 곳에 전송하기 위해 사용됩니다.
파일 필터링 : 업로드할 파일의 유형이나 크기 등을 기반으로 파일을 필터링 할 수 있습니다.
파일 이름 및 저장 경로 설정 : DiskStorage 엔진을 사용할 때 업로드된 파일의 이름과 저장 경로를 지정할 수 있습니다.
다중 파일 업로드 : multer는 한 번의 요청으로 여러 파일을 동시에 업로드하는 것을 지원합니다.
파일 크기 제한 : 업로드될 파일의 최대 크기를 설정할 수 있습니다. 이를 통해 큰 파일의 업로드를 제한 할 수 있습니다.
데이터 파싱 : multipart/form-data 요청의 데이터를 파싱하며 파일 외의 필드 데이터도 req.body에서 사용할 수 있게 해줍니다.
설치 방법
npm install multer
기본 사용 예제
const express = require('express')
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
const app = express()
app.post('/profile', upload.single('avatar'), function (req, res, next) {
// req.file 은 `avatar` 라는 필드의 파일 정보입니다.
// 텍스트 필드가 있는 경우, req.body가 이를 포함할 것입니다.
})
app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
// req.files 는 `photos` 라는 파일정보를 배열로 가지고 있습니다.
// 텍스트 필드가 있는 경우, req.body가 이를 포함할 것입니다.
})
const cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
app.post('/cool-profile', cpUpload, function (req, res, next) {
// req.files는 (String -> Array) 형태의 객체 입니다.
// 필드명은 객체의 key에, 파일 정보는 배열로 value에 저장됩니다.
//
// e.g.
// req.files['avatar'][0] -> File
// req.files['gallery'] -> Array
//
// 텍스트 필드가 있는 경우, req.body가 이를 포함할 것입니다.
})
multer를 활용한 이미지 업로드
기능들만 구현해놓은 웹페이지에 multer를 이용한 이미지 업로드 작업 진행
프로젝트 구조
사용한 모듈
npm init -y
npm install bcrypt cors dotenv express express-session jsonwebtoken multer mysql2 nodemon sequelize
Backend
config/index.js
데이터베이스 연결 설정
const config = {
dev: {
database: process.env.DATABASE_NAME,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
host: process.env.DATABASE_HOST,
dialect: "mysql",
},
};
module.exports = config;
controllers/loginController.js
로그인 관련 유효성 검사 및 토큰 발급
const { User } = require("../models");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
exports.Login = async (req, res) => {
const { user_id, user_pw } = req.body;
try {
if (!user_id) {
console.log("아이디를 입력하세요.");
return;
}
if (!user_pw) {
console.log("비밀번호를 입력하세요.");
return;
}
const data = await User.findOne({ where: { user_id } });
if (!data?.user_id) {
console.log("없는 아이디입니다.");
return;
}
const compare = bcrypt.compareSync(user_pw, data.user_pw);
if (compare) {
console.log("login success");
const token = jwt.sign(
{
name: user_id,
},
process.env.ACCESS_TOKEN_KEY,
{
expiresIn: "5m",
}
);
req.session.accessToken = token;
res.redirect("http://127.0.0.1:5500/frontEnd/index.html");
} else {
console.log("비밀번호가 틀립니다.");
}
} catch (error) {
console.error(error);
}
};
controllers/mypageController.js
프로필 이미지 미리보기, 프로필 이미지 파일 업데이트 기능
const { User } = require("../models");
const multer = require("multer");
const path = require("path");
exports.Mypage = async (req, res) => {
const { decoded } = req;
try {
const data = await User.findOne({ where: { user_id: decoded.name } });
// console.log(data.dataValues);
res.redirect("http://127.0.0.1:5500/frontEnd/mypage.html");
} catch (error) {
console.error(error);
}
};
exports.userInfo = async (req, res) => {
const { decoded } = req;
try {
const { id, user_id, img } = await User.findOne({
where: { user_id: decoded.name },
});
const userInfo = {
id,
user_id,
img,
};
res.json(userInfo);
} catch (error) {
console.error(error);
}
};
exports.UpdateImg = multer({
storage: multer.diskStorage({
destination: (req, file, done) => {
console.log("----------file -----------");
console.log(file);
console.log("----------file -----------");
done(null, "uploads/");
},
filename: (req, file, done) => {
const ext = path.extname(file.originalname);
console.log("ext");
console.log(ext);
const filename =
path.basename(file.originalname, ext) + "_" + Date.now() + ext;
console.log("filename");
console.log(filename);
done(null, filename);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
exports.UpdateUserImg = async (req, res) => {
const { decoded } = req;
const { file } = req;
try {
// console.log(file.filename);
const img = "/img/" + file.filename;
await User.update({ img }, { where: { user_id: decoded.name } });
} catch (error) {
console.error(error);
}
};
controllers/signupController.js
회원가입 관련 기능
const { User } = require("../models");
const bcrypt = require("bcrypt");
exports.SignUp = async (req, res) => {
const { user_id, user_pw } = req.body;
const img = "/img/redposion.png";
try {
const data = await User.findOne({ where: { user_id } });
if (!data?.user_id) {
const hash = bcrypt.hashSync(user_pw, 10);
await User.create({
user_id,
user_pw: hash,
img,
});
res.redirect("http://127.0.0.1:5500/login.html");
} else {
console.log("이미 가입한 아이디입니다.");
}
} catch (error) {
console.error(error);
}
};
middleware/imgUpload.js
multer 기능
const multer = require("multer");
const path = require("path");
// mutler 함수 안에 매개변수로 객체 형태 인자 전달
// storage 속성을 통해서 업로드된 파일을 어디에 저장시킬지 지정
exports.Upload = multer({
// diskStorage : 서버 컴퓨터 하드디스크에 파일을 업로드
// 객체로 인자값을 전달
storage: multer.diskStorage({
// destination 속성 : 파일이 저장될 폴더를 설정
destination: (req, file, done) => {
// 콜백 함수 done : 두번쨰 인자값으로 폴더의 이름을 설정
// 서버 컴퓨터 폴더명
// 첫번째 매개변수 : 에러처리 부분
// 두번째 매개변수 : 파일이 저장될 폴더 이름
done(null, "uploads/");
},
// filename 속성 : 매개변수 file.originalname 은 클라이언트가 업로드한 파일의 이름을 나타냄
// file.originalname : 사용자가 업로드한 파일 원본명
filename: (req, file, done) => {
// extname 메소드 : 파일의 경로를 매개변수로 받고 파일의 확장자를 추출
const ext = path.extname(file.originalname);
console.log("ext");
console.log(ext);
// 파일을 저장하는데 이름이 같으면 안되기 때문에 파일이름 뒤에 날짜를 붙여줌
// basename 메소드 : 확장자를 추가, 제거 할 수 있음
const filename =
path.basename(file.originalname, ext) + "_" + Date.now() + ext;
// done 첫번째 매개변수 : 에러 처리 부분
// 두번째 매개변수 : 저장할 파일명
done(null, filename);
},
}),
// 파일의 최대 사이즈 설정
// 5MB 설정
limits: { fileSize: 5 * 1024 * 1024 },
});
middleware/loginChk.js
로그인 상태 체크
const { User } = require("../models");
const jwt = require("jsonwebtoken");
exports.isLogin = async (req, res, next) => {
const { accessToken } = req.session;
try {
await jwt.verify(
accessToken,
process.env.ACCESS_TOKEN_KEY,
(err, decoded) => {
if (err) {
console.log("다시 로그인하세요");
} else {
req.decoded = decoded;
next();
}
}
);
} catch (error) {
console.error(error);
}
};
models/index.js
데이터베이스 초기 설정
const config = require("../config");
const Sequelize = require("sequelize");
const seq = new Sequelize(config.dev);
const User = require("./users");
User.init(seq);
const db = {};
db.sequelize = seq;
db.User = User;
module.exports = db;
models/users.js
user 테이블 설정
const { DataTypes, Model } = require("sequelize");
class User extends Model {
static init(seq) {
return super.init(
{
user_id: {
type: DataTypes.STRING(20),
allowNull: false,
},
user_pw: {
type: DataTypes.STRING(64),
allowNull: false,
},
img: {
type: DataTypes.STRING(100),
},
},
{
sequelize: seq,
underscored: false,
modelName: "User",
tableName: "upload_users",
charset: "utf8",
collate: "utf8_general_ci",
}
);
}
}
module.exports = User;
routers/loginRouter.js
login 관련 라우팅 설정
const router = require("express").Router();
const { Login } = require("../controllers/loginController");
router.post("/", Login);
module.exports = router;
routers/mypageRouter.js
mypage 관련 라우팅 설정
const router = require("express").Router();
const { isLogin } = require("../middleware/loginChk");
const {
Mypage,
userInfo,
UpdateImg,
UpdateUserImg,
} = require("../controllers/mypageController");
router.get("/", isLogin, Mypage);
router.get("/userinfo", isLogin, userInfo);
router.post(
"/updateimg",
isLogin,
UpdateImg.single("updateimg"),
UpdateUserImg
);
module.exports = router;
routers/signupRouter.js
회원가입 관련 라우팅 설정
const router = require("express").Router();
const { SignUp } = require("../controllers/signupController");
router.post("/", SignUp);
module.exports = router;
routers/uploadRouter.js
파일 업로드 관련 라우팅 설정
const router = require("express").Router();
const { Upload } = require("../middleware/imgUpload");
// Upload.single 매개변수로 form에서 이미지 파일을 가지고 있는 input의 name을 작성
router.post("/", Upload.single("upload"), (req, res) => {
const { file, body } = req;
console.log(file, body);
res.send("save file");
});
module.exports = router;
app.js
// 이미지 업로드
// 이미지 파일을 서버측 컴퓨터에 폴더 저장
// 파일의 경로를 설정하고 서버측에서 이미지 파일을 가져와서 보여줌
// 사용할 모듈
// express path multer
// multer : 모듈을 사용해서 이미지 업로드. 파일이 저장될 경로나 파일의 확장자 이름들을 설정해서 파일을 저장
const express = require("express");
const session = require("express-session");
const path = require("path");
const multer = require("multer");
const cors = require("cors");
const dot = require("dotenv").config();
const { sequelize } = require("./models");
const uploadRouter = require("./routers/uploadRouter");
const signupRotuer = require("./routers/signupRouter");
const loginRotuer = require("./routers/loginRouter");
const mypageRouter = require("./routers/mypageRouter");
const app = express();
app.use(
cors({
origin: "http://127.0.0.1:5500",
credentials: true,
})
);
sequelize
.sync({
force: false,
})
.then((e) => {
console.log("database connect");
})
.catch((err) => {
console.error(err);
});
app.use(
session({
secret: process.env.SESSION_KEY,
resave: false,
saveUninitialized: false,
})
);
app.use(express.urlencoded({ extended: false }));
app.use("/img", express.static(path.join(__dirname, "uploads")));
// json 형식의 데이터를 전달 받았을때 json 파싱을 해서 자바스크립트 객체로 변환 시켜주는 미들웨어
app.use(express.json());
app.use("/upload", uploadRouter);
app.use("/signup", signupRotuer);
app.use("/login", loginRotuer);
app.use("/mypage", mypageRouter);
app.listen(8080, () => {
console.log("Server On");
});
Frontend
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<input type="text" id="imgs" />
<input type="file" id="file" />
<button id="uploadBtn">img upload</button>
<a href="http://127.0.0.1:5500/frontEnd/login.html">login</a>
<a href="http://127.0.0.1:5500/frontEnd/signup.html">signup</a>
<a href="http://127.0.0.1:8080/mypage">mypage</a>
</body>
<script>
uploadBtn.onclick = () => {
// new Formdata() : formData를 동적으로 생성
const form = new FormData();
// html 상에서 name으로 키값을 전달하던 부분을 append 메소드로 설정
// 첫번째 매개변수 : 키
// 두번째 매개변수 : 값
form.append("imgs", imgs.value);
// file은 버퍼 데이터로 들어 오기 때문에 files로 생성됨
form.append("upload", file.files[0]);
// 파일을 보낼때 파일의 데이터를 폼데이터로 보낸다고 설정
// 헤더의 내용으로 인코딩된 폼 데이터로 전송한다고 설정
axios
.post("http://127.0.0.1:8080/upload", form, {
"Content-Type": "multipart/form-data",
})
.then((e) => {
console.log("upload success");
console.log(e.data);
})
.catch((err) => {
console.error(err);
});
};
</script>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h2>login page</h2>
<form action="http://127.0.0.1:8080/login" method="post">
<label for="">id</label>
<input type="text" name="user_id" />
<label for="">pw</label>
<input type="text" name="user_pw" />
<button>login</button>
</form>
</body>
</html>
mypage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<style>
.profile img {
width: 100px;
height: 100px;
border: 1px solid;
}
</style>
</head>
<body>
<h2>mypage</h2>
<div class="profile">
<img id="profileImg" />
</div>
<div>
<span>user_id : </span>
<span id="userId">user</span>
</div>
<label for="">프로필 이미지 변경</label>
<input type="file" id="file" />
<button id="changeImg">img change</button>
</body>
<script>
// 유저 프로필 사진 업로드
changeImg.onclick = async () => {
const form = new FormData();
form.append("updateimg", file.files[0]);
axios
.post(
"http://127.0.0.1:8080/mypage/updateimg",
form,
{ withCredentials: true },
{
"Content-Type": "multipart/form-data",
}
)
.then((e) => {
console.log("유저 프로필 이미지 수정 완료");
getUserInfo();
})
.catch((err) => {
console.error(err);
});
};
// 유저의 정보를 가져와 mypage에 데이터 입력
async function getUserInfo() {
try {
const { data } = await axios.get(
"http://127.0.0.1:8080/mypage/userinfo",
{
withCredentials: true,
}
);
const img = "http://127.0.0.1:8080" + `${data.img}`;
profileImg.setAttribute("src", img);
userId.innerHTML = data.user_id;
console.log(data);
} catch (error) {
console.error(error);
}
}
getUserInfo();
</script>
</html>
signup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h2>signup page</h2>
<form action="http://127.0.0.1:8080/signup" method="post">
<label for="">id</label>
<input type="text" name="user_id" />
<label for="">pw</label>
<input type="text" name="user_pw" />
<button>signup</button>
</form>
</body>
</html>
실습 순서
1. 회원가입 페이지에서 회원가입 진행
2. 로그인 후 메인페이지에서 이미지 업로드
3. backend uploads 폴더에 이미지 업로드
4. mypage에서 파일 선택 후 img change 버튼 클릭
5. 이미지 파일이 uploads 폴더에 업로드 되고 화면에 보이는 프로필 이미지 변경 확인
'Nodejs' 카테고리의 다른 글
[NodeJs] tesseract를 활용한 OCR (0) | 2024.03.20 |
---|---|
[NodeJs] Cors와 Axios를 활용한 간단한 게시판 만들기 (0) | 2023.09.06 |
[NodeJs] WebSocket을 활용한 좌석예매 하기 (0) | 2023.09.06 |
[NodeJs] WebSocket을 활용한 채팅방 구현 (0) | 2023.08.31 |
[NodeJs] Sequelize 알아보기 (0) | 2023.08.30 |