결제 위젯 paymentsData 파싱 오류 해결

함민혁·2026년 4월 22일

결제 위젯 paymentsData 파싱 오류 해결 과정

문제 상황

결제 위젯 URL에서 paymentsData에 들어가는 orderName 값이 아래처럼 %를 포함할 때 오류가 발생했다.

맛있는 다라딸기! 59%%%

실제 URL에서는 %가 URL 인코딩되어 아래처럼 보였다.

...paymentsData=%7B...%22orderName%22%3A%22...59%25%25%25%22%7D

이 상태에서 웹 결제 위젯 페이지가 파라미터를 파싱할 때 에러가 났다.


처음 확인한 내용

앱 쪽 결제 진입 코드는 아래 흐름이었다.

  1. Cart.tsx (line 285) 에서 결제 데이터 객체를 JSON.stringify(data)로 문자열화
  2. Payments.tsx (line 46) 에서 encodeURIComponent(paymentsData)로 URL query param에 넣음
  3. 웹뷰가 /payments-widgets 페이지를 열어 해당 query param을 읽음

즉 앱 쪽에서는 paymentsData를 정상적으로 한 번만 인코딩하고 있었다.


웹에서 확인한 원인

문제가 있던 웹 코드는 PaymentsWidgets.tsx (line 52) 의 이 부분이었다.

const parsedPaymentsData = JSON.parse( decodeURIComponent(paymentsDataParam), );

여기서 핵심은:

  • URLSearchParams.get('paymentsData') 는 이미 URL 디코딩이 끝난 값을 반환한다.
  • 그런데 그 값을 다시 decodeURIComponent(...) 하면, JSON 안에 들어 있던 %%% 를 또 해석하려고 하면서 URI malformed 가 발생한다.

즉 문제는 URL 자체가 아니라, 웹에서 한 번 더 디코딩한 것이었다.


실제로 파라미터가 어떻게 보였는지

URL에는 이런 식으로 들어간다.

paymentsData=%7B%22orderId%22%3A%22ORD-20260422-0007%22%2C%22customerKey%22%3A%22USER-20251226-0004%22%2C%22orderName%22%3A%22맛있는+다라딸기!+59%25%25%25%22%7D

하지만 브라우저에서 아래 코드를 실행하면:

const searchParams = new URLSearchParams(window.location.search); const paymentsDataParam = searchParams.get('paymentsData');

paymentsDataParam 값은 이미 이렇게 된다.

'{"orderId":"ORD-20260422-0007","customerKey":"USER-20251226-0004","orderName":"맛있는 다라딸기! 59%%%"}'

따라서 이 값은 바로 JSON.parse(...) 하면 된다.


중간에 검토했던 방안

처음에는 혹시 과거 호환성이나 다른 진입 경로 때문에 이중 인코딩된 값도 들어올 수 있을 가능성을 열어두고, 아래처럼 방어 코드를 잠깐 고려했다.

const parsePaymentsDataParam = (value: string) => { try { return JSON.parse(value); } catch { return JSON.parse(decodeURIComponent(value)); } };

이 방식은 다음 상황을 대비한 것이다.

  • 누군가 이미 encodeURIComponent 된 값을 다시 encodeURIComponent 해서 넘긴 경우
  • 직접 URL을 조립해서 잘못 호출한 경우
  • 다른 진입점이 존재하는 경우

왜 그 방어 코드를 최종적으로 제거했는지

확인 결과 현재 구조는 다음과 같았다.

  • 직접 URL 호출 없음
  • 다른 진입점 없음
  • 앱에서 웹뷰로 들어오는 경로 하나만 존재
  • 해당 경로에서 paymentsData는 정확히 한 번만 인코딩됨

이 조건이면 decodeURIComponent fallback을 남겨두는 것보다, 현재 계약에 맞게 단순하게 처리하는 편이 더 맞다.

이유는:

  • 잘못된 입력이 조용히 통과하는 것을 막을 수 있음
  • 나중에 인코딩 규약이 깨졌을 때 더 빨리 발견 가능
  • 현재 실제 동작 계약과 코드가 정확히 일치함

최종 수정 내용

PaymentsWidgets.tsx (line 52) 에서 아래처럼 수정했다.

변경 전

const parsedPaymentsData = JSON.parse( decodeURIComponent(paymentsDataParam), );

변경 후

const parsedPaymentsData = JSON.parse(paymentsDataParam) as PaymentsData;

이후 orderName 이 맛있는 다라딸기! 59%%% 여도 정상 처리된다.


as PaymentsData 에 대한 판단

as PaymentsData 는 런타임 검증이 아니라 TypeScript 타입 단언이다.

의미는:

  • 없어도 동작 자체는 가능
  • 있으면 이후 코드에서 타입 추론이 조금 더 명확해짐
  • 실제 데이터 검증 역할은 하지 않음

즉 현재 수정의 본질은 as PaymentsData 가 아니라, decodeURIComponent 를 제거한 것이다.


이번에 정리된 결론

  1. 앱은 paymentsData 를 정상적으로 한 번만 인코딩하고 있었다.
  2. 웹에서 URLSearchParams.get() 으로 꺼낸 값은 이미 디코딩된 상태였다.
  3. 그 값을 다시 decodeURIComponent() 해서 % 포함 상품명에서 URI malformed 가 발생했다.
  4. 직접 URL 호출이나 다른 진입점이 없으므로 fallback 없이 JSON.parse(paymentsDataParam) 만 사용하는 것이 가장 정확하다.

최종 상태

현재 웹 결제 위젯 페이지는 아래 규칙으로 동작하면 된다.

const searchParams = new URLSearchParams(window.location.search);
const paymentsDataParam = searchParams.get('paymentsData');

if (!paymentsDataParam) {
  // 예외 처리
}

const parsedPaymentsData = JSON.parse(paymentsDataParam);

이 구조면 맛있는 다라딸기! 59%%% 같은 값도 문제없이 처리된다.

profile
Born to be FE developer 🧑🏻‍💻

0개의 댓글