![[인프런 워밍업 클럽 3기 풀스택 ] 4주차 발자국](https://cdn.inflearn.com/public/files/blogs/8d2536fa-67d0-4b1b-aef6-d46c9dd1a0d6/Thumbnail.png)
[인프런 워밍업 클럽 3기 풀스택 ] 4주차 발자국
목차
회원가입 & 로그인
Supabase Auth 소개
인증 구현 - Confirmation URL 방식 (이메일 코드 전송)
인증 구현 - 6-digit OTP 방식
채팅 기능 구현
Supabse Realtime 소개
실시간 채팅 구현
유저별 마지막 접속 시간 구현
4주차 제출
메시지 삭제 시 '이 메세지는 삭제되었습니다'로 표시
1. 회원가입 & 로그인
[Supabase Auth 소개]
다양한 인증 방식을 지원하는 인증 시스템
사용자 인증 및 권한 관리를 웹사이트에서 할 수 있음
이메일 인증, Magic Link를 통한 비밀번호 없는 로그인, 전화번호를 통한 로그인, 소셜 로그인, 그리고 기업용 SSO 등
다양한 방법으로 사용자 인증을 지원
소셜 로그인 방식 지원
전화번호 인증은 Twilio 등의 외부 Provider를 사용하여 진행 가능
인증 방식은 JWT나 Session을 사용하여 설정
next.js를 사용하는 경우, 별도의 설정 문서를 통해 Supabase Auth 설정을 진행
[인증 구현 - Confirmation URL 방식]
supabase 접속 - Authentication - Emails 페이지에서 이메일로 전송 될 내용과 링크를 설정
{.confirmationURL} 템플릿을 활용해 인증 링크 설정
[가입하기] 버튼 클릭 시 인증 주소가 담긴 이메일을 보냄
메일 전송 후 인증된 주소로 접속 시 가입 확인 완료를 체크함
supabase.auth.signup({..})로 인증 코드 주소를 통해 로그인 세션 획득
setConfirmationRequired로 가입하기 완료를 체크함
// 가입하기 완료 확인 체크
const [confirmaitonRequired, setConfirmationRequired] = useState(false);
const supabase = createBrowserSupabaseClient();
const signupMutation = useMutation({
mutationFn: async () => {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
// 유저가 회원가입 끝나고 이메일 링크 클릭 => 서버처리 완료 => 해당 url로 다시 이동
emailRedirectTo: "http://loacalhost:3000/signup/confirm",
},
});
if (data) {
setConfirmationRequired(true);
}
if (error) {
alert(error.message);
}
},
});
Web Client에서 받은 인증 코드 값을 활용해 로그인 세션을 획득
route.ts 에서 code 값이 있으면 supabase.auth.exchangeCodeForSession(code)를 통해 세션 획득
export async function GET(request: Request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
if (code) {
const supabase = await createServerSupabaseClient();
await supabase.auth.exchangeCodeForSession(code); // 세션 획득
}
return NextResponse.redirect(requestUrl.origin); // 화면 메인페이지로 이동
}
획득한 세션이 존재하면 자동으로 로그인 됨
[인증 구현 - 6-digit OTP 방식]
supabase 접속 - Authentication - Emails 페이지에서 이메일로 전송 될 내용과 인증 코드 설정
{.Token} 템플릿을 활용해 인증 코드 설정
[가입하기] 버튼 클릭 시 인증 코드가 담긴 이메일을 보냄
메일 전송 후 인증 코드를 입력하면 가입 확인 완료를 체크함
supabase.auth.verifyOtp({...})로 otp token값을 이용해 로그인 세션 획득
const verityOtpMutation = useMutation({
mutationFn: async () => {
const { data, error } = await supabase.auth.verifyOtp({
type: "signup",
email,
token: otp,
});
if (data) {
setConfirmationRequired(true);
}
if (error) {
alert(error.message);
}
},
});
획득한 세션이 존재하면 자동으로 로그인 됨
2. 채팅 기능 구현
[Supabse Realtime 소개]
연결된 클라이언트와 메시지를 주고 받는 실시간 서비스를 제공
Broadcast
모든 사용자에게 동일한 데이터를 전송하는 방식
채팅 시스템에서 실시간 메시지 전송, 동시 알림 보낼 때 등에 유용
Presence
현재 연결된 사용자를 실시간으로 추적하는 방식
실시간 채팅에서 현재 접속 중인 사용자 표시, 같은 세션에 있는 유저 보여주는 등에 유용
Postgres Changes
데이터베이스에서 발생하는 변경 사항을 실시간으로 추적하는 방식
사용자는 DB의 상태 변경을 실시간으로 모니터링, 필요에 따라 즉각 반응 가능
[실시간 채팅 구현]
supabase 페이지 - Table Editor - message DB 클릭
오른쪽 상단의 [Realtime off] 버튼을 클릭해 [Realtime on] 으로 변경
채팅으로 전송되는 메시지 DB에 저장하기
chatActions.ts
전송하는 메시지 내용과, 유저 id값을 받아 DB에 insert
export async function sendMessage({ message, chatUserId }) {
const supabase = await createServerSupabaseClient(); // 현재 로그인한 유저
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error || !session.user) {
throw new Error("User is not authenticated");
}
const { data, error: sendMessageError } = await supabase
.from("message")
.insert({
message,
receiver: chatUserId,
sender: session.user.id,
});
if (sendMessageError) {
throw new Error(sendMessageError.message);
}
return data;
}
채팅 중인 메시지 모두 가져오기
chatActions.ts
현재 채팅중인 user ID 값을 받아서 전송되는 메시지 값을 select
export async function getAllMessages({ chatUserId }) {
// 현재 나와 채팅중인 메시지 모두 가져오기
const supabase = await createServerSupabaseClient();
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error || !session.user) {
throw new Error("User is not authenticated");
}
const { data, error: getMessagesError } = await supabase
.from("message")
.select("*")
.or(`receiver.eq.${chatUserId},receiver.eq.${session.user.id}`) // 상대방 또는 나
.or(`sender.eq.${chatUserId},sender.eq.${session.user.id}`)
.order("created_at", { ascending: true });
if (getMessagesError) {
return [];
}
return data;
}
메시지를 전송하고 메시지 창에 표시하기
ChatScreen.tsx
sendMessageMutation: 메시지 DB로 전송 후, 성공하면
setMessage(""): 메시지 입력 창 초기화
getAllMessagesQuery.refetch(): 메시지 내용 가져와서 보여주기
getAllMessagesQuery: 상대 유저 id 값으로 현재 대화중인 메시지 가져오기
const sendMessageMutation = useMutation({
mutationFn: async () => {
return sendMessage({
message,
chatUserId: selectedUserId,
});
},
onSuccess: () => {
setMessage(""); // 메시지 입력 칸 비우기
getAllMessagesQuery.refetch(); // 메시지 가져오기
},
});
const getAllMessagesQuery = useQuery({
queryKey: ["messages", selectedUserId],
queryFn: () => getAllMessages({ chatUserId: selectedUserId }),
});
// 새로고침 없이 대화를 실시간으로 표시
useEffect(() => {
const channel = supabase
.channel("message_postgres_changes")
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "message",
},
(payload) => {
if (payload.eventType === "INSERT" && !payload.errors) {
getAllMessagesQuery.refetch();
}
}
)
.subscribe();
return () => {
channel.unsubscribe();
};
}, []);
[유저별 마지막 접속 시간 구현]
channel의 config내 key 값(로그인한 유저 id)을 넣어 실제 로그인한 유저의 상태를 트래킹함
presence에서 sync 사용
channel이 subscribe 되었다면 새로운 precence status 가 들어옴
channel.track 안에 트래킹 하고 싶은 오브젝트(현재 날짜)를 넣어 가져오기
const supabase = createBrowserSupabaseClient();
useEffect(() => {
const channel = supabase.channel("online_users", {
config: {
presence: {
key: loggedInUser.id,
},
},
});
channel.on("presence", { event: "sync" }, () => {
const newState = channel.presenceState();
console.log(newState);
});
channel.subscribe(async (status) => {
// 구독이 완료되지 않았을 때 종료
if (status !== "SUBSCRIBED") {
return;
}
// 구독 성공시 새로운 presence status가 들어옴
// channel.track안에 트래킹하고 싶은 오브젝트 넣기
const newPresenceStatus = await channel.track({
onlineAt: new Date().toISOString(),
});
});
return () => {
channel.unsubscribe();
};
}, []);
가져온 현재 활동 시간을 직접적으로 넣을 수 없어 atoms.ts에서 정의
export const presenceState = atom({
key: "presenceState",
default: null,
});
현재 활동 시간 표시하기
ChatPeopleList.tsx
onlineAt에 presence가 있으면 유저의 onlineAt을 가져옴
const [presence, setPresence] = useRecoilState(presenceState);
...
channel.on("presence", { event: "sync" }, () => {
const newState = channel.presenceState();
// newState를 그냥 쓰면 값을 마음대로 바꿔버릴 수 있기 때문에
// 다시 파싱하여 오브젝트를 깔끔하게 셋팅해줌
const newStateObj = JSON.parse(JSON.stringify(newState));
setPresence(newStateObj);
});
...
return (
<div className="h-screen min-w-60 flex flex-col bg-gray-50">
{getAllUsersQuery.data?.map((user, index) => (
<Person
onClick={() => {
setSelectedUserId(user.id);
setSelectedUserIndex(index);
}}
index={index}
isActive={selectedUserId === user.id}
name={user.email.split("@")[0]}
onChatScreen={false}
onlineAt={presence?.[user.id]?.[0]?.onlineAt}
// onlineAt={new Date().toISOString()}
userId={user.id}
/>
))}
3. 4주차 제출
[메시지 삭제]
메시지 삭제 시 '이 메세지는 삭제되었습니다'로 표시
메시지 삭제 쿼리
메시지 변경 및 is_deleted를 true로 변경
export async function updateMessage({ messageId }: { messageId: string }) {
const supabase = await createBrowserSupabaseClient();
const {
data: { session },
error: authError,
} = await supabase.auth.getSession();
if (authError || !session?.user) {
throw new Error("User is not authenticated");
}
const { data, error: updateError } = await supabase
.from("message")
.update({ message: "이 메시지는 삭제되었습니다", is_deleted: true }) // 메시지 내용 변경
.eq("id", messageId);
if (updateError) {
throw new Error(updateError.message);
}
return data;
}
메시지 삭제시 실시간 업데이트
const updateMessageMutation = useMutation({ mutationFn: async ({ messageId }: { messageId: string }) => { return updateMessage({ messageId }); }, onSuccess: () => { // 성공적으로 메시지를 업데이트한 후 메시지 목록을 다시 가져옵니다. getAllMessagesQuery.refetch(); }, });
댓글을 작성해보세요.