전편에 이어서, 프론트 쪽 로그인 흐름을 재설계한 내용을 정리한다.
백엔드 아키텍처에 맞게 흐름을 개선하고, 오류 상황에 대한 강건성을 높이는 데 포커스를 맞추었다.

설정 및 초기화
먼저 페이지 초기화 시 다음 함수가 실행된다.
우선 이벤트 리스너들을 초기화하고 URL 파라미터에서 인증 코드가 존재하는지 확인한다.
async function initializePage() {
try {
// 기본 이벤트 리스너 초기화
initializeEventListeners();
disablePreLoginFeatures();
// URL 파라미터 체크
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');
if (authCode) {
await handleAuthenticationFlow();
return;
}
인증 코드가 없는 경우 토큰의 유효성을 검증한다.
const isValid = await TokenManager.validateTokenSet();
if (!isValid) {
const beforelogin = document.getElementById('beforelogin');
// 이하 생략: 토큰 코드가 없을 경우 로그인 화면이 표시됨
}
return;
}
유효한 토큰이 있을 경우 사용자 정보를 추출하고 세션 목록을 불러온다.
// 5. 유효한 토큰이 있는 경우의 초기화
const idToken = localStorage.getItem('auth_token');
if (idToken) {
const tokenPayload = parseJwt(idToken);
if (tokenPayload?.sub) {
userId = tokenPayload.sub;
localStorage.setItem('userId', userId);
// 중략: 토큰 존재할 경우 UI 업데이트 및 사용자 이름 표시
if (userId) {
await fetchSessions(userId);
}
} catch (error) {
console.error('Initialization failed:', error);
// ...
}
}
인증 흐름 관련
아래 함수는 토큰 발급에 사용된다. authCode를 받아 Cognito 엔드포인트에 요청을 보내고, 반환된 ID token, Access token, Refresh token들을 localstorage에 저장한다.
async function getToken(authCode) {
const headers = new Headers({
'Authorization': 'Basic ' + btoa(config.clientId + ':' + config.clientSecret),
'Content-Type': 'application/x-www-form-urlencoded'
});
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: config.redirectUri
});
const response = await fetch(`${config.domain}/oauth2/token`, {
method: 'POST',
headers,
body
});
const data = await response.json();
// 토큰 저장
localStorage.setItem('auth_token', data.id_token);
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
console.log('received token successfully');
return data;
}
인증 흐름은 아래 함수에서 확인할 수 있다.다음 코드들은 URL에서 authCode를 추출해 Token으로 교환하고 사용자 정보 표시, 세션 초기화 등을 담당한다.
먼저 URL 파라미터에서 authCode를 확인하고 Token 개체를 반환받는다.
async function handleAuthenticationFlow() {
try {
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');
if (!authCode) return false;
const tokenData = await getToken(authCode);
다음으로 URL에 남아 있는 authCode를 제거한다.
window.history.replaceState({}, document.title, window.location.pathname);
//중략: 로그인 후 UI 표시, 주요 기능 활성화
여기서는 payload에서 userId(sub)를 추출하고 localstorage에 저장한다.
// userId 설정
const tokenPayload = parseJwt(tokenData.id_token);
if (tokenPayload?.sub) {
userId = tokenPayload.sub;
localStorage.setItem('userId', userId);
}
idtoken에서 이메일 정보를 추출해 표시하고, 세션을 불러온다.
const userInfo = await getUserInfo(tokenData.id_token);
document.getElementById('userinfo').innerText = userInfo.email;
updateProfileButton(userInfo);
initializeEventListeners();
if (userId) {
await fetchSessions(userId);
}
console.log('handle authentication complete');
return true;
} catch (error) {
console.error('Authentication error:', error);
document.getElementById('userinfo').innerText = 'Error fetching user info.';
return false;
}
}
토큰 관리
API call이 일어나기 전 토큰의 유효성을 확인하고, 만료되었다면 새로 갱신한다. 해당 함수는 모든 API call에 호출되어 유효성 검증에 사용된다.
async function validateTokenBeforeRequest() {
const token = localStorage.getItem('accessToken');
const expiry = localStorage.getItem('tokenExpiry');
if (!token || !expiry || Date.now() >= parseInt(expiry)) {
// 토큰 갱신 대기
await refreshTokens();
}
return localStorage.getItem('accessToken');
}
위에서 다룬 토큰 갱신은 아래 코드들로 이루어진다. API call로 새 토큰을 발급받고 새 토큰을 업데이트한다. 실패할 경우 자동으로 로그아웃된다.
async function refreshTokens() {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
throw new Error('No refresh token available');
}
// refreshtoken으로 새 토큰 발급
try {
const response = await fetch(`${FLASK_URL}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh_token: refreshToken })
});
if (response.ok) {
const data = await response.json();
// 전역 변수 및 localStorage 모두 새 토큰으로 업데이트
accessToken = data.access_token;
tokenExpiryTime = Date.now() + (data.expires_in * 1000);
localStorage.setItem('accessToken', data.access_token);
localStorage.setItem('tokenExpiry', tokenExpiryTime.toString());
return data.access_token;
} else {
throw new Error('Token refresh failed');
}
} catch (error) {
// 토큰에 문제 있을 경우 로그인 리다이렉션
localStorage.clear();
redirectToLogin();
throw error;
}
}
토큰이 만료되었거나 만료가 임박한 경우에도 토큰 갱신을 수행해야 한다. 갱신에 실패하더라도 현재 토큰이 만료되지 않았다면 해당 토큰을 계속 사용할 수 있도록 한다.
async function ensureValidToken() {
//localstorage에서 현재 토큰 호출
const idToken = localStorage.getItem('auth_token');
const refreshToken = localStorage.getItem('refresh_token');
if (!idToken || !refreshToken) {
throw new Error('No tokens available');
}
if (isTokenExpired(idToken)) {
// 토큰이 만료된 경우 갱신 시도
await refreshTokens(refreshToken);
} else if (needsRefresh(idToken)) {
// 만료가 임박한 경우 갱신 시도
try {
await refreshTokens(refreshToken);
} catch (error) {
// 갱신 실패했지만 현재 토큰이 아직 유효한 경우 계속 진행
if (!isTokenExpired(idToken)) {
console.warn('Token refresh failed but current token still valid');
} else {
throw error;
}
}
}
}
사용자 정보 및 Session
아래 함수로는 사용자 정보(sub, 이메일, 닉네임)을 호출한다.
위에서 다룬 ensureValidToekn()이 여기서도 사용된다.
async function getUserInfo(token) {
// API 호출 전 토큰 유효성 확인 및 갱신
await ensureValidToken();
// 갱신된 토큰으로 API 호출
const currentToken = localStorage.getItem('auth_token');
const response = await fetch(config.authEndpoint, {
headers: { Authorization: currentToken }
});
const userData = await response.json();
console.log('userdata received');
return JSON.parse(userData.body);
}
최종적으로는 사용자 경험을 해친다는 이유로 제거하였지만 사용자가 장기간 비활성화되어있을 경우 자동으로 로그아웃을 수행하는 함수도 구축했었다.
function initializeSessionCheck() {
let lastActivity = Date.now();
// 사용자 활동 가지
['click', 'keypress', 'scroll', 'mousemove'].forEach(event => {
document.addEventListener(event, () => {
lastActivity = Date.now();
});
});
// 주기적으로 세션 상태 체크
setInterval(async () => {
const inactiveTime = (Date.now() - lastActivity) / 1000;
if (inactiveTime >= config.sessionDuration) {
await handleLogout();
}
}, 60000); // 1분마다 확인
}
로그아웃
로그아웃은 아래 함수가 담당한다. 모든 토큰과 사용자 정보를 localstorage에서 제거하고, 사용자를 리다이렉션시킨다.
function handleLogout() {
// 모든 토큰 제거
localStorage.removeItem('auth_token');
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('userId');
const userinfoElement = document.getElementById('userinfo');
if (userinfoElement) {
userinfoElement.innerText = '';
}
const logoutUrl = `${config.domain}/logout?client_id=${config.clientId}&logout_uri=${encodeURIComponent(config.logoutRedirectUri)}`;
window.location.href = logoutUrl;
console.log('handlelogout');
}