유저 그룹 생성 기능 구현

Jiyoung·2021년 6월 11일
0

이번 우리 팀의 프로젝트에서 가장 큰 핵심이 바로 '유저 그룹 생성'이었다. 우리가 기획했던 서비스가 개인일기와 그룹일기를 쓸 수 있도록 하는 것이었기 때문에, 그룹일기를 쓰기 위해서는 특정 그룹을 만들고 거기에 해당 유저를 추가할 수 있는 기능이 꼭 필요했다. 프론트엔드 팀원들과 여러 번의 회의 끝에 내가 생각한 로직은 아래와 같았다.

  1. 로그인 한 유저가 그룹 일기 작성하기를 누르면 그룹 생성 창이 뜸
  2. 초대할 유저의 메일 주소를 입력
  3. 메일 유효성 검사(회원 가입된 유저인지 확인, 가입된 유저만 초대할 수 있음)
  4. 메일 발송과 동시에
    4.1 그룹이 생성되고,
    4.2 회원 가입된 유저 정보가 포함된 JWT 토큰이 생성됨
  5. 메일을 받은 유저가 메일의 초대 링크를 클릭하면 그룹에 자동으로 추가됨

내가 실제로 코드로 구현해야 할 부분은 3 ~ 5번까지였는데 3번을 가장 나중에 구현했기에 먼저 4번부터 순서대로 살펴보겠다.

  • (메일 발송과 동시에) 그룹이 생성됨
//그룹 생성. owner는 그룹을 생성한 사람
 const authorization = req.headers.authorization;
 const token = authorization.split(' ')[1];
 const data = jwt.verify(token, process.env.ACCESS_SECRET);

 const groupInfo = await Group.create({
            groupName: groupName,
            owner: data.id
        })  // 여기서 groupdId가 생성됨.

메일 발송과 동시에 그룹이 생성될 수 있도록 먼저 그룹을 생성하는 코드를 작성해주었다. owner는 그룹을 생성한 유저이다. 메일을 보냄과 동시에 그룹이 만들어지므로 여기서 바로groupId가 생성된다.

  • (메일 발송과 동시에) 회원 가입된 유저 정보가 포함된 JWT 토큰 생성
const inviteTokenFirst = jwt.sign({ userId, email: email1, groupId }, process.env.ACCESS_SECRET, { expiresIn: '20m' }) 
// 초대하려는 사람의 userId, email, groupId 정보가 담긴 토큰 생성.

JWT토큰을 사용하여 초대하고자 하는 유저의 회원정보(userId, email, groupId)를 암호화한 토큰을 생성해주었다. 그런데 코드를 작성하다가 조금 헤맸었던 게 위 코드로는 메일을 1명에게만 보낼 때는 상관없지만, 2명에게 보내게 되면 나머지 1명의 유저 정보가 담긴 토큰은 없다는 것이었다. 즉, 2명 각각의 유저 정보가 담긴 토큰이 필요했다. 한참을 고민한 끝에 메일을 1명에게 보낼 때(if (email.length === 1))와, 2명에게 보낼 때(if (email.length === 2) )로 조건을 나눠주었다. 참고로 우리 팀이 기획한 일기쓰기 웹 서비스는 그룹일기를 최대 3명이 같이 쓸 수 있는 컨셉이기 때문에 메일을 보내는 수를 2명까지만 나눈 것이다.

이렇게 하면 메일을 1명에게 보낼 때는 토큰을 한개만 만들면 되고, 2명에게 보낼 때는 아래처럼 토큰을 한개 더 만들어주기만 하면 된다.

const inviteTokenSecond = jwt.sign({ userId: userId2, email: email2, groupId }, process.env.ACCESS_SECRET, { expiresIn: '20m' })

  • 메일 발송

그러면 이제 메일 발송은 어떻게 하느냐?! 메일을 발송할 수 있는 모듈NodeMailer를 이용했다.
먼저 npm패키지로 모듈을 설치하고,

npm install nodemailer

메일을 보내기 위한 transporter 객체를 설정해주면,

let transporter = nodemailer.createTransport(transport[, defaults])

메일을 보낼 수 있게 된다.

transporter.sendMail(data[, callback])

공식문서에 있는 예시 코드를 참고하여 전체 코드를 작성하였다. 실제 메일을 보내기 위해 transporter객체로 gmail서버를 사용하였고, 메일에 넣을 초대링크 주소에 메일을 받게 될 유저의 정보가 담긴 토큰을 넣어주었다. 여기서도 위에서와 마찬가지로 메일을 보내려는 유저 수에 따라 조건을 분기하여, 1명에게 메일을 보낼 때는 url을 1개만 만들었고, 2명에게 보낼 때는 url을 하나 더 만들고 info도 하나 더 만들어주었다. url은 따로 라우터를 만들어야 한다.

const nodemailer = require("nodemailer");

async function main() {
  
  const { email } = req.body //req.body로 email 주소 받음
  
  //transporter객체 정의(gmail서버 사용)
  const transporter = nodemailer.createTransport({
    service: 'gmail',
    host: "smtp.gmail.com",
    port: 587,
    secure: false, 
    auth: {
      user: process.env.NODEMAILER_USER, 
      pass: process.env.NODEMAILER_PASS, 
    },
  });

  //메일에 넣을 초대링크 주소
  const url = `https://api.picanote.me/invite/?token=${inviteTokenFirst}`
  
  // 메일 전송
  const info = await transporter.sendMail({
    from: process.env.NODEMAILER_USER, // sender address
    to: `${email[0]}`, // list of receivers
    subject: "PicaNote 그룹일기 초대 메일입니다.", // Subject line
    html: "<p>아래 링크를 누르면 그룹으로 초대됩니다.</p>" + "<a href=" + url + ">초대링크</a>", 
  });

return res.status(200).json({ message: '그룹 초대 메일이 발송되었습니다.' })
}

main();

이렇게 하니 신기하게도 내 실제 메일주소로 메일이 왔다😱


아 참, 여기서 한가지 중요한 점은 gmail서버를 사용하려면 보안 설정을 낮춰줘야된다는 것!
설정은 여기서


그리고 참고로 테스트 할 때는 아래처럼 transporter로 Mailtrap이라는 서비스를 이용하여 여러번 테스트를 해보았다.

  const transporter = nodemailer.createTransport({
    host: "smtp.mailtrap.io",
    port: 2525,
    secure: false, 
    auth: {
      user: testAccount.user, 
      pass: testAccount.pass, 
    },
  });

이렇게 Mailtrap 계정으로 메일이 온 것을 확인할 수 있다.


나는 테스트를 위해 무려 429통의 메일을 보내봤나 보다..ㅎㅎ😱


  • 메일을 받은 유저가 메일의 초대 링크를 클릭하면 그룹에 자동으로 추가됨

처음에 이 부분은 어떻게 구현해야할지 감이 전혀 안 왔지만, 질문 시간에 엔지니어분께 몇 번 질문하고 답변을 적어놓았다가 코드 구현하면서 몇 번씩 읽어보다 보니, 어느 순간 '아~~ 이게 그 말이었구나!' 하며 감을 잡게 되었다.

기본적인 로직은 먼저 초대 메일을 받은 유저가 메일 안의 링크를 누르면, 그 안에 있던 토큰을 받아서 해독한 후 해당 유저 정보를 DB에 저장하면 되는 식이다.
여기서 토큰을 어떻게 받아올지를 몰라서 한참을 고민했었는데, req.query로 받아오면 되는 거였다!

 //초대 메일을 받은 유저가 메일 안의 링크를 누르면 그룹에 자동으로 추가됨.
 //메일 발송할 때 만든 inviteToken을 여기로 어떻게 가져올 수 있나?? => req.query로 가져올 수 있음.
        const inviteToken = req.query.token
        const inviteData = jwt.verify(inviteToken, process.env.ACCESS_SECRET);
        console.log(inviteData)

        Users_groups.create({
            UserId: inviteData.userId,
            GroupId: inviteData.groupId
        }) // Users_groups에 초대하려는 유저 정보 추가.

        res.status(201).json({ message: '그룹에 초대되었습니다.' })
        console.log("inviteData", inviteData)
    }
}

여기까지 구현하면 초대 링크를 클릭하는 순간 해당 유저가 그룹으로 자동 추가된다.
나중에 알게 된 사실이지만, 나란 사람 여기까지 구현하면서 테스트 하느라 그룹을 무려 436개나 만들었다는 것..ㅋㅋ😂😂👏👏 그만큼 힘든 과정이었다😭

.
.


  • 메일 유효성 검사(회원 가입된 유저인지 확인, 가입된 유저만 초대할 수 있음)

위의 과정을 코드로 구현하고 나서 이제 다 끝났다 생각했는데 이 부분이 예상치 못하게 가~~장 어려워서 갖은 삽질을 다 해야했다.

이 부분은 이메일을 1명에게만 보낼 때와 2명에게 보낼 때 각각의 조건 안에 넣어주었다. 회원가입된 유저의 이메일이 아니라면, 즉 DB에서 이메일과 일치하는 유저 정보를 찾았을 때 그 값이 null이라면 그 이메일은 빈 배열 안에 넣어 걸러지도록 하였다. 이렇게 하면 유효한 값이 아닐 때는 이메일도 발송되지 않게 된다.

내가 어려웠던 점은 이 조건을 어느 부분에 어떻게 넣어야 할지였다. 전역 조건에도 넣어보고, 단순히 이메일이 없으면 메일이 발송되지 않게도 해봤는데 이렇게 하면 유효한 이메일을 입력해도 발송이 되지 않는다거나 유효하지 않는 이메일을 입력해도 발송이 된다거나 하는 크고 작은 문제들이 발생하였다.

아무튼 많은 시행착오 끝에 시도하고 성공한 방법은 위에서 설명한대로, 그리고 아래의 코드처럼 분기된 조건 안에 메일 유효성 검사 조건을 넣어주는 것이었다.

const arr = []
            const email1 = email[0]
            const userInfo1 = await User.findOne({ where: { email: email1 } })
            // 회원가입된 이메일이 아닐 때
            if (userInfo1 === null) {
                arr.push(email1)
                return res.status(400).json({ message: `${arr[0]}의 이메일이 유효하지 않습니다.` })
            }

이렇게하여 드.디.어. 유저 그룹 생성 기능을 구현해 내었다..!! 정말 쉽지 않은 과정이었지만, 우리 팀의 웹 서비스에서 꼭 필요한 기능을 구현해냈다는 점에서 성취감이 컸다. 그리고 이 기능을 구현해봄으로써 앞으로 어떤 기능이든 (삽질은 많이 하더라도) 결국엔 구현해낼 수 있을거라는 자신감을 얻게 되었다😎


참고 자료:

profile
경계를 넘는 삶

0개의 댓글