-
프로덕션 환경에서 체크하던 중, 결제가 되지 않는 문제가 발생했다.
개발 환경에서는 정말 아무 문제 없이 돌아가던 기능이었고, 배포까지 마무리한 상황이었기 때문에
어? 이게 왜 안되지?🤔.... 싶은 마음에 바로 로그를 찍어보기 시작했다.
💥 현재 에러 상황
결제 후 리다이렉션 되는 페이지에서 404에러가 발생하고 있었다.
콘솔을 보니 URL 파라미터 중 하나가 undefined로 뜨면서 페이지가 터진 상태였다.
직감적으로 404는 나의 문제다....라는 느낌이 와바박 들었다😂
근데 또 신기한게 orderId 같은건 URL 상에 분명히 잘 들어오고 있었다.
대체 뭐지...?
문제를 추적하기 위해 코드를 다시 열어보았다.콘솔 찍어가며 문제 원인 추적하기🤔
1. 결제 버튼 쪽? (✅ 이상없음)
먼저 PaymentButton 컴포넌트(결제 버튼)를 다시 확인했다.여기서는 사용자가 결제 버튼을 눌렀을 때, Toss 결제 창을 띄우고,
결제가 완료되면 successUrl로 잘 리다이렉트 될 수 있도록 처리되어 있었는데, 아무 문제도 없없다.
2. 결제 성공 페이지 (❌ 문제 발생)
문제는 PaymentSuccess 페이지에서 발생했다.
결제 후 넘어온 페이지에서 URL 파라미터를 잘 받았는지 콘솔을 찍어보니,
address 값이 undefined로 나오는 것이었다.
❌ 문제점 1) address 값이 undefined
`useShippingAddress`에서 비동기로 데이터를 불러오고 있는데,
이 로딩이 끝나기 전에 결제 처리 로직이 실행되면서 문제가 생긴 것 같았다.
해결 방법
useShippingAddress 훅에서, isLoading을 함꼐 받아오고
address가 로딩 중일 땐 결제 처리 로직을 실행하지 않도록 조건을 추가했다
// 기존 const { data: address } = useShippingAddress(session?.user.id); // 변경 const { data: address, isLoading: addressLoading } = useShippingAddress(session?.user.id); // 조건 추가 if (addressLoading || !address) return;
이걸로 첫 번째 문제는 해결!
그런데 끝이 아니었다...? 😂
이전의 내가 짠 레거시 코드 (ㅋㅋㅋ)를 보면서 오랜만에 보다보니 아니......이거 왜 이렇게 짠겨...? 하는 부분에 있어서
결제 버튼 컴포넌트(PaymentButton.tsx)에서 성공 리다이렉션 URL을 간단하게 수정 했었,,,다...
수정한 코드
await tossPayments.requestPayment("카드", { amount, orderId, orderName, successUrl: `${window.location.origin}/payment/success`, //이부분 수정함!!! failUrl: `${window.location.origin}/payment/fail`, });
이전 코드
successUrl: `${ window.location.origin }/payment/success?orderId=${orderId}&amount=${amount}&orderName=${encodeURIComponent(orderName)}`,
기존코드는 이렇게 일일이 보내고 있었음 (...)
이렇게 일일이 설정할 필요 없이, 자동으로 paymentKey, amount, orderId의 파라미터를 보내준다.
하지만 이렇게 수정하고 나니,
orderName(상품이름)을 받아올 수 없어서 이번엔 orderName이 undefined 가 뜨고 있었다 😂
고민의 괴리..?
orderName을 다시 이전처럼 url로 파라미터를 보내줄까? 생각했다.
하지만 URL로만 봤을때 orderId를 가진 사용자가 무엇을 주문했는지 대충 유추할 수 있다는 것과
민감 데이터 유출 가능성이라는 관점에서 생각해보면 (...)
당연히 보안상 이렇게 하면 안될 것 같은 느낌이 들었다.
그리고 아무리 공식문서를 뒤져봐도, orderName에 대한 정보는 포함되지 않았다 (문서상에도 명시가 안되어있음...)
그래서 다시 로직을 생각해 보니
내가 애초에 탄탄하게 미리 설계했더라면...? 😂사용자가 결제중일때 미리 DB에 저장해서 사용자가 결제성공하면 DB에 그대로 저장하고,
만약 결제를 취소하거나 오류로 인해 결제가 안됐을 경우는 DB를 삭제하면 더 좋지 않을까? (...) 라는 설계를 생각했을 것 같다.
그러면 나중에 사용자가 결제를 하려다 취소한 것도 추적이 가능하고 히스토리도 대충 알것같지 않은가.....!!!!!
하지만 지금 최선의 리팩토링을 생각했을 때...지금 잘 활용하고 있는 로컬스토리지에 orderName을 저장하는 방법 밖에 없었다.
Recoil의 전역상태를 생각했으나, 리다이렉션 하게되면 새로고침이 된 상태로 이동하기 때문에전역상태로는 다시 값이 없어지네? 생각하여 결국 로컬스토리지를 활용할 수 밖에 없었다.
결국 다시 돌아와서 문제점을 다시 정리해보자면
❌ 문제점 2) orderName이 undefined?
해결 방법
localStorage를 활용해서 결제 요청 전에 orderName을 저장하고, 결제 성공 시 해당 값을 불러와 사용했다.
// PaymentButton.tsx 결제하기 버튼 localStorage.setItem("order_name", orderName); // PaymentSuccess.tsx 결제 완료 후 성공 페이지 const orderName = searchParams.get("orderName") ?? localStorage.getItem("order_name") ?? "주문상품"; // 결제 성공 시 orderName 로컬스토리지에서 제거하기 useEffect(() => { if (isPaymentProcessed) { localStorage.removeItem("order_name"); } }, [isPaymentProcessed]);
이걸로 결제 상품명도 정상적으로 들어오게 됐다!
사실 이 방법도 완벽한건 아니지만, 지금으로서는 최선이라고 생각하였다....!
그래서 드디어 결제도 다시 됐나!?!?? 했는데.....
결제 자체는 성공했지만, 무언가 또 잘못되었다. 😂
결제를 성공한 데이터들은 전부 콘솔에 잘 찍히고 있었지만, 조건이 또 만족하지 못했다
이번엔 위에서 부터 고쳤던 orderId 는 제대로 있는 것을 확인했는데, 결국 남은것은 paymentKey 조건이 만족하지 않다는 것이었다.
❌ 문제점 3) Toss 인증 API 호출 실패
이번엔 paymentKey부분 관련 컴포넌트를 확인해보니...
현재 백엔드에서 Toss 결제 승인 API를 호출하는 로직을
클라이언트에서 Toss서버로 직접 요청 하고 있었다. (바보냐..???)
Toss의 Secret Key는 절대로 클라이언트에 노출되면 안되는 민감한 보안 정보다.
근데 나는 지금까지 이걸 클라이언트에서 직접 호출하고 있었던 것이었다..;;; (맙소사)
해결 방법
→ API Route로 옮기는 작업으로 진행 !
Next.js에서 제공하는 /app/api 경로에 route.ts 파일을 만들고, 결제 인증을 서버에서 처리하도록 로직을 옮겼다.
// app/api/payment/confirm/route.ts import { NextRequest, NextResponse } from "next/server"; export async function POST(response: NextRequest) { try { const { paymentKey, orderId, amount } = await response.json(); if (!paymentKey || !orderId || !amount) { return NextResponse.json( { message: "결제 승인에 필요한 항목이 누락되었습니다.", error: "paymentKey, orderId, amount는 필수 항목입니다.", }, { status: 400 } ); } const tossRes = await fetch(`${process.env.PAYMENT_CONFIRM_URL}`, { method: "POST", headers: { Authorization: `Basic ${Buffer.from( `${process.env.TOSS_SECRET_KEY}:` ).toString("base64")}`, "Content-Type": "application/json", }, body: JSON.stringify({ paymentKey, orderId, amount }), }); const data = await tossRes.json(); if (!tossRes.ok) { return NextResponse.json( { message: data.message || "Toss 결제 승인 실패", ...data }, { status: tossRes.status } ); } return NextResponse.json(data); } catch (error) { return NextResponse.json( { message: "결제 승인 처리 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }, { status: 500 } ); } }
클라이언트에선 이제 이 API Route(/api/payment/confirm)만 호출하면 된다.
보안상 안전하고, Toss Key도 노출되지 않으니까 깔끔!
✅ 결제 성공! 드디어 끝!
이렇게 모든 문제를 하나하나 정리하고 수정한 후,
결제도 잘 되고, 결제 데이터도 Supabase에 잘 저장되는 걸 확인했다!!! 🎉
📝 회고
이번 문제를 겪으면서 다시 한번 느낀 점
기능이 잘 작동하더라도 그 작동의 원리와 이유를 명확히 이해하지 못한다면
리팩토링 이후에 문제가 생겼을 때 제대로 대응할 수 없다.
결제처럼 민감한 기능은 특히 더......
작동 방식, 보안 구조, 상태 관리까지 모두 설계 단계부터 꼼꼼히 생각해둬야
나중에 덜 고생하게 된다는 걸 이번에 아주 제대로 배웠다. 🥲
이제 진짜 결제 페이지 오류는 끝!
(아직 할 일은 산더미 ^0^....)
'✨ 기억보다 기록을 > 트러블슈팅' 카테고리의 다른 글
프리렌더링 성능개선 + generateMetadata 리팩토링 (0) 2025.05.09 🔥자유게시판 성능 개선 SSul (Supabase + React Quill 최적화) (1) 2025.05.02 자유게시판 대댓글 삭제 이슈 (Supabase Foreign Key 문제) (0) 2025.03.05 뱃지 컴포넌트 CSS 프리렌더링 문제 (0) 2024.11.22 IceCraft - 마피아 게임 페이지 CSS 리팩토링 과정 기록 (0) 2024.08.12 댓글