계기
Next.js를 사용하는 이번 프로젝트에서 서버 요청을 하려고 보니 커스텀 엑시오스가 필요했다.
그래서 이전 리액트 프로젝트에서 사용했던 나의 커스텀 엑시오스를 사용하려하니 문제가 발생했다.
1. 로컬스토리지 사용
나는 개인프로젝트하면 토큰을 로컬스토리지에 저장을 한다.
딱히 쿠키의 장점을 모르겠고, 유저의 사용허가를 받아야한다는 점에서 로컬스토리지가 더 이점이 많아보였다.
하지만 Next.js의 서버컴포넌트에서는 로컬스토리지를 사용하지 못하기에 로컬스토리지에서 쿠키를 사용하게 되었다.
2. 서버 / 클라이언트 간 쿠키공유
이 부분이 가장 큰 걸림돌이었는데,
next-client-cookies 라이브러리를 사용해서 서버단에서 쿠키를 사용했었다.
해당 라이브러리에 클라이언트 컴포넌트에서 쿠키를 가져오지 못하는 것은 아니었지만,
훅을 사용해 쿠키를 가져오기 때문에 커스텀 엑시오스에서 사용하기에는 적합하지 않았다.
해결방법
나의 해결법은 그냥 쿠키 관리 라이브러리 두개를 동시에 운용하는 것이었다.
cookies-next
이전의 문제점이었던 훅을 통한 쿠키관리가 아닌 일반 함수처럼 사용 가능하게 되어있는 라이브러리이다.
그렇기에 내가 할 일은 서버 요청을 보냈을때, 이 요청이 온 곳이 서버인지 클라이언트인지 파악하고 그에 맞게 라이브러리를 사용해 쿠키를 가져오는 일이었다.
Next.js에서 현재 환경이 서버인지 클라이언트인지 알려면,
if (typeof window === "undefined") {
// 서버
} else {
// 클라이언트
}위와 같이 작성하면 된다.
그런데, 커스텀 엑시오스에서는 토큰을 가져오고 저장하고, 삭제하는 모든 로직이 있는데, 그 시점마다 모두 if문을 달아주는 일은 매우 비효율적이다. 내가 하기 싫다.
그렇기 때문에 나는
export const cookieManager = {
set: async (key: string, value: string) => {
if (typeof window === "undefined") {
const cookieStore = await getCookies();
cookieStore.set(key, value);
} else {
setCookie(key, value);
}
},
get: async (key: string) => {
if (typeof window === "undefined") {
const cookieStore = await getCookies();
return cookieStore.get(key);
} else {
return getCookie(key);
}
},
delete: async (key: string) => {
if (typeof window === "undefined") {
const cookieStore = await getCookies();
return cookieStore.remove(key);
} else {
return deleteCookie(key);
}
},
};이렇게 유틸리티를 만들어서
export const responseErrorHandler = async (error: AxiosError) => {
const originalRequest = error.config as CustomAxiosRequestConfig;
if (originalRequest && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = await cookieManager.get(REFRESH_TOKEN_KEY);
if (refreshToken) {
if (!isRefreshing) {
isRefreshing = true;
try {
const { data } = await axios.post(`${API_URL}/auth/reissue`, {
refreshToken,
});
const newAccessToken = data.data.accessToken;
const newRefreshToken = data.data.refreshToken;
await cookieManager.set(ACCESS_TOKEN_KEY, newAccessToken); //쿠키 설정
await cookieManager.set(REFRESH_TOKEN_KEY, newRefreshToken); // 쿠키 설정
onRefreshed(newAccessToken);
originalRequest.headers[
REQUEST_TOKEN_KEY
] = `Bearer${newAccessToken}`;
return solveAxios(originalRequest);
} catch (refreshError) {
await cookieManager.delete(ACCESS_TOKEN_KEY); //쿠키 삭제
await cookieManager.delete(REFRESH_TOKEN_KEY); //쿠키 삭제
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return new Promise((resolve) => {
addRefreshSubscriber((newToken: string) => {
originalRequest.headers[REQUEST_TOKEN_KEY] = `Bearer${newToken}`;
resolve(solveAxios(originalRequest));
});
});
} else {
return Promise.reject(error);
}
}
return Promise.reject(error);
};위의 커스텀엑시오스에 적용했다.
결론
생각지도 못한 부분에서 애를 먹어서 많은 시간이 허비되었지만, Next.js에 대한 새로운 지식을 얻게되었다.
사실 자료를 찾아보았을때, 서버용 커스텀엑시오스와 클라이언트용 커스텀엑시오스를 따로 만들어서 사용하는 사람들도 많이 보이고 가장 보편적인 것 같았지만, 로그인시에 서버와 클라이언트용 쿠키를 따로 저장하는게 너무너무너무 싫어서 위의 방식을 채택했다.
훈수는 언제나 환영