회원가입의 흐름을 생각해보기

회원가입을 구현하기에 앞서, 먼저 웹앱에서 회원가입을 할 경우 어떤 흐름을 따라가게 되는지 간단하게 생각해보기로 합니다.

  1. 먼저, 메인 화면에서 회원 가입 버튼을 클릭하도록 만듭니다.
    • 버튼을 클릭하면 회원 가입 페이지로 이동할 수 있도록 링크를 설정합니다.
  2. 아이디, 닉네임, 이메일, 비밀번호, 생년월일을 입력 받습니다.
    • 회원 가입 페이지에서 사용자로부터 필요한 정보를 입력받기 위한 폼을 제공합니다.
    • 아이디, 닉네임, 이메일, 비밀번호, 생년월일 등 입력이 필요한 필드를 생성합니다.
    • 각 입력 필드에는 해당 정보를 입력하도록 안내하는 레이블을 붙입니다.
    • 입력 필드의 유형을 적절하게 선택하여 사용자가 편리하게 입력할 수 있도록 합니다. 예를 들어, 비밀번호는 - type=”password”를 사용하여 입력 내용을 가립니다.
    • 폼 제출용 버튼을 추가하여 사용자가 입력을 완료하고 회원 가입을 진행할 수 있도록 합니다.
  3. 아이디, 닉네임, 이메일, 비밀번호, 생년월일 등이 정해놓은 규칙에 맞는지 체크합니다.
    • 사용자가 입력한 정보가 정해진 규칙에 맞는지 확인하는 유효성 검사를 수행합니다.
    • 아이디와 닉네임은 길이 제한, 특수 문자 사용 여부 등을 확인합니다.
    • 이메일은 올바른 형식인지 확인합니다. 정규표현식을 사용하여 이메일 형식 (가운데 @ 마크가 들어가고, 뒤에 .으로 구분된 도메인 형식의 string이 오는지) 을 검증할 수 있습니다.
    • 비밀번호는 길이, 대소문자 포함 여부, 특수 문자 사용 여부 등을 확인합니다.
    • 생년월일은 유효한 날짜인지 확인합니다. 필요할 경우 이 단계에서 연령 제한을 걸 수도 있습니다.
    • 유효성 검사에 실패한 경우 사용자에게 적절한 오류 메시지를 표시하고 다시 입력하도록 안내합니다.
      • 입력을 받으면서 동시에 체크할 수도 있고, 입력이 끝난 후 다음 input 칸으로 이동할 때 메시지를 띄울 수도 있으며, submit 을 할 때 안내 메시지를 보여줄 수도 있습니다. 이용자 경험을 생각하면서 적절히 구현할 필요가 있습니다.
    • 모든 경우를 직접 구현하려면 상당히 고생스러우므로, React Hook Form 이나 Zod 같은 패키지를 사용합니다.
  4. 아이디, 닉네임, 이메일에 중복이 없는지 데이터베이스를 조회하여 체크합니다.
    • 사용자가 입력한 아이디, 닉네임, 이메일이 이미 사용 중인지 확인하기 위해 데이터베이스를 조회합니다.
    • (본 글에선 Prisma를 사용하여 PostgreSQL 데이터베이스에 쿼리를 보내 중복 여부를 확인할 것입니다.)
    • 중복이 발견되면 사용자에게 해당 정보가 이미 사용 중임을 알리는 오류 메시지를 표시합니다.
      • 중복을 언제 체크할지, 타이밍을 생각할 필요가 있습니다. 한 글자 한 글자 입력을 받으면서 체크할 것인지, 입력이 끝나고 다른 input으로 옮겨갈 때 체크할 것인지, 혹은 “중복 확인”과 같은 버튼을 마련할지, 아니면 마지막 submit 이 확정되었을 때 체크할 것인지 선택할 수 있습니다. 역시 이용자 경험을 생각하며 적절히 구현합니다.
    • 중복이 없는 것이 확인되면 회원 가입 프로세스를 계속 진행합니다.
    • 이를테면 이메일에 중복이 있을 경우 (기존에 계정을 이미 만들었다는 것이 확실할 경우) 바로 로그인 화면으로 이행시킬 수 있습니다.
  5. (옵션) 이메일 혹은 전화번호 인증을 통해 이 연락처가 실제로 존재하는 것인지 체크합니다.
    • 사용자가 입력한 이메일 또는 전화번호로 인증 코드를 발송합니다.
    • 이메일 인증의 경우 Nodemailer와 같은 라이브러리를 사용하여 이메일을 발송할 수 있습니다. Twilio SendGrid나 AWS SES 등을 사용할 수 있습니다.
    • 전화번호 인증의 경우 Twilio나 네이버 클라우드 SENS와 같은 서비스를 사용하여 SMS로 인증 코드를 발송할 수 있습니다. (생각보다 비쌉니다…)
    • 사용자가 받은 인증 코드를 입력하면 서버에서 인증 코드를 검증합니다.
    • 인증에 성공하면 회원 가입 프로세스를 계속 진행합니다. 인증에 실패하면 적절한 오류 메시지를 표시합니다.
  6. 비밀번호를 암호화합니다.
    • 사용자의 비밀번호를 안전하게 보관하기 위해 암호화합니다.
    • bcrypt와 같은 암호화 라이브러리를 사용하여 비밀번호에 솔트를 삽입하고, 해시화합니다.
    • 해시화된 비밀번호를 데이터베이스에 저장합니다. 평문 비밀번호를 직접 저장하지 않습니다.
  7. 사용자 정보를 데이터베이스에 저장하고 로그인 화면 혹은 메인 화면으로 유도합니다.
    • 유효성 검사, 중복 확인, 인증 등의 과정을 모두 통과한 사용자 정보를 데이터베이스에 저장합니다.
    • 본 글에선 Prisma (+SQL 등의 데이터베이스) 를 사용하여 사용자 정보를 데이터베이스에 삽입하는 쿼리를 실행할 것입니다.
    • 저장이 성공적으로 완료되면 사용자를 로그인 화면 또는 메인 화면으로 리다이렉트합니다.
    • 사용자 세션을 생성하고 로그인 상태를 유지합니다. (NextAuth를 사용하면 편리합니다)
    • 로그인하지 않은 사용자가 내부 페이지들에 접근하는 것을 막아둡니다.

생각보다 번잡합니다.

최소한의 폼 준비

회원가입을 구현하기에 앞서, 먼저 아무 기능도 없는 더미 페이지를 만들어 놓습니다. 회원가입 페이지의 경우 useState 를 사용하기 위한 변수들을 미리 만들어두었습니다.

언어는 TypeScript, 사용할 React 프레임워크는 App Router 기능이 있는 Next.js 14 입니다. (앱 라우터는 Next.js 13부터 추가되었습니다.) 아예 안 꾸미기는 조금 허전하니 디자인을 위해 Tailwind CSS를 사용했습니다.

메인 페이지 (app/page.tsx)

// app/page.tsx
import Link from "next/link";

const MainPage = () => {
  return (
    <div className="w-full *:font-medium items-center flex flex-col justify-between min-h-screen h-full">
      <div className="my-auto items-center flex flex-col gap-5"> 
        <h1 className="text-4xl">웹앱에서 인증 구현하기</h1>
        <div className="w-full items-center flex flex-row gap-3">
        <Link href="/login">
          <button className="rounded-md bg-slate-100 text-black px-3 py-1">로그인</button>
        </Link>
        <Link href="/register">
          <button className="rounded-md bg-slate-100 text-black px-1.5 py-1">회원가입</button>
        </Link>
      </div>
      </div>
    </div>
  );
};

export default MainPage;
an example of a main page

회원가입 페이지 (app/register/page.tsx)

// app/register/page.tsx
"use client"; // Client Component임을 지정해줘야 합니다

import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { authOptions } from "../api/auth/[...nextauth]/route";

const RegisterPage = () => {
  // useState로 입력받을 항목들을 미리 만들어줬습니다. 
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [email, setEmail] = useState("");
  const [birthday, setBirthday] = useState("");
  const router = useRouter();

  const handleRegister = async (e: any) => {
    e.preventDefault();

   // submit 버튼을 누르면 작동할 회원가입 함수입니다. 
   // 이 안에 자세한 내용이 들어갈 예정입니다. 
};

  return (
    // 회원가입을 받기 위한 간략한 템플릿입니다 
    <div className="bg-black-100 min-h-screen justify-center h-max flex flex-col items-center">
    <form 
    onSubmit={handleRegister}
    className="gap-4 flex flex-col justify-center w-72 *:pl-2">
      <span className="text-white -mt-1 -mb-3">유저명</span>
      <input
        className="py-0.5 bg-black-100 border-2 border-slate-100 rounded-md text-black"
        type="text" // 항목의 타입을 text로 지정해줬습니다
        value={username} 
        placeholder="username" // 입력 전에 보여줄 글자입니다 
        required
        onChange={(e) => setUsername(e.target.value)} // 입력이 발생할 때 username 변수의 값을 변경합니다
      />
      <span className="text-white -mt-1 -mb-3">비밀번호</span>
      <input
        className="py-0.5 bg-black-100 border-2 border-slate-100 rounded-md text-black " 
        type="password"
        value={password}
        required
        placeholder="password"
        onChange={(e) => setPassword(e.target.value)}
      />
      <span className="text-white -mt-1 -mb-3">비밀번호 확인</span>
      <input
        className="py-0.5 bg-black-100 border-2 border-slate-100 rounded-md text-black " 
        type="password"
        value={confirmPassword}
        required
        placeholder="confirm Password"
        onChange={(e) => setConfirmPassword(e.target.value)}
      />
      <span className="text-white -mt-1 -mb-3">이메일</span>
      <input
        className="py-0.5 bg-black-100 border-2 border-slate-100 rounded-md text-black"
        type="email" // type을 email 로 지정할 경우 가운데 골뱅이가 있는지를 체크해줍니다
        value={email}
        placeholder="email"
        required
        onChange={(e) => setEmail(e.target.value)}
      />
      <span className="text-white -mt-1 -mb-3">생년월일</span>
      <input
        className="py-0.5 bg-black-100 border-2 border-slate-100 rounded-md text-black"
        type="date" // type을 date로 할 경우 브라우저에 따른 날짜 선택기를 이용할 수 있습니다
        value={birthday}
        required
        placeholder="birthday"
        min="1900-01-01" 
        max="2999-12-31" // min과 max 를 지정해놓지 않을 경우 특정 브라우저에서 문제가 생길 수 있습니다
        onChange={(e) => setBirthday(e.target.value)}
      />
      <button type="submit" className="rounded-md bg-slate-100 text-black px-2 py-1">회원가입</button>
    </form>
    <span className="text-white">이미 계정이 있나요? <Link className="underline" href="/login">로그인</Link></span>
    </div>
  );
};

export default RegisterPage;
an example of a register page

Updated:

Leave a comment