[글또] 6. [Next.js , Spring Boot] - 이벤트 상품 판매 기능 배포 후기

jinvicky·2025년 1월 1일
0

Intro

상품을 판매하는 기능을 기획부터 배포까지 2명이서 진행한 경험에 대해서 적었습니다.
이번에는 실제로 Payapp 덕분에 실제 결제를 연동해볼 수 있었고, 목표했던 대부분의 것들을 이루었기 때문에 만족하고 있습니다.

프로젝트를 시작한 이유

  • 사용자 경험을 얻고 싶다
  • 테스트 결제 말고 실제 결제를 연동하고 내역을 관리해보고 싶다
  • 내가 만든 상품을 판매하고 사용자들로부터 반응을 보고 향후 방향을 잡고 싶다
  • 이커머스 회사가 추구하는 방향에 조금 더 가까워지고 싶다

20살 부트캠프 팀플을 포함해서 지금까지 여러 번의 사이드 프로젝트를 해봤지만, CRUD의 반복에 그쳐서 고민이 많았습니다. 또한 커미션을 진행하면서 신청자분들이 직접 제가 만든 것들을 사용하고 추가 요청사항을 주실 때 가장 구체적이고 좋은 개선을 할 수 있었기에 이번 목표는 테스트에 그치지 말고 꼭 실제 사용자 경험을 얻어내자 가 되었습니다.

그림 커미션에 관련해서는 개발자들 사이에서 잘 알려진 주제가 아니었어서 1인 체제 기획~개발을 해왔는데 협업의 장점을 생각해서 전 프론트엔드 동료분께 협업을 요청했고 ui와 기능 상에서 큰 도움을 받았습니다.

기획과 화면 설계

크레페, TMM, Witchform 사이트를 생각하면서 만들었습니다.
다만 크레페처럼 신청자와 협의 후 금액을 산정해서 결제 요청을 하는 것이 아니라 정기권처럼 할인된 가격으로 패스를 구매한다는 느낌에 초점을 두어 기획했습니다.

계획만 하다가 어영부영 시간이 늘어지지 않는가?

실제로 기획을 어디까지 하는 것이 좋은가? 개발자로서는 판단이 잘 안 섰습니다;; 하지만 저번 프로젝트들은 너무 실제 운영하는 사이트들의 모든 점을 가져오려고 했다는 점에서 개인의 능력 범위를 지나치게 벗어나 오히려 좌절되었습니다. 실패를 교훈삼아 데드라인을 올해 말로 정했고, 무조건 배포까지 하자!를 최우선으로 정했습니다.

과정

전 프론트 경험을 살려서 tailwindcss와 material ui로 최소한의 레이아웃만 잡고 뼈대를 설명해서 동료분께 전송하면 동료분이 반응형과 디자인 측면에서 조언을 주신 것을 기반으로 파트를 나누어 공동으로 작업했습니다. 인원도 시간도 적어서 기획서나 PPT 등은 생략했습니다;
(이것이 가능하기 위해서 기존에 커미션 경험들을 끌어모아서 머리를 쥐어짜야 했습니다ㅎ)

결제 기능 연동 - Payapp

작게 시작하고 싶은데 쉽지가 않다

대부분 결제 PG를 연동하려면 사업자가 필수이고, 그 외에도 알림톡과 같은 서비스 연동에도 사업자가 요구됩니다. 물론 당연한 것이고 실제로 사업자도 등록했다가 폐지해봤다가~ 업종 심사 관련한 전반적인 것들에 대해 공부도 해봤습니다.
하다보니 지칠 뿐만 아니라 주객이 전도되었다는 걸 느꼈습니다.
이 프로젝트로 큰 사업을 하고 싶다기 보다는 개발 전반 과정을 반복해서 능숙해지는 것이 저의 목적에 더 가까웠기에 동료와 머리를 맞대다가 Payapp에 대해서 알게 되었습니다.

Payapp이란?

Payapp은 사업자 등록 없이 결제를 연동해 볼 수 있는 API 및 스크립트를 제공합니다. Payapp은 비사업자의 판매 내역 또한 주민등록번호로 국세청에 신고를 합니다. (크레페 사이트 또한 사업자 등록을 체크하지 않지만 매년 신고를 자동으로 진행하고 있습니다. 5월에 종소세 신고)
샘플 코드가 굉장히 old해서 (jsp….php….) 분석에 꽤 애먹었습니다(함께 고생하신 동료분)

연동하기 이전에 가입은 물론 페이앱에서 요구하는 심사를 거쳐야 했고, 보증보험에 가입해야 했습니다. 하지만 이 모든 과정 또한 사업자 등록/관리에 비하면 매우 쉬웠습니다.
보증보험비는 연에 15000원 정도 부과됩니다.

연동

Payapp에서는 feedbackurlreturnurl이 가장 중요합니다. 해당 값에 지정한 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 설계

이 프로젝트의 시작은 리뷰 게시판이었습니다. 무료로 주는 몽고DB를 사용했는데, 현재 저의 기술 스택은 Spring Boot + MySQL이었고, 상품과 주문, 결제 관계 설정에는 RDBMS가 더 적합하다고 판단하여 이번 기능은 MySQL로 개발했습니다.
aws가 너무 비싸서 개인 서버를 구매했는데… 1월 13일에 온다는 군요.....그래서 Google Cloud MySQL 무료를 사용했습니다.
기존에는 pk를 위해서 auto_increment나 uuid를 주로 사용했는데 uuid는 가독성이 안 좋아서 이번에 {도메인 접두사}-{년월시분초}-{난수4자리}를 적용했습니다.

https://cloud.google.com/sql/mysql?hl=ko

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을 설정했더니 해결되었습니다.

https://velog.io/@ghwns9991/스프링-부트-3.2-매개변수-이름-인식-문제

결제가 완료되었을 때는 주문자의 이메일로 결제 내역을 전송합니다.
구글이메일을 사용했는데 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으로 업그레이드하는 것으로 해결했습니다.

https://stackoverflow.com/questions/78682673/invalid-value-type-for-attribute-factorybeanobjecttype-java-lang-string-in-sp

프론트엔드

처음에는 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로 설정하면 테스트 결제를 진행할 수 있으며 결제 금액은 몇 분 뒤 자동으로 취소됩니다.
관리자 페이지를 통해서 금액을 확인하며 유효성 전반과 백엔드를 수십번 테스트합니다.

마무리

꾸준히 하는 것이 가장 중요하다는 어른들 말씀을 이번에 비로소 이해한 것 같습니다;;
꾸준한 것은 당연하다고 생각했는데 이것을 개발하면서 굉장히 다시 하고 싶다, 마음에 들지 않는다라는 마음이 강하게 들어서 멈출 때가 있었는데
어떻게든 끝은 봐야 한다~로 마음먹었던 덕분에 일단락을 할 수 있었던 것 같습니다.

https://ktalk-review.netlify.app/event/product

Contributors

jinvicky
✉️ Email: jinvicky17@gmail.com
💻 Github: https://github.com/jinvicky

wkdu0723
✉️ Email: wkdu0712@naver.com
💻 Github: https://github.com/wkdu0723

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글