상세 컨텐츠

본문 제목

[NEXT] 당근마켓 (2-2) - 로그인 구현

React/당근마켓 클론

by 래모 2023. 2. 10. 19:59

본문

시작하기 앞서 이 과정은 pscale이 연결되어야 가능함

따라서 콘솔에서 pscale connect - [프로젝트명]을 입력해주자

 

로그인 로직은 다음과 같은 과정을 거친다

1. email이나 phone을 백엔드에 넘기기 --있음?--> login, --없음?-->sign in

2. 토큰 생성 -- 랜덤숫자를 생성하여 유저와 연결

3. Twillo를 사용해 유저에게 토큰(랜덤숫자)를 문자 or 메일보내기

4. 토큰을 입력하여 백엔드로 보내고 일치하다면 로그인하기

 

1️⃣ 로그인 화면에서 DB에 유저정보 있는지 확인하기

위의 창 에서 유저정보를 로그인하거나 생성할 것이다.

 

email이나 phone 정보를 입력했을 때 그 정보를 통해 유저가 있는지 확인하고

없다면 새로운 유저 정보를 만들어 줄 것이다.

// /pages/api/users/enter.tsx

import withHandler from "@libs/server/withHandler";
import { NextApiRequest, NextApiResponse } from "next";
import client from "../../../libs/client/client";

async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { phone, email } = req.body;
  let user;
  
  if (email) { 
    user = await client.user.findUnique({ // client에서 정보를 찾는다
      where: {
        email, 
      }
    })
    if (user) console.log("found it.") // 유저가 있으면
    if (!user) {
      console.log("Did not found. Will create")
      user = await client.user.create({ // 유저가 없으면
        data: {
          name: "Anonymous",
          email,
          }
        })
    }
  }
  if (phone) { 
    user = await client.user.findUnique({ 
      where: {
        phone: +phone,
      }
    })
    if (user) console.log("found it.") // 유저가 있으면
    if (!user) {
      console.log("Did not found. Will create")
      user = await client.user.create({ // 유저가 없으면
        data: {
          name: "Anonymous",
          phone: +phone,
          }
        })
    }
  }

  return res.status(200).end();
}

export default withHandler("POST", handler);

위 처럼 길게 쓸수도 있지만! prisma에서는 upsert라는 기능을 제공한다.

 

upsert?

기존 데이터를 업데이트하거나 새 데이터베이스 레코드를 생성하는 코드

where(찾기), create(생성), update(업데이트) 이 세 가지의 파라미터를 모두 적어줘야한다.

 

upser를 사용해서 코드를 변경하면 다음과 같다.

...
  const { phone, email } = req.body;
  const payload = phone ? {phone:+phone} : {email}

  const user = await client.user.upsert({
    where: {
      ...payload
    },
    create: {
      name:"Annoyous",
      ...payload
    },
    update:{},
  })
  

  return res.status(200).end();
}

export default withHandler("POST", handler);

 

2️⃣ 토큰 생성 구현

토큰에 대한 새로운 모델을 만들어 주어야 한다.

// prisma.scema.prisma

...
model Token {
  id        Int      @id @default(autoincrement())
  payload   String   @unique
  user      User     @relation(fields: [userId], references: [id]) 
  // 이러면 기존 User모델과 연결된다.
  userId    Int
  createdAt DateTime @default(now())
  updateAt  DateTime @updatedAt

  @@index([userId]) // 노란색 오류 떠서 추가한 코드
}

스키마 파일을 수정 후

npx prisma db push

를 통해 db를 업데이트 시킨다.

 

이휴 prisma studio를 다시 실행시키면

token이 모델로 잘 뜬다.

 

이제 다시 api를 수정해보자

그 전에 connectOrCreate에 대해 알아보자

 

connectOrCreate

connectOrCreate는 ID 또는 고유 식별자로 기존 관련 레코드에 레코드를 연결하거나 레코드가 존재하지 않는 경우 새 관련 레코드를 생성한다.

 

즉 이 코드를 쓰면 연결과 생성을 같이 할 수 있다

기존의 user에 대한 코드를 수정하고 다음과 같이 작성한다.

// pages/api/users/enter.tsx


import withHandler from "@libs/server/withHandler";
import { NextApiRequest, NextApiResponse } from "next";
import client from "../../../libs/client/client";

async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { phone, email } = req.body;
  const user = phone ? { phone: +phone } : { email };
  const payload = Math.floor(100000 + Math.random() * 900000) + "";

  const token = await client.token.create({
    data: {
      payload,
      user: {
        connectOrCreate: {
          where: {
            ...user,
          },
          create: {
            name: "Anonymous",
            ...user,
          }
        }
      }
    }
  })

  return res.status(200).end();
}

export default withHandler("POST", handler);

 

이후 다시 sand8594@naver.com으로 로그인을 하면

 

토큰이 잘 연결 된 것을 볼 수 있다.

 

3️⃣ 유저에게 토큰값 보내기

 

문자 보내기

twilio를 사용해 준다.

우선 https://www.twilio.com/ 에 가입해서

SID, MESSAGE SID, TOKEN 값을 .env파일에 저장해준다

message sid 값은 Twilio사이트에서 Messaging < Services에서 생성해주면 된다.

 

이후 twilio를 설치해준다.

npm i twilio

 

enter파일에 가서 수정해보자

// pages/api/users/enter.tsx
...
import twilio from 'twilio'

const twilioClient = twilio(process.env.TWILIO_SID, process.env.TWILIO_TOKEN)
...
  if (phone) {
    const message = await twilioClient.messages.create({
      messagingServiceSid: process.env.TWILIO_MSID,
      to: process.env.MY_PHONE!,
      // 시험하는 단계임으로 무조건 내 핸드폰으로 가게 설정
      // 필수로 있어야 함으로 뒤에 !을 붙여줬다
      body:`Your login token is ${payload}`
    })
    return res.json({
      ok:true,
    })
    console.log(message);
  }
  
}
...

SID와 TOKEN값으로 twilio를 불러주고 그것을 통해 누구에게 보낼지 지정한 형태이다

 

메일 보내기

sendgrid를 이용해준다.(twilio에 인수되었다고 함)

https://sendgrid.com/solutions/email-api/ 여기도 가입해주고 API KEY 값을 얻어준다

 

코드는 다음과 같다!

...
import mail from "@sendgrid/mail"

mail.setApiKey(process.env.SENDGRID_KEY!);

...
  } else if (email) {
    const email = await mail.sendMultiple({
      from: "sand8594@naver.com",
      to: "sand8594@naver.com",
      subject: "Your Carrot Market Verification Email",
      text: `Your token is ${payload}`,
      html: `<strong>Your token is ${payload}</strong>`,
    })
    console.log(email)
  }
...

 

 

4️⃣ 토큰을 통해 로그인하기

 

그 이전에 useMutation을 다음과 같이 수정시킨다.

import withHandler, { ResponseType } from "@libs/server/withHandler";
import { NextApiRequest, NextApiResponse } from "next";
import client from "../../../libs/client/client";
import twilio from 'twilio'
import mail from "@sendgrid/mail"

mail.setApiKey(process.env.SENDGRID_KEY!);

const twilioClient = twilio(process.env.TWILIO_SID, process.env.TWILIO_TOKEN)

async function handler(
  req: NextApiRequest,
  res: NextApiResponse<ResponseType>
) {
  const { phone, email } = req.body;
  const user = phone ? { phone: +phone } : email ? { email } : null;
  if (!user) return res.status(400).json({ ok: false }); // user가 없다면
  const payload = Math.floor(100000 + Math.random() * 900000) + "";

  const token = await client.token.create({
    data: {
      payload,
      user: {
        connectOrCreate: {
          where: {
            ...user,
          },
          create: {
            name: "Anonymous",
            ...user,
          }
        }
      }
    }
  })
  if (phone) {
    ...
  } else if (email) {
    ...
  }
   return res.json({
    ok: true,
  });
}

export default withHandler("POST", handler);

 

로그인을 담당하는 enter.tsx에서 토큰 정보를 담당하는 새 form을 생성시킨다.

// /pages/enter.tsx
...
interface TokenForm {
  token: string;
}

interface MutationResult {
  ok: boolean;
}
...
  const [confirmToken, { loading:tokenLoading, data:tokenData}] = useMutation<MutationResult>("/api/users/confirms");
  const { register: tokenRegister, handleSubmit: tokenHandleSubmit } = useForm<TokenForm>();
...
        {data?.ok ? 
          <form onSubmit={tokenHandleSubmit(onTokenValid)} className="flex flex-col mt-8 space-y-4">
           
              <Input 
              register = {tokenRegister("token")} 
              name="email" 
              label="Confirmation Token" 
              type="number" required />
            
            
            <Button text={tokenLoading ? "Loading" : "Confirm Token"} />
          </form>  
        : 
          //phone과 mail에 관한 form
        }
...

이전 api요청의 결과로 유저가 없다면 ok:false를 있다면 ok:true를 return 하고

enter에선 그것을 확인하여 보여준다.

 

 

관련글 더보기