리코일 , 파이어베이스 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에서 비동기 작업을 할 경우, 동일한 요청이라 판단되면 캐시화시킨 데이터를 동일하게 제공하기 때문에, 불필요한 서버 호출을 막을 수 있습니다.(최적화)
댓글