트위터 클론 코딩 - 회원가입

김재한·2024년 1월 30일
0

트위터 클론코딩

목록 보기
4/6

회원가입 화면

회원가입 화면

// main.tsx

export default function Main() {
  return (
    <>
      <div className={styles.left}>
        <Image src={zLogo} alt="logo" />
      </div>
      <div className={styles.right}>
        <h1>지금 일어나고 있는 일</h1>
        <h2>지금 가입하세요.</h2>
        <Link href="/signup" className={styles.signup}>계정 만들기</Link>
        <h3>이미 트위터에 가입하셨나요?</h3>
        <Link href="/login" className={styles.login}>로그인</Link>
      </div>
    </>
  )
}

main 페이지에서 계정 만들기 버튼을 클릭하면 /signup 페이지로 라우팅 이동 시키고 /signup 페이지에서 @modal/(.i)/flow/signup 으로 Parallel Routes & Intercepting 시켜야 한다.

하지만 서버 컴포넌트(/signup)에서는 useRouter를 사용할 수 없을뿐더러 redirect 시킬경우 인터셉팅이 작동하지 않는다. 따라서 Client Component(RedirectToSignup.tsx)를 만들어 라우팅 시켜준다.

// (beforeLogin)/signup/page.tsx
import Main from "@/app/(beforeLogin)/_component/Main";
import {auth} from "@/auth";
import {redirect} from "next/navigation";
import RedirectToSignup from "@/app/(beforeLogin)/signup/_component/RedirectToSignup";

export default async function Signup() {
    const session = await auth();

    if (session?.user) {
        redirect('/home');
        return null;
    }
    return (
        <>
            <RedirectToSignup />
            <Main/>
        </>
    );
}
// RedirectToSignup.tsx
"use client";

import {useEffect} from "react";
import {useRouter} from "next/navigation";
export default function RedirectToSignup() {
    const router = useRouter();

    useEffect(() => {
        router.replace('/i/flow/signup');
    }, []);

    return null;
}

즉, 로그인 버튼을 클릭하면 /login/page.tsx 로 이동했다가 내부 RedirectToLogin.tsx 컴포넌트에 의해 (beforeLogin)/i/flow/login/page.tsx로 이동하는데, 인터셉팅 되어 (beforeLogin)/@modal/i/flow/login/page.tsx 화면이 보여진다.

회원가입 With Server Action

회원가입 기능은 Server Action으로 처리했으며 발생하는 에러는 크롬 개발자 도구가 아닌 프론트 서버(터미널) 에서 확인이 가능하다.

SignupModal 컴포넌트에서는 useFormState 훅을 사용했다.

// (beforeLogin)/_component/SignupModal.tsx
"use client"
import style from './signup.module.css';
import onSubmit from '../_lib/signup'
import BackButton from "@/app/(beforeLogin)/_component/BackButton";
import { useFormState, useFormStatus } from 'react-dom'

function showMessage(message: string | undefined) {
    switch (message) {
        case 'no_id':
            return '아이디를 입력하세요.';
        case 'no_name':
            return '닉네임을 입력하세요.';
        case 'no_password':
            return '비밀번호를 입력하세요.';
        case 'no_image':
            return '이미지를 업로드하세요.';
        case 'user_exists':
            return '이미 사용 중인 아이디입니다.';
        default:
            return ''

    }
}

export default function SignupModal() {
    const [state, formAction] = useFormState(onSubmit, { message: '' });

    const {pending} = useFormStatus();

    return (
        <>
            <div className={style.modalBackground}>
                <div className={style.modal}>
                    <div className={style.modalHeader}>
                        <BackButton/>
                        <div>계정을 생성하세요.</div>
                    </div>
                    <form action={formAction}>
                        <div className={style.modalBody}>
                            <div className={style.inputDiv}>
                                <label className={style.inputLabel} htmlFor="id">아이디</label>
                                <input id="id" name="id" className={style.input} type="text" placeholder="" required/>
                            </div>
                            <div className={style.inputDiv}>
                                <label className={style.inputLabel} htmlFor="name">닉네임</label>
                                <input id="name" name="name" className={style.input} type="text" placeholder="" required/>
                            </div>
                            <div className={style.inputDiv}>
                                <label className={style.inputLabel} htmlFor="password">비밀번호</label>
                                <input id="password" name="password" className={style.input} type="password" placeholder="" required/>
                            </div>
                            <div className={style.inputDiv}>
                                <label className={style.inputLabel} htmlFor="image">프로필</label>
                                <input id="image" name="image" className={style.input} type="file" accept="image/*" required/>
                            </div>
                        </div>
                        <div className={style.modalFooter}>
                            <button type="submit" className={style.actionButton} disabled={pending}>가입하기</button>
                            <div className={style.error}>{showMessage(state?.message)}</div>
                        </div>
                    </form>
                </div>
            </div>
        </>)
}
// (beforeLogin)/_lib/signup.ts
"use server"

import {redirect} from "next/navigation";
import {signIn} from "@/auth";

export default async (prevState: any, formData: FormData) =>{
  // Server Action
  // 서버에서 실행되므로 브라우저에 노출되지 않는다.
  if(!formData.get('id') || !(formData.get('id') as string)?.trim()){
    return {message: 'no_id'}
  }
  if(!formData.get('name') || !(formData.get('name') as string)?.trim()){
    return {message: 'no_name'}
  }
  if(!formData.get('password') || !(formData.get('password') as string)?.trim()){
    return {message: 'no_password'}
  }
  if(!formData.get('image') ){
    return {message: 'no_image'}
  }

  formData.set('nickname', formData.get('name') as string)
  let shouldRedirect = false;

  try{
    const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/users`,{
      method: 'post',
      body: formData,
      // 회원가입 시 세션 쿠키를 브라우저에 등록하기 위해 추가한 옵션
      credentials: 'include'
    })

    if(response.status === 403){
      return {message: 'user_exists'}
    }

    shouldRedirect=true
    console.log('$$$$ signUp success: ', response)

    // 회원가입 후 로그인 처리
    // Server Action 이므로 @/auth 의 signIn 을 사용한다 ( /next-auth/react X)
    await signIn("credentials", {
      username: formData.get('id'),
      password: formData.get('password'),
      redirect: false,
    })

    console.log('$$$$ signIn success: ', response)

  }catch (e) {
    console.error(e)
  }

  if(shouldRedirect){
    // redirect 는 try&catch 문에서는 사용이 불가능하기 때문
    redirect('/home')
  }
}

Session을 사용하기 위해서는 cookie가 브라우저에 등록되어야 하는데, 그 것을 위해 fetch 부분에 {credentials: 'include'} 옵션을 추가해준 것이다.

회원가입이 성공적으로 완료되면 signIn을 호출해 로그인 처리했다.

//next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/upload/:slug',
        destination: `${process.env.NEXT_PUBLIC_BASE_URL}/upload/:slug`, // Matched parameters can be used in the destination
      },
    ]
  },
}

module.exports = nextConfig

api에서 이미지 url 을 '/upload/~~ ' 형식으로 내려주기 때문에 rewrites 기능을 사용해 주소를 변경해 주었다.

💡 rewrites
특정 경로를 다른 경로로 매핑시키는 기능이며 next.config.js 파일에서 사용 가능하다.

0개의 댓글