V-logue

[Node.js] Nodemailer로 이메일 인증 구현 본문

발자취/Node.js

[Node.js] Nodemailer로 이메일 인증 구현

보그 2022. 8. 2. 23:50

항해99의 프로젝트 마지막 주차를 앞두고,

이메일 인증기능을 만들게 됐다.

 

우리 런데브(renDev)는 유저의 이메일을 아이디로 사용하고 ,

유저와 유저간 소통이 이메일을 통해도 이루어지기 때문에, 유저가 실제 자신이 사용하는 email을

회원가입시 인증을 진행하고 가입하는게 무척 건설적이라는 나의 의견으로

내가 밀어붙여 만들기로 했다.

 

전부터 생각 했던건데 이번 프로젝트를 진행하면서 꼭 필요한 기능이라고 생각했다.

실제 유저의 이메일을 기반으로 한다면 좀 더 확실한 서비스를 제공할 수 있다는 생각이 지배적이었기 때문에

반드시 반드시 필요했고, 유저 피드백때도 등장했던 내용이다.

 

일단 Node에서 메일을 보내기 위해서는 npm의 패키지인 nodemailer가 필요하다   

			npm install nodemailer

nodemailer를 설치했다면, config파일에 mail.js를 생성하고 다음과 같이 입력한다.

const nodemailer = require('nodemailer')

const sendMail = nodemailer.createTransport({
  pool: true, 
  maxConnections: 1,
  service: 'naver', // 메일을 보낼 서비스 site
  host: 'smtp.naver.com', // site의 host값
  port: 465, // site의 내 계정의 port값 , 값이 다를 수도 있기 때문에 꼭 확인해야 한다.
  auth: {
    user: process.env.NODEMAILER_USER, // mail을 보낼 service 사이트의 ID
	pass: process.env.NODEMAILER_PASS // mail을 보낼 service 사이트의 Password
  },
})

module.exports = sendMail

위와 같이 입력했다면, 이제 controller에 sendMail 모듈을 불러와서 사용해야 한다.

const sendMail = require('../../../config/mail');

exports.checkUserId = async (req, res) => {
  try {
    var { userId } = await postUserIdSchema.validateAsync(req.body);
  } catch (err) {
    if (err) {
      console.log(err);
    }
    return res.status(400).send({ errorMessage: "이메일 형식에 맞지 않습니다." });
  }

  try {
    const user = await User.findOne({ where: { userId } });
    if (user) {
      return res.status(400).send({ errorMessage: "중복된 아이디 입니다." });
    } else {
      var emailNum = Math.random().toString(36).slice(-5)
      sendMail
          .sendMail({
            from: `renDev <${process.env.NODEMAILER_USER}>`, // 메일을 보낸 사람으로 표시되는 값
            to: userId, // 메일을 받는 사람의 email 값
            subject: 'renDev 인증번호가 도착했습니다.', // 메일 제목
            text: `사람과 미지의 조우 renDev입니다.`, // 메일 내용
            html: `
            <div style="text-align: center;">
            <img src=사용하고 싶은 image url주소 입력>
              <h1 style="color: #ECE0F8"></h1>
              <br />
              <h2>이메일 인증번호는 ${emailNum} 입니다. // 임의의 랜덤 인증번호 5자리
              </h2>
            </div>
          `, // html 값을 사용함으로 메일 내용을 꾸밀 수 있다.
          })
      };
      const sql3 = `SELECT MAX(code) FROM email WHERE userId='${userId}'`
      const existUser = await sequelize.query(sql3, { type: sequelize.QueryTypes.SELECT})
      console.log(existUser)
      if(existUser){
      const sql2 = `DELETE FROM email WHERE userId='${userId}'`
      await sequelize.query(sql2, { type: sequelize.QueryTypes.DELETE })
      const sql = `INSERT INTO email (userId, code) VALUES ('${userId}', '${emailNum}');`;
      await sequelize.query(sql, { type: sequelize.QueryTypes.INSERT })}
      else {
      const sql = `INSERT INTO email (userId, code) VALUES ('${userId}', '${emailNum}');`;
      await sequelize.query(sql, { type: sequelize.QueryTypes.INSERT })
      }
      return res.status(200).send({ message: "사용 가능한 아이디 입니다. 메일이 발송 되었습니다." });
  } catch (err) {
    if (err) {
      console.log(err);
      return res.status(400).send({ errorMessage: "다시 한 번 시도해 주세요" });
    }
  }
};

일단 이메일인 아이디를 중복확인 하면서 메일을 보내게 되는데, 이건 버튼식으로 실행된다.

중복확인 버튼을 누르면 body값으로 들어온 userId로 이메일이 보내지게 된다.

메일의 형식은 sendMail({ })안에 들어간 형식대로 전달되게 되며, 메일을 보내면서 몇가지 상황을 고려하게 됐다.

 

일단 처음 생겼던 문제는, email 테이블을 조회했을 때 테이블에 아무런 데이터가 존재하지 않는다면

mySQL syntax error가 발생하기 때문에 이를 처리해주기 위해서 테이블에 select문을 돌릴때 값에 MAX( )라는

옵션을 추가했다.

 

그리고 만약에 email 테이블에 data가 존재한다면 userId값을 조건으로 email 테이블에 존재하는

기존의 발송된 code값을 전부 지워버리고 마지막에 발송된 code를 사용하도록 만들었다.

이는 유저가 중복확인을 여러번 할 경우 인증번호가 여러번 발송되고, userId값으로 조회된 행이

여러개 존재하기 때문에 인증번호 일치여부를 확인할 수 없어서 마지막으로 발송된 인증번호를

사용하기 위함이다.

 

한편, email 테이블에 data가 존재하지 않는다면 insert문으로 바로 인증번호를 발송하고 code를

테이블에 집어넣도록 만들었다.

그리고 이제 발송된 인증번호를 비교해서 인증번호가 일치하는지 확인해야 한다.

exports.checkEmailNum = async (req, res) => {
  try{
    const { code, userId } = req.body

    if(code === "" || code === undefined || code === null || !code){
      return res.status(400).send({ errorMessage: "인증번호를 입력해 주세요"})
    }
    const sql = `SELECT * FROM email where userId='${userId}'`
    const query = await sequelize.query(sql, { type: QueryTypes.SELECT })
    const querys = query.map(data => data.code);
    const checkEmail = querys.reduce(function (acc, cur) {
      return acc.concat(cur);
    });
    
    if(code === checkEmail){
      return res.status(200).send({ message: "인증번호가 일치합니다."});
    } else {
      return res.status(400).send({ errorMessage: "인증번호가 일치하지 않습니다." })
    }
  }catch(err){
    console.log(err)
    return res.status(400).send({ errorMessage: "인증번호가 일치하지 않습니다."})
  }
}

body값으로 들어온 code를 비교해 code가 일치한다면, 인증번호가 일치한다는 메세지를 내보내고

그렇지 않다면 일치하지 않는다는 메세지를 내보냄으로서 인증번호를 확인한다.

만약 인증번호가 입력되지 않았을 경우 인증번호를 입력하도록 예외처리를 했다.

 

이제 마지막으로, 회원가입을 보면

exports.signUp = async (req, res) => {
  try {
    var { password, passwordCheck, policy } = await postUsersSchema.validateAsync(req.body);
  } catch (err) {
    return res.status(400).send({ errorMessage: "작성 형식을 확인해주세요" });
  }
  var { userId, nickname, code } = req.body;
  
  .....

  const sql = `SELECT * FROM email WHERE userId='${userId}'`;
  const sql2 = `DELETE FROM email WHERE userId='${userId}'`
  
  try {
    if (password === passwordCheck) {
      const [bcryptPw, idExist, nickExist, query] = await Promise.all([
        (password = bcrypt.hashSync(password, saltRounds)),
        User.findOne({ where: { userId } }),
        User.findOne({ where: { nickname } }),
        sequelize.query(sql, { type: QueryTypes.SELECT })
      ]);

      const checkEmail = query.map(data => data.code).toString()

      if (code  === undefined || code === null || !code) {
        return res.status(400).send({ errorMessage: "인증번호를 입력해 주세요" })
      }

      if (code !== checkEmail) {
        return res.status(400).send({ errorMessage: "인증번호를 확인해 주세요"})
      }

      if (!idExist && !nickExist) {
        const users =
        await User.create({ userId, nickname, password: bcryptPw, policy, profileImage, refreshToken });
        await sequelize.query(sql2 ,{type:sequelize.QueryTypes.DELETE});
        res.status(200).send({ users: users, message: "회원가입을 축하합니다." });
      }
      
 ......
 
 ...

마지막으로 회원가입을 진행할 때도 만약 유저가 인증번호를 공란이나 틀리게 입력하고 가입버튼을 누른다면,

에러 메세지를 띄우고, 인증번호가 올바른 절차를 거쳐 입력된 상태라면

회원가입이 실행되고, email 테이블에 남아있는 인증코드를 지워버리게 된다.

 

이로서 이메일 인증이 완료됐다.

Comments