문제 : Cookies can only be modified in a Server Action or Route Handler.
axios.interceptor.response에서 토큰이 만료됨을 인지하고 서버에 토큰을 재발급받아 토큰을 설정하는 과정에서 발생한 에러이다. Cookie는 server action 혹은 Route Handler에서만 수정이 될 수 있다며 참고할 링크를 넘겨주고 있다.
원인
axios.interceptor.response는 server action도, Route Handler도 아니기 때문에 쿠키를 조회할 수는 있지만 설정할 수는 없다.
해결 과정
그렇다. server 측에서 cookie를 조회는 할 수는 있지만, 설정할 수는 readonly속성의 값이다. 쿠키를 설정하기 위해서는 에러 메시지처럼 Route Handler를 이용하거나, middleware.ts를 이용하면 된다.
Middleware.ts
💡 Middleware란?
Middleware는 서버로의 요청이 끝나기 전에 실행하는 함수이다. 서버로부터 응답이 고지 전에 rewriting, redirecting, 요청 수정 등을 수행할 수 있다.
// app/middleware.ts
const middleware = async (request: NextRequest) => {
const accessToken = request.cookies.get(AUTH_KEYS.accessToken)?.value;
const refreshToken = request.cookies.get(AUTH_KEYS.refreshToken)?.value;
if (!accessToken || !refreshToken)
return NextResponse.redirect(new URL('/join', request.nextUrl.origin));
try {
const {
token: { accessToken: reIssuedAccessToken, refreshToken: reIssuedRefreshToken },
} = await postReissue(
{ accessToken, refreshToken },
{ headers: { 'X-AUTH-TOKEN': accessToken } }
);
const response = NextResponse.next();
response.cookies.set(AUTH_KEYS.accessToken, reIssuedAccessToken);
response.cookies.set(AUTH_KEYS.refreshToken, reIssuedRefreshToken);
return response;
} catch (e) {
console.log(e);
return NextResponse.redirect(new URL('/join', request.nextUrl.origin));
}
};
-
accessToken과refreshToken을 쿠키로부터 얻는다.const accessToken = request.cookies.get(AUTH_KEYS.accessToken)?.value; const refreshToken = request.cookies.get(AUTH_KEYS.refreshToken)?.value; -
accessToken혹은refreshToken이 없다면 회원가입 페이지(/join)로redirect한다.if (!accessToken || !refreshToken) return NextResponse.redirect(new URL('/join', request.nextUrl.origin)); -
accessToken혹은refreshToken모두 있다면, 토큰을 재발급하는 함수인postReissue를 실행한다.try { const { token: { accessToken: reIssuedAccessToken, refreshToken: reIssuedRefreshToken }, } = await postReissue( { accessToken, refreshToken }, { headers: { 'X-AUTH-TOKEN': accessToken } } ); }catch(e){ // .. }// apis/auth/apis.ts export const postReissue = async (ReissueData: ReissueRequest, requestInit?: RequestInit) => privateFetch.post<ReissueResponse>('/auth/token-reissue', { body: JSON.stringify(ReissueData), ...requestInit, });poseReissue함수이다. 인자로 전달받은accessToken과refreshToken을 가지고 서버에 새로운accessToken을 요청한다.이 때, fetch 기반의 커스텀 인스턴스인
privateFetch를 이용했다. axios 기반의 인스턴스가 아닌 fetch 기반의instance를 이용한 이유는, axios는 XMLHttpRequest기반의 라이브러리인데 middleware에서 XMLHttpRequest API를 지원하지 않기 때문이다.axios를 사용할 경우, 사진과 같이 에러가 발생한다.
관련 글 :
결론을 간략하게 이야기하면 이러한 경우 axios대신 fetch를 이용하라는 내용이다.
CustomFetch
// apis/customFetch.ts
class CustomFetch {
async common<T>(route: string, requestInit?: RequestInit): Promise<T> {
const response = await fetch(`${BASE_API_URL}/api/v1${route}`, {
...requestInit,
credentials: 'include',
headers: new Headers({
'content-type': 'application/json',
...requestInit?.headers,
}),
});
if (response.ok) return await response.json();
throw Error(response.status + '');
}
// ..
}
const customFetch = new CustomFetch();
export { customFetch };
headers, base url등을 미리 설정한 customFetch인스턴스를 만들었다. 이 customFetch를 middleware.ts처럼 axios를 사용하지 못하는 곳에서 사용하면 된다.
결론
cookies이용한 Cookie설정은 server action 혹은 Route Handler에서밖에 안된다는 것을 깨달았다.
또한 내가 사용하는 프레임워크에 대해 깊이 아는 것의 중요성을 느꼈다. 앞으로는 공식문서를 자주 읽어봐야겠다. 무엇이 어떤건지 모두 알지는 못하더라도 무엇이 있는 지 정도는 알아두자. Middleware라는 것도 이번 이슈를 해결하며 알게 되었다.