전편에서 보았듯이 세팅 끝났고, 이제 서로 맡은 바에 최선을 하기 시작한다!
나는 이제 인증 관련 개발을 맡았다. 로그인, 회원가입, 토큰 관리 등을 맡으려고 하고있다.
우선 토큰을 어떻게 처리해야할지 생각해보자.
백엔드에서 토큰 방식은 JWT로 보내주신다. 액세스토큰은 30분, 리프레쉬토큰은 7일로 유효기간을 잡고 있다. 리프레쉬 토큰은 로컬스토리지에 넣고, 액세스토큰은 페이로드에 사용자 정보인 이메일이 들어가 있어 이거는 노출이 안되게 로컬 변수에 넣어야하나 싶었지만 일단은 로컬스토리지로 하기로 협의했다.
간단하다. 로컬스토리지에 넣고, 읽어올 함수만 구현한다.
// utils/accessTokenHandler.ts
export const saveAccessTokenToLocalStorage = (accessToken: string) => {
if (typeof window !== "undefined") {
localStorage.setItem("accessToken", accessToken);
}
};
export const getAccessTokenFromLocalStorage = () => {
if (typeof window !== "undefined") {
return localStorage.getItem("accessToken") || "";
}
};
// utils/refreshTokenHandler.ts
export const saveRefreshTokenToLocalStorage = (refreshToken: string) => {
if (typeof window !== "undefined") {
localStorage.setItem("refreshToken", refreshToken);
}
};
export const getRefreshTokenFromLocalStorage = () => {
if (typeof window !== "undefined") {
return localStorage.getItem("refreshToken") || "";
}
};
이렇게 하고, save는 로그인할 때와 리프레쉬로 액세스 토큰 가져올 때 쓸거고, get은 이제 권한이 필요할 때, header나 로그인 됐는지 안됐는지의 조건에 쓸 것이다.
이제 토큰 저장 관리는 끝냈다. 우리 웹의 특징은 로그인을 해야 메인으로 넘어갈 수 있는, 보안성이 중요한 커뮤니티라 매 요청마다 이 토큰이 맞는지 확인하는 단계를 거쳐야한다.
interceptor를 가져와 전편에서 만들었던 axios instance
에 요청 및 응답으로 가져올 수 있게 만들어보자.
요청 하기 전 가로채보자.
instance.interceptors.request.use(
async (config) => {
const accessToken = getAccessTokenFromLocalStorage();
// 만료되지 않은 access token이 있는 경우에는 해당 토큰을 사용
if (accessToken !== null) {
config.headers.Authorization = accessToken;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
request
인터셉터를 만들었다. config
를 만들어서 매 요청에 토큰을 넣으려고 했다.
getAccessTokenFromLocalStorage()
함수를 이용하여 localStorage에 저장된 access token을 가져온 뒤, 요청 헤더에 Authorization
필드로 첨부했다.
물론 instance에 토큰을 헤더에 담아 보내는 초기 세팅을 했지만 혹시 모를 오류와 최신화된 액세스토큰을 반영하기 위해 요청 시 토큰을 담도록 만들었다.
여기가 가장 중요한 포인트다. 먼저 기능을 어떻게 만들지 생각했냐면
이렇게 하려고 했다. 이제 구현해보자.
instance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const { config, response } = error;
// 에러 응답이 없으면 error 리턴 - 다른 에러로 response.status 가 없을 수 있음.
if (!response) {
return Promise.reject(error);
}
// 리프레시 토큰으로 엑세스 토큰 재발급
const accessToken = await getAccessToken();
if (response.status === 401) {
saveAccessTokenToLocalStorage(`Bearer ${accessToken}`);
config.headers.Authorization = `Bearer ${accessToken}`;
return axios(config);
}
if (response.status === 404) {
localStorage.clear();
window.location.href = "/login";
}
return Promise.reject(error);
}
);
use는 총 2개의 인자를 받는다. 성공 시, 실패 시.
이렇게 만들었다. 이제 getAccessToken()
을 구현해보자.
// 기능 : 리프레시 토큰으로 엑세스 토큰 재발급
const getAccessToken = async () => {
try {
const refresh = getRefreshTokenFromLocalStorage();
// null 처리
if (refresh === null) {
throw new Error("리프레시 토큰이 존재하지 않습니다.");
}
const response = await instance.get("/auth/refresh", {
headers: {
refreshToken: refresh,
},
});
// 토큰 재발급에 성공한 경우
if (response.status === 200) {
const {
data: { grantType, accessToken, refreshToken },
} = response;
// 새로운 access token을 로컬 스토리지에 저장
saveAccessTokenToLocalStorage(`${grantType} ${accessToken}`);
// 새로운 refresh token이 발급된 경우에는 해당 토큰도 로컬 스토리지에 저장
saveRefreshTokenToLocalStorage(`${refreshToken}`);
return accessToken;
}
// refresh token이 만료된 경우 1개
if (response.status === 404) {
throw new Error(`${response.data.message}`);
}
// 그 외의 경우에는 에러를 발생시킴
throw new Error(`토큰 재발급에 실패했습니다. 상태 코드: ${response.status}`);
} catch (error) {
// 리프레시 토큰 만료 에러 핸들링 2개
console.log("리프레시 토큰 오류", error);
localStorage.clear();
window.location.href = "/login";
}
};
은근 길지만 뭐 별거 없다.
accessToken
리턴/login
으로 출발이런 식으로 했다!
interceptor를 이용하여 HTTP 통신할 때 주의할 점들을 고르기 위해 만들어 봤다. 이번이 처음이라 좀 어려웠는데 그래도 Example 보면서 하니 쉬운 편이었다. 다음엔 로그인 페이지를 만드는 과정에 대해서 기술 하긋다.
메인때 로그인 부분 했었는데 디테일이 살발하네요 ㅜㅠ 잘 보고 갑니당