개발자가 될래요
Next + Socket.io 로 채팅 구현(5) 본문
채팅 기능을 얼추 구현했다면 이번엔 로그인 구현을 하려고 한다.
로그인은 자체 로그인과 소셜 로그인 둘 다 구현하려고 한다.
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를 하기 전까지 회원가입이 되지 않는다고 한다.
굳이 이메일 인증이 필요하지 않다면, 꼭 체크를 해제해야 한다.


앞서 언급했던 AuthRetryableFetchError 외에도 여러가지 오류가 발생하긴 했다...
트리거, SQL 쿼리문도 잘 모르고, 에러를 고쳐보려 이것저것 Supabase내에 설정을 건드리다 보니 많은 시간이 소요되게 됐다.
'프로젝트' 카테고리의 다른 글
| Next.js 소셜 로그인 구현 (1) | 2024.08.31 |
|---|---|
| Next + Socket.io 로 채팅 구현(6) (0) | 2024.08.30 |
| Next + Socket.io 로 채팅 구현(4) (0) | 2024.08.21 |
| Next + Socket.io 로 채팅 구현(3) (0) | 2024.08.19 |
| Next + Socket.io 로 채팅 구현(2) (0) | 2024.08.17 |