[오늘의삽질#1] 카카오페이 연동 시 POST 요청

KG·2021년 5월 25일
4

오늘의삽질

목록 보기
1/2
post-thumbnail
post-custom-banner

🧐 발단

최근 팀 프로젝트에서 카카오페이 모듈을 연동해야 할 일이 있었다. 카카오 디벨로퍼 사이트에서 관련 문서들이 워낙 잘 정리되어 있어서 쉽게 할 수 있지 않을까 생각했지만 의외의 복병을 만나 사소한 부분에서 많은 시간을 쏟았다.

리액트 네이티브(React-Native)에서 Webview를 통해 카카오페이 웹 모바일 화면을 띄울려고 시도했다. Webview로 띄울려고 했기 때문에 웹 페이지는 리액트를 사용해서 구현했다. 또한 카카오서버로 요청을 보내기 위해 axios 라이브러리를 깔아 관련 요청과 응답을 다루려고 했다.

💎 전개

실제 PG사와 연동하지 않고 테스트만 진행해볼려고 했기 때문에 공식문서를 참고했다. 공식문서에 따르면 카카오페이를 이용한 단건결제 사용 방법이 친절하게 안내되어 있다. 이때 가맹점코드(CID)를 실제 결제에서는 요구하지만, 만약 테스트만을 목적으로 한다면 이 코드를 TC0ONETIME로 사용하여 가상으로 결제를 진행하며 테스트 할 수 있다.

먼저 카카오페이를 연동하기 위한 사전작업이 필요하다. Kakao Developer에서 어플리케이션을 추가하도록 하자. 어플리케이션을 추가하면 여러 개의 앱 키를 받을 수 있는데, 카카오페이 연동을 위해 필요한 키는 ADMIN 키이므로 이를 잘 모셔두자.

추가적으로 플랫폼을 등록해주어야 한다. 본인은 Webview로 해당 화면을 띄워줄 것이기에 웹 플랫폼으로 이를 등록했다. 그리고 개발서버에서 테스트를 진행하기 위해 http://localhost:3000을 사이트 도메인에 추가해주었다. 만약 사이트 도메인을 추가하지 않는다면 요청이 정상 승인 되지 않으므로 꼭 위에 언급한 작업을 미리 해주도록 하자!

결제준비를 위한 요청관련 세부 사항은 공식문서에서는 다음과 같이 소개하고 있다.

POST /v1/payment/ready HTTP/1.1
Host: kapi.kakao.com
Authorization: KakaoAK {APP_ADMIN_KEY}
Content-type: application/x-www-form-urlencoded;charset=utf-8

이를 보고 가장 먼저 들었던 생각은 Host 환경에 따른 Proxy 설정이었다. 우선적으로 로컬환경에서 테스트를 진행해볼려고 했기 때문에 당연히 CORS 문제를 맞닦드릴 수 밖에 없는 것은 기정 사실. 따로 서버를 두고 서버에서 요청을 처리하여 CORS 문제를 우회할 수도 있겠지만 별도로 서버를 구축하기는 싫었기 때문에, CRA에서 깔려있는 WebpackdevServerProxy 설정을 이용해서 우회해야겠다고 생각했다. 관련 설정은 매우 간단하게 CRA로 만든 리액트 프로젝트에서 package.json에 단 한줄만 추가해주면 된다.

"proxy": "https://kapi.kakao.com"

오늘날 CRA로 설치된 리액트 프로젝트에서 대부분의 편의기능을 이미 Webpack이 탑재해 놓고 있는 경우가 많아서 단순히 저 한 줄을 추가하여 Proxy 설정을 뚫어줄 수 있다.

간단히 CORS이슈와 Proxy에 대한 점을 아주 간단하게 짚고 넘어가보자 한다.

1) CORS(Cross-Origin Rescource Sharing)

교차 출처 리소스 공유라고도 한다. 이는 추가 HTTP 헤더를 사용해서, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제라고 MDN에 기재되어 있다.

개념이 어려운데 기억할 점은 크게 두 가지이다. CORS 정책은 먼저 보안 상의 이유로 도입되었다는 점과, API를 요청한 곳의 도메인 주소와 API Endpoint의 도메인 주소가 서로 다를 때 발생하는 이슈라는 점이다.

이때 동일 출처(Origin)에 대한 여부는 위 그림에서 보이는 URL의 구성 요소 중에 Protocol(Scheme), Host, Port 이 세 가지 요소로 판단한다. 따라서 URL에 추가적으로 기입될 수 있는 PathQuery String은 무엇이 되든간에 상기 3가지 요소가 동일하다면 동일 출처로 인식한다.

이때 CORS 정책은 브라우저 상에서 구현된 보안 정책이다. 즉 서버와 서버가 통신할 때는 별도로 관련 정책을 서버단에 구현하지 않았다면 CORS 이슈가 발생하지 않을 수도 있다.

2) Proxy

프록시는 그 용어가 가진 뜻(대리인) 때문에 참 많은 분야에서 사용하는 단어인 것 같다. 대표적으로 웹 서버에서는 Forward ProxyReverse Proxy 라는 개념이 있고, 이젠 기억이 가물가물하지만 방화벽 설정 기법에도 Proxy 관련 용어가 심심찮게 쓰였던 것 같다. 사실 리액트의 Webpack devServer에서 제공하는 Proxy 역시 이러한 개념과 크게 다르지는 않다.

특히 이 기능은 로컬환경에서 개발할 때 임시방편으로 유용하게 프론트엔드 단에서 CORS 문제를 우회할 수 있는 방법 중 하나이다. 여기서 해결이라는 단어가 아닌 우회라는 표현을 썼다는 점에 주목하자.

위에서 Proxy 설정을 package.json 파일에 추가해준 것처럼 특정 주소를 명시해주면, 로컬 환경에서 외부로 요청을 보낼 때 마치 자신의 주소가 Proxy에 기입된 주소인 것 마냥 프록싱 해주기 때문에 마치 CORS 정책을 지키는 것과 같은 효과를 만들 수 있다. 즉 일종의 브라우저의 뒷통수를 때리는(?) 기법이다.

다만 어디까지나 임시방편으로 주로 로컬 환경에서 개발용으로 테스트를 진행할 때 유용하다. 만약 프로덕션 환경으로 배포가 되는 경우엔 더 이상 Webpack devServer의 환경으로 돌아가지 않기 때문에 이는 무용지물이 된다.

CORSProxy에 대해 더 자세히 참고하고 싶다면 이 링크를 참고하자.

🙄 착오

문제는 프록시 설정도 하고 ADMIN 키도 발급받고 도메인도 등록했건만 정상적으로 응답을 받아올 수 없었다. axios 모듈의 문제인가 싶어 fetch API를 사용해보기도 하고 GET 요청부터 다른 요청메서드를 번갈아 사용했지만 계속해서 실패 메시지가 떴다.

일단 요청에 필요한 파라메터를 다음과 같이 정의해주었다. 자세한 파라메터의 정의는 공식문서를 참고하자. 그리고 axios 요청은 다음과 같이 진행해주었다.

const config = {
  next_redirect_pc_url: "",
  tid: "",
  params: {
    cid: "TC0ONETIME",
    partner_order_id: "partner_order_id",
    partner_user_id: "partner_user_id",
    item_name: "동대문엽기떡볶이",
    quantity: 1,
    total_amount: 22000,
    vat_amount: 0,
    tax_free_amount: 0,
    approval_url: "http://localhost:3000",
    fail_url: "http://localhost:3000",
    cancel_url: "http://localhost:3000",
  },
};

export default config;

...

const { params } = kakaopayConfig;

useEffect(() => {
  const postKakaopay = async () => {
    const data = await axios.post('/v1/payment/ready', params, {
      headers: {
        Authorization: `KakaoAK ${MY_ADMIN_KEY}`,
        "Content-type": "application/x-www-form-urlencoded;charset=utf-8"
      }
    });
  }
  postKakaopay();
}, []);

공식문서를 잘 참고한 분들은 바로 문제점을 캐치하셨을 수도 있다. 그 원인은 참으로 간단하고 허무했는데, 일단 공식문서를 꼼꼼하게 확인하지 않았던 내 잘못이 가장 크다.

보통 axios를 이용해서 서버와 API 통신을 주고 받을 땐 대부분 RestAPI 방식을 사용했고, 이때 POST 방식으로 서버에서 요청을 보낼 땐 주로 application/json 방식을 사용했다. 때문에 공식문서에서 버젓이 알려주고 있는 POST 요청의 Content-type을 제대로 보지도 않고 무조건 json 방식으로 요청을 보내고 있던 것이었다.

위로 올라가보면 공식문서 스펙에 기재된 POST 요청의 Content-typeapplication/x-www-form-urlencoded 타입인 걸 알 수 있다. 솔직히 말하면 처음 보는 타입이었고, 애초에 Content-type이 이렇게 세분화 되고 있다는 점도 몰랐기 때문에 이 점을 아예 간과하고 넘어갔던 것 같다. 그래서 이 기회에 POST 요청의 Content-type을 살짝 다루고 넘어가보자.

Content-type은 이 외에도 multipart/form-data 또는 text/plain 등의 타입도 있지만 해당 포스트에서는 위에서 언급한 두 가지만 중점적으로 살펴보려 한다.

1) application/x-www-form-urlencoded

이 경우는 보통 웹 페이지 개발 시 제이쿼리(JQuery) 등을 사용해 Ajax로 서버에 데이터를 요청하는 경우나 HTML Form을 사용해서 요청하는 경우에 Content-type을 따로 기입하지 않으면 이를 디폴트 값으로 세팅하여 서버로 전송한다고 한다.

이 방식의 가장 큰 특색은 마치 GET 요청을 받아올 때처럼 URL에 데이터 페이로드를 붙여 요청을 전송한다는 점이다. 즉 HTTP Request MessageBody를 사용해 &로 분리되고 = 기호로 값과 키를 연결하는 key-value로 인코딩 되는 방식이다.

이는 사실 다소 옛날에 사용되던 방식으로 요즈음에는 잘 사용하지 않는 타입이라고 한다. 본인 역시 POST 요청과 관련된 Rest API를 배울때 'POST 요청은 URL에 데이터가 노출되지 않으므로 보안이 조금 더 뛰어나다는 등'의 설명을 여러 번 접한 바 있는데, 이를 통해서도 요즈음의 POST 요청은 대부분 application/json 타입으로 처리되는 듯 하다. 관련해서는 스택오버플로우 게시글에서도 언급하고 있다.

2) application/json

보통 대부분이 익숙한 타입이지 않을까 싶다. POST 요청으로 서버에 데이터 전송 시에 데이터를 json 타입으로 보낸다. 이는 HTTP Request MessagePayload를 보내는 것으로 그 형태는 우리가 잘 알고 있는 json 타입의 객체이다. HTTP 요청의 Body에 실어보내기 때문에 URL에서 노출이 되지 않는다는 특징이 있다.

또한 오늘날 Rest API 방식으로 API를 구성할 때는 대부분 json 방식으로 데이터를 요청 및 응답할 것을 권장하고 있다. 그렇기 때문에 공식문서를 분명 읽고 접근했음에도 이 부분에 대해 깊게 생각하지 못하고 평소 하던 방식대로 요청을 진행했던게 아닐까 싶다.

추가적으로 HTTP 1.1이라는 점도 명시해두고 있는데, 이 부분은 API를 사용하는데 있어 크게 중요한 부분은 아니지만 각 버전별 HTTP의 차이점도 간단하게 살펴보고 넘어가자.

1) HTTP 1.0

  • 원래 0.9 버전부터 시작되었다고 하지만, 사실상 1.0 버전부터 본격적으로 상용화 되었기 때문에 이를 기점으로 보는 시선이 많다.

  • 단순히 open / operation / close 구조의 3-ways-handshaking 방식을 취한다.

  • 상태코드가 응답값 시작 부분에 포함되어 요청에 대한 성공과 실패를 바로 확인할 수 있게 되었다.

  • 앞서 다룬 Content-type의 도입으로 HTML 파일 외에 다른 문서들도 전송이 가능하게 되었다.

  • HTTP 1.0은 지속적으로 TCP 세션을 유지할 수 없어 매 요청 컨텐크마다 세션을 맺어야 한다.

2) HTTP 1.1

  • HTTP의 첫번째 표준이다.

  • 메서드에 OPTIONS, PUT, DELETE, TRACE 등이 추가 되었다.

  • 기존 1.0 버전에 비해 성능 향상이 이루어졌다. 일정 시간 클라이언트 서버와 API 서버 간 세션을 유지하여 반복적으로 일어나는 통신의 연결과 끊김 빈도 수를 줄였다.

  • 파이프라이닝 기법을 통해 응답 속도를 높여 페이지 뷰의 속도를 개선했다. 이는 이전 요청이 완전히 전송 완료되기 전에 다음 요청을 전송하게끔 하여 레이턴시를 낮추는 기법의 일종이다.

  • 버츄얼 호스팅이 가능해져 하나의 IP에 여러 개의 도메인 운영이 가능해졌다.

3) HTTP 2.0

  • HTTP 1.1이 텍스트 프로토콜이라면 2.0은 이진(binary) 프로토콜이라 할 수 있다.

  • 때문에 텍스트 형태의 리소스보다 속도 및 효율성 부분에서 더욱 빠르다.

  • 스트림으로 한 번의 커넥션을 맺고 동시에 여러개의 데이터를 주고 받을 수 있다. 따라서 기존 HTTP 1.1 방식에서 부하를 줄이기 위해 사용하던 이미지 스프라이트, 도메인 분할 같은 임시 방편을 사용하지 않아도 된다.

  • 이전 헤더 내용과 중복되는 필드를 재전송 하지 않도록 하여 데이터를 절약한다. 또한 헤더 압축방식으로 Huffman Coding을 사용해 데이터 전송 효율을 높인다.

HTTP 프로토콜의 역사는 1990년대로 거슬러 올라가는 만큼 그 역사가 방대하다. 때문에 많은 자료가 있고 각각의 특색 역시 세분화 되어있지만 나름 큰 줄기만 뽑아 정리해봤다. 만약 추가적으로 더 자세하게 참고하고 싶다면 아래의 링크들을 참고해보자.

추가로 구글 개발자 도구를 통해 현재 도메인이 어떤 버전의 HTTP 프로토콜을 지원하는지 확인할 수 있다. 개발자도구의 네크워크 탭에서 Protocol 기능을 활성화 후 새로고침하면 어떤 버전을 사용하는지 볼 수 있다. 심지어 구글의 경우는 h3-29 프로토콜을 사용하고 있는데 벌써 3.0 버전의 HTTP 프로토콜을 도입하고 있음을 보여준다.

😎 해결

원인을 알았으니 포맷에 맞게 형태를 바꾸어 axios 요청을 날려주도록 하자. axios 공식문서를 참고하면 axios를 이용해서 application/x-www-form-urlencoded 요청을 보내는 경우에는 관련 데이터를 config 설정 영역에 담아 보내주어야 한다. 즉 위에서 headers 설정을 하는 부분에 데이터를 넘겨준다.

그리고 실제로 넘겨주는 데이터는 null로 처리하자. axios.post 요청의 두 번째 인자는 서버로 전송되는 json 형태의 객체인데 우리는 해당 타입으로 값을 넘겨주는 것이 아니기 때문에 해당 데이터를 사용하지 않기 때문이다.

const { params } = kakaopayConfig;

useEffect(() => {
  const postKakaopay = async () => {
    // post 요청의 json 데이터는 null로 처리하고
    const data = await axios.post('/v1/payment/ready', null, {
      params,	// config 설정에 데이터를 담아 넘겨준다.
      headers: {
        Authorization: `KakaoAK ${MY_ADMIN_KEY}`,
        "Content-type": "application/x-www-form-urlencoded;charset=utf-8"
      }
    });
  }
  postKakaopay();
}, []);

이제 정상적으로 원하는 데이터와 화면을 불러올 수 있게 되었다. 👍👍👍

🤞 결론

공식문서를 잘 참고하지 않고 넘어간 잘못도 있지만, 아무래도 오늘날 잘 쓰이지 않는 방식으로 API 요청을 처리하고 있을 거란 상상을 못 했기 때문에 단순하지만 오랜 시간이 소요되지 않았나 싶다.

이 부분에 대한 나의 공부가 부족한 탓에 오랜 시간이 소요되었겠지만, 관련해서 추가적인 언급이 있었다면 더 좋지 않았을까란 생각이 들었다. 개인적으로 카카오페이가 그렇게 옛날에 개발되어 서비스되었다곤 들지 않는데 다소 옛날에 쓰이던 타입을 계속 고수하고 있다는 점도 의문이 들었다. 보안상의 이점이라던가 유지보수에 장점이 있는지 추가적으로 찾아보았지만, 관련해서는 왜 아직 해당 방식을 사용하는지에 대한 결론은 찾을 수 없었다.

무엇보다 이렇게 시간 들여가며 카카오페이 연동을 뚫었지만, 리액트 네이티브와 Webview 연동에 또 하나의 문제를 넘지 못해 결국 카카오페이를 포기했다는게 사실 제일 코미디가 아닐까. 😂

References

  1. https://developers.kakao.com/docs/latest/ko/kakaopay/single-payment
  2. https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
  3. https://evan-moon.github.io/2020/05/21/about-cors/
profile
개발잘하고싶다
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 8월 4일

잘봤습니다 :)

답글 달기