본문 바로가기

기타/마켓컬리 클론코딩

[React/Node] 마켓컬리 클론코딩 12 - 메일 인증

  • 메일 인증 기능

 

 

마무리한 줄 알았으나 뭔가 찝찝해서 둘러보니 메일 인증 구현한 걸 정리하지 않았다..

 

10번 게시물에서 이어지는 글!

https://memo-code.tistory.com/45

 

마켓컬리 클론코딩 10 - 로그인/로그아웃

로그인/로그아웃 (로그아웃 버튼 css를 까먹었다..) 처음해보는 작업이라 그래도 좀 오래 걸릴거라 생각했지만, 예상보다 많이 걸린 작업이었다.온갖 에러와 정보 사이에서 어찌저찌 만들기는

memo-code.tistory.com

 

 

 

 

이 부분의 목적은 이메일의 여부를 확인하기 위한 인증 딱 한가지

그래서 특별히 프론트엔드 측에서 손볼건 없다. 그냥 코드 확인을 위한 input만 '인증 코드 받기'버튼을 클릭하면 생기도록 하면 된다.

 

{codeInput && (
	<div className="code_check">
    	<div className="title"></div>
        <div className="code info">
        	<input name="code" type="text" placeholder="인증 코드를 입력해주세요."
            value={code} onChange={change} />
        </div>
        <div className="check_button">
        	<input className="bt_style" type="button" value="인증코드 확인" onClick={code_check} />
        </div>
    </div>
)}

 

이 코드를 이메일 입력 코드 밑에다 넣으면 끝난다.

여기서 codeInput은 인증코드 받기 버튼을 눌렀을 때 제대로된 이메일 형식이라면 true값이 저장되고, {}안의 코드가 false가 아니게 되므로 출력되는 방식이다.

 

아 물론 인증코드 받기 버튼과 인증코드 확인 버튼에 대한 엔드포인트 설정은 해주어야한다.

 

const email_verification = () => {  //이메일 확인 버튼 클릭시
        axios.post('http://localhost:8000/api/signup_email', { email: userData.email })
            .then(() => {
                alert('전송완료');
                setCodeInput(true);
            })
            .catch((err) => {
                console.log(err);
                alert('전송실패');
            });
}
    

const code_check = () => {  //인증 코드 확인 버튼 클릭시
    axios.post('http://localhost:8000/api/code_check', { code: code, email: userData.email })
        .then(() => {
            alert('이메일 인증에 성공했습니다.');
        })
        .catch((err) => {
            console.log(err);
            alert('인증 코드 확인에 실패했습니다.');
        });
}

 

 

눈여겨봐야할 부분은 백엔드 측의 nodemailer이다.

 

일단 사용을 위해 라이브러리를 설치해주자.

npm i nodemailer

 

이제 사용하기 전에 간단히 정리하고 넘어가자면, nodemailer는 보내는 사람의 정보와 받을 사람의 메일 주소, 그리고 메일 내용만 있으면 되는 간단한 라이브러리이다.

보내는 메일 계정에 접근할 권한과 상대 주소만 있으면 보내는 건 라이브러리의 객체를 활용하면 된다.

그렇기에 이제 해야할 일은 보내는 메일 계정(본인의 메일 계정)에 대한 접근 권한을 제공하여야한다.

 

여기서는 gmail을 이용하였고, 다른 방법도 있음을 알린다.

 

gmail의 계정을 사용하기 위해선 '앱 비밀번호'라는 것이 필요하다.

이것이 무엇인가 하면, 우리가 로그인 할 때 계정이름과 비밀번호가 필요한데, 이 비밀번호를 대체해줄 무언가이다.

좀 다르지만 비유하자면 OTP쯤으로 이해하면 될 듯하다.

 

앱 비밀번호에 대한 내용은 구글에 많이 널려있으니 자세히 설명은 안하겠지만, 이상하게도 설명하는대로 따라해봤지만 '앱 비밀번호' 탭이 보이지 않았기에 나의 방식을 설명하고 넘어가겠다.

평범한 방식은 'google 계정 관리' - '보안' 탭 - 'Google에 로그인하는 방법' 항목 - 앱 비밀번호 탭이 존재해 설정하면 끝난다. (2단계 인증이 비활성화되어 있다면 보이지 않는다고 한다. 활성화 시켜주자)

만약 방금 말한 방식대로 되지 않는다면 그냥 

https://myaccount.google.com/apppasswords

해당 링크로 다이렉트로 이동해 설정해주면 된다.

 

설정이 완료되었다면 제공받은 16자리의 비밀번호는 잘 저장해두도록 하자.

 

이제 두가지 설정만 해주면 된다.

const transporter = nodemailer.createTransport({  //보내는 사람 계정 정보
    service: 'gmail',
    port: 587,
    secure: false,
    auth: {
        user: "memojung@gmail.com",
        pass: "제공받은 16자리 앱 비밀번호", (ex. pass: "ndlkwaafdhlkfds", )
    },
});


const mailOptions = {  //보내는 메일의 정보(from, to, 제목, 내용)
    from: "memojung@gmail.com",
    to: "받을 사람 메일(여기서는 입력받은 메일)",
    subject: '이메일 인증 테스트',
    text: `아래의 코드를 사용하여 회원가입을 계속하세요!\n\n [  임의로 생성한 인증 코드  ]`,
};

 

아까 언급했듯 계정 권한(정보)와 메일 정보(내용 및 상대 주소)에 대한 내용을 설정해주는 것이다.

이제 이걸 보내는 방법은 라이브러리를 사용하면 되는데,

transporter.sendMail(mailOptions, (error, info) => {  //메일 전송
	if (error) {
		console.error("전송 실패: ", error);
		res.status(500).send("이메일 전송 실패");
	}
	else {
		console.log("전송 성공! info: ", info.response);
		res.status(200).send("이메일 전송 성공");
	}
});

이렇게 하면 된다.

 

이제 저기 보이는 '임의로 생성한 코드'만 해결하면된다.

여기에 사용한 코드는

const mailCode = Math.random().toString(36).substring(2);

인데, 하나하나 뜯어서 해석하자면,

Math.random() : 0~1사이의 임의의 소수 반환 / 0.123456
toString(36) : 숫자를 36진법을 이용한 문자열로 변환 / 0.tms7fxk3uff
substring(2) : 문자열의 2번째자리 까지 제거하고 반환 / tms7fxk3uff

 

 

이제 전부 준비됐으니, 원하는 형식에 맞게 코드를 구성해주면 된다.

 

const db = require('./DBinfo');
const nodemailer = require('nodemailer');

//메일 인증
const transporter = nodemailer.createTransport({
    service: 'gmail',
    port: 587,
    secure: false,
    auth: {
        user: "memojung@gmail.com",
        pass: "nhupefwkdqalgosn", //가짜 비밀번호
    },
});

const postEmail = (req, res) => {
    const email = req.body.email;
    const mailCode = Math.random().toString(36).substring(2);

    const deleteQuery = "DELETE FROM kurly.mailCode WHERE email = ?;"

    db.query(deleteQuery, [email], (err, result) => {
        if (err) {
            console.error("Error deleting existing code:", err);
            return res.status(500).send("서버 에러: 기존 코드 삭제 실패");
        }
        const insertQuery = "INSERT INTO kurly.mailCode (email, code) VALUES (?, ?);";

        db.query(insertQuery, [email,mailCode], (err, result) => {
            if (err) {
                console.error("Error inserting new code:", err);
                return res.status(500).send("서버 에러: 새 코드 삽입 실패");
            }
            
            const mailOptions = {
                from: "memojung@gmail.com",
                to: email,
                subject: '이메일 인증 테스트',
                text: `아래의 코드를 사용하여 회원가입을 계속하세요!\n\n [  ${mailCode}  ]`,
            };

            transporter.sendMail(mailOptions, (error, info) => {
                if (error) {
                    console.error("X", error);
                    res.status(500).send("이메일 전송 실패");
                }
                else {
                    console.log("OK", info.response);
                    res.status(200).send("이메일 전송 성공");
                }
            });
        })
    });
}

const postCode = (req, res) => {
    const { code, email } = req.body;
    let mail_code = '';
    let query = "SELECT code FROM kurly.mailCode WHERE email = ?;";
    db.query(query, [email], (err, result) => {
        if (result.length === 0) {
            return res.status(400).send('잘못된 인증 정보');
        }
        mail_code = result[0].code;
        console.log('Stored code:', mail_code);
        if (mail_code === code) {
            query = "DELETE FROM kurly.mailCode WHERE email = ?;"
            db.query(query, [email], (err, result) => {
                res.status(200).send('인증 성공');
            });
        } else {
            res.status(400).send('잘못된 인증 정보');
        }
    })

}

module.exports = { postEmail, postCode };

 

여기서 사실은 메일은 제대로 전송이 되는데, 클라이언트 측에서는 전송 실패 팝업이 뜨는 문제가 있었다.

서버와 클라이언트 간의 통신에는 요청과 응답이 존재해야만 하는데, 만약 요청에 대한 응답이 반환되지 않는다면 클라이언트 측에서는 시간 초과로 에러가 발생한 것으로 간주한다고 한다.

즉, 저기 보이는 res.status(200).send();가 응답의 역할을 하는 것이다.

별로 중요하지 않아 보여서 작성하지 않았는데, 잘 체크해야할 듯 하다.

 

 

아무튼 이렇게 해서 진짜 끝났다.