예제로 이해하는 await/async 문법

by 토스페이먼츠

지난 포스트에서는 콜백 함수 대신 Promise로 비동기 작업을 깔끔하게 처리하는 방법을 알아봤어요. 근데 Promise에도 콜백 함수와 비슷한 단점이 있어요. 오늘은 Promise의 단점을 알아보고, async/await 문법으로 어떻게 비동기 작업을 더 효율적으로 처리할 수 있는지 토스페이먼츠 결제위젯 예시로 자세히 살펴볼게요.

비동기 작업과 Promise

비동기 작업이란 특정 코드의 로직이 끝날 때까지 기다리지 않고, 나머지 코드를 먼저 실행하는 것이에요. 빠른 페이지 로딩을 위해 웹사이트 개발에 비동기 작업이 사용돼요.

하지만 동기적으로 순서대로 불러야 하는 코드가 있으면 어떻게 할까요? 콜백 함수를 사용할 수 있지만 콜백을 계속 호출하면 코드가 복잡해지고 에러도 처리하기 어려워요. 이런 콜백의 단점을 보완한 Promise를 사용할 수 있는데요. Promise는 비동기 함수의 결과를 담고 있는 객체에요.

Promise는 세 개의 상태를 가질 수 있어요.

  • 대기(Pending): 비동기 함수가 아직 시작하지 않은 상태
  • 성공(Fulfilled): 비동기 함수가 성공적으로 완료된 상태
  • 실패(Rejected): 비동기 함수가 실패한 상태

Promsie가 대기 상태에서 상태가 바뀌면 then(), catch() 함수를 사용해서 성공 상태의 Promise, 실패 상태의 Promise를 각각 처리할 수 있어요.

토스페이먼츠에서 결제를 요청하는 requestPayment() 메서드는 Promise를 반환해요. 아래와 같이 then()에서 성공 로직을 추가하고, catch()에서 에러 로직을 추가할 수 있어요.

paymentWidget.requestPayment({
  orderId: "t9JI0Bs1SVdJxRs8yjiQJ",            
  orderName: "토스 티셔츠 외 2건",                    
})
.then(function (data) {
  console.log(data);
	// 성공 처리
})
.catch(function (error) {
	// 에러 처리
  if (error.code == "NEED_CARD_PAYMENT_DETAIL") {
    console.log(error.message);
  }

하지만 Promise에도 콜백과 비슷한 문제가 일어날 수 있어요. then() 체인을 길게 이어 나가면 콜백 체인과 마찬가지로 코드의 가독성이 떨어지고 에러가 어디서 일어났는지 보기 어려워요.

Promise가 뭔지 아직 헷갈린다면 Promise 실전에서 사용해보기 글을 먼저 읽어보세요.

그래서… aync/await란?

콜백, Promise 체인의 단점은 asyncawait로 보완할 수 있어요. 위에 나온 Promise 예시를 async/await 문법으로 바꿔서 더 자세히 알아볼게요.

먼저, 결제를 요청하고 결제 요청의 결과를 처리하는 handleSubmit() 함수를 정의할게요. 함수를 정의할 때 async를 함수 앞에 붙이면, “이 함수는 비동기적인 함수이고 Promise를 반환한다”라고 선언하는 거예요. 반환 값이 Promise 생성 함수가 아니어도 반환되는 값을 Promise 객체에 넣는 거죠.

async function handleSubmit() {
	...
	return paymentData
	// return Promise.resolve(paymentData) // 위와 같은 결과
}

awaitasync 함수 안에만 사용할 수 있는 특별한 문법인데요. Promise를 반환하는 함수 앞에 await를 붙이면, 해당 Promise의 상태가 바뀔 때까지 코드가 기다려요. Promise가 성공 상태 또는 실패 상태로 바뀌기 전까지는 다음 연산을 시작하지 않는 것이죠.

예시를 보면 더 쉬운데요. 아래와 같이 결제를 요청하는 requestPayment() 메서드 앞에 await가 있어요. 결제 요청이 잘 완료되어 paymentData(Promise 객체)의 상태가 바뀐 이후에만 console.log(paymentData)가 실행돼요. 비동기 작업을 동기적으로 바꾸는 것이죠.

async function handleSubmit() {
  const paymentData = await paymentWidget.requestPayment({
    orderId: "KOISABLdLiIzeM-VGU_8Z", // 주문 ID(직접 만들어주세요)
    orderName: "토스 티셔츠 외 2건" // 주문명
  });
  console.log(paymentData);
	return paymentData
}

사실상 awaitthen()과 같은 역할을 하는데, 콜백 함수를 등록할 필요가 없기 때문에 더 편리해요. 체이닝으로 인해 코드가 복잡해질 일도 없죠.

근데 에러는 어떻게 처리할까요? async/await로 코드를 동기적으로 바꾸는 것으로 생각할 수 있다고 했는데요. 그럼 이제 간단히 try/catch를 사용하면 돼요. 아래 코드에서는 requestPayment()에서 실패 상태의 Promise를 반환하면, 바로 catch 블록을 실행해요.

async function handleSubmit() {
      try {
        const paymentData = await paymentWidget.requestPayment({
          orderId: "KOISABLdLiIzeM-VGU_8Z", // 주문 ID(직접 만들어주세요)
          orderName: "토스 티셔츠 외 2건" // 주문명
        });
				console.log(paymentData);
        return paymentData;
      } catch (error) {
        console.log(error.message);
      }
    }

결제 연동에서 Promise 주의점

기본적으로 await/async로 Promise를 처리하는 방법을 알아봤는데요. 토스페이먼츠 결제위젯 SDK에서 Promise를 사용할 때 다음 세 가지를 주의하세요.

✅ 리다이렉트 파라미터를 설정할 필요가 없어요

결제를 요청하는 requestPayment() 메서드에는 successUrlfailUrl 파라미터가 있는데요. 구매자가 카드사・은행 앱에서 결제 인증을 완료하면 결과에 따라 자동으로 sucessUrlfailUrl로 이동해요. 이걸 리다이렉트 방식이라고 불러요. 하지만 Promise는 페이지가 이동하면 사용할 수 없어요. Promise를 사용할 때는 두 파라미터를 설정하지 마세요.

✅ Promise는 PC에서만 사용하세요

토스페이먼츠 결제 연동할 때는 PC 환경에서만 Promise를 사용할 수 있어요. 모바일 환경에서는 구매자가 카드사・은행 앱으로 이동하기 때문에 Promise를 받을 수 없어요. 모바일 환경에서는 반드시 리다이렉트 방식을 사용하세요.

✅ iframe을 사용하세요

requestPayment() 메서드에서 windowTarget 파라미터로 결제창이 열리는 프레임을 설정할 수 있어요. 기본값은 iframe 이지만, self도 사용할 수 있어요. self로 파라미터를 설정하면 브라우저가 결제창으로 이동하기 때문에 Promise를 정상적으로 받을 수 없어요. Promise를 사용할 때는 iframe으로 설정하거나, windowTarget파라미터를 안 넘기면 돼요.

await/async 직접 사용해보기

이제 실행 가능한 코드로 await/async의 작동 방법을 직접 볼게요.

결제위젯을 초기화하고 렌더링하는 자세한 방법은 토스페이먼츠 결제위젯 연동 가이드를 참고하세요. 여기서는 await/async를 사용하는 handleSubmit() 함수만 자세히 살펴볼게요.

아래 있는 코드를 실행 가능한 환경에 복사해보세요. 그리고 requestPayment() 앞에 붙은 await 문법을 삭제하고 ‘결제하기’ 버튼을 눌러보세요. 그럼 아래 그림의 왼쪽 화면과 같이 콘솔에 대기 상태인 Promise가 로그된 것을 볼 수 있어요. 결제 요청이 완료되기 전에 비동기적으로 다음 연산을 시작했기 때문이죠.

await을 다시 추가하고 ‘결제하기’ 버튼을 눌러보세요. 정상적으로 결제 정보를 입력하면 오른쪽 화면과 같이 콘솔에 결제 데이터가 잘 출력돼요. 결제를 정상적으로 완료하고 싶다면 Promise로 돌아온 데이터로 결제 승인을 요청하세요.

에러도 어떻게 되는지 살펴볼게요. await 문법을 다시 삭제하세요. 카드사를 선택하지 않은 상태로 ‘결제하기’를 누를게요. 역시 대기 상태인 Promise가 로그돼요. await 문법을 추가하면, 아래 왼쪽 화면과 같이 에러가 잘 출력돼요. catch 블록에는 에러를 처리하는 로직을 추가해보세요.

<head>
  <meta charset="utf-8" />
  <script src="https://js.tosspayments.com/v1/payment-widget"></script>
  <link rel="stylesheet" type="text/css" href="src/styles.css" />
</head>
<body>
  <div id="payment-method"></div>
  <div id="agreement"></div>
  <!-- 결제위젯, 이용약관 영역 -->
  <div id="payment-method"></div>
  <div id="agreement"></div>
  <div class="wrapper">
    <button class="button" type="primary" id="payment-button">
      결제하기
    </button>
  </div>
  <script>
    const clientKey = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq";
    const customerKey = "ihCbPOLuHP5b0sqP4RKaG"; // 내 상점에서 고객을 구분하기 위해 발급한 고객의 고유 ID
    const button = document.getElementById("payment-button");
    // ------  결제위젯 초기화 ------
    const paymentWidget = PaymentWidget(clientKey, customerKey); // 회원 결제

    // ------  결제위젯 렌더링 ------
    const paymentMethodWidget = paymentWidget.renderPaymentMethods(
      "#payment-method",
      { value: 15000 },
      { variantKey: "DEFAULT" } // 렌더링하고 싶은 결제 UI의 variantKey
    );
    // ------  이용약관 렌더링 ------
    paymentWidget.renderAgreement("#agreement");

    // ------ '결제하기' 버튼 누르면 결제창 띄우기 ------
    button.addEventListener("click", handleSubmit);
    async function handleSubmit() {
      try {
        const paymentData = await paymentWidget.requestPayment({
          orderId: "KOISABLdLiIzeM-VGU_8Z", // 주문 ID(직접 만들어주세요)
          orderName: "토스 티셔츠 외 2건" // 주문명
        });
        console.log(paymentData);
        return paymentData;
      } catch (error) {
        console.log(error.message);
      }
    }
  </script>
</body>

함께 읽으면 좋을 콘텐츠

📍 Promise 실전에서 사용해보기

📍 결제 요청, 인증, 승인… 이게 다 뭔가요?

📍 React로 결제 페이지 개발하기 (ft. 결제위젯)

Writer 박수연 Graphic 이은호, 이나눔

    의견 남기기
    토스페이먼츠

    고객사의 성장이 곧 우리의 성장이라는 확신을 가지고 더 나은 결제 경험을 만듭니다. 결제가 불편한 순간을 기록하고 바꿔갈게요.