firebase 활용 회원가입, 로그인 구현
학원 수업 Day35
웹사이트 규모가 작을 경우 가입된 회원의 데이터 관리는 자체 DB에서 하는 것보다 firebase를 이용해 구글에서 관리하게 하는 것이 보안 등의 측면에서 편리하다.
firebase
firebase 를 react 폴더에서 설치한다.
npm i firebase
firebase 를 사용하기 위해 src 폴더에 firebase.js 파일을 생성한다.
//firebase추가
import firebase from 'firebase/compat/app';
//firebase auth 추가
import 'firebase/compat/auth';
const firebaseConfig = {
// APP_인증_정보
};
//firebase 초기화
firebase.initializeApp(firebaseConfig);
//firebase 내보내기
export default firebase;
redux-toolkit
로그인 상태를 redux-toolkit 으로 관리하기 위해 react 폴더에서 npm 명령어를 이용해 redux-toolkit 과 react-redux 를 설치한다.
npm i react-redux @reduxjs/tookit
Slice 파일을 생성해서 createSlice()
메서드로 초기값과 리듀서를 생성한다.
import { createSlice } from '@reduxjs/toolkit';
먼저 createSlice
를 import
시킨다.
const userSlice = createSlice({
name: 'user',
initialState: {
displayName: '',
uid: '',
accessToken: ''
},
reducers: {
loginUser: (state, action)=>{
state.displayName = action.payload.displayName;
state.uid = action.payload.uid;
state.accessToken = action.payload.accessToken;
},
logoutUser: state=>{
state.displayName = '';
state.uid = '';
state.accessToken = '';
}
}
});
createSlice() 로 인스턴스의 이름, 초기값, reducers 를 작성한다.
데이터를 통신으로 받아와서 사용하는 것이 아닌 고정값을 사용할 것이기 때문에 extraReducers가 아닌 일반적인 reducers로 작성한다.
로그인 시 정보를 저장할 메서드 loginUser 와 로그아웃 시 정보를 비울 logoutUser 메서드를 생성했다.
store 내의 정보를 받아오기 위해 index.js 에서 configureStore()
메서드로 스토어를 생성해 Provider 로 App 을 감싸 store 를 전송한다.
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import userSlice from '만들어둔 slice 파일경로';
const store = configureStore({
reducer: {
user: userSlice
},
middleware: (getDefaultMiddleware)=>getDefaultMiddleware({serializableCheck: false})
})
<Provider store={store}>
<App />
</Provider>
Join
1차적으로 구글에 실제 비밀번호와 가입 정보를 저장한 후, 게시글 작성에 필요한 최소한의 정보를 MongoDB에 저장한다.
먼저 유저 별 고유번호를 생성하기 위해 DB 의 counter document 에서 userNum 을 초기값 1로 하여 추가한다.
back-end
node 폴더의 index.js 에서 user 라우터를 지정한다.
app.use('/api/user', require('./router/userRouter.js'))
기존에 생성해두었던 counterSchema.js 에서 userNum 의 데이터 타입을 Number 로 정의한다.
const counterSchema = new mongoose.Schema({
name: String,
communityNum: Number,
userNum: Number, // userNum 추가
}, { collection: 'Counter'});
node/router 폴더에 userRouter.js 를 생성한다.
저장하는 로직은 게시글 작성과 같다.
DB에 직접 추가한 userNum 값을 받아와서 프론트로부터 넘겨받은 데이터에 추가한 뒤, counter 의 userNum 값을 1 더하고 저장한 결과값을 프론트로 넘겨준다.
const express = require('express');
const router = express.Router();
const { User } = require('../model/userSchema');
const { Counter } = require('../model/counterSchema');
router.post('/join', (req, res)=>{
Counter.findOne({name: 'counter'})
.exec()
.then(doc=>{
const temp = req.body;
temp.userNum = doc.userNum;
const userData = new User(temp);
userData.save()
.then(()=>{
Counter.updateOne({name: 'counter'}, {$inc: {userNum: 1}})
.then(()=>{
res.json({success: true})
})
})
.catch(err=>{
console.log(err);
res.json({success: true})
})
})
});
module.exports = router;
가입 정보 저장용 스키마파일인 userSchema.js 파일을 생성한다.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
userNum: Number,
email: String,
displayName: String,
uid: String
}, {collection: 'Users'});
const User = mongoose.model('Users', userSchema);
module.exports = { User };
userNum 은 자체적으로 관리하기 위한 값이며 uid 는 firebase에서 부여한 고유 id 값이다.
front-end
join.js 파일을 만들어서 회원가입 페이지를 작성한다.
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import firebase from '../firebase';
input 정보를 담을 State 와, 가입 취소 또는 저장 성공 후 페이지 이동을 일으키기 위해 useNavigate()
를 활성화 시켜준다.
const navigate = useNavigate();
const [ Email, setEmail ] = useState('');
const [ Pwd1, setPwd1 ] = useState('');
const [ Pwd2, setPwd2 ] = useState('');
const [ Name, setName ] = useState('');
jsx로 실제 가입 폼을 만든다.
input value 로 State 를 넣어주고, onChange 이벤트로 State 를 변경해준다.
<input type='email' value={Email}
placeholder='이메일 주소를 입력하세요'
onChange={e=> setEmail(e.target.value)}
autoComplete='email'
/>
<input type='password' value={Pwd1}
placeholder='비밀번호를 입력하세요'
onChange={e=> setPwd1(e.target.value)}
autoComplete='new-password'
/>
<input type='password' value={Pwd2}
placeholder='비밀번호를 재입력하세요'
onChange={e=> setPwd2(e.target.value)}
autoComplete='new-password'
/>
<input type='text' value={Name}
placeholder='이름을 입력하세요'
onChange={e=> setName(e.target.value)}
/>
<button onClick={()=>navigate(-1)}>Cancel</button>
<button onClick={handleJoin}>Join</button>
취소버튼 클릭 시 이전 페이지로 이동하고, 가입버튼 클릭시 가입 핸들 이벤트로 연결한다.
그리고 핸들 이벤트를 작성한다.
먼저 입력된 내용이 비어있거나, 비밀번호를 맞게 입력했는지 검증절차를 거친 후 async await 로 firebase 와 통신해서 저장 후 로그인 페이지로 이동시킨다.
if (!(Name && Email && Pwd1 && Pwd2)) {
return alert('모든 양식을 입력하세요.');
}
if (Pwd1 !== Pwd2) {
return alert('비밀번호를 동일하게 입력하세요.');
}
// 입력 조건 통과 후 저장
// async await 로 firebase 와 통신해서 인증처리
const createdUser = await firebase.auth().createUserWithEmailAndPassword(Email, Pwd1);
await createdUser.user.updateProfile({
displayName: Name
})
Email 과 비밀번호 입력으로 가입정보를 저장하기 위해 firebase 의 createUserWithEmailAndPassword()
메서드를 사용해서 해당 값을 넘겨 신규 유저를 생성하고, 그 후에 updateProfile()
메서드로 displayName 을 업데이트 한다.
업데이트 직후, 해당 저장값을 다시 객체에 담아서 자체 DB에 저장하고 메세지 출력 후 로그인 페이지로 이동한다.
가입 후 자동 로그인이 안되고 직접 로그인 하도록 유도하기 위해 DB 저장 전에 firebase 및 store 로그인 상태를 초기화한다.
// firebase로 부터 인증 정보값을 받아 객체에 담기
const item = {
email: createdUser.user.multiFactor.user.email,
displayName: createdUser.user.multiFactor.user.displayName,
uid: createdUser.user.multiFactor.user.uid,
}
firebase.auth().signOut();
dispatch(logoutUser());
axios.post('/api/user/join', item).then(res=>{
if (res.data.success) {
alert('회원가입에 성공했습니다.');
navigate('/login');
} else {
return alert('회원가입에 실패했습니다.');
}
})
Login
const navigate = useNavigate();
const [ Email, setEmail ] = useState('');
const [ Pwd, setPwd ] = useState('');
const [ Err, setErr ] = useState('');
정보 전달을 위한 State 와 페이지 이동을 위한 useNavigate()
를 import
해준 후 초기값을 생성한다.
그리고 jsx로 input 및 button 을 생성하고 이벤트를 부여한다.
에러 메세지를 출력하기 위한 요소도 추가한다.
<input type='email' value={Email}
placeholder='이메일 주소를 입력하세요.'
onChange={e=>setEmail(e.target.value)}
autoComplete='email'
/>
<input type='password' value={Pwd}
placeholder='비밀번호를 입력하세요.'
onChange={e=>setPwd(e.target.value)}
autoComplete='current-password'
/>
<button onClick={handleLogin}>Login</button>
<button onClick={()=>navigate('/join')}>Join</button>
{Err &&
<p>
{Err}
</p>
}
로그인 이벤트 핸들을 작성한다.
공백 여부를 확인 후 firebase 의 signInWithEmailAndPassword()
메서드로 인증한 후 메인페이지로 이동시킨다.
에러메세지 출력을 위해 catch
문으로 넘어오는 err.code
에 따른 메세지를 State 에 담는다.
const handleLogin = async ()=>{
if (!(Email && Pwd)) {
return alert('이메일과 비밀번호를 입력하세요.');
}
try{
await firebase.auth().signInWithEmailAndPassword(Email, Pwd);
navigate('/');
} catch (err) {
console.log(err.code);
if (err.code === 'auth/invalid-email') {
setErr('이메일 형식을 확인하세요.')
} else if (err.code === 'auth/user-not-found') {
setErr('존재하지 않는 이메일 입니다.')
} else if (err.code === 'auth/wrong-password') {
setErr('비밀번호 정보가 일치하지 않습니다.')
} else {
setErr('로그인에 실패했습니다.')
}
}
}
login 출력, logout 처리
Header에서 로그인 시 로그인/회원가입 버튼을 지우고 로그인 상태 및 로그아웃 버튼을 출력하기 위해 아래 메서드들을 import
한다.
import { useDispatch, useSelector } from 'react-redux';
import { logoutUser } from '../redux/userSlice';
import firebase from '../firebase';
store 에 저장된 유저 정보를 받아온다.
const User = useSelector(store=> store.user);
store 에 저장된 유저 정보의 유무에 따른 조건으로 화면을 출력한다.
로그아웃 버튼에는 로그아웃 이벤튼 핸들을 연결한다.
{User.accessToken
?
<>
{User.displayName} 님 환영합니다.
<Link onClick={handleLogout}>Logout</Link>
</>
:
<>
<NavLink to='/login'
style={({isActive})=> isActive ? activeStyle : null}>
Login
</NavLink>
<NavLink to='/join'
style={({isActive})=> isActive ? activeStyle : null}>
Join
</NavLink>
</>
}
로그아웃 핸들 이벤트를 작성한다.
a 태그를 사용해서 e.preventDefault()
를 해주었는데, 다른 태그라면 불필요할 것이다.
firebase의 signOut()
메서드로 로그아웃 처리를 하고 dispatch() 로 logoutUser() 함수를 호출해서 store를 비워준다.
const handleLogout = (e)=>{
e.preventDefault();
firebase.auth().signOut();
dispatch(logoutUser());
alert('로그아웃 되었습니다.');
navigate('/');
}
로그아웃 메세지 출력 후 페이지를 이동시킨다.