React popup window와 소통하기

2023년 08월 27일
7

목적

  • 서드파티 결제 서비스를 연동한다.
  • 일반 modal이 아닌 pop-up window를 사용하여, 부모-자식 window 간 메시지 통신을 한다.

서드파티 popup 예시 이미지서드파티 popup 예시 이미지


Process

process overviewprocess overview

  1. payment 요청
    • 클라이언트에서 서버로 요청
    • 성공, 실패, 취소에 따른 popup callback(redirect) url을 payload에 포함
  2. payment API 요청
    • 서버측에서 payment 서비스에 API call
  3. Payment API Response
    • payment 서비스에서 서버로 API response (결제 가능 URL 포함)
  4. Payment URL 반환
    • 결제 진행 가능 URL 을 클라이언트로 전달
  5. Payment URL redirect
    • 전달받은 결제 진행 URL로 연결
    • popup 창을 띄우고, 해당 URL 로 redirect
  6. Payment 진행
    • payment 서비스 자체 로직에 따라 결제 진행
    • 회원가입, 로그인, 잔액 부족 등 예외 처리는 서드파티 서비스에서 일괄 담당
  7. 결제 결과에 따른 callback URL로 popup redirect
    • 성공, 실패, 취소 화면으로 이동
    • 1번 과정에서 전달한 redirect url 참고
  8. 결과 화면으로 이동
    • popup 상태에 따라 부모 window 최종 결과 화면으로 이동

메소드

결제 진행 URL 요청

const getPaymentUrl = async () => {
  // 성공, 실패, 취소시 redirect할 url을 모두 전달
  const payload = { 
    ...,
    successUrl : "example.com/callback/success",
    failureUrl : "example.com/callback/fail",
    cancelUrl : "example.com/callback/cancel"
  };
 
  const result = await getData(url, payload);
  return result?.data.link
};
const openPopup = link => {
  // 팝업 window의 크기 지정
  const width = 500; 
  const height = 800; 
 
  // 팝업을 부모 브라우저의 정 중앙에 나열
  const left = window.screenX + (window.outerWidth - width) / 2;
  const top = window.screenY + (window.outerHeight - height) / 2;
  const windowFeatures = `width=${width},height=${height},left=${left},top=${top}`;
 
  // 팝업을 열고 window 속성 지정
  const popup = window.open(link, 'payment', windowFeatures);
};

window.open()

새로운 브라우저 창 or 탭을 여는 함수

window.open(url, windowName, windowFeatures);
  • url
    • open될 창에 표시될 콘텐츠 url
    • 외부 웹 페이지 or 현재 웹사이트의 다른 페이지
  • windowName (옵션)
    • 새로 열릴 window의 이름
    • 추후 새 window 조작, 참조시 사용
    • 사용자 지정 문자열 혹은 빈 문자열, _blank, _self, _parent, _top 을 일반적으로 사용
  • windowFeatures (옵션)
    • 예시 : width=800,height=600,resizable=no
    • 새 창의 속성을 지정하는 문자열
    • 창의 크기, 위치, 스크롤바 여부 등 포함
    • 주요 속성
      • width, height, left, top : 새 창의 크기 및 위치
      • resizable : 창의 크기 조절 가능 여부. yes or no
      • scrollbars : 스크롤바 표시 여부. yes or no

popup으로부터 전달받은 데이터 처리

const getDataFromPopup = e => {
  // 동일한 origin의 이벤트만 처리하도록 제한
  if (e.origin !== window.location.origin) return;
 
  const { result } = e.data;
 
  // 팝업으로부터 데이터를 정상 수신
  if (result) {
    const { status, data } = result;
 
    // 띄워둔 popup을 close
    popup?.close();
    setPopup(null);
 
    // 성공, 실패, 취소 화면으로 redirect
    if (status === 'success') { redirect(success) }
    if (status === 'fail') { redirect(fail) }
    if (status === 'cancel') { redirect(cancel) }
  }
};
API 요청시 결과별 callback URL을 전달했는데 별도로 처리하는 이유

처음 전달한 URL들은 popup이 redirect할 URL이다.
이동한 popup url에서, 현재 상태를 부모 window에게 메시지 송신하고,
받은 메시지를 기반으로 부모 window를 추가로 redirect시키는 것


컴포넌트

부모 window

export const Parent = ({...}) => {
  // popup 관련 state
  const [popup, setPopup] = useState();
 
  useEffect(() => {
    // open된 팝업이 없다면 아무일도 하지 않는다.
    if (!popup) return;
 
    // popup으로부터 data를 전달 받을 listener 등록    
    window.addEventListener('message', getDataFromPopup, false);
 
    // effect cleaner (when component unmount)
    return () => {
      window.removeEventListener('message', getDataFromPopup);
      // 팝업 초기화
      popup?.close();
      setPopup(null);
    };
  }, []);
 
  // 제출 버튼 클릭시 url 요청 후 popup 연결
  const onSubmit = async () => {
	const url = await getPaymentUrl();
    openPopup(url)
  }
 
  return (<Component onSubmit={callAPI}/>)
};
const PopupCallback = () => {
  const { status } = useParams();
  const statusList = ['success', 'fail', 'cancel'];
 
  // popup이 아닌 경우 (일반 브라우저에서 url 입력으로 접속시) 메인으로 redirect
  if (!window.opener) { redirect('index') }
 
  // 올바르지 않은 slug 접근시 popup 종료
  if (!status || !statusList.includes(status)) { window.close() }
 
  useEffect(() => {
    // url을 기반으로 status, callback 정보를 paymentResult 객체에 저장
    const paymentResult = { status, data };
 
    // popup을 연 부모 window에 데이터 메시지 전송
    window.opener.postMessage({ paymentResult }, window.location.origin);
  }, []);
 
  return <div>{status}</div>;
};
 
export default PopupCallback;

window의 'message' event

웹 페이지 내에서 다른 창 또는 프레임과의 메시지 교환, 통신 이벤트

  • 메시지 송신
    • postMessage() 함수 사용
    • [수신 window].postMessage( message, origin )
  • 메시지 수신
    • window.addEventListener('message', callback) 으로 이벤트 감지 후 콜백 호출
    • e.data 로 message 접근
주의사항

다른 도메인 간(Cross-Origin)에도 통신 가능하므로, 보안에 신경써야함
getDataFromPopup 함수에서 아래와 같은 로직을 추가한 이유

// 동일한 origin의 이벤트만 처리하도록 제한
if (e.origin !== window.location.origin) return;

Reference