Supabase 로그인 기능 구현하기
주의
본 포스트에 소개된 내용 중 마지막 부분에 해당하는 조건부 렌더링은 리액트의 특성 상 제대로 작동하지 않는다.
Next.js 등을 이용하여 서버 사이드 렌더링을 하지 않는 이상 서버와 통신하여 인증 상태를 받아 오는 방식으로 조건부 렌더링을 하는 것은 리액트의 특성 상 불가능에 가까운 영역이므로 글쓴이와 같이 이틀 간 안 되는 것으로 씨름하다 탈모를 촉진시키지 마시고, 학습용으로 참고만 하길 바란다.
간단한 상황에서 리액트만으로 해결한 방법이 있는데 이는 별도로 React 카테고리에서 별도로 포스팅 하였다.
상황
- 이메일 가입 회원
- 회원가입이 되어 authentication 테이블에 유저 정보가 있는 경우를 가정
로그인 함수
supabase에서는 내장 메서드를 제공한다.
이 내장 메서드를 사용하면 로그인과 동시에 accessToken이 로컬 스토리지에 저장된다.
로컬 스토리지에 토큰이 저장되면 인증 상태가 새로고침을 하더라도 풀리지 않는다. 내장 메서드만으로 이 기능을 제공하니 매우 편리하다.
그리고 로그아웃 메서드를 사용하면 토큰도 로컬 스토리지에서 삭제된다.
const { data, error } = await supabase.auth.signInWithPassword({
email: 'example@email.com',
password: 'example-password',
})
당연히 위 함수를 그대로 사용하면 안 되고, 사용자에게 이메일(아이디), 비밀번호를 입력받은 뒤 위 자리에 상태로 넣어 주어야 하고, 로그인 실패에 대한 에러 처리도 해야 한다.
아래 코드는 스타일드 컴포넌트는 제외했다.
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import supabase from '../api/supabaseClient';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isButtonDisabled, setIsButtonDisabled] = useState(true);
const navigate = useNavigate();
// 이메일 형식 정규식으로 설정 (a@a.a 만 입력 되어도 통과됨)
// 진짜 이메일만 받고 싶으면 골뱅이와 마침표 앞 뒤로 서식을 잘 정해보길 바람
const validateEmail = (email) => {
return /\S+@\S+\.\S+/.test(email);
};
// 로그인 함수
const handleLogin = async (event) => {
event.preventDefault(); // 폼 제출 시 페이지 리로드 방지
let errorMessage = '';
// 유효성 검사
if (!validateEmail(email)) {
errorMessage = '이메일 형식을 확인해주세요';
} else if (password.length < 6) {
errorMessage = '비밀번호는 6자리 이상으로 설정해주세요';
}
if (errorMessage) {
setError(errorMessage);
return;
}
setError('');
try {
const { data, error } = await supabase.auth.signInWithPassword({
email: email, // 이 부분을 이메일과 비밀번호로 변경
password: password,
});
if (error) {
const signUpError = `회원가입 중 에러가 발생했습니다.: ${error.message}`;
setError(signUpError);
console.error(signUpError);
} else {
alert(`${data.user.email} 님의 방문을 환영합니다!`);
console.log('로그인 완료:', data);
navigate('/');
}
} catch (error) {
const loginError = `회원가입 중 에러가 발생했습니다.: ${error.message}`;
setError(loginError);
console.error(loginError);
}
};
const handleEmailChange = (e) => {
setEmail(e.target.value);
if (!validateEmail(e.target.value)) {
setError('이메일 형식을 확인해주세요');
} else {
setError('');
}
};
const handlePasswordChange = (e) => {
setPassword(e.target.value);
setError(''); // 에러 메시지 초기화
};
useEffect(() => {
// 버튼 활성화/비활성화 상태 업데이트
if (email && password && !error && validateEmail(email)) {
setIsButtonDisabled(false);
} else {
setIsButtonDisabled(true);
}
}, [email, password, error]);
return (
<Container>
<Content>
<ImgWrapper>
<img src="https://kmfvncclriiektxphias.supabase.co/storage/v1/object/public/images/public/Festiall_Model.png?t=2024-06-18T03%3A56%3A18.517Z" alt="user login" />
</ImgWrapper>
<Forms>
<Form className="block" id="login-up" onSubmit={handleLogin}>
<Title>로그인 페이지</Title>
<InputBox>
<Icon className='bx bx-at'></Icon>
<Input
type="email"
placeholder="이메일"
value={email}
onChange={handleEmailChange}
/>
</InputBox>
<InputBox>
<Icon className='bx bx-lock'></Icon>
<Input
type="password"
placeholder="비밀번호"
value={password}
onChange={handlePasswordChange}
/>
</InputBox>
{error && (
<ErrorMessage>
{error}
</ErrorMessage>
)}
<Button type="submit" disabled={isButtonDisabled}>
로그인
</Button>
</Form>
</Forms>
</Content>
</Container>
);
};
export default Login;
// const handleLogout = () => {
// clearAccessToken();
// alert('로그아웃 성공');
// };
// // handleLogout을 다른 컴포넌트에서 사용할 수 있도록 전역 상태에 저장
// useAuthStore.setState({ handleLogout });
- 폼을 제출하는 함수인 로그인 함수(회원가입도 마찬가지) 첫 시작은 e.preventDefault()로 유효성 검사 통과 못할 시 양식이 제출되는 동작을 막아야 함.
- form 태그에 로그인 함수를 onSumbit 이벤트 핸들러에 연결.
- 양식 제출하는 button의 type 속성은 sumbit으로 설정.
로그아웃 함수
supabase에서 제공하는 내장 함수는 아래와 같다.
const { error } = await supabase.auth.signOut()
매우 짧고 간결하다. 저대로 쓰는 것이 아니라 이것도 JWT 인증 세션을 서버에서 만료 시키는 서버와의 통신 과정이 들어가기 때문에 에러 처리 등을 해주면 좋다.
또한 위 내장 함수는, 로컬 스토리지에 저장 되어 있는, 유저의 인증여부(토큰) 또한 삭제 시켜주는 로직도 포함되어 있어 편리하다.
본인은 로그아웃 버튼이 헤더나 외부에 있을 것이기 때문에 커스텀 훅으로 생성하겠다.
// hooks/useLogout.js
import { useNavigate } from "react-router-dom";
import supabase from "../components/api/supabaseClient";
const useLogout = () => {
const navigate = useNavigate();
const handleLogout = async () => {
try {
const { error } = await supabase.auth.signOut();
if (error) throw error;
} catch (error) {
alert('로그아웃 중 오류가 발생했습니다. :' + error.message);
return;
}
alert('로그아웃 되었습니다. 또 만나요!');
navigate('/');
};
return handleLogout;
};
export default useLogout;
위 커스텀 훅을 헤더 등 다른 컴포넌트에서 재사용 하려면 import 후 사용하면 된다.
// 커스텀 훅 import
import useLogout from '../../hooks/useLogout';
// 사용할 함수 이름으로 할당하기
const handleLogout = useLogout();
// 버튼 태그나 컴포넌트에 onClick 이벤트 핸들러로 연결하기
<Button type="button" onClick={handleLogout}>
로그아웃
</Button>
로그인 상태 전역으로 공유하기(Zustand)
로컬 스토리지에 토큰이 저장되어 있긴 하지만, 이걸로 로그인 상태를 알기엔 매번 세션을 불러오는 메서드를 필요한 컴포넌트에서 매번 실행해야 하기 때문에 번거롭다.
사용자 로그인 상태에 따라 보여줘야 할 버튼이나 컴포넌트, 페이지가 따로 정해져 있다면 전역으로 사용자 인증 상태를 공유해서 사용해야 한다.
패키지 설치
yarn add zustand
Zustand 스토어 설정
// src/store/store.js
import { create } from 'zustand';
const useAuthStore = create((set) => ({
isSignedIn: false,
setSignIn: (status) => set({ isSignedIn: status }),
}));
export default useAuthStore;
- import { create } from 'zustand';
- zustand에서 상태 스토어를 생성하는 함수임. 이 함수를 사용하면 우리가 알고 있는 state와 setState를 정의할 수 있는 함수를 반환해 줌.
- const useAuthStore = create( (set) => ( {} ) );
- useAuthStore라는 커스텀 훅을 zustand 스토어를 만들겠다는 선언임.
- 매개 변수로 오는 set은 상태 변경을 하기 위해 필요한 함수임. 이 스토어를 끌어다 쓰는 곳에서 함수가 실행되면 새로운 상태를 인수로 받아서 상태를 업데이트 해 줌.
- 중괄호 내부에 상태, 상태 변경 함수를 객체 형태로 정의하면 됨.
- { isSignedIn: false }
- isSingedIn이라는 상태를 만든 것이고, 초기값을 false로 설정한 것임.
- { setSignIn: (status) => set( { isSignedIn: status} ) }
- 상태를 변경하는 함수임.
- status를 인수로 받아서 isSignedIn이라는 상태(바로 위에서 정의한 것)의 값을 status로 바꿔 줌.
- 활용하기 나름인데 본인은 checkSignIn() 이라는 함수를 만들어서, 함수를 실행하면 supabase에서 true, false를 받아오게 할 것임.
로그인 상태 체크 함수 작성
// checkSignIn.js
import { supabase } from './supabaseClient';
import useAuthStore from './useAuthStore';
async function checkSignIn() {
const { data: { user } } = await supabase.auth.getUser();
const setSignIn = useAuthStore.getState().setSignIn;
setSignIn(!!user);
return !!user;
}
export default checkSignIn;
- import { supabase } from './supabaseClient';
- supabase 라이브러리 설치해야 됨. 그래야 supabase에서 정해 둔 축약된 메서드를 사용할 수 있음. 이 코드의 경우 ~.auth.getUser() 같은 함수를 말함.
- import useAuthStore from './useAuthStore';
- zustand 스토어에서 만든 커스텀 훅인 useAuthStore을 가져옴.
- 이걸 가져와서 supabase에서 가져온 로그인 상태 정보를 여기 상태 변경 함수에 매개 변수로 전달하여 전역 상태로 설정한 isSignedIn의 상태를 바꿀 것임.
- async function checkSignIn() {...};
- 이 함수는 크게 supabase에서 user 정보를 가져오는 내장 함수임.
- const { data: { user } } = await supabase.auth.getUser();
- supabase에서 현재 로그인된 사용자의 정보를 가져옴.
- awiat 키워드를 사용해서 getUser() 함수가 종료될 때가지 기다림.
- const { data: { user } } : 응답 결과로 반환해준 값에서 user 객체만 추출함. 여기에는 로그인된 사용자의 정보가 담겨있음.
- const setSignIn = useAuthStore.getState().setSignIn;
- zustand 스토어에서 setSignIn 함수를 가져옴. 이 함수에 매개 변수로 방금 supabase에서 받은 데이터를 넘겨 주기 위함.
- setSignIn(!!user);
- supabase에서 받아온 user 객체를 매개 변수로 전달해서 user 객체가 존재하면 true, 존재하지 않으면 false라는 불리언 값으로 반환된다. 그냥 쓰면 객체가 들어 가 버린다.
- 자바스크립트에서 !! NOT 논리 연산자를 두 번 사용하면 그 값이 truthy한 값이면 true를, 반대는 false라는 불리언 값으로 반환해준다.
- return !!user;
- 그렇게 생성된 불리언 값을 return한다.
* 참고 supabase getUser(); 응답값 예시
{
"data": {
"user": {
"id": "11111111-1111-1111-1111-111111111111",
"aud": "authenticated",
"role": "authenticated",
"email": "example@email.com",
"email_confirmed_at": "2024-01-01T00:00:00Z",
"phone": "",
"confirmed_at": "2024-01-01T00:00:00Z",
"last_sign_in_at": "2024-01-01T00:00:00Z",
"app_metadata": {
"provider": "email",
"providers": [
"email"
]
},
"user_metadata": {
"email": "example@email.com",
"email_verified": false,
"phone_verified": false,
"sub": "11111111-1111-1111-1111-111111111111"
},
"identities": [
{
"identity_id": "22222222-2222-2222-2222-222222222222",
"id": "11111111-1111-1111-1111-111111111111",
"user_id": "11111111-1111-1111-1111-111111111111",
"identity_data": {
"email": "example@email.com",
"email_verified": false,
"phone_verified": false,
"sub": "11111111-1111-1111-1111-111111111111"
},
"provider": "email",
"last_sign_in_at": "2024-01-01T00:00:00Z",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"email": "example@email.com"
}
],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"is_anonymous": false
}
},
"error": null
}
필요한 컴포넌트에서 사용
// 아무 컴포넌트
import useAuthStore from './store';
import checkSignIn from './checkSignIn';
function App() {
const isSignedIn = useAuthStore( (state) => state.isSignedIn );
useEffect( () => {
checkSignIn();
}, []);
return ();
}
export default App;
- import useAuthStore from './store';
- zustand에서 만든 커스텀 훅이다. 이 훅이 있어야 전역 상태에 접근할 수 있다.
- import checkSignIn from './checkSignIn';
- supabase에서 유저 정보를 받아오는 함수를 작성했던 내용이다.
- 이 함수를 실행시켜서 supabase에서 현재 로그인 한 유저의 객체를 받아오면 그 객체가 스토어에서 불리언 값으로 저장되는 로직이 이미 짰었다.
- const isSignedIn = useAuthStore( (state) => state.isSignedIn );
- 하나의 패턴이다. useAuthStore 커스텀 훅을 실행시켜서 state를 불러오는데 그 중에서도 isSignedIn이라는 상태의 값을 불러오겠다는 의미이다. 아무것도 안 하면 기본값은 false다.
- useEffect(()=>{ checkSignIn() ,[] );
- supabase에서 유저 정보를 받아 오는 역할을 하고, 컴포넌트 마운트 시 최초 1회 실행된다.
- 이 객체 정보로 zustand 스토어에 정의한 함수에 불리언 값으로 넣는 것는 것을 실행하면 그 값이 const isSignedIn 변수에 저장된다.
- 그러면 이제 이 isSignedIn으로 로그인 중인지 아닌지 그 컴포넌트에서 판단할 수 있다.
그런데 여기서 끝나면 너무 좋겠지만, logout() 함수를 제작해서 실행했을 때 이 로직이 한 번 더 반복돼서 인증 상태를 다시 false로 돌려주어야 한다.
// 기존 로그아웃 버튼 이벤트 핸들러
<Button bgColor={"red"} onClick={handleLogout()}>로그아웃</Button>
// 변경된 로그아웃 버튼 이벤트 핸들러
<Button bgColor={"red"} onClick={handleLogoutAndCheckSignIn}>로그아웃</Button>
supabase에서 제공하는 logout만 했던 함수를 한 번 더 감싸서 같이 묶어 위 로직을 한 번 더 실행해줘야 한다.
supabase에서 user 객체를 가져오는 checkSignIn() 함수만 실행해주면, zustand 스토어의 상태가 변경되기 때문에, 리렌더링이 자동으로 발생한다.
const handleLogoutAndCheckSignIn = async () => {
await handleLogout();
checkSignIn();
};
유저 인증 상태(전역 상태)에 따라 로그인, 로그아웃 버튼 조건부 렌더링
로그인을 했든, 로그아웃을 했든 전역 상태로 그 여부가 관리되고는 있지만 조건부 렌더링을 하지 않아 똑같은 버튼이 렌더링 된다.
{isSignedIn ? (
<button onClick={로그아웃함수}>로그아웃</Button>
) : (
<>
<Button onClick={() => navigate("/signup")}>회원가입</Button>
<Button onClick={() => navigate("/login")}>로그인</Button>
</>
)}
구조를 보면 삼항 연산자다.
isSignedIn ? 로그아웃 버튼 : 회원가입, 로그인 버튼
형티인데, 유저가 로그인 한 상태(true)라면 로그아웃 버튼을 보여주고, false라면 회원가입과 로그인 버튼을 fragment로 묶어서 렌더링 하는 방식으로 구현했다.
'Programing > Server' 카테고리의 다른 글
supabase 카카오 소셜 로그인 기능 구현하기 (0) | 2024.06.20 |
---|---|
supabase 유저 이메일 말고 유저 아이디가 일치하면 업데이트 가능하도록 정책 설정하기 (0) | 2024.06.19 |
Supabase 회원가입 기능 구현하기 (0) | 2024.06.18 |
Supabase Quick Start (0) | 2024.06.17 |
Glitch를 이용해서 json-server 생성하기 (1) | 2024.06.14 |
댓글