결제 기능이라는게 개발자 입장에선 생각 이상으로 매우 까다롭다. 그래서 장고 서버에서 아임포트로 결제 모듈을 구현한 경험을 공유하고자 한다. 여기서는 가장 일반적인 1회성 결제에 대해서만 구현을 했다.

 

 

  일단 용어부터 정리하자.

 

PG사 : 결제대행사. 내가 어떤 상품에 대해 XX카드로 5만원 결제를 원한다고 요청을 보내면, 온라인 상에서 실제로 XX카드를 들고 XX카드사에 결제를 해 주는 회사이다. 

가맹점 : 이 PG사들과 계약을 맺고 PG사를 통해 결제를 맡기는 점포. 즉 우리의 웹 서버이다.

 

 

 구현 방법이나 코드를 원한다면 다음 포스팅(업로드 예정)으로 가면 되지만, 아임포트가 왜 필요하고 결제과정에선 무슨 일이 일어나는지 아는게 구현이나 오류 해결에 큰 도움이 될 것이다. 

 

 

0. 결제 프로세스의 이론적 설명

 이 부분에 대한 자세한 내용은 https://github.com/iamport/iamport-manual/tree/master/%EC%9D%B8%EC%A6%9D%EA%B2%B0%EC%A0%9C 여기를 보면 된다.

아래의 사진이나 내용도 모두 여기서 참고했음을 밝힌다.

 

 

 

  일단 이론적으로는 이런 구조로 결제가 이루어진다. 눈에 띄는 특징은 카드정보가 우리의 웹 서버를 거치지 않고 다이렉트로 카드사 서버로 전달되는데, 이게 법적 제한으로 일부 카드사나 PG사 등을 제외하고는 카드정보를 저장할 수 없다. 그 외에는 가맹점 서버에서 PG 서버로 결제 요청을 전달하고, PG 서버에서는 카드사와 실제 결제를 수행하고, 그 결과(결제성공/실패)를 가맹점 서버에 돌려주는 구조이다. 그래서, 실제 결제 프로세스는 아래와 같이 이루어진다.

 

 

 

 

 

 위의 그림과 사실 큰 차이 없다. 대신, 구매자 브라우저에서 전달되는 카드정보(카드번호, CVC 등)이 가맹점 서버에서는 알 수 없도록 전달된다는 사실만 기억하면 될 것이다. 위 그림에서, 우리는 3,4,7,8만 신경쓰면 되고 PG 서버로 데이터가 넘어간 이후 다시 응답을 받을 때까지는 신경쓸 필요가 없다.

 

 

 PG사들도 결제 모듈을 모두 지원하고 있고, 개발자들에게 제공하고 있다. 그래서, 여기까지만 해도 결제에는 아무 문제 없다. 그렇지만 아임포트를 도입한다고 가정하자. 아임포트는 바로 가맹점 서버와 PG 서버의 사이에 있다. 그림판으로 조악하게나마 그림을 다시 그려보면 이렇게 될 것이다.

 

 

 즉 아임포트의 도입을 통해, PG사와의 통신 과정도 래핑을 하게 된다. 그러면, 이제 이런 의문이 생길 것이다. 왜 굳이 아임포트가 중간에 들어가야 하는가? 바로 이게 결제에 아임포트를 사용해야 하는 이유가 될 것이다. 그래서 내가 생각한 이유들을 아래에 정리해 놓았다.

 

 

1. PG사와의 복잡한 연동과정

 나는 처음으로 사용한 PG사가 KCP인데, 처음에는 아임포트 없이 직접 결제를 구현하고자 호기롭게 매뉴얼을 다운받아 열어봤다. 일단 메뉴얼도 수십 페이지인데, 그야말로 읽다가 정신이 나갈 것 같았다. 일단 서버에다가도 PG사와의 소켓 통신을 위한 모듈을 이것저것 깔고, 서버 설정도 만져야하고 뭐뭐 깔아야하고 할일이 수두룩했다. 주고받는 데이터도 양식이 복잡했다. 예제는 PHP랑 JSP밖에 없어서 별 도움도 안 됐다. 이러한 귀찮은 과정들을 미리 해 놓은 서버가 있어서, 내가 거기다가 결제에 필요한 정보만 보내면 알아서 다 해주는 서버가 있다면 어떨까? 그게 바로 아임포트 서버이다. 그냥 결제에만 신경쓸 수 있게 해 주는 것이다.

 

 

2. 복수 PG사 사용시 편리함

   당장 배달의 민족만 봐도, 배달음식을 결제할 때 카카오페이, 스마일페이, 네이버페이, 신용카드 등 여러 가지 결제 방식을 지원한다. 카카오톡을 안 쓰는 사람은 거의 없지만 카카오페이를 안 쓰는 사람은 많듯이, 하나의 결제 방식으로 모든 고객들을 커버할 순 없다. 그래서 여러 개의 결제방식을 지원하는 게 중요한데, 당연히 PG사들마다 결제 모듈이 제각각이다. 즉 1번의 저 귀찮은 과정들을 전부 다 따로 해야 한다. 아임포트는 PG사와 상관없이 결제에 정형화된 API 형식을 사용한다. 그래서 가맹점 서버에서 결제 모듈을 재활용 하기 용이하다... 사실 용이함을 넘어 그냥 결제 파라미터 하나만 바꾸면 거의 모든 PG사에 돌려 쓸 수 있다.

 

 

2-2. 매출 관리의 용이함

 이건 개발과는 크게 상관없는 내용이지만, 여러 개의 PG사를 운용할 경우 각 PG사마다 결제 내역이 있을 것이다. 이 결제 내역들을 아임포트 관리자 콘솔에서 한꺼번에 보고 관리할 수 있다. 

 

 

 

3. 환불이 쉬움

 이게 대체 장점이 맞나 하겠지만.... 개발자의 관점에서 내가 제일 사랑하는 기능이다.

 아임포트 결제로그 페이지에서, 성공한 결제에는 취소하기 버튼이 생긴다. 이거 누르면 바로 환불이 된다. 결제 기능 테스트할때, 처음에는 돈 깨질거 각오하고 1000원짜리 디버그용 상품(참고로 PG사마다 결제 최소금액이 있어서 그 이하로는 결제가 안 된다. KCP는 천원이다.) 만들어서 해볼까 했는데, 환불이 너무 쉬워서 결제 기능 테스트할떄 요긴하게 써 먹었다. PG사마다 '테스트 결제'라는 기능이 있긴 한데, 이거는 그냥 잘 돌아가는지 정도 확인하는데만 쓰고, 반드시 실제 결제로 테스트 하는 걸 권장한다. 아래에서 설명하겠지만 결제 모듈은 상상이상으로 까칠해서, 생각 이상으로 안될 때가 많다...

 

 

 

 대충 이런 이유로 아임포트를 도입하는게 개발 코스트 절약과 개발자의 정신건강을 위해 좋다. 일단 PG사 하나만 쓰는건 무료고, 여러개의 PG사를 쓸 때만 한번 돈을 지불하고 계정을 업그레이드 하면 된다. 

 

 

 

1. 아임포트 세팅 및 PG사 계약

 

 참고 :

https://docs.iamport.kr/

 

아임포트 docs

본 매뉴얼은 기존의 아임포트 매뉴얼의 리뉴얼 버전입니다. 아직 모든 컨텐츠가 준비되지 않은 베타 버전이며 지속적으로 업데이트 될 예정입니다. 따라서 결제 연동 시 기존의 아임포트 매뉴

docs.iamport.kr

 

https://www.iamport.kr/getstarted

 

개발자를 위한 무료 결제연동 API, 아임포트

아임포트를 import하세요. 아임포트의 풍부한 REST API 기능을 이용하면 결제연동이 훨씬 쉬워집니다. 지금 시작하세요.

www.iamport.kr

 

 과정도 쉽고 한글로 되어있어서 따로 설명은 필요 없을 것 같다. 회원가입하고 쓰면 된다.

 아임포트 docs의 경우엔 '가맹점 정보 확인하기', '일반결제 연동하기', 'IMP.request_pay - param, rsp 객체' 이 세 페이지만 참고해도 기본적인 결제를 구현하는 덴 지장 없을 것이다.

 

 

  이 과정에서 주의할 점은, 단순히 테스트 결제라면 상관없지만, 실제 결제기능을 위해선 PG사와 계약을 하고 PG사로부터 가맹점 id를 발급받아야 한다. 이 때, 직접 PG사와 계약하면 안되고 아임포트 내의 PG사 가입 페이지를 통해, 아임포트를 거쳐서 계약을 해야 한다. 구조상 아임포트 서버에서 결제가 이루어지기 때문이다. 물론 아예 가맹점 id를 아임포트에서 소유하는 건 아니고, PG사 페이지에서도 정상적으로 가맹점 취급을 받는다.

 

 

  이걸 모르고 직접 PG사와 계약을 했다가 결제가 안 돼서 아임포트 측에 문의를 하고, 최종적으론 아임포트를 통해 재계약을 하는 등 많은 삽질을 했었다... 참고로, PG사와 계약하는 것도 사업자등록증을 보내거나 쇼핑몰 심사 등의 과정이 있기 때문에 주 단위로 걸린다. 나는 개발 외적인 비즈니스 업무는 맡지 않았지만, 제3자의 눈으로 봐도 수많은 메일과 서류가 오고가는 등 많이 피곤해 보이는 작업이었다.

 

 

 이게 짧으면 1주일, 길면 2주일 정도고 중간에 뭔가 문제가 있으면 당연히 수정요청을 받는다. 생각보다 심사 조건이 까다롭다. 사이트에 연락처나 계좌 등이 있어야 하고, 상품 갯수도 일정 이상이여야 한다. 사실 상품 갯수도 제한이 있다는 것이 의외였는데, "이걸 누가 사?" 가 절로 나오는 상품을 사이트에 넣으며 불편한 진실을 어렴풋이 깨달았다...

 

 

 

 다음 편에선 실제 코드를 통해 아임포트를 세팅하고, 검증하는 과정에 대해 다뤄보겠다.

 

 

 

 

 

 실제 서버를 배포하면, 불행하게도 항상 따라오는 게 버그다. 물론 철저한 코딩과 테스트, 경우의 수 계산으로 미연에 방지하는 게 가장 좋겠지만 현실적으로 불가능하니, 차선책으론 에러가 발생했을 때, 문제 해결을 위한 데이터를 최대한 모으는 게 중요하다고 생각했다.

 

 

 

 물론 Django에서 아래와 같이 자체적으로 로깅을 지원하긴 하는데, 특히 이용자가 많아질 수록 텍스트로 이루어진 로그는 보기도 불편하고 무엇보다 내가 원하는 특정 상황(=특정 세션)에 대한 부분만 추출하기 너무 까다로웠다. 그렇다고 설정을 만져 Error 이외의 로그를 거르기엔 뭔가 찝찝했다.

 

 

 그래서 무료로 이용할 수 있는 에러 모니터링 툴에 대해서 찾아보던 중, Sentry에 대해 알게 되었다. 그래서 실제 서비스에 적용해서 사용해 보니, 매우 간편하면서 강력해서 큰 도움이 되었다. 그래서 센트리의 설치나 간단한 활용법에 대해서 공유하고자 한다.

 

 

1. 설치하기

https://sentry.io/

 

Application Monitoring and Error Tracking Software

Self-hosted and cloud-based error monitoring that helps software teams discover, triage, and prioritize errors in real-time.

sentry.io

  간단히 회원가입하고, 바로 프로젝트를 하나 만들면 된다. 프로젝트에서 모니터링을 진행할 프로그램을 지정할 것이다.  자기가 사용하는 언어/프레임워크를 고르면 그에 맞춰서 코드를 만들어 준다. Python -> Django를 고르면 된다. 그러면 아래와 같이 하라는 튜토리얼 페이지가 뜬다.

 

 

 

 

  sentry-sdk 라이브러리를 설치하고 Settings.py에 넣으면 된다. 물론 위 정보는 프로덕션 서버의 설정 파일에 따로 넣는 것을 추천한다. 필요하지 않다면 개발 서버에서는 굳이 에러 트레킹을 할 필요가 없기 때문이다.

 

 

  그 아래는 0으로 나누기 에러를 발생시키고 테스트 해보는 부분이 있는데, 간단한 테스트이므로 가볍게 한번 해 보면 될 것이다.

 

 

2. 활용하기 - 프로젝트

  프로젝트 세팅을 마친 후, 화면을 살펴 보자. 왼쪽의 목록에서 여러 가지 메뉴들이 있는데, 차례대로 Projects, Issues, Performance를 살펴볼 예정이다. 우선 Projects 메뉴에 가서, 만든 프로젝트를 눌러 보자.

 

  이 화면은 프로젝트 상세 페이지이다. 내가 돌리고 있는 현재 서버의 상태를 전체적으로 조망할 수 있다.

 

  Crash Free Sessions는 에러 없이 정상적으로 진행된 요청의 비율을 나타낸다. 당연히 이 비율이 높아야 서버가 정상적으로 돌아가고 있다고 간주할 수 있을 것이다. 내 서버는 약 99.9%의 신뢰도를 나타내고 있다. 이 0.1%가 왜 생겼는지는 아래에서 설명하도록 하겠다.

 

  Number of Releases는 이 프로젝트가 가진 릴리즈의 수인데, 쉽게 말해서 웹 서버의 버젼이 몇 개가 있었는지, 즉 몇 번의 변화가 있었는지를 뜻한다. 나는 따로 관련 설정을 안 해서 0이라고 나왔다. 아마 설정을 하면 특정 버젼에선 에러가 많았고 특정 버젼에선 적었다 식으로 모니터링이 가능할 텐데, 굳이 이 기능이 필요할 것 같진 않아서 나는 쓰지 않았으니 관심 있다면 사용해 봐도 될 것이다.

 

  Apdex는 성능 관련 지표인데, 지정된 시간 안에 요청에 대한 응답이 왔는지에 대한 비율이다. 이건 Settings->Perfomance에 가면 해당 값이 있는데, 기본값으론 300ms가 설정되는 것 같다. 성능 트래킹을 원한다면 이 부분도 활용할 수 있을 것이다.

 

 

3. 활용하기 - 이슈

 

 

 Issues 페이지이다. 사실 센트리를 즐겨찾기에 설정해 놨는데, 들어가면 자동으로 이 페이지로 뜬다. 그만큼 사실상 센트리에서 가장 중요한 부분 중 하나이다. 여기서 서버에서 발생한 에러들을 관리할 수 있다. 발생 시간이나 에러 내용, 어떤 URL로 접속했을 때 에러가 발생했는지 등이 적혀 있다. 여러번 발생하는 에러는 아래와 같이 이벤트(=요청 횟수)와 유저를 구분하여 중첩되어 기록된다. 즉, 여러 명의 유저가 이런 문제를 겪은 건지, 아니면 한 명이 똑같은 에러를 여러 번 발생시킨 건지 구분할 수 있다. 이 점이 Sentry가 에러를 보기 편하게 해주는 강점 중 하나라고 생각한다.

 

 

 

 참고로, 에러들이 꽤 많이 떠 있는데, 장고 서버를 운영하면서 몇 가지 발생할 수 있는 에러들이 있다.

 

 

  우선 Invalid HTTP_HOST header: 'www.blockfinex.com'. You may need to add 'www.blockfinex.com'... 등등 HTTP_HOST 에러가 대부분인데, 이건 유저가 아니라 그냥 인터넷 사이트들을 무차별적으로 취약점 스캔하는 봇들이다. 자세히 보면 접근하는 URL도 정상적이지 않다. 아무나 걸려라 하는 식으로 스캔하고 다니는 거라, 에러가 나면서 차단당하는 게 맞다. 해당 에러는 설정을 통해 이  유형의 에러는 무시하도록 할 수 있는데, 혹시 진짜 관찰이 필요한 에러들도 무시될까봐 따로 건들지 않았다.

 

 

 참고로 settings.py의 ALLOWED_HOST를 설정하지 않거나 *(모든 호스트 허용)으로 해 두면 이 에러는 안 뜰 것이다. 보안에 좋지 않으니 *보다는 실제 사용하는 호스트 이름으로 설정해 두자... 잘 모르겠으면 [{서버의 공인 아이피 주소}, '127.0.0.1', {서버의 도메인 주소}] 정도로만 해 둬도 될 것이다.

 

 

인터넷에 공개된 서버라면 반드시 받을 수밖에 없는 공격이다...

 

 

 

 그리고, OSError 같은 경우엔 개발 환경에서는 발생하지 않았지만 프로덕션 환경에서는 주기적으로 발생하던 에러인데, 이 에러는 다른 에러들과 다르게 Django의 세션 관련 데이터가 누락되어 있다.

 

 장고와 관련된 정보가 아예 누락되는 걸로 봐선 장고 밖에서 발생하는 에러로 추정되는데, 구글링 결과 장고의 문제는 아니고 wsgi로 사용하는 프레임워크인 uwsgi 관련 버그였다.

 

stackoverflow.com/questions/59026602/uwsgi-oserror-write-error-during-get-request

 

uwsgi: OSError: write error during GET request

Here is the error log that I received when I put my application in the long run. Oct 22 11:41:18 uwsgi[4613]: OSError: write error Oct 22 11:41:48 uwsgi[4613]: Tue Oct 22 11:41:48 2019 -

stackoverflow.com

 쉽게 말해서, 클라이언트와 서버가 요청을 주고받는 중 클라이언트는 어떤 이유로던 통신을 끊었는데, 서버(정확히는 uwsgi)에서는 요청을 계속 처리하고 있어서 생기는 오류이다. 이건 서버 설정을 만지면 해결될 수 있는 오류이다. 사실 이 에러 때문에 서비스에 문제가 생긴 적은 없었던 것 같다...

 

 

4. 활용하기 - 이슈 - 에러 상세보기

 

 위에 뜬 에러들 중 하나를 눌러 보면 에러의 상세한 내용을 볼 수 있다. 아래는 실제로 발생한 에러를 캡처한 것이다.

 

 

 

 우선 우측 상단의 이벤트, 유저를 통해 1명의 유저에게 이 에러가 12번 발생했다는 것을 알 수 있다. 또한, 해당 유저가 사용했던 브라우저와 운영 체제 역시 확인할 수 있다. 현재 보고 있는 건 가장 최근에 발생한 에러 정보로, 이전에 발생한 에러 정보를 보고 싶다면 events 아래의 숫자를 클릭하면 지금까지 발생한 동일한 에러 리스트가 뜬다. 각각을 클릭하면 각 에러에 대한 정보를 알 수 있다.

 

 

 

 또한, users 밑의 숫자(1)을 클릭해도 해당 에러를 겪은 유저들의 목록과 비율이 표시된다.

 

 

 

 

  센트리에서 에러가 발생한 상황의 정보를 수집하여 보여주는데, 우선 실행시킨 코드에서 에러가 발생한 부분을 표시해 준다. 여기는 기존 파이썬에서 보여주는 StackTrace와 같다.

 

 

 

 

  

 

 다음으로, breadcrumbs라 해서 사용자가 사이트에서 어떤 행동을 했는지 보여준다. 이 부분이 가장 핵심인데, Django 로거의 info 로그는 물론, 해당 사용자의 요청으로 인해 발생한 추가적인 HTTP 요청(심지어 타 사이트에 대한 API 요청이다)이나 DB 조작도 모두 보여준다. 이 부분을 보안상 가려야 하는 것이 유감일 정도로 상세하게 나온다. '카테고리(CATEGORY)'에서 httplib은 HTTP 요청, query는 DB 쿼리, django는 Django 로거를 나타낸다. 아마 더 많은 종류가 있을 것이다.

 

 

 그리고, 해당 view는 body에 데이터를 담은 POST 요청을 받아 특정한 동작을 수행하는데, 이 POST 요청에 대한 정보 역시 body의 내용을 포함해 쿠키, 헤더 등 모든 정보가 기록된다. 이외에도, 현재 유저가 django의 auth 시스템을 이용해 로그인 된 상태라면 해당 유저의 정보 역시 기록된다.

 

 

 

 

 

  Details 옆에 Activity 탭에서는, 해당 에러에 대해 기록을 할 수 있다, 깃헙의 issue와 비슷하다. 아마 여러 명이서 협업을 할 때 담당자를 배정하고, 의견 등을 교환하는 데 사용할 수 있을 것이다. 물론 아래처럼 그냥 개인 메모용으로 써도 된다. 

 

 

  마지막으로, 특정 에러에 대해 Resolve나 Ignore 등으로 체크를 할 수 있다. Resolve는 해결됨, Ignore는 무시이다. 체크할 시 해당 에러에 대해서 빨리 해결하라고 굵은 글씨로 강조하지 않게 된다. 단, 이그노어의 경우엔 이미 발생한 해당 에러에 대해서만 무시이고, 아예 관련 에러를 전체적으로 무시하려면 설정을 만져야 한다.

 

 

 

  이외에도 특정 에러 발생시 이메일 등으로 알림을 날리는 기능도 있지만 아마 간단히 활용하는 데는 이 정도만 해도 충분할 것이다.

 

 

 

 

 

5. 성능 분석

 

 이건 에러 모니터링과는 크게 관련은 없는 기능인데, 성능 측정엔 도움이 되는 부분이다. Perfomance에 들어가면 볼 수 있다.

 

 

  서버의 성능과 관련된 각종 지표들을 볼 수 있는 곳이다. 만약 데이터가 표시되지 않는다면 오른쪽 위의 기간을 좀 더 넓게 만져보자. Duration p75는 75%의 요청이 해당 시간보다 빠르게 처리되었다는 뜻이고, throughput은 분당 해당 수치만큼의 요청이 들어왔다는 뜻이고, Failure rate는 말 그대로 에러가 난 비율(HTTP 코드 4XX), apdex는 위에서 설명한 대로 제한 시간 내에 요청이 처리되었는지의 비율이다. 아래에는 관련 그래프들이 나타나 있다.

 

 

 

  Failure Rate가 꽤 높다. 대략 5명 중에 한 명이 오류가 난다는 것인데, 사실 정상이다. 왜냐하면 위에서 말한 취약점 스캔 봇들이 상상 이상으로 엄청나게 긁어가기 때문이다. 어떤 요청들이 있었는지 이 성능 페이지에서 바로 확인할 수 있는데, 한번 살펴보자.

 

 

 php를 사용하지도 않는 사이트인데 관련 URL로 많이 들어온다. 저게 워드프레스 쪽에서 사용되는 관리자 페이지나 설정 파일인데, 아무나 걸려라식 무차별 대입 공격이고 내 서버에는 저런 파일들이 존재하지 않으므로 전부 404 Not Found로 표시되는 것이다. 이게 바로 20%의 failure rate의 정체이다.

 

 

 

 

 위에서 소개한 센트리의 기능들은 극히 일부이다. 일단 무료 버젼에 있는 기능들만 소개한 거고 아마 유료 버젼엔 더 많은 기능들이 있을 것이다. 무료 기능으로도 꽤나 유용하게 사용할 수 있고 무엇보다  설치가 매우 간편하다는게 마음에 들었다. 장고 서버가 있다면 에러 모니터링을 공부해 보는 용도로도 유용할 것 같다.

  Django에서 아임포트를 이용한 결제 시스템을 만들던 중, 문제가 있었다. 결제를 완료한 후, 결제 정보를 POST method를 통해 서버로 보내야 하는데, 서버로 보내도 항상 받은 데이터가 empty로 표시가 되었다. 처음에는 프론트엔드 쪽에서 문제가 있는줄 알았는데, 알고 보니 Django의 view에서 POST를 받을 때 모든 데이터를 받을 수 있는 게 아니었다.

 

  아래는 장고 공식 문서에서 Request.POST의 설명 중 일부이다.

https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpRequest.POST

 

A dictionary-like object containing all given HTTP POST parameters, providing that the request contains form data. See the QueryDict documentation below. If you need to access raw or non-form data posted in the request, access this through the HttpRequest.body attribute instead.

 

Request and response objects | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

  쉽게 말해, POST를 통해 form 데이터 이외의 데이터들은 request.POST.('요소') 와 같은 방식으로 받을 수 없고 request.body와 같이 바이트 스트림으로 직접 받아와서 처리해야 한다. 다행히, 생각보다 어렵진 않았다.

 


 

  나는 딱히 이미지와 같은 바이너리 형식의 데이터를 전송하는 게 아니므로, json 형태로 데이터를 주고받기로 약속했다. 보내는 쪽에서 json 형태로 보내도록 아래와 같이 코딩하자.

 

var paymentdata = {
    // 서버로 보낼 데이터(dictionary 형태)
}

jQuery.ajax({
      url: "{% url 'destination' %}",
      type: "POST",
      dataType: "json",
      data: JSON.stringify(paymentdata)
}).done(function (data) {
      alert("결제가 성공적으로 진행되었습니다.");
}).fail(function (data) {
      alert("결제중 서버와의 통신에 문제가 발생하였습니다.\n원인: " + data.reason);
})

 

 보내는 코드는 데이터만 json으로 확실하게 보낸다면 어떤 형태이든 상관없다.

받는 view 부분 역시 이 두줄이면 충분하다.

    import json
    
    # 생략
    
    if request.method == 'POST':
        payment_data = json.loads(request.body)

 

json으로 보내고 json으로 받는 점만 기억하면 큰 문제가 없을 것이다.

  위와 같은 스트링 데이터 이외의 이미지 등의 바이너리 데이터들을 주고받을 때에도 똑같은 방식으로 request.body를 이용해 처리하면 큰 문제 없을 것이다.

 


 

  추가로, 장고 기본 form을 보면 항상 {% csrf_token %} 이 있었던 걸 기억할 것이다.

아마 위 상태 그대로 form을 보낸다면 csrf 때문에 에러가 날 것이다.

이건 말 그대로 프론트엔드에서 보낼때 해당 토큰을 추가하면 되고,

 

jinmay.github.io/2019/04/09/django/django-ajax-csrf/

 

Django에서 ajax post 요청시 csrf token 문제 해결하기

Django에서 ajax 사용할때 발생하는 csrf 문제장고에서 ajax를 조금씩 섞어서 사용하다보면 POST 요청을 보낼때 문제가 발생하게 된다. POST 요청에서는 보안상의 문제로 csrf token을 필요로하게 되는게 �

jinmay.github.io

  이 블로그에 코드를 비롯하여 해결책이 명확하게 나와 있으니 참고하면 되겠다.

참고로 1번은 저 블로그 주인장분도 설명하셨지만 보안상으로 매우 안좋은 방법이니까 2번 방법을 강력히 권장한다.

문 열기 귀찮다고 현관문을 없애버리는 사람은 없을 것이다.

 

 

 

 

 

 django에는 기본적으로 email 발송 기능이 내장되어 있다. 그런데, 인터넷에 관련 예제를 찾아 보면 전부 gmail 발송이고, daum 메일(hanmail.net / daum.net) 발송에 대한 예제는 거의 없어서 정리하게 되었다.

 

 이미 gmail로 발송을 해 봤다면 기본적인 맥락은 거의 유사하다.

 

 

 

1. Daum 메일 설정

 

daum 메일 -> 환경설정에 들어가자.

 

 

 웹서버를 통한 메일 발송은 SMTP라는 프로토콜을 사용하게 되는데, 그러려면 다음과 같이 사용 설정을 해야 한다. 기본값으로 '사용 안 함'으로 되어있으니, 꼭 잊지 말고 바꿔야 한다.

 

 

 

 

2. django의 settings.py 변경

 

EMAIL_HOST = 'smtp.daum.net' 		 # 메일 호스트 서버
EMAIL_PORT = 465 			 # 서버 포트
EMAIL_HOST_USER =  '사용할 이메일'
EMAIL_HOST_PASSWORD = '이메일 비밀번호'
EMAIL_USE_SSL = True
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER	 # 기본 발신자
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

 

 

SMTP 설정 페이지 바로 밑에 나온 안내문이다. 이대로 설정하되, 이미 gmail로 구현해 본 적이 있다면 다른 점이 몇개 있다.

 

  • Port : gmail은 587 포트를 사용하는데 여기서는 465 포트를 사용해야 한다.
  • EMAIL_USE_SSL : 내가 이 글을 쓰게 된 이유이다. 처음에 gmail을 쓰다가 다음 메일로 교체했는데, 어떤 오류도 없이 그냥 이메일 발송 페이지에서 웹서버가 멈춰 버리는 일이 있었다. 값을 쭉 점검해본 결과, 이 설정값에 문제가 있었다. 이전엔 EMAIL_USE_TLS = True 를 설정값으로 이용했다. TLS와 SSL은 유사하지만 엄연히 다른 프로토콜이었고, 이 값을 EMAIL_USE_SSL로 바꾸니까 정상적으로 작동되었다. 참고로, EMAIL_USE_SSL 과 EMAIL_USE_TLS를 동시에 넣으면 하나만 쓰라고 에러가 난다. 이 부분 반드시 체크하자.

 

 발송 과정은 django.core.mail을 활용하면 되고 워낙 예제가 많아서 생략한다.

  Django에서 기본적으로 제공해주는 로그인 관련 뷰와 템플릿은 매우 편하다. 대략적으론 다음과 같이 사용할 수 있다.

 

yourapp/urls.py

from django.contrib.auth import views as auth_view

urlpatterns = [
    path('login/',
    auth_view.LoginView.as_view(template_name='registration/login.html'), name='login')
    ]

 

templates/registration/login.html

        <div class="alert alert-info">로그인</div>
        <form action="" method="post">
            {% csrf_token %}
            {{form.as_p}}
            <input class="btn btn-primary" type="submit" value="Login">
        </form>

 

 

   그런데, 이 상태에선 기능적으로는 문제가 없지만, 사용자 경험면에서는 문제가 좀 있다.

 

  위의 코드로 만들어진 로그인 화면이고, 위의 Please ~ 문구는 비밀번호를 틀리면 나오는 문구이다.

나는 템플릿엔 어떠한 텍스트도 넣지 않았으니 아마 form에서 자체적으로 산출된 텍스트들일 것으로 추정할 수 있다.

 

  이 글을 보는 사람들은 거의 개발자일 것이니 이 정도 영어는 신경쓰이지 않을 수도 있겠지만,

일반 사용자들은 그렇지 않을 것이다. 그래서, 이 영어 범벅 폼을 개조해서 한글화를 해 보기로 했다.

오류 문구는 물론, Email과 Password 역시 내가 DB에 저장해 놓은 이름이 그대로 출력되고 있기 때문에 이 역시 바꿔보기로 했다.

 


 

  우선, LoginView부터 살펴보도록 하자. 파이참을 사용한다면 해당 클래스에 커서를 대고 Ctrl+B로 해당 클래스의 정의로 바로 넘어갈 수 있다.

 

  밑에 자잘한 함수들이 있으니 이 뷰가 어떻게 동작하는 지 관심이 있다면 읽어보자.

form_class = AuthenticationForm

에만 주목하면 해당 이름의 Form을 로그인에 활용하는 것으로 추정할 수 있고, 밑에서 get_form_class() 라는 함수를 통해 해당 form을 사용하도록 설정하는 코드가 있다.

  그러므로 우리가 수정해야 할 건 이 Form일 것이다.

 

 

 

  아래의 error_messages를 보면 위에서 봤던 익숙한 문구들이 보인다. 이제 이 부분을 전부 수정하면 될 것이다.

수정해야 할 부분을 찾았으니, 이제 코드로 작성해보자.

 

 

 

yourapp/forms.py

from django.contrib.auth.forms import AuthenticationForm

class CustomAuthenticationForm(AuthenticationForm):
    error_messages = {
        'invalid_login': (
            "비밀번호나 이메일이 올바르지 않습니다. 다시 확인해 주세요."
        ),
        'inactive': ("이 계정은 인증되지 않았습니다. 인증을 먼저 진행해 주세요."),
    }

 

 

에러 메세지들은 수정했으니, 이제 필드 이름을 수정할 차례이다. 해당 클래스의 __init__ 함수를 보자.

 

 

 유저네임 필드의 최대 길이와 라벨명 등을 설정하고 있다. 해당 부분을 오버라이딩 하면 될 것이다.

 

 

* 글쓴이는 username 대신 email을 쓰고 있는데? 

 

  맞다. 나는 User 클래스를 아예 새로 만들어서 쓰고 있다. 그래서 로그인에서 ID 없이 이메일과 비밀번호만으로 로그인을 하고 있고, username이라는 필드는 아예 모델에 없다. 그래도 지금까지 위 코드가 정상 작동 했다. 왜냐하면,

self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)

이 부분 때문이다. 장고의 기본 User 모델에 username이라는 필드가 있고 해당 필드를 로그인시에 활용하는 건 맞다.

  일단, 이 Form에서의 username이라는 이름은 그냥 필드 이름 그 이상도 이하도 아니다.

두 이름이 겹치는 건 일단 그냥 우연이라고 생각하면 된다. 중요한 건, 이 Form에서의 username 필드를 정의할 때, Usermodel의 USERNAME_FIELD를 이용해 정의를 한다. 즉 로그인 인증에서 사용하는 유저 모델의 설정을 따라간다.

그리고 나는 커스터마이징한 유저 모델에서 USERNAME_FILED를 email이라고 정의해 두었기에, 맨 위의 로그인 창에서 Email이라고 표시된 것이다.

 


 

위의 관찰 내용을 이용하여 커스터마이징을 계속해 보자.

 

yourapp/forms.py

from django.contrib.auth.forms import AuthenticationForm

class CustomAuthenticationForm(AuthenticationForm):
    error_messages = {
        'invalid_login': (
            "비밀번호나 이메일이 올바르지 않습니다. 다시 확인해 주세요."
        ),
        'inactive': ("이 계정은 인증되지 않았습니다. 인증을 먼저 진행해 주세요."),
    }

    def __init__(self, request=None, *args, **kwargs):
        super(CustomAuthenticationForm, self).__init__(*args, **kwargs) # 꼭 있어야 한다!
        self.fields['username'].label = '이메일'
        self.fields['password'].label = '비밀번호'

 

 

  원한다면 길이 제한 등도 건들 수 있지만 여기서는 필요하지 않아서 따로 수정하지 않았다.

이대로 하기 전에, 다시 한번 원본 코드를 보고 이 부분을 주목해보자.

        if self.fields['username'].label is None:
            self.fields['username'].label = capfirst(self.username_field.verbose_name)

username 필드의 라벨이 지정되어 있지 않으면, 해당 username 필드의 verbose_name을 가져온다고 되어 있다.

그렇다면, 내가 커스터마이징했던 모델에서 verbose_name을 지정한다면 최소한 이메일 부분은 한글화가 되지 않을까?

 

 

class User(AbstractBaseUser):
    email = models.EmailField(
        verbose_name='이메일',
        max_length=255,
        unique=True,
    )
    
    USERNAME_FIELD = 'email'
    # ...

 

이렇게 지정하고, migration을 진행한다면...

 

 

  잘 적용 되었다. 이렇게 된다면 username 필드는 따로 건들 필요가 없게 되었다.

이런 식으로 매번 form class나 템플릿을 만지며 라벨을 일일이 달아 주는 건 비효율적일 것이다. 그래서 직접 form의 label을 수정하는 것보다, 가능하다면 위와 같이 verbose_name을 적용하는게 제일 좋다.

 

 

 

코드를 수정하자.

 

yourapp/forms.py

from django.contrib.auth.forms import AuthenticationForm

class CustomAuthenticationForm(AuthenticationForm):
    error_messages = {
        'invalid_login': (
            "비밀번호나 이메일이 올바르지 않습니다. 다시 확인해 주세요."
        ),
        'inactive': ("이 계정은 인증되지 않았습니다. 인증을 먼저 진행해 주세요."),
    }

    def __init__(self, request=None, *args, **kwargs):
        super(CustomAuthenticationForm, self).__init__(*args, **kwargs) # 꼭 있어야 한다!
        self.fields['password'].label = '비밀번호'

 

 

폼은 완성되었다. 이제 뷰를 수정하자.

 

yourapp/views.py

 

from django.contrib.auth import views as auth_view
from .forms import CustomAuthenticationForm

class CustomLoginView(auth_view.LoginView):
    form_class = CustomAuthenticationForm

 

Form까지 적용이 완료되었다. 이제 뷰를 교체하자.

 

 

yourapp/urls.py

 

from .views import CustomLoginView

urlpatterns = [
    path('login/',
    CustomLoginView.as_view(template_name='registration/login.html'), name='login')
    ]

 

 

이제 결과를 보자.

 

 

  모두 한글화가 되었다. 

맨 아래의 Login 버튼은 Form에 포함된 게 아니라 템플릿에 들어 있는 요소여서 바로 수정할 수 있을 것이다. 

 

 

  이외에도 커스터마이징 할 수 있는 요소가 많이 있다.

여기서 다루지 않은 속성이나 함수들도 적절히 오버라이딩 한다면 훨씬 많은 걸 할 수 있을 것이다.

 

+ Recent posts