영상은 찍어두지 못했는데, 카카오톡으로 토론 채팅방을 공유하고 공유받은 사용자가 서비스에 접속하게 될 때 문제가 발생했다.
해당 사용자는 세션이 없기 때문에, 로그인을 해야 한다.
그런데 로그인 후 분명 사용자 로직상으로는 로그인 전에 들어가려고 했던 채팅방으로 연결되어야 하지만, 홈으로 이동해버리는 문제가 있었다.
이를 해결한 과정을 포스트하려고 한다.
먼저, 나는 next14, auth.js를 사용하고 있다.
이를 통해 카카오톡과 구글 로그인을 적용한 상태이며, 이를 참고하여 callbackUrl을 활용하고자 했다.
가장 먼저 떠오른 아이디어는, 쿠키에 담아두는 것이었다.
사용자가 접속하려고 하는 경로를 쿠키에 담고, 로그인 시 해당 쿠키에 데이터가 남아있으면 그 경로로 redirect 시키는 것이다.
하지만 마음에 들지는 않았다.
이미 세션 쿠키로 새로고침을 대비해 채팅방 정보를 저장하고 있기 때문에 여기서 쿠키에 뭔가를 더 담기는 싫었다.
그렇다면 localStorage는 어떨까?
localStorage도 마찬가지였다.
쓰고있는 공간은 없지만, 쿠키와 비슷한 이유로 이렇게 문제를 마주칠 때마다 storage에 저장하는 것은 좋지 못하다는 생각이 들었다.
그래서 storage 저장이 아닌 다른 방법으로 문제를 해결해보려고 했다.
storage를 사용하지 않고 callbackUrl을 적용하기 위해 현재 사용중인 기술들을 훑어봤다.
auth에서 제공하는 redirect를 통해 로그인 후 어디로 페이지를 이동시킬 것인지를 결정할 수 있었다.
하지만 이것만 사용하기엔 로그인 로직이 복잡해 부족했다.
Athens 로그인 로직은 Auth + 백엔드 같이 동작하기 때문이다.
로그인 로직은 다음과 같다.
위와 같은 로직으로 로그인/회원가입이 수행된다.
위 로직을 수행하기 위해 / 페이지뿐만 아니라 temp-token으로 access-token을 받을 페이지 login/auth 를 추가하여 로더를 띄워주었다.
그래서 auth에서 제공하는 redirect 객체의 반환값은 /login/auth를 해주어야 했다.
그렇기에 단순히 redirect 객체의 반환값으로 callbackUrl을 추가한다고 끝나는게 아니었다.
어떻게 수정했는지는 이제부터!
세션이 없기 때문에 middleware에서 로그인 페이지로 redirect 시킬 것이다.
여기에 callbackUrl을 searchParams로 추가하여 재설정해준다.
// middleware.ts
if (isNull(session)) {
const redirectUrl = req.nextUrl.pathname + req.nextUrl.search;
const loginUrl = new URL(`${process.env.NEXT_CLIENT_URL}/`);
loginUrl.searchParams.set('callbackUrl', redirectUrl);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
그럼 로그인 페이지로 이동하면서 경로상에 callbackUrl로 사용자가 접속하려고 했던 경로가 뜨게 된다.
<Link> 태그 href에 callbackUrl을 추가하여 전달하기서버에게 Link 태그로 SNS 로그인/회원가입을 요청하기 때문에, 제목 그대로 href 에 callbackUrl을 추가하면 된다.
여기서 한 가지 문제가 발생했다.
이미 서버에서는 redirect_uri를 클라이언트로부터 searchParams로 전달받기 때문에 redirect_rui 안에 searchParams를 넣을 수가 없다.
// 전달되게 되는 url 경로
...?redirect_uri=${origin/login/auth}?callbackUrl=${사용자 접속 경로}
이 문제는 서버에게 url을 encode하여 전달하면 해결된다.
기존에는 서버에서 올바르지 않은 경로로 인식하게 되어 에러가 발생하지만, encode하여 보내면 서버에서 decode하여 올바르게 인식하게 된다.
나는 page.tsx 에서 searchParams에서 callbackUrl을 뽑아 컴포넌트로 전달해주었다.
// SNSLogin.tsx
const getRedirectUri = (provider: string) => {
let redirectUri = `${process.env.NEXT_PUBLIC_CLIENT_URL}/login`;
if (!isNull(callbackUrl)) {
redirectUri += `?callbackUrl=${callbackUrl}`;
}
const encodedRedirectUri = encodeURIComponent(redirectUri);
const finalUrl = `${process.env.NEXT_BASE_URL}/oauth2/authorization/${provider}?redirect_uri=${encodedRedirectUri}`;
return finalUrl;
};
redirect_uri 에 searchParams로 callbackUrl까지 포함하여 경로를 만들어준다.
이후 해당 값을 encodeURIComponent 를 통해 encode 해준다.
이후 redirect_uri에 encode한 url을 넣어주면 된다.
그럼, 서버에서 해당 url을 해석하여 올바르게 인식하게 된다.
기존 / 페이지에서 temp-token을 받아 로그인을 해주기 위해 페이지를 이동시킨다.
/ -> /login/auth
// app/login/route.ts
import isNull from '@/utils/validation/validateIsNull';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const token = searchParams.get('temp-token');
const callbackUrl = searchParams.get('callbackUrl');
const error = searchParams.get('error');
if (error) {
return NextResponse.redirect(`${process.env.NEXT_CLIENT_URL}/`);
}
if (!isNull(callbackUrl)) {
return NextResponse.redirect(
`${origin}/login/auth?user=${token}&callbackUrl=${callbackUrl}`,
);
}
return NextResponse.redirect(`${origin}/login/auth?user=${token}`);
}
이 부분은 기존에 작성되어 있던 부분이다.
흐름상 작성해주었다.
const getUserAccessToken = useCallback(
async (authuser: string) => {
const tempToken = await signInWithCredentials(authuser);
if (tempToken.success) {
try {
await signIn('credentials', {
accessToken: tempToken.accessToken,
});
} catch (error) {
showToast('로그인에 실패했습니다. 다시 시도해주세요.', 'error');
router.replace('/');
}
} else if (!tempToken.success) {
showToast('로그인에 실패했습니다. 다시 시도해주세요.', 'error');
router.replace('/');
}
},
[router],
);
이제 서버에서는 redirect_uri 로 1번과 같은 값을 받았기 때문에 temp-token을 만들어 redirect_uri 경로로 해당 temp-token을 전달해 줄 것이다.
redirect: async ({ url, baseUrl }) => {
if (url) {
const { search, pathname } = new URL(url);
const callbackUrl = new URLSearchParams(search).get('callbackUrl');
if (!isNull(callbackUrl)) {
return callbackUrl.startsWith('/')
? `${baseUrl}${callbackUrl}`
: callbackUrl;
}
// 로그인 성공 시 리다이렉트할 페이지
if (pathname === '/login/auth' || pathname === '/') {
return `${baseUrl}/home`;
}
// if (origin === baseUrl) return url;
}
return baseUrl;
},
먼저, url 에서 searchParams 에 있는 callbackUrl을 뽑아 존재하는지 확인한다.
만약 존재한다면, callbackUrl로 이동시킨다.
callbackUrl이 존재하지 않다면 기존처럼 홈으로 이동시킨다.
이러면 끝!
로그인 뿐만 아니라, 세션 없이 세션이 필요한 페이지로 이동하려고 할 때 callbackUrl로 해당 경로가 저장된다.
이후 로그인을 하게 되면 엑세스하려고 했던 페이지로 바로 이동하게 된다.
원래는 로그인 후 바로 홈으로 넘어가던 것을 사용자 정보 페이지(처음 세션 없이 접속하려고 했던 페이지)로 이동하게 되었다.