Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자가 될래요

Next + Socket.io 로 채팅 구현(5) 본문

프로젝트

Next + Socket.io 로 채팅 구현(5)

Youcan 2024. 8. 24. 23:19

채팅 기능을 얼추 구현했다면 이번엔 로그인 구현을 하려고 한다.

 

로그인은 자체 로그인과 소셜 로그인 둘 다 구현하려고 한다.

npm install next-auth @next-auth/supabase-adapter @supabase/supabase-js

 

next-auth와 supabase를 이용하기 위해 라이브러리부터 설치해줬다.

 

Supabase는 오픈소스로, 관계형 데이터베이스(RDBMS)인 PostgreSQL 기반이다.

다양한 DB가 존재하지만, supabase를 이용한 이유 중에 하나는 계정관리 / 소셜로그인 / 이메일 인증 등 복잡한 사용자 인증 과정을 Supabase에서 제공한다는 점이다.

(물론 직전 프로젝트에서 사용해봤기 때문에 다른 것들보다 익숙하게 느껴지는 점도 있다.)


그럼 일단 로그인과 회원가입을 위한 페이지부터 만들어 준다.

app/(pages)/(auth)/login/page.tsx


"use client";

import { signIn } from "next-auth/react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import InputTag from "@/app/components/shared/InputTag";

export default function LoginPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const router = useRouter();

  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-100">
      <div className="w-full max-w-md p-8 space-y-8 bg-white rounded shadow-lg">
        <h2 className="text-2xl font-bold text-center">로그인</h2>
        <form onSubmit={} className="space-y-6">
          <InputTag
            label="이메일"
            id="email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
          <InputTag
            label="비밀번호"
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          <div className="flex items-center justify-between">
            <Link
              href="#"
              className="text-sm text-indigo-600 hover:text-indigo-500"
            >
              아이디 찾기
            </Link>
            <Link
              href="#"
              className="text-sm text-indigo-600 hover:text-indigo-500"
            >
              비밀번호 찾기
            </Link>
          </div>
          <button
            type="submit"
            className="w-full py-2 text-white bg-indigo-600 rounded hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
          >
            로그인
          </button>
        </form>
        <div className="relative flex justify-center text-sm">
          <span className="px-2 bg-white text-gray-500">또는</span>
        </div>
        <div className="text-sm text-center text-gray-500">
          계정이 없으신가요?
          <Link
            href="/signup"
            className="text-indigo-600 hover:text-indigo-500"
          >
            회원가입
          </Link>
        </div>
      </div>
    </div>
  );
}



일단 로그인 페이지를 만들어 주었다.

 

그리고 회원가입 페이지도 만들어 준다.

app/(pages)/(auth)/signup/page.tsx


"use client";

import { useState } from "react";
import Link from "next/link";
import InputTag from "@/app/components/shared/InputTag";

export default function SignupPage() {
  const [nickname, setNickname] = useState<string>("");
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");
  const [confirmPassword, setConfirmPassword] = useState<string>("");

  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-100">
      <div className="w-full max-w-md p-8 space-y-8 bg-white rounded shadow-lg">
        <h2 className="text-2xl font-bold text-center">회원가입</h2>
        <form className="space-y-6">
          <InputTag
            label="이메일"
            id="email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
          <InputTag
            label="닉네임"
            id="nickname"
            type="text"
            value={nickname}
            onChange={(e) => setNickname(e.target.value)}
          />
          <InputTag
            label="비밀번호"
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          <InputTag
            label="비밀번호 확인"
            id="confirmPassword"
            type="password"
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
          />
          <button
            type="submit"
            className="w-full py-2 text-white bg-indigo-600 rounded hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
          >
            회원가입
          </button>
        </form>
        <div className="relative flex justify-center text-sm">
          <span className="px-2 bg-white text-gray-500">또는</span>
        </div>
        <div className="text-sm text-center text-gray-500">
          이미 계정이 있으신가요?
          <Link href="/login" className="text-indigo-600 hover:text-indigo-500">
            로그인
          </Link>
        </div>
      </div>
    </div>
  );
}

회원가입 정보는 일단은 이메일, 닉네임, 비밀번호만...



그리고 회원가입을 위한 API 코드를 작성했다.

app/api/auth/signup/route.ts


import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function POST(req: NextRequest) {
  try {
    const { email, password, nickname } = await req.json();
    const { data: user, error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        data: { nickname },
      },
    });

    if (error) {
      console.log("회원가입 실패:", error);
      return NextResponse.json({ error: error.message }, { status: 400 });
    }

    console.log("회원가입 성공:", user);
    return NextResponse.json({ user }, { status: 201 });
  } catch (error) {
    console.error("서버 오류:", error);
    return NextResponse.json({ error: "서버 오류" }, { status: 500 });
  }
}

 

(.env에 들어갈 값들은 Supabase -> Project Setting -> API 에서 확인 할 수 있다.)

 

 

만약 Supabase의 public.table에 접근한다면 

    const { data: user, error } = await supabase
    .from('테이블이름')
    .select().eq() 등등....


으로 코드를 작성하겠지만.. 난 authentication 기능을 활용하기 위해 supabase.auth.signUp을 이용했다.

 

    const { data: user, error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        data: { nickname },
      },
    });

이 코드를 통해 Supabase Authentication의 raw_user_meta_data에 데이터를 추가한다.

 

이 meta_data를 클라이언트에서 직접 사용할 수도 있지만, 이는 보안 상의 문제로 Supabase에선 권장하지 않는다.

그래서 public Schema에 users 라는 이름의 테이블을 만들고, auth schema의 데이터를 users schema가 참조하는 방법을 사용한다.

create table
  users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), -- 각 사용자 레코드에 대한 고유 식별자
  email VARCHAR(100) UNIQUE NOT NULL,
  nickname VARCHAR(100),
  created_at TIMESTAMPTZ DEFAULT NOW(),
    auth_id UUID REFERENCES auth.users(id) ON DELETE CASCADE -- 외래 키로 auth.users 테이블과 연결
  );

create or replace function public.handle_new_user() 
returns trigger as $$
begin
  -- public.users 테이블에 데이터 삽입
  insert into public.users (
    id, email, nickname, auth_id
  )
  VALUES (
    NEW.id,
    NEW.email, 
    NEW.raw_user_meta_data ->> 'nickname',
    NEW.id
  );

  return new;
end;
$$ language plpgsql security definer;


-- 트리거 정의: auth.users 테이블에 새로운 사용자가 추가될 때마다 실행
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

이 SQL 문을 Supabase 내의 SQL Editor에 입력하고 작동시킨다.

 

그리고 모든 준비를 끝마쳤으니 회원가입을 시도한다.

.......?????

AuthRetryableFetchError 504 에러와 함께 회원가입이 실패한다...

 

사실 이 에러 덕분에 며칠동안 회원가입 기능을 완성할 수가 없었는데....

 

 

Authentication -> Providers -> Email에 들어가 Confirm email를 비활성화 하니 회원가입이 제대로 되었다.

Confirm email를 활성화하면 사용자가 메일에 접속하여 verify를 하기 전까지 회원가입이 되지 않는다고 한다.

굳이 이메일 인증이 필요하지 않다면, 꼭 체크를 해제해야 한다.

 

authentication에 등록된 user의 정보
public의 users 테이블에 자동으로 연동된 유저 정보

 

 


 

앞서 언급했던 AuthRetryableFetchError 외에도 여러가지 오류가 발생하긴 했다...

트리거, SQL 쿼리문도 잘 모르고, 에러를 고쳐보려 이것저것 Supabase내에 설정을 건드리다 보니 많은 시간이 소요되게 됐다.