Firebase로 Auth 기능을 구현해보았다.

·2024년 4월 29일
0

봄내음 프로젝트

목록 보기
5/13


5일동안 Firebase로 Auth 기능들을 구현해보았다. 처음 사용해보다보니 초기 설정에 어려움을 많이 겪었는데 그래도 조금씩 진행해나가다보니 필수적인 Auth 기능들은 거의 구현이 끝났다. 물론 기능적인 한계에 의해 구현을 못했거나, 의도했던 Flow와는 다르게 흘러가게 바뀌었다. 그 점들을 찬찬히 설명해보면서 기록을 남기고자 한다.

Firebase 설정

Firebase 이동하기
위 링크로 이동하게 되면, 아래와 같은 화면이 나타나게 된다. 우측 상단에 Go to console 버튼을 클릭한다.

console로 이동하면 다음과 같은 화면을 볼 수 있을 것이다. 현재 프로젝트를 생성한 뒤라, bomnae-mmm이 있지만, 프로젝트 추가를 클릭하여 봄내음 프로젝트를 생성한다.

프로젝트 이름과 기타 설정을 해준 뒤, 프로젝트를 생성해주면 아래와 같은 화면이 나타날 것이다.

앱에 Firebase를 추가하기 위해 아래 부분을 이용할 수 있다. 봄내음은 웹이므로, 웹을 선택해주면 된다.

앱을 등록해주면 다음과 같은 화면이 나타날 것이다. firebase를 설치하고, 코드를 복사해준다.

src/firebase/config.jsx를 만들고 복사한 코드를 붙여준다. (현재 보이는 코드는 환경변수가 설정되어 있기 때문에 예시와 다르다.) 마지막 줄에 appAuth와 appFireStore를 다음과 같이 작성해준 뒤 export 해준다. (필수적이진 않지만, 이렇게 해두면 편리하다.)

API key와 같이 민감한 정보들을 그대로 두면 안 되기 때문에 이에 대한 처리를 해주어야 한다. firebaseConfig 안에 있는 값들을 복사해준다. root에 .env 파일을 생성한다. (꼭! src 폴더가 아닌 root에 생성해야 한다.)

.env 파일에 복사한 값을 붙여넣기 해준 다음, 아래와 같이 변경해줄 것이다.
1. apiKey, authDomain 등 변수 명을 REACT_APP_변수명 형태로 바꾸어준다.
2. 변수 사이에 쉼표(,)를 모두 제거해준다.
3. 변수명: "값" 형태를 REACT_APP_변수명 = "값" 형태로 바꾸어준다.
4. (선택) 따옴표("")를 제거해준다. -> 제거하지 않아도 되지만, 간혹 오류가 날 때 따옴표를 제거하거나 붙여주면, 해결된다.

그 뒤, config.jsx에서 값들을 전부 process.env.REACT_APP_변수명 형태로 바꾸어준다.
※ 이 곳에서는 쉼표와 :을 사용한다. 값만 바꿔줄 뿐 다른 부분은 건들지 않도록 주의한다.

만약 위 방법을 다 실행했을 때 오류가 발생한다면 아래 항목들을 순서대로 확인해보자.

.env가 root에 있는가? src 폴더안에 있으면 안 된다.
.env 파일에서 변수들 사이에 쉼표( , )를 넣었는가? 쉼표를 제거해야 한다.
환경변수에 따옴표를 넣거나 빼어보았는가?
Ctrl + C와 npm start로 재시작을 해보았는가?

필자는 4번째에서 문제가 있었다. npm start를 해보니 잘 작동했다... (내다버린 1시간...)

이렇게하면 Firebase를 사용할 준비가 끝난다.

회원을 생성하기 위해 회원가입부터 구현해보았다. 회원가입을 진행하기 위해 로그인 방법을 결정해주어야 한다. 초기 계획은 아이디/비밀번호와 카카오톡 로그인이었다. 하지만, Firebase에서 카카오톡 로그인은 지원하지 않는다. 못하는 것은 아니지만..! 하지만, 구현이 단순하지는 않아서 고민을 하게 되었다. 고민을 하게 된 주 원인은 서버 적인 부분보다 Front적인 부분에 집중하고 싶어서였다. Front 부분만 구현하기 아쉬워서 대안으로 Firebase를 선택한 것이지. 틈 하나 없는 완벽한 서버를 만들고 싶었던 것은 아니기에.. 우선은 필수적인 기능들만 구현하기로 했다. 물론 구글 로그인은 Firebase에서 제공하기 때문에 개발이 쑨조롭게 끝나면 추가적으로 구현해보기로 했다. 즉, 회원가입은 '아이디/비밀번호'만을 사용하고자 했는데, 이마저도 살짝 변경되었다.

원래 아이디/비밀번호를 이용하고, 이메일을 추가적으로 받아 아이디/비밀번호 찾기에 이용하고 싶었으나, Firebase에서는 이메일 형태의 로그인 서비스를 제공했다.

하지만, 이메일 형태로 결정하니 아이디 찾기가 불가능하다는 이야기를 들었다. 본인인증을 구현해서라도 아이디 찾기를 구현해야 하나에 대한 고민이 정말 많이 들었다. 하지만.. 위와 같은 이유로 아이디 찾기를 제공하지 않기로 했다.

참고사항

작성된 코드는 현재 완전히 개발된 것이 아닌 1차 개발 코드이므로, 추후 수정과정을 거칠 예정입니다. 또한, 에러처리는 테스트를 위한 단순 에러처리입니다. 처리하지 못한 에러는 추후 처리 될 예정입니다.

회원가입

<Input type="text" placeholder="아이디(이메일)" onChange={(e) => setUserEmail(e.target.value)} />
<Input type="password" placeholder="비밀번호" onChange={(e) => setPassword(e.target.value)} />
<Input type="password" placeholder="비밀번호 확인" onChange={(e) => setcheckPW(e.target.value)} />
<Input type="text" placeholder="닉네임" onChange={(e) => setNickname(e.target.value)} />
<Button onClick={validation}>회원 가입</Button>

회원가입 시, 이메일/비밀번호/비밀번호 확인/닉네임을 입력받고 회원 가입을 클릭하게 되면 validation 함수가 실행된다.

const validation = () => {
	if (userEmail.trim() !== '' && password.trim() !== '' && checkPW.trim() !== '' && nickname.trim() !== '') {
		if (password === checkPW) {
			let idCheck = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/i; //이메일 형식 테스트
			if (idCheck.test(userEmail)) {
				let pwCheck = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-_])(?=.*[0-9]).{8,}$/; //특수문자 포함 8자리 이상
				if (pwCheck.test(password)) {
					register();
				} else {
                
	**에러 처리는 생략**
    
};

validation에서는 모든 입력 값이 ""이 아닌지를 확인한다. 혹여나 스페이스만 입력했을 경우를 대비하기 위해 trim을 공백을 제거해주었다.
모든 입력 값이 ""이 아닐 경우, 비밀번호와 비밀번호 확인에 입력한 값이 일치하는지 확인해준다.
비밀번호가 일치할 경우, idCheck 정규식을 통해 email 형식에 맞춰 작성되었는지 확인해준다.
email 형식일 경우, pwCheck 정규식을 통해 특수문자 포함 8자리 이상으로 입력되었는지 확인해준다.
모든 검사를 통과할 경우 register 함수를 실행해준다.

const register = async () => {
	try {
      setLoading(true);
      const credential = await createUserWithEmailAndPassword(appAuth, userEmail, password);
      const user = credential.user;
      const userDoc = doc(collection(appFireStore, 'users'));
      await setDoc(userDoc, {
        uid: user.uid,
        email: userEmail,
        nickname: nickname,
        profile_image: '',
        is_admin: false,
        created_at: new Date().toISOString(),
      });
      navigate('/');
    } catch (error) {
      console.log(error.message);
    } finally {
      setLoading(false);
    }
};

createUserWithEmailAndPassword()를 이용해 입력받은 email과 비밀번호로 회원을 생성해준다. 프로젝트에서는 유저 정보를 이용해 프로필 변경 등 이용해야 할 정보들이 있기 때문에 해당 입력 값들을 DB에 저장해주어야 한다. Firebase에서는 DB도 제공해주는데, 실시간 데이터베이스와 FireStore 두 가지가 있다.
데이터베이스 선택: Cloud Firestore 또는 실시간 데이터베이스 둘의 차이는 공식 문서를 참고하면 된다. FireStore가 쿼리가 더 쉽다고 하기도 했고, 실시간 데이터베이스보다 최근에 나온 DB이기에 FireStore를 선택하게 되었다. 일반적으로 FireStore를 많이 사용한다고 한다.


Firestore는 빌드-Firesotre Database에서 확인할 수 있다.

데이터베이스 만들면 다음과 같은 화면이 나타나는데 위치를 Seoul로 선택해주면 된다.

이제 이 데이터베이스에 user 정보를 저장할 것이다.

const credential = await createUserWithEmailAndPassword(appAuth, userEmail, password);
const user = credential.user;
const userDoc = doc(collection(appFireStore, 'users'));
await setDoc(userDoc, {
	uid: user.uid,
	email: userEmail,
	nickname: nickname,
	profile_image: '',
	is_admin: false,
	created_at: new Date().toISOString(),
});

데이터베이스에 users 콜렉션에 해당 유저를 저장하고자 한다. setDoc()을 이용하고, 저장할 값들을 작성하면 된다. user를 구분하기 위해 uid를 저장하고, profile_image를 ''(기본값)으로, is_admin을 false로 저장한다. is_admin은 관리자 계정인지 확인하기 위함이므로, 특정 경우를 제외하고 항상 false이다.

회원가입이 성공하면 다음과 같이 유저 정보가 DB에 저장되게 된다.

로그인/로그아웃

회원가입에 성공했으니 로그인/로그아웃을 구현해보자.

<Input type="text" placeholder="아이디(이메일)" onChange={(e) => setUserEmail(e.target.value)} />
<Input
	type="password"
	placeholder="비밀번호"
	onChange={(e) => setPassword(e.target.value)}
	onKeyUp={(e) => checkCapsLock(e)}
/>
{capsLockFlag && <Caps>Caps Lock이 켜져 있습니다.</Caps>}
{logInFail && <Caps>로그인 실패 계정이 올바른지 확인해주세요.</Caps>}
<Button onClick={validation}>로그인</Button>

로그인 버튼을 클릭하면 validation이 실행된다. validation은 회원가입과 비슷하므로 생략한다. 로그인에서는 회원가입보다는 유한 형식 검사를 한다. 모든 입력이 되었는지, 아이디가 이메일 형식인지만 검사한다. 형식 검사가 성공하면 login 함수를 실행한다.

const login = async () => {
	try {
      const user = await signInWithEmailAndPassword(appAuth, userEmail, password);
      const user_docs = await getDocs(query(collection(appFireStore, 'users'), where('email', '==', user.user.email)));
      user_docs.forEach((u) => {
        setProfileImg(u.data().profile_image);
        setIsAdmin(u.data().is_admin);
        setMemberId(u.data().uid);
      });
      setLogIn(true);
      navigate('/');
    } catch (error) {
      console.log(error);
      setLogInFail(true);
    } finally {
      setLoading(false);
    }
};

signInWithEmailAndPassword()를 이용해 이메일 로그인을 진행한다. 로그인한 뒤, 회원 정보를 redux에 저장해주어야 하므로, DB에서 회원 정보를 가져와야 한다. getDocs를 이용해 email이 같은 user 데이터를 가져온 뒤, 해당 데이터에 profile_img와 is_admin, uid를 각각 redux에 저장해준다. 로그인이 성공하면, navigate를 이용해 main으로 이동해준다.

const handleLogOut = async () => {
    try {
      await signOut(appAuth);
      setLogIn(false);
    } catch (error) {
      console.log(error);
    }
};

로그아웃은 완전 간단하게 구현할 수 있다. 로그아웃은 Header에서 수행되므로, Header에 로그아웃 버튼이 클릭되면 handleLogOut이 수행된다. signOut()을 통해 로그아웃을 할 수 있다. 로그아웃이 되면 redux 변수 값을 변경해주어야 한다. (현재는 테스트용 코드로 필수적인 login 값만 변경되었다.)

redux 값들은 새로고침하게 되면 state가 유지되지 않는다. 그렇기 때문에 state를 유지시켜주어야 한다. storage에 저장해주는 방식도 있지만, 더 간단하게 redux 값들을 유지해주는 라이브러리가 있어 이번 프로젝트에서는 redux-persist를 사용하게 되었다.
redux에 대한 자세한 내용은 프로젝트 끝난 뒤에, 자세하게 올라올 예정이다. redux에 대한 자세한 공부가 없이 사용되었기 때문에 조금 더 공부할 필요성이 있기 때문에... 완벽하게 공부한 뒤에 상세하게 올리고자 한다.

사용자 재인증

Firebase에서는 최근에 로그인 한 기록이 없는 경우 인증 정보가 만료되어 회원 탈퇴, 비밀번호 변경 등 사용자 정보 변경이 불가능하다. 그렇기 때문에 밑에 언급할 회원 탈퇴/비밀번호 변경은 사용자 재인증을 거친 뒤, 인증된 경우에만 수행하도록 하였다.

const reAuthentication = async () => {
    let password = '';
    try {
      const { dismiss } = await Swal.fire({
        title: '본인 인증',
        html: '비밀번호를 입력해주세요.',
        input: 'password',
        inputPlaceholder: '비밀번호',
        showCancelButton: true,
        focusConfirm: false,
        preConfirm: () => {
          password = Swal.getInput().value;
          if (!password) {
            Toast.fire({
              icon: 'error',
              html: '비밀번호를 입력해주세요.',
            });
            return false;
          }
        },
      });

      if (dismiss === Swal.DismissReason.cancel) {
        return false;
      }

      if (password) {
        const user = appAuth.currentUser;
        const email = user.email;
        const credential = EmailAuthProvider.credential(email, password);
        await reauthenticateWithCredential(user, credential);
        return true;
      }

      return false;
    } catch (error) {
      Toast.fire({
        icon: 'error',
        html: '비밀번호가 일치하지 않습니다.',
      });
      return false;
    }
};

prompt를 통해 비밀번호를 입력받는다. 비밀번호를 입력받은 경우, reauthenticateWithCredential()을 통해 재인증을 진행한다. 재인증이 성공할 경우, true를 반환하고, 비밀번호를 입력하지 않거나, 재인증이 실패하거나 prompt를 닫는 등 재인증이 성공하지 않은 모든 경우에는 false를 반환한다.

회원 탈퇴

const handleDelete = async () => {
    try {
      if (await reAuthentication()) {
        const user = appAuth.currentUser;
        await deleteUser(user);
        const usersCollection = collection(appFireStore, 'users');
        const q = query(usersCollection, where('uid', '==', id));
        const querySnapshot = await getDocs(q);
        const userDocRef = querySnapshot.docs[0].ref;
        await deleteDoc(userDocRef);
        handleLogOut();
        Toast.fire({
          icon: 'success',
          html: '정상적으로 탈퇴되었습니다.<br>이용해주셔서 감사합니다.',
        });
      }
    } catch (error) {
      Toast.fire({
        icon: 'error',
        html: '오류가 발생했습니다.',
      });
    }
};

재인증이 true를 반환할 경우, deleteUser()를 이용해 회원 탈퇴를 수행한다. 회원이 탈퇴할 경우, DB에서도 정보가 삭제되어야 하므로, redux에 저장된 id(uid) 값과 같은 uid를 가지는 users 데이터를 찾는다. 해당 데이터를 deleteDoc()을 이용해 DB에서 삭제해준다. 회원 탈퇴가 성공한 경우, 해당 계정은 로그아웃을 진행하여, Main으로 이동해준다.

비밀번호 변경

const handleReset = async () => {
    try {
      const user = appAuth.currentUser;
      const res = await updatePassword(user, password);
      await handleLogOut();
      Toast.fire({
        icon: 'success',
        html: '비밀번호가 재설정됐습니다.<br>재로그인해주세요.',
      });
    } catch (error) {
      Toast.fire({
        icon: 'error',
        html: '오류가 발생했습니다.',
      });
    } finally {
    }
};

비밀번호 변경 시, 비밀번호와 비밀번호 확인을 입력받고, 두 값이 모두 입력되고 두 값이 같으며, 비밀번호 형식을 지켰을 경우 handleReset을 실행한다. updatePassword()를 이용해 비밀번호를 변경해주고, 로그아웃 처리를 하며 Main으로 이동해준다.

아직 해결하지 못한 부분은 기존 비밀번호와 바뀐 비밀번호가 같아도 비밀번호 변경이 진행된다는 점이다. (물론 문제는 없지만, 같을 경우 비밀번호를 바꾸지 않는 게 맞지 않을까 싶으면서도.. 기존 비밀번호와 일치하다고 표시할 방법도 표시하는 게 맞는지도 의문이 든다. 그렇다고 DB에 비밀번호를 hash로 저장한 뒤, 비교하는 게 효과적일까.. 싶은 생각이 들었다.) 이 부분은 일단 좀 더 고민을 해봐야 할 것 같다. 큰 오류는 아니니까!

또 해결해야 하는 부분은 비밀번호 찾기 -> 비밀번호 재설정 부분이다. 로그인을 한 상태에서는 의도대로 변경이 가능했지만, 비로그인 시에는 의도대로 변경이 불가능하기 때문이다. 일단 Firebase에서는 비밀번호 재설정 이메일을 전송해주는 기능이 있다. 하지만, 부가적인 기능이 없다. 가장 큰 문제는 현재 봄내음의 비밀번호 조건은 특수문자 일부 포함, 8글자 이상이다. 하지만 Firebase에서의 기본 비밀번호는 6글자 이상이다. 이렇게 되면, 조건이 달라지게 된다. 이를 Firebase에 맞추어야 할지... 비회원 비밀번호 재설정 방식을 이메일이 아닌 다른 방식으로 고민해봐야 하는지... 이 부분은 고민이 좀 더 필요할 것 같다.


Firebase 쉽다매!!! 쉽다매!!! 쉽다매!!! 물론... 직접 서버를 구현했다면 이런 얘기 절대 못했겠지만......... 하루하루 왜 안되지? 이게 왜 안 돼? 이건 왜 돼?에 익숙해져가고 있다... 해당 내용은 사실 2일정도면 다 해결할 수 있는 간단한 구현이다. 이걸 왜 5일이나 구현했냐면... 갑자기 공태기 와서.. 공부가 넘 하기 싫었다랄까나 ㅎㅎㅎㅎㅎ 그래서 너무 억지로 개발하면, 재미도 없고 남는 것도 없을 것 같아서 하루 4시간씩만 개발했다. 기능 하나하나씩 해나가고 있다. 예상했던 기간보다 여유롭기도 하고 그래서 빨리 끝내는 개쩌는 모습을 보여주자! 했지만.. 그냥 천천히 대신 완벽하게 하려고 노력 중이다. 아직 에러처리도 해야하고 부가적인 코드들을 더 넣어줘야 하지만 그래도 회원 기능은 거의 구현이 끝났다. 굿.
다음에는 스토리지를 해결한 뒤에 돌아올 것이다. 이미지 업로드만 하면, 정말 프로젝트 50%는 끝난 것이다. 굿. 프로젝트 끝내면 엽떡에 허니콤보 먹어야지.

참고자료

.env 파일에서 API key 관리하던 중 에러가..?
[React] Firebase Authentication
Firebase - 웹에서 Firebase auth로 로그인/회원가입 구현

profile
Frontend🍓

0개의 댓글