
로그인시켰더니 홈으로 가버리네? (매출 30% 까먹은 썰)
사용자가 상품 결제를 하려다 로그인 창이 떠서 로그인을 했는데, 뜬금없이 메인 페이지로 이동된다면? 당황한 사용자는 그대로 이탈합니다. 로그인 후 원래 페이지로 되돌려보내는 리다이렉트 구현 방법과, 그 과정에서 발생할 수 있는 보안 취약점(Open Redirect)을 막는 방법을 공유합니다.

사용자가 상품 결제를 하려다 로그인 창이 떠서 로그인을 했는데, 뜬금없이 메인 페이지로 이동된다면? 당황한 사용자는 그대로 이탈합니다. 로그인 후 원래 페이지로 되돌려보내는 리다이렉트 구현 방법과, 그 과정에서 발생할 수 있는 보안 취약점(Open Redirect)을 막는 방법을 공유합니다.
프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

비트코인은 블록체인의 일부입니다. 이중 지불 문제(Double Spending), 작업 증명(PoW)과 지분 증명(PoS)의 차이, 스마트 컨트랙트, 그리고 Web 3.0이 가져올 미래까지. 개발자 관점에서 본 블록체인의 모든 것.

쇼핑몰을 런칭하고 첫 주, 데이터를 보는데 정말 이상한 현상이 있었습니다. "장바구니 담기" → "결제하기" 버튼 클릭률은 꽤 높은데, 실제 최종 결제 완료율(Conversion Rate)이 너무 낮았습니다. 이탈률이 거의 70%에 육박했죠. "아니, 결제하겠다고 마음먹고 버튼까지 누른 사람들이 왜 다 도망가는 거지?"
너무 답답해서 Hotjar(사용자 화면 녹화 도구)를 설치해서 사용자들의 행동을 훔쳐봤습니다. 그리고 저는 제 모니터를 부술 뻔했습니다.
세상에, 로그인 성공 후 원래 보려던 페이지(결제 페이지)로 보내주지 않고, 바보같이 무조건 홈(/)으로 보내버리고 있었던 겁니다.
이 사소한 디테일 하나가 매출의 30%를 날려먹고 있었습니다. 이건 기술적 버그가 아니라 UX의 재앙이었습니다.
이 문제를 해결하려면 로그인 페이지가 "사용자가 어디서 왔는지"를 기억해야 합니다. State 관리 라이브러리(Redux, Zustand)를 쓸 수도 있지만, 가장 확실하고 쉬운 방법은 URL을 이용하는 겁니다.
보통 redirect, callbackUrl, returnTo, next 같은 이름을 쿼리 스트링(Query String)으로 사용합니다.
사용자가 /checkout 페이지에 접근하려는데 로그인이 안 되어 있다면?
그냥 /login으로 쫓아내지 말고, 등 뒤에 "/checkout 에서 왔음"이라는 쪽지를 붙여서 보냅니다.
/* Next.js Middleware 또는 Protected Route 처리 */
export function middleware(request: NextRequest) {
const currentUser = request.cookies.get('currentUser');
// 1. 로그인이 안 되어 있고, 보호된 페이지에 접근하려 할 때
if (!currentUser && request.nextUrl.pathname.startsWith('/checkout')) {
const loginUrl = new URL('/login', request.url);
// 2. 현재 페이지 경로를 'redirect' 파라미터로 붙임
// 예: /login?redirect=/checkout
loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
return Response.redirect(loginUrl);
}
}
이제 주소창은 https://myshop.com/login?redirect=/checkout 이렇게 바뀝니다.
이제 로그인 페이지(Login Component)는 단순히 홈으로 이동(router.push('/'))하면 안 됩니다.
URL에 있는 redirect 값을 읽어서, 그곳으로 보내줘야 합니다.
/* Login Page Component */
import { useRouter, useSearchParams } from 'next/navigation';
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
// 3. URL에서 'redirect' 값을 읽음. 없으면 기본값 홈('/')
const redirectUrl = searchParams.get('redirect') || '/';
const handleLogin = async (e) => {
e.preventDefault();
const result = await loginApi(email, password);
if (result.success) {
// 4. 원래 가려던 곳으로 이동!
router.push(redirectUrl);
}
};
// ...
}
이 간단한 코드 10줄을 배포하자마자, 결제 페이지 도달률이 2배로 뛰었습니다. 사용자의 흐름(Flow)을 끊지 않는 것이 비즈니스에 얼마나 큰 영향을 주는지 뼈저리게 느꼈습니다.
기분 좋게 "매출 올렸다!" 하고 퇴근하려는데, 보안팀 친구가 제 어깨를 잡았습니다. "야, 이거 Open Redirect 취약점 있는데? 너 큰일 나."
"오픈 리다이렉트? 그게 뭔데?"
해커가 이런 피싱 링크를 만들어서 사용자들에게 메일을 뿌린다고 상상해 보세요.
이벤트 당첨! 로그인하고 확인하세요:
https://my-shop.com/login?redirect=https://hacker-site.com
my-shop.com 도메인만 보고 "아, 공식 사이트네" 하고 안심합니다.redirect 파라미터에 적힌 https://hacker-site.com으로 사용자를 보냅니다.소름 돋지 않나요? 제 친절한 리다이렉트 기능이 해커의 하이패스가 될 수 있습니다. 실제로 구글, 페이스북 같은 대기업들도 초기에 이 취약점으로 곤욕을 치렀습니다.
해결책은 간단합니다. "도메인 검증(Validation)"을 하면 됩니다. 리다이렉트 할 주소가 우리 사이트 내부(Internal Path)인지 확인하고, 외부 도메인이면 무조건 홈으로 보내버리는 겁니다.
const safeRedirect = (path: string) => {
// 1. 방어: "http"나 "https"로 시작하면 무조건 차단 (외부 도메인일 확률 99%)
if (path.startsWith('http:') || path.startsWith('https:')) {
console.warn(`[Security] 외부 리다이렉트 시도 차단: ${path}`);
return '/';
}
// 2. 방어: "/"로 시작하지 않으면 차단 (상대 경로 공격 방지)
if (!path.startsWith('/')) {
return '/';
}
// 3. 방어: "//"로 시작하는 경우도 차단 (Protocol Relative URL 공격)
// 예: //google.com 은 브라우저에서 http://google.com 으로 해석됨
if (path.startsWith('//')) {
return '/';
}
// 4. 방어: 제어문자(CRLF) 등이 포함된 경우 차단
if (/[\\x00-\\x1F\\x7F]/.test(path)) {
return '/';
}
return path; // 통과! 안심하고 보내도 됨.
};
// 사용 방법
router.push(safeRedirect(redirectUrl));
이제 해커가 ?redirect=https://hacker.com을 넣어도, 제 코드는 "어? 외부 주소네?" 하고 강제로 홈(/)으로 보내버립니다.
사용자는 안전하게 보호됩니다.
만약 우리 회사가 여러 도메인(shop.com, blog.shop.com)을 써서 외부 리다이렉트를 허용해야 한다면요?
그럴 땐 화이트리스트를 씁니다.
const ALLOWED_DOMAINS = ['myshop.com', 'blog.myshop.com'];
const isSafeDomain = (url) => {
try {
const parsedUrl = new URL(url);
return ALLOWED_DOMAINS.includes(parsedUrl.hostname);
} catch (e) {
return false; // URL 파싱 실패하면 무조건 차단
}
};
보안은 "막는 것"이 아니라 "허용할 것만 허용하는 것"입니다.
기능 구현은 끝났지만, UX를 좀 더 다듬어 볼까요?
사용자가 '장바구니 담기(POST)'를 눌렀는데 로그인이 풀려있다면?
로그인하고 돌아오면 장바구니에 상품이 담겨있을까요? 아니요, 보통은 그냥 페이지 덩그러니 있겠죠.
이럴 땐, 장바구니 정보를 localStorage나 쿠키에 임시 저장했다가, 로그인 후 리다이렉트 되었을 때 복구해 주는 로직이 필요합니다. (이건 좀 복잡하니 다음 글에서 다루겠습니다.)
?redirect= 뒤에 엄청 긴 URL이 붙으면 보기 싫을 수 있습니다.
이럴 땐 base64로 인코딩해서 넘기거나, 세션 스토리지에 저장하는 방법도 있습니다.
하지만 심플한 게 최고입니다. 저는 그냥 쿼리 스트링을 선호합니다.
만약 login 페이지의 redirect 값이 다시 login 페이지라면?
사용자는 영원히 로그인 페이지를 맴돌게 됩니다.
if (path === '/login') return '/'; 같은 방어 코드를 한 줄 넣어주세요.
로그인 리다이렉트는 쇼핑몰, 커뮤니티, SaaS, 어드민 사이트 어디서든 필수 기능입니다. 하지만 잘못 구현하면 매출을 깎아먹거나(UX), 보안 구멍(Security)을 뚫어버릴 수 있는 양날의 검입니다.
오늘의 3줄 요약:?redirect=...)을 쓰면 구현이 쉽습니다.
safeRedirect 함수 필수!)여러분의 로그인 페이지는 친절한가요? 아니면 사용자를 미아로 만들고 있나요?
지금 바로 F12를 눌러 확인해 보세요.