Day31_Yummy-yummy!_팀프로젝트3_요리레시피_뉴스피드사이트만들기_02

정소현·2024년 8월 30일
0

스파르타

목록 보기
39/50

☘️ 본격적인 프로젝트 개발 시작

나는 이번에 프로젝트, 깃 초기세팅을 하고 supabase를 이용해 회원가입, 로그인, 로그아웃을 하고 해당 사이트들의 디자인을 맡았다. 회원가입할 때의 정보를 auth 에서 public으로 연결해 정보들을 사용할 수 있도록 연결해주었다.
굉장히 시간을 많이 소비하고 헤매서 너덜너덜 해졌지만... 많은 것을 배웠다...!

🌼 1. supabase 셋업 & 환경변수 설정

  • 먼저 supabase를 연결하기 위해 supabase 회원가입을 진행하고
  • 프로젝트를 생성한 후 데이터베이스 설정을 해주었다.
  • 이후 나의 vscode와 연결을 시켜주기 위해 아래 명령어를 입력해주었다.
yarn add @supabase/supabse-js
  • 프로젝트 URL과 API(anon)키를 연결해주었다.
  • 환경변수(.env.local) 셋업
    - supabase의 계정정보 등 보안이 필요한 정보들은 .env.local파일에 담아서 github에 공유되지 않도록 했다.
    • root directory에 .env.local이라는 파일을 만들었다.
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;

🌼 2. 로그인, 회원가입 사이트 구조 만들기

아래는 styled-components와 custom hook 으로 만든 회원가입, 로그인 사이트이다.


  • 공통적으로 input과 button이 많이 들어가기 때문에 이를 컴포넌트화 해주어 재사용할 수 있게 처리해주었다.
    다른 페이지에서도 input과 button이 많이 사용되는데 디자인적으로 많은 차이가 있어서 추후에 팀원들과 상의하에 정리해볼 예정이다.
// 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를 받지않아 그렇게 된 것이었다...
이번에 크게 데인만큼 조심 또 조심할 것 같다.

🌼 3. 로그인, 로그아웃, 회원가입 기능개발

맨 처음 기능개발을 시작했던 페이지는 signup페이지였는데 코드를 작성하다보니 공통적으로 쓰이는 상태관리 요소가 정말 많아 react hook context 를 사용하여 코드를 작성해주었다.

  • 먼저 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,
      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>
  );
};
  • 이후 Context를 사용하는 커스텀 훅을 만들어 사용성에 편리성을 주었다.
  • Router.jsx에서 AuthProvider를 전역에 뿌려주었다.
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>
    </>
  );
};

✨ 회원가입 기능

  • 먼저 input으로 받아야 할 값은 이메일, 비밀번호, 비밀번호확인, 닉네임 이었다. 따라서 useState()를 사용해 각각의 상태를 만들어주고 onChange를 사용해 각각의 상태를 저장할 수 있도록 만들어주었다.
  • supabase docs에있는 회원가입 방법을 사용해 각각의 데이터를 저장시켜주고 비밀번호와 비밀번호확인란이 일치하지 않거나 모든 칸을 입력하지 않았으면 에러메세지를 띄워 사용자가 조건에 부합하게 작성할 수 있도록 해주었다.
  • 회원가입에 성공하면 useNavigate()를 사용해 로그인 페이지로 이동할 수 있도록 만들었다.
// 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");
    }
  };

✨ 로그인 기능

  • 여기서도 supabase docs의 로그인 방법을 넣어주고
    로그인을 실패하면 에러 메세지를 띄워주고 성공메세지를 초기화.
    로그인을 성공하면 에러메세지를 초기화하고 홈 화면으로 이동할 수 있도록 만들었다.
  • 로그인 기능을 개발하면서 header에 있는 로그아웃 이 사용자가 로그인을 했을 때는 로그아웃과 마이페이지가 나타나고 사용자가 로그아웃인 상태에서는 로그인 버튼과 회원가입이 나타나도록 변경해야겠다고 생각했다.
  • 사용자가 로그인 상태인지 확인할 수 있는 state를 추가로 만들었고
    맨 처음 로딩했을 때 로그인 상태인 지 확인할 수 있도록 useEffect()를 활용하여 첫번째 렌더링 때 체크할 수 있도록 만들어주었다.
  • 로그아웃 기능도 만들어준 후 로그인 상태를 명시적으로 초기화를 시켜주었다.
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);
  • 각각의 sign-up, sign-in 페이지에서 정보를 받아올 수 있도록 로직을 만들어준 뒤 useAuth를 불러와 구조분해할당으로 필요한 함수와 state들을 꺼내왔고 연결해주었다.
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를 연결하여 데이터를 다방면으로 활용할 수 있도록 연결해주는 트리거를 만들었다.

  1. supabase Table Editor에서 새로운 테이블을 만들어주고
    columns를 생성해 id-uuid, email-text, nickname-text
    로 지정해준 후 foriegn(외부) 를 auth -user 로 연결해주어 정보를 받아올 수 있게 하였다.
  2. SQL Editor에서 트리거를 만들어 auth -user에 정보가 저장되면 내가 생성해 둔 userinfo에도 정보가 저장되도록 만들어주었다.
-- 새 함수 생성
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 사용을 느끼며 성장하고 발전하고 있음을 느낀다
아직 갈 길이 많이 멀지만 오늘도 차근차근 나아가보자!!

0개의 댓글