☘️ 본격적인 프로젝트 개발 시작
나는 이번에 프로젝트, 깃 초기세팅을 하고 supabase를 이용해 회원가입, 로그인, 로그아웃을 하고 해당 사이트들의 디자인을 맡았다. 회원가입할 때의 정보를 auth 에서 public으로 연결해 정보들을 사용할 수 있도록 연결해주었다.
굉장히 시간을 많이 소비하고 헤매서 너덜너덜 해졌지만... 많은 것을 배웠다...!
yarn add @supabase/supabse-js
ex) //.env.local
VITE_SUPABASE_URL = "MY_SUPABASE_URL"
VITE_SUPABASE_KEY = "MY_SUPABASE_KEY"
// 나는 vite로 만들어 주었기 때문에 .env.local에 VITE가 꼭 붙어야한다.
// supabaseClient.js
import { createClient } from "@supabase/supabase-js";
const SUPABASE_PROJECT_URL = import.meta.env.VITE_SUPABASE_URL;
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_KEY;
const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY);
export default supabase;
아래는 styled-components와 custom hook 으로 만든 회원가입, 로그인 사이트이다.
// SignInput.jsx
import React from "react";
import styled from "styled-components";
const SignInput = ({ placeholder, type, onChange }) => {
return (
<SignInputStyled
type={type}
placeholder={placeholder}
onChange={onChange}
></SignInputStyled>
);
};
const SignInputStyled = styled.input`
width: 350px;
height: 40px;
border: 1px solid var(--gray3-color);
border-radius: var(--border-radius-sm);
font-size: 1.5rem;
font-weight: 500;
font-family: var(--font-family);
text-indent: 20px;
&::placeholder {
font-size: 10px;
}
`;
export default SignInput;
props로 placeholder,type, onChange를 받아 각각의 기능들을 구현할 수 있도록 만들었다.
Supabase docs에 있는 회원가입, 로그인 예시를 사용해 실행을 했는데 계속해서 422에러가 발생했다. 여기저기 물어보고 약 6~7시간 동안 해결을 못 하고 코드를 처음부터 썼다가 지웠다가를 반복했는데 한참을 지나 발견했다..
props로 onChange를 빼먹었다는 사실을 ....
console.log으로 email, id, nickname을 찍어보았을 때 아무것도 받지않아 supabase 데이터베이스나 API key가 잘못된 줄 알았는데 props를 받지않아 그렇게 된 것이었다...
이번에 크게 데인만큼 조심 또 조심할 것 같다.
맨 처음 기능개발을 시작했던 페이지는 signup페이지였는데 코드를 작성하다보니 공통적으로 쓰이는 상태관리 요소가 정말 많아 react hook context 를 사용하여 코드를 작성해주었다.
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [nickname, setNickname] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [signIn, setSignIn] = useState(false);
const navigate = useNavigate();
// user 로그인 상태 확인
const checkSignIn = async () => {
const { data: session } = await supabase.auth.getSession();
const isSignIn = !!session?.session;
setSignIn(isSignIn);
};
useEffect(() => {
// 로그인 확인
checkSignIn();
}, []);
// user 회원가입
const handleSignUp = async () => {
if (password !== confirmPassword) {
setError("비밀번호가 일치하지 않습니다");
return;
} else if (!email || !password || !confirmPassword || !nickname) {
setError("모든 칸을 입력해주세요");
return;
}
// 회원가입
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
nickname,
},
},
});
if (error) {
setError("회원가입 실패: " + error.message);
setSuccess("");
return;
} else {
setError("");
setSuccess("회원가입 성공!");
navigate("/sign-in");
}
};
// user 로그인
const handleSignIn = async () => {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
options: {
data: {
nickname,
},
},
});
if (error) {
setError("로그인 실패: " + error.message);
setSuccess("");
} else {
setError("");
setSuccess("로그인 성공!");
await checkSignIn();
navigate("/");
}
};
// user 로그아웃
const handleLogout = async () => {
await supabase.auth.signOut();
setSignIn(false); // 로그아웃 후 로그인 상태 초기화
navigate("/"); // 홈으로 이동
};
return (
<AuthContext.Provider
value={{
email,
setEmail,
password,
setPassword,
confirmPassword,
setConfirmPassword,
nickname,
setNickname,
error,
success,
handleSignUp,
handleSignIn,
handleLogout,
signIn,
checkSignIn,
}}
>
{children}
</AuthContext.Provider>
);
};
const Router = () => {
return (
<BrowserRouter>
<AuthProvider>
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/detail" element={<DetailPage />} />
<Route path="/mypage" element={<MyPage />} />
<Route path="/write" element={<WritePage />} />
<Route path="/sign-in" element={<SignInPage />} />
<Route path="/sign-up" element={<SignUpPage />} />
</Routes>
</Layout>
</AuthProvider>
</BrowserRouter>
);
};
맨 처음에는 App.jsx에서 AuthProvider를 전역에 뿌려주었다.
지금 App.jsx는
const App = () => {
return (
<>
<GlobalStyle />
<Router />
</>
);
};
이런 구조를 띄고 있는데 이전 코드는 이런 구조를 띄고 있었다.
이후 로그인, 회원가입을 한 이후 홈페이지화면으로 useNavigate를 사용하려고 하니 AuthProvider가 Router를 띄고 있어 이동이 불가해 지금과 같은 형태로 변경해주었다.
const App = () => {
return (
<>
<GlobalStyle />
<AuthProvider>
<Router />
</AuthProvider>
</>
);
};
// user 회원가입
const handleSignUp = async () => {
if (password !== confirmPassword) {
setError("비밀번호가 일치하지 않습니다");
return;
} else if (!email || !password || !confirmPassword || !nickname) {
setError("모든 칸을 입력해주세요");
return;
}
// 회원가입
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
nickname,
},
},
});
if (error) {
setError("회원가입 실패: " + error.message);
setSuccess("");
return;
} else {
setError("");
setSuccess("회원가입 성공!");
navigate("/sign-in");
}
};
import React, { createContext, useState, useContext, useEffect } from "react";
import supabase from "../../base-camp/supabaseClient";
import { useNavigate } from "react-router-dom";
// Context 생성
const AuthContext = createContext();
// Provider 컴포넌트
export const AuthProvider = ({ children }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [nickname, setNickname] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [signIn, setSignIn] = useState(false);
const navigate = useNavigate();
// user 로그인 상태 확인
const checkSignIn = async () => {
const { data: session } = await supabase.auth.getSession();
const isSignIn = !!session?.session;
setSignIn(isSignIn);
};
useEffect(() => {
// 로그인 확인
checkSignIn();
}, []);
// user 회원가입
const handleSignUp = async () => {
if (password !== confirmPassword) {
setError("비밀번호가 일치하지 않습니다");
return;
} else if (!email || !password || !confirmPassword || !nickname) {
setError("모든 칸을 입력해주세요");
return;
}
// 회원가입
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
nickname,
},
},
});
if (error) {
setError("회원가입 실패: " + error.message);
setSuccess("");
return;
} else {
setError("");
setSuccess("회원가입 성공!");
navigate("/sign-in");
}
};
// user 로그인
const handleSignIn = async () => {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError("로그인 실패: " + error.message);
setSuccess("");
} else {
setError("");
setSuccess("로그인 성공!");
await checkSignIn();
navigate("/");
}
};
// user 로그아웃
const handleLogout = async () => {
await supabase.auth.signOut();
setSignIn(false); // 로그아웃 후 로그인 상태 초기화
navigate("/"); // 홈으로 이동
};
return (
<AuthContext.Provider
value={{
email,
setEmail,
password,
setPassword,
confirmPassword,
setConfirmPassword,
nickname,
setNickname,
error,
success,
handleSignUp,
handleSignIn,
handleLogout,
signIn,
checkSignIn,
}}
>
{children}
</AuthContext.Provider>
);
};
// Context를 사용하는 커스텀 훅
export const useAuth = () => useContext(AuthContext);
import React from "react";
import styled from "styled-components";
import SignInput from "../SignInput";
import SignButton from "../SignButton";
import Logo from "../../../components/Logo";
import { useAuth } from "../../../contexts/AuthContext";
import { useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
const SignUpPage = () => {
const {
email,
setEmail,
password,
setPassword,
confirmPassword,
setConfirmPassword,
nickname,
setNickname,
error,
success,
handleSignUp,
} = useAuth();
return (
<>
<Container>
<LogoBox>
<Logo />
</LogoBox>
<SignUp>
<SignP>간편가입</SignP>
<InputBox>
<InputLabel>이메일</InputLabel>
<SignInput
type="email"
placeholder="이메일 입력"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<InputLabel>비밀번호</InputLabel>
<SignInput
type="password"
placeholder="비밀번호 입력"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<InputLabel>비밀번호 재입력</InputLabel>
<SignInput
type="password"
placeholder="비밀번호 재입력"
onChange={(e) => setConfirmPassword(e.target.value)}
value={confirmPassword}
/>
<InputLabel>닉네임</InputLabel>
<SignInput
type="text"
placeholder="닉네임 입력"
onChange={(e) => setNickname(e.target.value)}
value={nickname}
/>
</InputBox>
<ButtonBox>
<SignButton
backgroundColor="--green-color"
textColor="white"
onClick={handleSignUp}
>
가입완료
</SignButton>
<Link to="/">
<SignButton backgroundColor="--beige-color" textColor="black">
취소
</SignButton>
</Link>
</ButtonBox>
{error && <ErrorMessage>{error}</ErrorMessage>}
{success && <SuccessMessage>{success}</SuccessMessage>}
</SignUp>
</Container>
</>
);
};
const Container = styled.div`
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const SignUp = styled.div`
width: 350px;
background-color: var(--gray4-color);
display: flex;
flex-direction: column;
gap: 20px;
text-align: center;
padding: 20px;
border-radius: var(--border-radius);
`;
const SignP = styled.p`
font-weight: 700;
font-size: 1.5rem;
`;
const LogoBox = styled.div`
width: 350px;
height: 100px;
display: flex;
justify-content: center;
margin-bottom: 40px;
`;
const InputLabel = styled.label`
text-align: left;
font-size: 15px;
font-weight: 700;
`;
const InputBox = styled.div`
width: 350px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 10px;
`;
const ButtonBox = styled.div`
font-family: var(--font-family);
margin: 0 auto;
width: 350px;
display: flex;
flex-direction: column;
gap: 1rem;
`;
const ErrorMessage = styled.p`
color: red;
font-size: 1rem;
`;
const SuccessMessage = styled.p`
color: green;
font-size: 1rem;
`;
export default SignUpPage;
기능을 개발한 이후 팀원분의 요청으로 supabase public 에 auth를 연결하여 데이터를 다방면으로 활용할 수 있도록 연결해주는 트리거를 만들었다.
-- 새 함수 생성
CREATE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
INSERT INTO public.userinfo (id, email, nickname)
VALUES (NEW.id, NEW.email, NEW.raw_user_meta_data->>'nickname');
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 새 트리거 생성
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
SQL은 잘 모르는 부분이라 열심히 서치해가면서 겨우겨우 연결에 성공했다... 이것도 배우면 좋을 것 같다...!
어제부터 거의 6~7시간동안 해결하지 못했던 문제가 너무나도 간단한 문제였다는 사실에 다시 한번 허탈함이 있었지만 그럼에도 그럴만한 가치가 있었다고 생각한다. 하나의 문제에 매달려 결국 해결했다고 생각하기 때문이다...!
처음 사용해보는 supabase에 회원가입, 로그인, 로그아웃 기능이었지만 이전보다는 능숙해진 context사용 비동기 함수의 사용, styled-components 사용을 느끼며 성장하고 발전하고 있음을 느낀다
아직 갈 길이 많이 멀지만 오늘도 차근차근 나아가보자!!