경매 사이트를 하나 만들어보기로 했다. Node Auction. 이름부터 그냥 직관적이다.
express, sequelize, passport, bcrypt — 조합은 뻔하다. 근데 뻔한 조합이라고 안 헷갈리는 건 아니더라.
models/index.js에서 폴더 안의 모든 모델 파일을 자동으로 읽어서 등록하게 짜여있다.
fs.readdirSync(__dirname)
.filter((file) => file.slice(-3) === ".js" && file.indexOf(".test.js") === -1)
.forEach((file) => {
const model = require(path.join(__dirname, file));
db[model.name] = model;
db[model.name].initiate(sequelize);
});
모델 파일 하나 추가하면 자동으로 db에 등록되고, 그 다음에 associate를 한 번 더 돌려서 관계를 맺는다. 파일만 던져놓으면 알아서 조립되는 구조라 처음엔 "어 이게 왜 되지" 싶었다.
User.init({
email: { type: Sequelize.STRING(40), unique: true },
nick: { type: Sequelize.STRING(15) },
password: { type: Sequelize.STRING(100) },
money: { type: Sequelize.INTEGER, defaultValue: 0 },
});
경매 사이트니까 낙찰되면 돈이 깎여야 한다. money 필드 하나가 이 프로젝트 전체의 존재 이유다. (뒤에서 스케줄러가 이 값을 깎는다. 지금은 그냥 필드만 만들어둔다.)
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) next();
else res.status(404).send("로그인 필요");
};
exports.isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) next();
else res.status(404).send("로그인한 상태입니다.");
};
별거 없다. req.isAuthenticated()는 Passport가 세션에 넣어준 값을 확인하는 것뿐이다. 근데 이 두 줄이 라우터 전체에 깔린다. 회원가입 페이지는 로그인 안 한 사람만, 상품 등록은 로그인한 사람만. 미들웨어로 빼두니까 라우터 코드가 훨씬 깔끔해졌다.
여기서 좀 헤맸다.
async (email, password, done) => {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser);
} else {
done(null, false, { message: "비밀번호가 일치하지 않습니다." });
}
} else {
done(null, false, { message: "가입되지 않은 회원입니다." });
}
};
done(err, user, info). 첫 번째는 진짜 에러(DB 터짐 같은 거), 두 번째는 성공한 유저, 세 번째는 "실패했지만 에러는 아닌" 경우의 메시지. 처음엔 로그인 실패했다고 done(error)부터 던졌더니 500 에러 페이지가 떴다. 아니, 비밀번호 틀린 게 서버 에러는 아니잖아. done(null, false, { message: "..." })로 고쳐야 컨트롤러 쪽 info.message로 정상적으로 넘어온다.
passport.authenticate("local", (authError, user, info) => {
if (authError) return next(authError);
if (!user) return res.redirect(`/?error=${info.message}`);
req.login(user, (loginError) => {
if (loginError) return next(loginError);
res.redirect("/");
});
})(req, res, next);
passport.authenticate가 함수를 반환하고, 그 함수를 다시 (req, res, next)로 호출한다. 처음 봤을 때는 "왜 함수가 함수를 리턴해" 싶었는데, 콜백 안에서 로그인 성공/실패에 따라 분기하려면 이 방식이 맞다. 커스텀 콜백을 안 쓰면 그냥 passport.authenticate("local")로 미들웨어처럼 바로 쓸 수도 있는데, 그럼 실패 메시지를 세밀하게 못 다룬다.
const hash = await bcrypt.hash(password, 12);
await User.create({ email, nick, password: hash, money });
솔트 라운드 12. 딱히 고민할 게 없었다. 이미 가입된 이메일이면 리다이렉트로 에러 쿼리만 붙여서 돌려보낸다.
여기까지 하니 회원가입 → 로그인 → 로그아웃이 다 돈다. 다음은 진짜 이 프로젝트의 본체, 상품 등록이랑 메인 화면 목록.