Firebase의 Authentication만 사용하고 싶다면

Woody·2021년 5월 13일
1
post-thumbnail

인트로

나의 경우 하나의 서비스를 만들 때

'로그인 기능을 넣어 말아?'

이 질문을 가장 먼저 하게 된다. 구현하는 데 꽤 오랜 시간이 걸려 시간을 쪼개 개발하고 싶지만, 뒤로 미루자니 다른 기능 구현이 안되는 정말 골칫거리가 따로 없다. 그래서 웬만하면 인증 없이 돌아가는 서비스를 생각해보려 노력한다(하지만 그런 서비스가 있기는 한지...).


그래서 로그인 기능이 생기면 이 User table을 고민해야 한다.


언제 어디서나 쉽게 사용되는 유저 테이블 (출처: https://www.freewebmentor.com/2016/07/sql-user-table-schema-yii2-starter-kit.html)


근데 유저 정보를 내 서버에 저장하기란 너무 부담스러운 일이다. 내 보안 실력은 보잘것 없기에 툭하면 이렇게 털리기 마련이며

실제로 프로젝트 중간에 MongoDB가 실제로 이거랑 비슷하게 털렸었다... 그냥 밀어버리고 다시 DB를 파긴 했지만. 요즘은 비트코인 말고 도지코인을 달라고 할지도 모르겠다 (출처: https://www.hackread.com/47-of-online-mongodb-databases-hacked-demanding-ransom/)


그래서 저장할 때는 비밀번호를 암호화해서 저장(salt 등등)하고, jwt 토큰을 만들고 로그인할 때는 또 decode 해서 비교하고!
그러다보면 '실제 서비스도 아닌데 이렇게 인증 열심히 구현해서 뭐하지' 하는 자괴감이 들기도 한다.

그래서 이번 프로젝트에서는 Firebase의 authentication을 사용해보기로 했다. 인증에 대한 부담을 줄이기 위해서였다. 그리고 구글이 나보다 보안은 철저히 지켜줄테니ㅎㅎ


회원가입

Firebase의 authetication 연동 방법은 공식 문서나 여러 블로그에 잘 설명되어 있으니 여기에 굳이 적지 않겠다. 나는 email/password 인증 방식을 사용하기로 했고 회원가입 및 로그인은 antd 라이브러리의 modal을 이용해 구현했다.
++ 여기서 firebase는 이미 initialzed된 instance이다.

// SignupModal.jsx
message.loading("회원가입 중입니다", 0);
const { email, password, realname, nickname } = await form.validateFields();
try {
  await firebase.auth().createUserWithEmailAndPassword(email, password);
  const uid = firebase.auth().currentUser.uid;
  const serverResult = await signup({ uid, realname, nickname });  // api server
  if (serverResult.success) {
    message.destroy();
    form.resetFields();
    hideSignupModal();
    dispatch(userLogin());
  } else {
    message.destroy();
    message.error(serverResult.message);
  }
} catch (e) {
  message.destroy();
  firebaseSignupErrorHandler(e);
}

다른 부분은 일반적인 구현과 크게 다를 것이 없지만 인증과 서비스 서버를 분리한 상황인 지금, 내가 생각하는 포인트는 이 부분이다.

const uid = firebase.auth().currentUser.uid;
const serverResult = await signup({ uid, realname, nickname });  // api server

회원가입이나 로그인을 진행하면 firebase.auth()의 currentUser가 설정되고 이 객체를 통해 uid 값을 가져올 수 있다. 이 uid와 함께 저장할 정보(여기에서는 이름과 닉네임)를 서버로 전송하면 회원가입 과정이 완료된다.

Firebase authenticationd을 이용하는데 유저 정보가 왜 필요하냐! 할 수도 있겠지만
유저 정보로 뭘 하려면 서버로 유저에 관한 무언가 저장을 하긴 해야한다.
다만 이제는 인증에 직접적인 영향을 끼치는 email과 비밀번호 대신 uid만 가지고 유저에 대한 정보를 저장하고 이용할 수 있게된 것이다. 이제 내 서버에서 유저 이메일과 비밀번호가 털릴 일은 없다!

다시 한 번 말하자면 나의(이 글의) 목적은 유저 정보를 저장하지 않겠다는 게 아니라 인증 과정을 Firebase에 맡기겠다는 것이다.


로그인

로그인은 회원가입과 크게 다르지 않다

// SigninModal.jsx
message.loading("로그인 중입니다", 0);
const { email, password } = await form.validateFields();
try {
  await firebase.auth().signInWithEmailAndPassword(email, password);
  message.destroy();
  dispatch(userLogin());
} catch (e) {
  message.destroy();
  firebaseSigninErrorHandler(e);
}

자동 로그인

여기서부터가 문제였다. 내가 직접 인증을 하면 토큰을 쿠키나 localStorage에 저장해놓고 이 값으로 로그인 여부를 확인하면 됐는데 지금까지 구현한 바로는 이 자동 로그인이 안된다. 유저 입장에서도 불편할 뿐더러 개발할 때도 귀찮아진다...

그래서 자동 로그인을 구현할 수 있는 방법을 찾기 시작했다. 방법은 생각보다 쉽게 찾아서 적용했다.

// SignupModal.jsx
try {
  await firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL);  // added
  await firebase.auth().createUserWithEmailAndPassword(email, password);
  const uid = firebase.auth().currentUser.uid;
  // ...
}


// SigninModal.jsx
try {
  await firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL);  // added
  await firebase.auth().signInWithEmailAndPassword(email, password);
  // ...
}

firebase.auth().setPersistence를 이용하면 인증 정보를 저장할 수가 있었다. 나는 로그인 상태를 계속 유지하려 LOCAL을 선택했지만 session으로 한정할 수도 있다. (Source: https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ko)

하지만 이렇게 저장만 해놓고 어떻게 쓰는지 알 수가 없었다. 새로고침을 했을 때 로그인 여부를 어떻게 알지...?
처음에는 그냥 상단 컴포넌트에서 effect 함수 내에서 firebase.auth().currentUser를 이용해 확인하려 했지만 결과는 항상 null이었다. 올바른 인증 정보인지 확인하려면 Firebase 서버를 거쳐야 할테니 당연한 결과였다.

한참을 검색한 후 찾은 방법은 firebase.auth().onAuthStateChanged라는 함수였다. 인증 정보가 바뀔 때마다(로그인 및 로그아웃) 실행되는 콜백 함수를 정해주는 함수다. 그래서 상단 컴포넌트에 다음과 같이 추가해줬다.

// 상단의 한 컴포넌트
const { initialized } = useSelector((state) => state.userTracker);

useEffect(() => {
  const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
    if (!initialized) {
      if (user) dispatch(userLogin());
      else dispatch(userCheckLogin());
    }
  });
  return unsubscribe;
}, [initialized]);

redux를 사용해 저장하고 있는 initialized라는 변수는 false가 초기값이고 앱 시작 시 로그인 여부를 확인해준다.

  • initialized = false일 때(앱이 시작되었을 때)
    • LOCAL에 저장해 로그인했으므로 Firebase가 알아서 인증 여부를 확인해주고, 그 결과가 onAuthStateChanged callback 함수의 user로 전달된다.
    • 이 유저가 없을 경우, 즉 usernull일 경우 initializedtrue로 바꿔준다.
    • user에 값이 존재할 경우 initialized뿐만 아니라 authorizedtrue로 바꿔주는 dispatch를 실행했다.

그러고나면 initialized 값이 바뀌기 때문에 effect 함수가 바뀌게 된다. 여기서 중요한 것은 return unsubscribe를 했다는 것인데, firebase.auth().onAuthStateChanged의 return값이 unsubscibe function이어서 이 함수를 사용하면 된다.
이제는 initialized = true이기 때문에 callback 함수 내부가 실행되지 않는다.

처음에는 onAuthStateChanged에 대해 잘 몰라 몇 가지 문제가 있었는데

  • cleanup으로 unsubscribe를 불러주지 않아 처음 한 번만 실행되는 게 아니라 로그인이나 로그아웃할 때마다 계속 실행되는 문제가 있었고
  • useEffect의 dependency로 initialized를 지정해주지 않아 항상 initialized = false가 되고 불필요하게 dispatch가 실행되는 문제가 있었다.

끝!

회원가입, 로그인, 로그아웃, 자동 로그인까지 인증 구현이 마무리되었다!
어찌된 일인지 더 오래 걸린 것 같기도 하지만 처음이라 그런 것이라 믿는다...

profile
코드야 내가 잘못했어

0개의 댓글