[Next] 인증되지 않은 사용자 OAuth 로그인 이후 이전 페이지로 리다이렉트 구현. NextJS App Router
[Next] 인증되지 않은 사용자 OAuth 로그인 이후 이전 페이지로 리다이렉트 구현. NextJS App Router
배경
최근 gsm-networking(우리 팀에서 만든 서비스)에 게시판 기능이 릴리스 되었다. 다양한 주제의 게시글을 작성하고 서로 이야기할 수 있는 기능이다. 이 게시판 기능의 트래픽을 만들기 위해 단체 채팅방에 내가 작성한 글의 링크를 공유했다.
여기서 문제를 확인할 수 있었다. 인증되어있지 않은 유저가 링크를 통해 접속한다면, server side fetching 이후 이런 화면을 보게 된다.
로그인을 진행한다면 메인 페이지로 이동된다. 그럼 유저는 원래 목적인 공유된 게시글을 바로 확인할 수 없다. 로그인 이후 다시 링크를 통해 접속하거나, 메인 페이지부터 원하는 게시글까지 직접 찾아가야 한다.
이런 user flow는 굉장히 비효율적이고, UX를 고려하지 않은 동작이다.
당연히 사용자는 로그인 이후 이전에 접속했던 페이지로 이동을 원하겠지만, 실제로 그렇지 않은 꽤 있다.
gsm-networking은 지금까지 적은 페이지 개수와 짧은 흐름을 갖고 있었기 때문에 이러한 동작이 고려되지 않았던 것 같다. 딱히 필요성을 못 느꼈다고 할 수 있겠다. 하지만 게시판 기능이 추가되고 링크를 통한 공유하기가 활성화된 지금은 꼭 UX적으로 도움이 되는, 로그인 이후 원하는 페이지로 리다이렉트 되는 기능을 구현할 필요가 있다.
user flow와 요구사항을 정리하면 아래와 같다.
요구사항
1. 인증되지 않은 사용자가 /abc path로 접속.
2. server side fetching으로 인증되지 않은 유저임을 확인 후 refresh 한다.
3. refresh 과정에서 refreshToken이 없다면 /auth/signin path로 리다이렉트 한다.
4. 로그인 과정에서 google OAuth 페이지로 이동 후 서버에서 / root path로 리다이렉트 한다.
5. 이전에 접속한 페이지(/abc)가 있다면 / 에서 /abc로 리다이렉트 한다.
내가 구현해야 했던 것은 5번의 사항이다.
해결 과정
처음에는 서버 측에 이 문제를 해결할 수 있는지 문의했다.
4. 로그인 과정에서 google OAuth 페이지로 이동 후 서버에서 / root path로 리다이렉트 한다.
로그인 시 서버에 param으로 redirect path를 넘겨주면 로그인 이후 해당 path로 리다이렉트 할 수 있을 줄 알았다. 하지만 서버 측에서는 고정된 값으로 리다이렉트 uri를 지정하기 때문에 불가능했다. 서버 측에 대한 지식이 부족하여 문의를 통해 답을 얻을 수 있었다.
자. 그럼 확실히 프론트에서 구현해야한다. 이제 프론트에서 어떻게 해결할 수 있을지 고민해 보았다.
먼저, 라우터 이벤트를 감지해서 로그인하기 전의 path를 저장할 수 있을까에 대한 생각을 했다. 그리고 바로 시도해 보았다.
Next page router에서는 userRouter를 사용한 router.events를 감지하여 쉽게 문제를 해결할 수 있었다. 그런데 gsm-networking은 app router를 사용하고 있었다. 때문에 router.events는 지원되지 않았다.
이 문제는 쉽게 해결할 수 있었다. 답은 공식문서에서 찾을 수 있었다.
https://nextjs.org/docs/app/api-reference/functions/use-router#router-events
Functions: useRouter | Next.js
API reference for the useRouter hook.
nextjs.org
app router에서는 usePathname과 userSearchParams를 이용하여 해결할 수 있었다.
그런데 이 문제는 시작에 불과했다.
session storage에 이전 path와 현재 path를 저장하여 path 저장은 가능했습니다. 하지만 OAuth 로그인 -> 루트 페이지 접속 -> 세션스토리지 확인 -> path가 있다면 리다이렉트 이런 flow는 비효율적이다. 클라이언트 측에서 루트 페이지에 접속하기 전에 서버 측에서 리다이렉트 시키는 것이 더 빠르다. 이는 곧 UX에 직접적으로 다가올 수 있다. 서버 측에서는 session stroage에 접근할 수 없다. 따라서 session storage에 path를 저장하는 방법은 사용 불가능하다.
(사실 로그인 시 google OAuth 페이지로 이동하며 session storage가 초기화될 줄 알았지만 이는 내 실수였다. 페이지 이동은 session stroage 초기화에 영향을 주지 않는다. 하지만 위에서 설명한 대로 session stroage를 사용하는 것은 좋지 않다.)
또한 페이지가 이동할 때마다 이를 감지하여 session stroage에 저장하는 방식은 비효율적이다. 직접적으로 사용자에게 느껴질 정도로 비효율적이진 않겠지만, 매번 path를 저장할 필요가 없다.
이후에 몇 가지 방법을 더 시도해 보았다.
1. google OAuth redirect uri를 프론트 측에서 지정하도록 하여 유동적으로 redirect.
gsm-networking은 OAuth 관련 로직을 대부분 서버 측에서 처리했다. 이를 프론트에서 처리하도록 일부 변경한 후 유동적으로 redirect uri를 지정할 수 있을까 했지만, redirect uri missmatch 오류가 생길 것이 뻔했다.
2. 브라우저의 history API를 이용하여 구현.
history API를 사용하면 이전 경로로는 쉽게 이동할 수 있지만, 그 경로가 gsm-networking의 경로인지, 혹은 그 경로가 안전한지 보장할 수 없다. 어떤 페이지가 history stack에 존재하는지 알 수 없다.
여기서 계속 고민만 하다가는 문제 해결까지 너무 오래 걸릴 것 같았다. 사수님(선배님)께 문제상황과 시도한 방법을 설명드리고 도움을 구했다. 사수님께서는 /auth/refresh에서 cookie에 path를 저장하라는 솔루션을 주셨다.
기존에 cookie에 path를 저장할 생각을 하지 못했던 이유는 의도하지 않은 페이지 이동이 생길까 봐서였다. cookie가 남아있어 갑자기 페이지가 이동하거나 원하지 않은 상황에서 cookie가 저장되어 페이지 이동을 야기할 경우가 떠올랐다.
/auth/refresh에서 cookie에 path를 저장하게 되면 의도하지 않은 path 이동을 일부 해결할 수 있었다. 로그인 페이지에 접속하는 사용자는 인증되지 않은 사용자이다. 인증되지 않은 사용자는 /auth/refresh에서 refreshToken이 존재하는지 확인하는 과정을 거쳐 로그인 페이지로 이동하기 때문에 /auth/refresh가 길목 역할을 한다고 말할 수 있다. 이 길목만 막으면 된다. refresh 함수의 인자로 redirect path를 넘겨 이를 저장한다.
자 이제 거의 모든 문제가 해결되었다.
하지만 최종 보스가 남았다.
로그인 이후 루트 페이지로 접속했을 때 쿠키에 redirectPath가 존재하면 redirect 시킨다.
const redirectPath = cookies().get('redirect')?.value;
if (redirectPath && redirectPath !== '/') {
return redirect(redirectPath);
}
이 상태라면 아직 의도하지 않은 redirect가 생길 수 있다. redirect와 함께 cookie가 제거되지 않기 때문에 다시 한번 루트 페이지 접속 시 다시 cookie에 존재하는 path대로 redirect 된다. 따라서 cookie를 제거하는 로직이 필요하다.
서버 측에서 redirect 함과 동시에 cookie 내부의 redirect 필드를 제거하면 가장 좋을 것이다. 하지만 이는 불가능하다.
아래 discussion에서 관련 내용을 확인할 수 있다. 302 status를 반환하면 된다고 하지만, redirect 하는 파일은 router handler가 아니다. route.ts가 아닌 page.tsx파일이다. NextResponse를 사용하지 않는다.
https://github.com/vercel/next.js/discussions/48434
Redirect and Set-Cookie in next 13 (new route.js) · vercel next.js · Discussion #48434
Summary I'm developing an auth api, using the new "app" folder route system. But I realized that it is not possible to redirect and set a cookie and vice versa. (my folder: "app/api/auth/route.js")...
github.com
문제를 해결하기 위해 아래와 같은 방법을 사용했다.
Provider에서 라우트 이벤트가 생기면 auth와 관련된 페이지인지 검사한다. 만약 auth와 관련된 페이지가 아니라면, cookie를 제거하는 util 함수를 호출한다. 쿠키가 중복되는 문제를 해결하기 위해 바로 쿠키의 값을 없애는 것이 아닌, 만료 기간을 설정하여 쿠키를 만료시킨다.
useEffect(() => {
if (!path.includes('auth')) deleteCookie('redirect');
}, [path, params]);
const deleteCookie = (cookieName: string) => {
document.cookie =
cookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
};
export default deleteCookie;
이렇게 모든 문제를 해결하여 요구사항을 만족할 수 있었다.
문제를 해결하기 위한 전체 코드는 아래의 PR에서 확인할 수 있다.
https://github.com/themoment-team/GSM-Networking-front/pull/140
[Hotfix] Update redirect that after login by frorong · Pull Request #140 · themoment-team/GSM-Networking-front
개요 💡 공유가 가능하도록 로그인 후 이전 페이지로 리다이렉트 할 수 있도록 했습니다. 작업내용 ⌨️ 요구사항 1.token이 만료된 유저가 /abc path로 접속 2.token 만료 확인 후 refresh -> /auth/signin이
github.com
100% 완벽한 해결 방법은 아니었겠지만, 나름 많은 고민을 하여 해결했기 때문에 뿌듯한 감정을 느꼈다. App router와 server side라는 개념 때문에 쉽지 않았다. 문제를 해결하면서 다시 한번 개념적인 원리에 대한 이해가 중요하다는 것을 깨달았다.
만약 글을 읽고 있는 분께서 광주소프트웨어마이스터고등학교 동문이라면 아래 링크에서 결과를 확인할 수 있습니다.
https://www.gsm-networking.com/community/board/1
로그인
로그인 페이지 입니다.
www.gsm-networking.com
글이나 해결 방법에 대한 피드백이 있다면 부탁드립니다. 저를 성장시켜 주는 질타는 언제나 환영입니다.