본문 바로가기
React.js

Recoil + Firebase Auth

by Zih0 2021. 10. 4.

리코일 , 파이어베이스 Auth

전역 상태관리 라이브러리인 Recoil을 연습해보려고 파이어베이스를 이용해서 로그인 및 회원가입 기능을 구현해봤습니다.

firebase 설정

firebase를 설치하니까 v9가 되면서, import 하는 방법이 약간 달라졌습니다.

이전에는 아래와 같이 불러 왔다면,

import firebase from 'firebase/app';

이제는 compat이 추가되었습니다.

import firebase from 'firebase/compat/app';

그리고 공식 라이브러리를 보면 v9은 필요한 함수를 하나하나 불러와서 사용하는 것을 권장하고 있습니다.
하지만 저는 이전 버전에서의 방식이 좀 더 관리하기에 수월하다고 판단하여 v8 기준으로 코드를 작성했습니다.( v9에서 v8 방식이 호환됩니다. )

아래처럼 firebase를 관리하는 파일을 만들었습니다.

//fb.js
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';

const firebaseConfig = {
    apiKey: process.env.REACT_APP_API_KEY,
    authDomain: process.env.REACT_APP_AUTHDOMAIN,
    projectId: process.env.REACT_APP_PROJECT_ID,
    storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_SENDER_ID,
    appId: process.env.REACT_APP_APP_ID,
    measurementId: process.env.REACT_APP_MEASUREMENT
  };
firebase.initializeApp(firebaseConfig);
export const Providers = {
    github: new firebase.auth.GithubAuthProvider()
}
export const authService = firebase.auth();
export default firebase;

Recoil

로그인 후, User 정보를 담을 Atom을 생성했습니다.
그리고 로그인 한 상태인지 확인할 Atom도 생성했습니다.

//authRecoil.js
import { atom } from 'recoil';

const authState = atom({
  key: 'authState',
  default: null,
  // TypeError: Cannot freeze 방지
  dangerouslyAllowMutability: true,
});

export const isLoggedInState = atom({
  key: 'isLoggedInState',
  default: false,
})


export default authState;

리코일은 저장하려는 객체를 재귀적으로 freeze 하고 있기 때문에, freeze가 불가능한 오브젝트(firebase User 객체 등)을 저장하면 TypeError: Cannot freeze 에러가 뜨는 것을 확인 할 수 있습니다.
이를 방지하기 위해서 dangerouslyAllowMutability: true 옵션을 추가해주었습니다.
ref:
https://github.com/facebookexperimental/Recoil/issues/406

다음으로는 RecoilRoot로 App을 감싸줍니다.
기존 리덕스나 Context API를 사용하듯이 감싸주면 됩니다.
저는 index.js에서 감싸주었습니다.

import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import App from './App';

ReactDOM.render(
            <RecoilRoot>
                <App />
            </RecoilRoot>
    document.getElementById("root")
);

Auth 페이지

그 다음으로는 로그인과 회원가입 페이지를 만들었습니다.
보시기 편하도록 스타일링과 리코일과 관련없는 부분은 제거했습니다.

// components/Auth/index.js
import React from 'react';
import AuthTitle from './AuthTitle';
import styled from 'styled-components';
import Card from '../../UI/Card';
import SocialLoginButton from '../../UI/SocialLoginButton';
import HorizonLine from '../../HorizonLine';
import AuthForm from './AuthForm';
import { authService, Providers } from '../../../fb';
import { useHistory } from 'react-router';
import { useSetRecoilState } from 'recoil';
import { authState } from '../../../recoil/authRecoil';
import { Link } from 'react-router-dom';

const Container = styled.div`
  ...
`;

const Wrapper = styled.div`
  ...
`;

const Linker = styled.div`
  ...
`;

function Auth({ authType, title }) {
  const history = useHistory();
  const setAuth = useSetRecoilState(authState);

  const socialAuthHandler = async () => {
    try {
      const auth = await authService.signInWithPopup(Providers.github);
      if (auth.user) {
        setAuth(auth.user);  // Recoil Atom에 User 객체 저장 
        history.push('/');
      }
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <Container>
      <AuthTitle>{title}</AuthTitle>
      <Wrapper>
        <Card style={{ width: '100%' }} isAuth={true}>
          <AuthForm authType={authType} />
        </Card>
        <Linker>
          {authType === 'login' ? (
            <>
              <span>Don't have an account?</span>
              <Link to="/join"> Create One →</Link>
            </>
          ) : (
            <>
              <span>Already have an account?</span>
              <Link to="/login"> Log in →</Link>
            </>
          )}
        </Linker>
        <HorizonLine text="OR" />
        <SocialLoginButton py={3} provider="github" onClick={socialAuthHandler}>
          {authType === 'login' ? '깃허브로 로그인' : '깃허브로 회원가입'}
        </SocialLoginButton>
      </Wrapper>
    </Container>
  );
}

export default Auth;
//components/Auth/AuthForm.js
import React from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router';
import { useSetRecoilState } from 'recoil';
import { authService } from '../../../../fb';
import { authState } from '../../../../recoil/authRecoil';
import Button from '../../../UI/Button';
import Input from '../../../UI/Input';
import useInput from '../../../../Hooks/useInput';

const Form = styled.form`
  ...
`;
const Label = styled.label`
  ...
`;
function AuthForm({ authType }) {
  const setAuth = useSetRecoilState(authState);
  const history = useHistory();
  const email = useInput('');
  const password = useInput('');
  const displayName = useInput('');
  const submitHandler = async (e) => {
    e.preventDefault();
    try {
      let auth;
      if (authType === 'login') {
        auth = await authService.signInWithEmailAndPassword(email.value, password.value);
      } else {
        auth = await authService.createUserWithEmailAndPassword(email.value, password.value);
      }
      if (auth.user) {
        setAuth(auth.user); // Recoil Atom에 User 객체 저장 
        history.push('/');
      }
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <Form onSubmit={submitHandler}>
      <Label htmlFor="email">Email</Label>
      <Input id="email" type="text" {...email} />
      <Label htmlFor="password">Password</Label>
      <Input id="password" type="password" {...password} />
      {authType === 'join' && (
        <> 
          <Label htmlFor="displayName">Nickname</Label>
          <Input id="displayName" type="displayName" {...displayName} />
        </>
      )}
      <Button py={3} type="submit">
        <span>Continue</span>
      </Button>
    </Form>
  );
}

export default AuthForm;

Firebase Subscribe

파이어베이스로 로그인을 할 경우에 onAuthStateChanged 함수를 통해 쉽게 자동로그인을 구현할 수 있습니다.

App.js에서 해당 작업을 진행해, 이전에 로그인했었으면 전역으로 해당 User 객체를 저장하도록 했습니다.

import Router from './router';
import { useSetRecoilState } from 'recoil';
import { authState, isLoggedInState } from './recoil/authRecoil';
import { useEffect, useState } from 'react';
import { authService } from './fb';

function App() {
  const setAuth = useSetRecoilState(authState);
  const setIsLoggedIn = useSetRecoilState(isLoggedInState);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = authService.onAuthStateChanged((authUser) => {
      if (authUser) {
        setAuth(authUser);
        setIsLoggedIn(true);
      }
      setIsLoading(false);
    });
    return () => {
      unsubscribe();
    };
  }, []);

  return (
    <div className="App">
      {isLoading ? <p>Loading..</p> : <Router />}
    </div>
  );
}

export default App;

Router

마지막으로 라우팅 파일을 작성했습니다. 로그인 했을 경우에는 로그인, 회원가입 페이지를 들어가지 못하게 작성해두었습니다.

import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isLoggedInState } from './recoil/authRecoil';
import Home from './routes/Home';
import Join from './routes/Join';
import Login from './routes/Login';

const Router = () => {
  const isLoggedIn = useRecoilValue(isLoggedInState);

  return (
    <BrowserRouter>
      <Switch>
        <Route path="/" exact component={Home} />
        <Route exact path="/join">{isLoggedIn ? <Redirect to="/" /> : <Join />}</Route>
        <Route path="/login">{isLoggedIn ? <Redirect to="/" /> : <Login />}</Route>
      </Switch>
    </BrowserRouter>
  );
};
export default Router;

결론

Recoil의 주요개념인 Atom을 활용해 전역 상태관리를 해봤습니다.
Atom뿐 만아니라 Selector, SelectorFamily도 주요한 개념이라 다음에는 활용해볼 계획입니다.
Selector에서 비동기 작업을 할 경우, 동일한 요청이라 판단되면 캐시화시킨 데이터를 동일하게 제공하기 때문에, 불필요한 서버 호출을 막을 수 있습니다.(최적화)

댓글