개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← Node Auction

세팅과 인증 — Sequelize, Passport, 그리고 done()의 세 얼굴

2026년 6월 10일
expresssequelizepassportbcrypt

경매 사이트를 하나 만들어보기로 했다. Node Auction. 이름부터 그냥 직관적이다.

express, sequelize, passport, bcrypt — 조합은 뻔하다. 근데 뻔한 조합이라고 안 헷갈리는 건 아니더라.

DB부터

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 모델 — money가 왜 있냐면

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가 세션에 넣어준 값을 확인하는 것뿐이다. 근데 이 두 줄이 라우터 전체에 깔린다. 회원가입 페이지는 로그인 안 한 사람만, 상품 등록은 로그인한 사람만. 미들웨어로 빼두니까 라우터 코드가 훨씬 깔끔해졌다.

Passport 로컬 전략 — done()이 세 개나 받는 이유

여기서 좀 헤맸다.

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")로 미들웨어처럼 바로 쓸 수도 있는데, 그럼 실패 메시지를 세밀하게 못 다룬다.

회원가입은 그냥 bcrypt

const hash = await bcrypt.hash(password, 12);
await User.create({ email, nick, password: hash, money });

솔트 라운드 12. 딱히 고민할 게 없었다. 이미 가입된 이메일이면 리다이렉트로 에러 쿼리만 붙여서 돌려보낸다.

여기까지 하니 회원가입 → 로그인 → 로그아웃이 다 돈다. 다음은 진짜 이 프로젝트의 본체, 상품 등록이랑 메인 화면 목록.

다음 글 →상품 등록과 메인 화면 — multer, 그리고 어제부터 지금까지