[내배캠 3기] 팀 프로젝트 Newsfeed - 로그인, 로그아웃 / 세션 유지
TIL 23. 11. 22
들어가며
내배캠 뉴스피드 팀 과제 2일차.
모달창을 이용한 로그인/로그아웃 기능을 담당하여 Firebase 설정과 우선 SNS 로그인/로그아웃을 구현하였다. 추가로 모든 컴포넌트에서 로그인한 유저의 정보에 접근할 수 있도록 Redux 상태 관리 라이브러리를 이용해 전역 상태로 관리하였다.
더 쉽게 사용할 수 있도록 커스텀 훅을 만들어서 테스트 해봤다. 추가로 Firebase를 학습한 내용과 개발 과정을 남기려고 한다.
Firebase란?
공식 문서에서는 firebase를 다음과 같이 소개하고 있다.
Firebase는 사용자의 사랑을 받는 앱과 게임을 빌드하고 성장시키는 데 도움이 되는 앱 개발 플랫폼입니다. 이 플랫폼은 Google이 지원하며 전 세계 수백만 개 회사에서 신뢰를 받고 있습니다.
Firebase는 구글이 제공하는 서버리스 플랫폼이다. 서버리스란 서버가 없는 것이 아니라, 서버를 개발하는데 필요한 인프라를 제공하여 개발자가 직접 서버를 만들거나 관리할 필요가 없음을 의미한다.
백엔드에 대한 지식이 없어도 백엔드에 필요한 주요 기능들을 Firebase에서 제공하기 때문에, 프론트엔드에서는 API 형태로 사용할 수 있다. 백엔드 개발자 없이 웹 서비스를 출시하기 위해서 Firebase를 사용한다.
이번 프로젝트에서 사용할 Firebase의 주요 서비스들
Firebase를 통해 데이터베이스를 다루고, 사용자 인증 등의 서버 기능을 웹 서비스에 적용할 수 있다.
- Authentication
- 사용자 인증, 즉 회원 가입, 로그인을 제공하는 서비스
- 이메일, 패스워드 로그인 뿐 아니라 SNS 로그인을 쉽게 구현 가능
- Firestore
- 문서(document) 중심의 NoSQL 데이터베이스
- Storage
- 파일을 업로드하고 다운로드
- FireStore에는 단일 문서 크기에 제한이 있어 대용량 파일 업로드하지 못해 Storage에서 대용량 파일을 업로드
Firebase 환경 설정
1. Firebase 프로젝트 생성
먼저 Firebase 프로젝트를 생성한다. 프로젝트 생성 후에 웹 앱을 등록한다.
프로젝트 생성 과정과 앱 등록 과정은 다루지 않는다.
2. Authentication
사용자 인증 기능을 구현하기 앞서 Firebase 콘솔에서 Authentication기능을 활성화해야 한다. 시작하기 버튼을 누른다.
Firebase 애플리케이션은 기본적으로 모든 기능이 비활성화 되어 있다. 제공하는 기능이 너무 많기 때문에 모두 활성화되어 있지 않다.
SNS 로그인 구현하기 위해 Sign-in method 탭에서 깃헙을 선택했다. 추후에 이메일/패스워드나 구글 로그인 등 하나씩 추가해 주면 된다. 깃헙 추가하는 방법은 아래 링크 참고.
3. Firebase 연동
먼저 firebase 패키지를 설치한다.
yasn add firebase
3-1. Authentication SDK 추가
src 폴더에 firebase.js
생성하여 firebase 초기 설정에 필요한 Authentication 관련 코드를 추가한다. SDK 코드는 Firebase 콘솔 프로젝트 설정에 있다.
// firebase.js
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGIN_ID,
appId: process.env.REACT_APP_APP_ID,
measurementId: process.env.REACT_APP_MEASUREMENT_ID,
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
initializeApp
함수에 설정들을 전달해 firebase 초기화한 것.
3-2. 환경 변수 설정
API_KEY와 DB 관련 데이터 등 secret key는 외부로 알려지기 원하지 않는다. 보안에 매우 취약하기 때문.
이러한 값들을 보안을 목적으로 혹은 유지보수를 용이하게 하기 위해 .env 파일에 환경변수로 만들어 변수를 꺼내와 사용한다. 방법은 아래 링크 참고.
SNS 로그인, 로그아웃 구현
컴포넌트와 비즈니스 로직을 분리하기 위해 firebase.js 파일에서 기능을 구현한다. 컴포넌트에서는 동작 방식을 몰라도 함수를 전달 받아 호출하기만 하면 된다.
Firebase SDK로 로그인 과정 처리를 참고해서 SNS 로그인, 로그아웃을 구현한다. Github SNS 로그인을 사용했다.
Firebase Authentication에서 GitHub을 설정했는지 꼭 확인해야 한다. 설정하지 않았다면 동작하지 않는다.
TMI: 이미 오류로 겪었다. FirebaseError: Firebase: Error (auth/configuration-not-found). Firebase 콘솔에서 원하는 인증 유형을 활성화했는지 확인하자.
GitHub 로그인
-
Google 제공업체 객체의 인스턴스를 생성한다.
- firebase에서 제공하는 GithubAuthProvider를 이용해 초기화
-
OAuth 요청과 함께 전송할 커스텀 OAuth 제공업체 매개변수를 추가로 지정
setCustomParameters
를 호출해 커스텀 매개변수 추가provider.setCustomParameters({ prompt: 'select_account' })
- 계정 선택창- 그 외의 인증 URI 매개변수
-
Google 제공업체 객체를 사용해 Firebase에 인증
- 로그인 페이지로 리다이렉트할 수 있으나 모달창을 띄우기로 하였으므로 로그인 버튼을 클릭하면 팝업 창을 띄우기
- 팝업 창을 사용하여 로그인 과정을 진행하려면
signInWithPopup
을 호출
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { GithubAuthProvider, getAuth } from 'firebase/auth';
// .. 생략
const auth = getAuth();
const provider = new GithubAuthProvider();
provider.setCustomParameters({ prompt: 'select_account' });
export const login = async () => {
return signInWithPopup(auth, provider)
.then((result) => {
const user = result.user;
console.log(user);
return user;
})
.catch(console.error);
};
GitHub 로그아웃
사용자를 로그아웃시키려면 signOut
을 호출한다.
export const logout = async () => {
return signOut(auth)
.then(() => {
console.log('Sign-out successful.');
return null;
})
.catch(console.error);
};
GitHub 로그인, 로그아웃 테스트
기능이 동작하는지 테스트하기 위해 button을 만들어서 클릭 이벤트의 콜백함수로 로그인과 로그아웃 함수를 전달했다.
import React from 'react';
import { login, logout } from 'config/firebase';
export default function Header() {
return (
<div>
<button onClick={() => login()}>Login</button>
<button onClick={() => logout()}>Logout</button>
</div>
);
}
▶ 테스트 결과
로그인, 로그아웃 기능 테스트
잘 작동한다.
사용자 로그인 세션 유지
그런데 새로고침하면 로그인 정보가 사라진다. 상태 관리를 하고 있지 않기 때문이다. 심지어 state로 관리를 해도 로그인 정보가 사라진다. 이유는 새로고침하여 페이지가 다시 그려지면 컴포넌트가 새롭게 만들어지고 상태도 새롭게 만들어진다.
이것이 전역 상태 관리가 필요한 이유
import React, { useState } from 'react';
import { login, logout } from 'config/firebase';
export default function Header() {
const [user, setUser] = useState(); // 리렌더링 되면 초기화
const handleLogin = () => login().then(setUser);
const handleLogout = () => logout().then(setUser);
console.log(user);
return (
<div>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
firebase는 로그인한 사용자의 세션을 보관하고 있다. 따라서 새로고침하더라도 로그인한 세션 정보가 남아있기 때문에 다시 로그인을 하지 않아도 된다.
사용자가 처음으로 로그인하면 신규 사용자 계정이 생성되고 사용자가 로그인할 때 사용한 사용자 인증 정보(사용자 이름과 비밀번호, 전화번호 또는 인증 제공업체 정보)에 연결됩니다. 이 신규 계정은 Firebase 프로젝트에 저장되며 사용자의 로그인 방법과 무관하게 프로젝트 내의 모든 앱에서 사용자 본인 확인에 사용할 수 있습니다. - firebase
사용자 세션 정보를 확인하려면 onAuthStateChanged
메서드를 사용한다.
onAuthStateChanged
메서드를 사용해 관찰자를 연결합니다. 사용자가 로그인되면 관찰자에서 사용자에 대한 정보를 가져올 수 있습니다.
사용자가 로그인되면 관찰자에서 사용자에 대한 정보를 가져올 수 있다. 사용자의 로그인 상태가 변경될 때마다 호출되어 첫 번째 인자로 auth 값을, 두 번째 인자로는 콜백 함수를 받는다. Auth 관련된 변화가 있을 때마다 사용자 인증 정보의 변화를 관찰할 수 있다.
사용자 로그인 세션 유지 테스트
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import {
GithubAuthProvider,
getAuth,
onAuthStateChanged,
signInWithPopup,
signOut,
} from 'firebase/auth';
const firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGIN_ID,
appId: process.env.REACT_APP_APP_ID,
measurementId: process.env.REACT_APP_MEASUREMENT_ID,
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth();
const provider = new GithubAuthProvider();
provider.setCustomParameters({ prompt: 'select_account' });
export const login = async () => {
return signInWithPopup(auth, provider)
.then((result) => {
const user = result.user;
console.log(user);
return user;
})
.catch(console.error);
};
export const logout = async () => {
return signOut(auth)
.then(() => {
console.log('Sign-out successful.');
return null;
})
.catch(console.error);
};
export const userStateChange = (callback) => {
onAuthStateChanged(auth, (user) => {
callback(user);
});
};
▶ 테스트 결과
사용자 세션 유지
import React, { useEffect, useState } from 'react';
import { login, logout, userStateChange } from 'config/firebase';
export default function Header() {
const [user, setUser] = useState();
const handleLogin = () => login().then(setUser);
const handleLogout = () => logout().then(setUser);
useEffect(() => {
userStateChange((auth) => console.log(auth));
}, []);
return (
<div>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
잘 작동한다. 이후에 Redux를 이용해 전역 상태로 관리해주었다.
전역 상태 관리
// configStore.js
import { createStore, combineReducers } from 'redux';
import userReducer from 'redux/modules/User';
const rootReducer = combineReducers({ userReducer });
const store = createStore(rootReducer);
export default store;
// User.js
const USER_LOGGEDIN = 'user/LOGGED_IN';
const USER_LOGIN = 'user/LOGIN';
const USER_LOGOUT = 'user/LOGOUT';
const initialState = {
user: null,
};
export const checkLoginStatus = (user) => ({
type: 'user/LOGGED_IN',
payload: user,
});
const userReducer = (state = initialState, action) => {
const userInfo = action?.payload?.providerData[0] ?? null;
switch (action.type) {
case USER_LOGGEDIN:
return { user: userInfo };
case USER_LOGOUT:
return { user: null };
case USER_LOGIN:
default:
return state;
}
};
export default userReducer;
커스텀 훅도 만들었다.
Usage
useLoggedIn
: 로그인 여부를 확인하는 커스텀 훅
import { useLoggedIn } from 'hooks/useAuth'
const { loginState, setLoginState } = useLoggedIn();
- loginState: 현재 user 객체 (로그인 되지 않은 계정은 null)
- setLoginState: 로그인 상태를 업데이트 함수
import { useDispatch, useSelector } from 'react-redux';
import { checkLoginStatus } from 'redux/modules/User';
export const useLoggedIn = () => {
const loginState = useSelector(({ userReducer }) => userReducer.user);
const dispatch = useDispatch();
const setLoginState = (user) => dispatch(checkLoginStatus(user));
return { loginState, setLoginState };
};
댓글남기기