상품을 판매하는 기능을 기획부터 배포까지 2명이서 진행한 경험에 대해서 적었습니다.
이번에는 실제로 Payapp 덕분에 실제 결제를 연동해볼 수 있었고, 목표했던 대부분의 것들을 이루었기 때문에 만족하고 있습니다.
20살 부트캠프 팀플을 포함해서 지금까지 여러 번의 사이드 프로젝트를 해봤지만, CRUD의 반복에 그쳐서 고민이 많았습니다. 또한 커미션을 진행하면서 신청자분들이 직접 제가 만든 것들을 사용하고 추가 요청사항을 주실 때 가장 구체적이고 좋은 개선을 할 수 있었기에 이번 목표는 테스트에 그치지 말고 꼭 실제 사용자 경험을 얻어내자 가 되었습니다.
그림 커미션에 관련해서는 개발자들 사이에서 잘 알려진 주제가 아니었어서 1인 체제 기획~개발을 해왔는데 협업의 장점을 생각해서 전 프론트엔드 동료분께 협업을 요청했고 ui와 기능 상에서 큰 도움을 받았습니다.
크레페, TMM, Witchform 사이트를 생각하면서 만들었습니다.
다만 크레페처럼 신청자와 협의 후 금액을 산정해서 결제 요청을 하는 것이 아니라 정기권처럼 할인된 가격으로 패스를 구매한다는 느낌에 초점을 두어 기획했습니다.
실제로 기획을 어디까지 하는 것이 좋은가? 개발자로서는 판단이 잘 안 섰습니다;; 하지만 저번 프로젝트들은 너무 실제 운영하는 사이트들의 모든 점을 가져오려고 했다는 점에서 개인의 능력 범위를 지나치게 벗어나 오히려 좌절되었습니다. 실패를 교훈삼아 데드라인을 올해 말로 정했고, 무조건 배포까지 하자!를 최우선으로 정했습니다.
전 프론트 경험을 살려서 tailwindcss와 material ui로 최소한의 레이아웃만 잡고 뼈대를 설명해서 동료분께 전송하면 동료분이 반응형과 디자인 측면에서 조언을 주신 것을 기반으로 파트를 나누어 공동으로 작업했습니다. 인원도 시간도 적어서 기획서나 PPT 등은 생략했습니다;
(이것이 가능하기 위해서 기존에 커미션 경험들을 끌어모아서 머리를 쥐어짜야 했습니다ㅎ)
대부분 결제 PG를 연동하려면 사업자가 필수이고, 그 외에도 알림톡과 같은 서비스 연동에도 사업자가 요구됩니다. 물론 당연한 것이고 실제로 사업자도 등록했다가 폐지해봤다가~ 업종 심사 관련한 전반적인 것들에 대해 공부도 해봤습니다.
하다보니 지칠 뿐만 아니라 주객이 전도되었다는 걸 느꼈습니다.
이 프로젝트로 큰 사업을 하고 싶다기 보다는 개발 전반 과정을 반복해서 능숙해지는 것이 저의 목적에 더 가까웠기에 동료와 머리를 맞대다가 Payapp에 대해서 알게 되었습니다.
Payapp은 사업자 등록 없이 결제를 연동해 볼 수 있는 API 및 스크립트를 제공합니다. Payapp은 비사업자의 판매 내역 또한 주민등록번호로 국세청에 신고를 합니다. (크레페 사이트 또한 사업자 등록을 체크하지 않지만 매년 신고를 자동으로 진행하고 있습니다. 5월에 종소세 신고)
샘플 코드가 굉장히 old해서 (jsp….php….) 분석에 꽤 애먹었습니다(함께 고생하신 동료분)
연동하기 이전에 가입은 물론 페이앱에서 요구하는 심사를 거쳐야 했고, 보증보험에 가입해야 했습니다. 하지만 이 모든 과정 또한 사업자 등록/관리에 비하면 매우 쉬웠습니다.
보증보험비는 연에 15000원 정도 부과됩니다.
Payapp에서는 feedbackurl
과 returnurl
이 가장 중요합니다. 해당 값에 지정한 url로 페이앱에서 결제 정보를 post로 던져주는데, 응답 형식이 SUCCESS
이지 않으면 70080 에러를 던집니다.
(자세한 내용은 개발자 가이드 참고 https://payapp.kr/dev_center/dev_center01.html)
localhost에서는 페이앱이 접속할 수 없으니 Render 서버에 배포해서 테스트를 진행했고, 프론트엔드에서는 ngrok를 사용해서 테스트를 진행할 수 있었습니다.
스프링 부트에서는 아래처럼 작성했습니다.
// 이벤트 판매용 feedback API
@PostMapping("/payapp-feedback")
public ResponseEntity<String> insertUpdatePayment (EventPaymentVO paymentVO) throws CommonException, MessagingException {
service.insertUpdatePayment(paymentVO);
return ResponseEntity.ok().body("SUCCESS");
}
@PostMapping("/payapp-redirect")
public ResponseEntity<Void> redirectToWebPage(EventPaymentVO paymentVO) {
log.info("결제완료 페이지로 이동합니다." + paymentVO);
String urlWithParams = "https://ktalk-review.netlify.app/event/payment/complete";
urlWithParams += "?state=" + paymentVO.getPayState();
urlWithParams += "&userName=" + paymentVO.getMemo();
return ResponseEntity.status(HttpStatus.FOUND) // 302 Found
.location(URI.create(urlWithParams)) // 결제완료페이지로 이동
.build();
}
프론트에서 파라미터를 셋팅할 때는 url을 아래와 같이 실배포 url을 설정합니다.
// … 코드 중략
PayApp.setParam(
"feedbackurl",
"https://ktalk-review-image-latest.onrender.com/api/~/payapp-feedback"
);
PayApp.setParam(
"returnurl",
"https://ktalk-review-image-latest.onrender.com/api/~/payapp-redirect"
);
사용자가 결제 버튼을 광클할 경우 중복 요청을 막아야 합니다. 이를 위해서 payapp에서 중복 요청 방지를 위해서 임의 변수로 var1
, var2
를 제공하고 있고,
주문번호를 프론트에서 생성해서 var
에 담아서 백엔드 단에서 주문당 결제가 1건만 진행되도록, 주문 또한 중복 방지를 처리했습니다.
이 프로젝트의 시작은 리뷰 게시판이었습니다. 무료로 주는 몽고DB를 사용했는데, 현재 저의 기술 스택은 Spring Boot + MySQL이었고, 상품과 주문, 결제 관계 설정에는 RDBMS가 더 적합하다고 판단하여 이번 기능은 MySQL로 개발했습니다.
aws가 너무 비싸서 개인 서버를 구매했는데… 1월 13일에 온다는 군요.....그래서 Google Cloud MySQL 무료를 사용했습니다.
기존에는 pk를 위해서 auto_increment나 uuid를 주로 사용했는데 uuid는 가독성이 안 좋아서 이번에 {도메인 접두사}-{년월시분초}-{난수4자리}
를 적용했습니다.
create table if not exists jinvicky.event_order
(
ID varchar(50) not null
primary key,
EVENT_PROD_ID varchar(50) not null,
USER_NAME varchar(100) not null,
USER_EMAIL varchar(255) not null,
PHONE varchar(20) null,
PRICE varchar(20) not null,
QUANTITY int not null,
STATUS varchar(20) not null,
RGTR_DT datetime not null,
constraint event_order_ibfk_1
foreign key (EVENT_PROD_ID) references jinvicky.event_product (ID)
);
create index EVENT_PROD_ID
on jinvicky.event_order (EVENT_PROD_ID);
create table jinvicky.event_payment
(
ID varchar(50) not null
primary key,
MUL_NO varchar(50) null,
EVENT_ORD_ID varchar(50) not null,
PRICE varchar(20) null,
PAY_TYPE varchar(20) null,
PAY_STATE varchar(10) not null,
REQ_DATE varchar(30) null,
PAY_DATE varchar(30) null,
constraint event_payment_ibfk_1
foreign key (EVENT_ORD_ID) references jinvicky.event_order (ID)
);
create index EVENT_ORD_ID
on jinvicky.event_payment (EVENT_ORD_ID);
create table jinvicky.event_product
(
ID varchar(50) not null
primary key,
NAME varchar(255) not null,
SUMMARY varchar(255) null,
CONTENT text null,
PRICE decimal(10, 2) not null,
DISCOUNT_RATE int null,
QUANTITY int not null,
THUMBNAIL varchar(255) null,
RGTR_DT datetime not null
);
개발하면서 어려웠던 점들이나 이슈들에 대해서 적어봤습니다.
배포는 -> https://velog.io/@jinvicky/render-docker-deploy
배포했는데 원격 서버 로그에서 파라미터를 인식 못한다 식의 에러가 발생했습니다.
@RequestParam
이나 @PathVariable
를 사용할 때 (“id”) 식으로 name을 설정했더니 해결되었습니다.
결제가 완료되었을 때는 주문자의 이메일로 결제 내역을 전송합니다.
구글이메일을 사용했는데 2단계 인증 후 앱 비밀번호를 어디서 조회하는 지 살짝 애먹었습니다. 구글 support와 velog로 해결했습니다.
https://velog.io/@tjddus0302/Spring-Boot-메일-발송-기능-구현하기-Gmail
https://support.google.com/accounts/answer/185833?visit_id=638711194001806109-1520578457&p=InvalidSecondFactor&rd=1
프로젝트에서 MyBatis를 설정하는 도중 이슈가 발생했습니다.
Invalid value type for attribute 'factoryBeanObjectType'
mybatis의 버전이 낮은 것이 원인이었고 3.0.1버전에서 -> 3.0.3으로 업그레이드하는 것으로 해결했습니다.
처음에는 useEffect, useState 위주의 개발을 했다가 useSWR이라는 캐싱 지원 라이브러리로 일부 기능을 전환했습니다.
material-ui 컴포넌트에 tailwindcss를 className으로 적용하면 디바이스 환경 관계없이 간헐적으로 css가 먹히는 경우가 발생했습니다.
둘 중 하나만 선택해서 사용하면 이슈는 해결됩니다.
결제를 할 때 팝업으로 새창이 열리고 결제가 끝나면 기존 창은 닫히고 결제 완료 페이지로 이동해야 합니다.
백엔드에서 returnurl로 리다이렉트하면서 파라미터를 같이 전달하면 프론트 단에서 아래와 같이 동작하도록 만들었습니다.
useEffect(() => {
const queryObject = Object.fromEntries(searchParams.entries());
const message = JSON.stringify(queryObject);
if (window.opener) {
window.opener.postMessage(message, "https://ktalk-review.netlify.app/");
window.close();
} else setIsOpener(false);
}, [searchParams]);
/** 부모창이 열려있으면 빈화면 */
if (isPaid && isOpener) return <></>;
Payapp은 userid를 payapptest로 설정하면 테스트 결제를 진행할 수 있으며 결제 금액은 몇 분 뒤 자동으로 취소됩니다.
관리자 페이지를 통해서 금액을 확인하며 유효성 전반과 백엔드를 수십번 테스트합니다.
꾸준히 하는 것이 가장 중요하다는 어른들 말씀을 이번에 비로소 이해한 것 같습니다;;
꾸준한 것은 당연하다고 생각했는데 이것을 개발하면서 굉장히 다시 하고 싶다, 마음에 들지 않는다라는 마음이 강하게 들어서 멈출 때가 있었는데
어떻게든 끝은 봐야 한다~로 마음먹었던 덕분에 일단락을 할 수 있었던 것 같습니다.
jinvicky
✉️ Email: jinvicky17@gmail.com
💻 Github: https://github.com/jinvicky
wkdu0723
✉️ Email: wkdu0712@naver.com
💻 Github: https://github.com/wkdu0723