All Articles

닥터마틴 웹사이트 클론 프로젝트 회고

프로젝트 요약

  • 프로젝트명 : 닥터마틴 프로젝트
  • 프로젝트 소개 : 영국 60년 정통 패션 브랜드 샌들, 슈즈, 부츠 등을 취급하는 세계적인 의류 기업 닥터마틴 웹사이트 클론
  • 팀명 : 닥터스트레인지(aka 닥터막차)
  • 기간 : 6/22(월) ~ 7/3(금), 12일
  • 인원 : 프론트엔드 3명, 백엔드 3명
  • 역할 : 백엔드
  • 담당 업무

    • 장고 프로젝트 초기 세팅
    • 웹사이트 데이터 크롤링
    • Mysql DB 구축
    • 데이터베이스 업로더 작성
    • 모델링
    • 장바구니 추가, 확인, 삭제 뷰 작성
    • 특정 상품이 노출되는 리스트 뷰 작성
  • 사용 기술

    • Django, Python
    • Beautiful Soup, Selenium
    • Mysql
    • Bcrypt, JWT
    • CORS headers
    • Git
  • 백엔드 GitHub / 프론트엔드 GitHub
  • POSTMAN API 문서
  • 데모 영상

프로젝트 목표

  • Scrum - 스크럼 진행 방식에 대해서 이해했고 Trello 와 같은 tool 을 활용하여 스크럼 방식 아래 프로젝트 진행할 수 있다
  • Standup Meeting - 매일 아침 미팅을 통해 어제 한 일, 오늘 할 일, blocker 세 가지를 공유하며 팀원들과 미팅을 진행할 수 있다.
  • Communication - 팀원들과 소통이 필요한 경우 올바른 방법을 통해 의견을 주고 받으며 조율할 수 있다.
  • Git - 기본적인 Flow에 따라 Git을 사용할 수 있으며, brach를 생성하고 올바른 이름과 내용을 commit message를 작성할 수 있다.
  • 문제 해결 능력 - 모르는 과제를 마주하는 경우 Google 검색, stackoverflow 등을 활용하여 문제를 해결할 수 있다.
  • Q&A - 스스로 문제 해결이 잘 안 되는 경우, 혹은 누군가가 도움을 요청하는 경우 동기, 혹은 멘토와 올바른 방법으로 질문과 대답을 주고 받을 수 있다.
  • 장고 초기세팅(프로젝트 생성, 앱 생성, MySQL DB연결)을 혼자서 할 수 있다.
  • one to one, one to many, many to many 개념을 알고 있다.
  • JOIN 기본 개념을 이해하고 있고, LEFT JOIN, RIGHT JOIN, INNER JOIN, OUTER JOIN의 차이점들을 이해하고 있다.
  • 요구사항에 맞게 데이터 베이스 모델링 설계를 할 수 있다.
  • HTTP 기본 개념 (요청/응답, stateless)를 이해하고 있고 메세지 구조를 이해하고 있다.
  • GET, POST 메소드 차이점을 알고, 프론트에서 넘어오는 데이터를 어떻게 처리해야 하는지 알고 있다.
  • 쿼리 스트링과 JSON으로 전달되는 데이터를 어떻게 받아서 처리하는지 알고 있다.
  • 프론트에서 회원가입한 유저정보를 데이터베이스에 저장할 수 있다.
  • 데이터 베이스에 저장된 User정보를 리턴하는 엔드포인트를 구현할 수 있다.
  • 장고 ORM을 사용하여 DB CRUD(Create, Read, Update, Delete)을 구현 할 수 있다.
  • Decorator를 구현 및 엔드포인트에 적용 할 수 있다.
  • RESTful API 개념을 이해하고 URL 주소를 RESTful 식으로 구현할 수 있다.
  • 프론트엔드 개발자와 소통하여 front 와 back을 연결 할 수 있다.
  • AWS에서 서버를 생성하여 django를 배포할 수 있다.
  • 장고의 폴더 구조를 이해하고 있으며 각 파일의 목적과 용도를 이해하고 있다. (views.py, urls.py, models.py)

닥터마틴을 선택한 이유

(TMI 대잔치 주의) 맵시나는 인생을 사는 것이 인생 목표인 나는 어렸을 적부터 닥터마틴이 맵시의 끝판왕이라고 생각했다. 교환학생 시절, 수많은 유럽 국가 중 영국을 뻔질나게 드나들었던 이유도 닥터마틴의 나라 = 본새나는 나라라고 여겼기 때문이다. 그땐 학생이라 돈이 없어서 빈티지 마켓에서 뒷굽이 다 닳은 1461 보라색과 6홀짜리 이름 모를 모델을 사서 소중히 신고 다녔다. 모두가 알듯 닥터마틴은 신발이라고는 믿을 수 없게 짐짝처럼 무거워 위탁수하물 무게를 상당부분 차지했음에도 불구하고 세 켤레를 뉘역뉘역 들고와서 발에 밴드를 덕지덕지 붙인 채 꿋꿋하게 신고다녔다.

한동안 닥태기가 와서 가지고 있던 것을 모두 처분했지만 최근 다시 닥마에 빠졌다. 어찌된 일인지 벌이가 없을 때만 닥터마틴 뽐뿌가 거하게 와서 이번에도 모종의 경로로 저렴하게 두 켤레 구매해서 간간히 신고 다녔다. 한창 닥터마틴 웹사이트에서 세일템이 뭐가 있나 들여다보면서 꽤 정성스레 만든 사이트라고 생각하고 있던 차라 클론하고 싶은 웹사이트로 닥터마틴을 내밀었다. 내 아이디어가 선정될 줄도 몰랐고 프론트 쪽이 많이 화려해보여서 2차 프로젝트로 넘어가겠거니 했는데, 띠용쓰~ 1차로 바로 진행했다. 팔자에도 없던 팀장 꼬리표를 달고.

프로젝트 시작 전

가장 어색했던 건 역시나 다른 사람과 함께 일한다는 점이다. 부트캠프에서 공부를 시작한 후에도 이전과 마찬가지로 면벽수행 마냥 혼자 코드를 쳤기 때문에 누군가와 같이 일하면서 조율하는 게 어색했다. 물론 프로젝트 전에도 같이 코딩 공부를 하긴 했다. 그러나 내 몫의 공부를 하다가 막히는 점을 다른 사람한테 물어보는 것과 프로젝트를 함께 진행하는 건 다르다. 하다못해 나는 키와 값 사이 콜론에 한 칸 간격을 주는데 다른 팀원은 붙여쓴다면 그것마저 불편할 수 있는 게 팀플이다. 살아온 세월만큼 켜켜이 쌓인 고집과 나만의 규칙 때문에 협업을 잘 할 수 있을까 무척 걱정했다. 마치 회사에서 일했던 적이 한 번도 없는 것처럼 협업을 두려워하며 프로젝트를 시작했다.

킥오프 미팅, 그리고 현실 복귀

스크럼, 애자일, 린, 스프린트, 데일리 스탠드업 미팅, 트렐로 등등. 스타트업 여러 곳을 거쳤던 나에게 대부분 익숙한 방법론 혹은 툴이지만 개발자로서 사용(?)하는 것은 처음이라 생각 외로 새로웠다. 게다가 스타트업에 다녔다고 하더라도 주로 브랜드 마케팅을 담당했기 때문에 전통적인 방식으로 일하는 경우가 많았다. (이 집이 까대기 맛집이라면서요?)

start-uht

현실은 코코아보다 더 가혹하다. 문빈같은 신입사원이 없거든.

개발자랑 협업할 때 맛만 보던 바로 그 lean~~하게 일하는 방식으로 프로젝트를 진행하려고 하니 내가 벌써 뭐라도 된 것마냥 심장이 뛰었다. 닥쳐올 고난을 몰랐던 들뜬 우리는 킥오프 미팅 때 엄청난 계획을 세우고 만다.

trello

패기 넘치는 backlog. 마치 1교시를 꽉꽉 채워넣은 신입생 시간표를 보는 것 같다.

다행히 멘토님의 중재로 현실적인 계획을 세울 수 있었다. 1차 프로젝트에서는 인당 1개씩만 구현해도 잘하는 거라고. 프로젝트를 끝내보니 알겠다. 그 말은 100% 맞는 말이다. 나보다 먼저 길을 걸은 사람의 말을 잘 듣자.

실력은 고통의 총합

지난 12일을 한 단어로 표현하자면 ‘고통’이다. 이제 겨우 기역, 니은을 뗐는데 갑자기 수필을 써야한다면 믿어지십니까? 그런데 말입니다, 그것이 실제로 일어났습니다. 크롤링 실습 한 번, 간단한 회원 가입, 로그인 뷰 한 번 짜보고 갑자기 제대로 돌아가는 사이트를 만들라고요? 가능합니까? 그런데 말입니다, 그것이 실제로 일어났습니다.

가장 강한 고통은 장바구니 뷰를 짜는 것이었고 가장 길게 지속된 고통은 모델링이었다. 장바구니 뷰는 생전 처음 해보는 것이고 그나마 실습해봤던 회원 가입, 로그인 뷰를 응용할 수도 없어서 0에서부터 시작하는 것이 너무나 고통스러웠다. 멘토님에게 바로 물어보자니 배울 기회가 사라지는 것 같아서 꾸역꾸역 구글신과 스택오버플로우 선생님과 갓동기님의 문을 두드렸다. 그래도 안 될 때, 그리고 시간이 없을 때 그제서야 멘토님에게 달려갔다. 하지만 너무 고통스러웠다. 진짜로. 다시 하라고 하면 할 수 있겠냐고? 해야지 뭐 어떡하겠어…

모델링은 말하자면 진짜 끝도 없다. 막상 뷰를 짜보니 테이블 구조가 어딘지 이상해서 프로젝트 막바지까지 모델 수정하고 데이터베이스 밀고 업로더 다시 만들어서 데이터 넣는 일을 반복했다. drop database 명령어를 쓴 숫자만큼 티어가 올라간다면 난 최상위 티어임을 자신할 수 있다.

modeling

세상의 시작과 끝. 모델링.

소통, 소통, 소통

이건 좀 억울한 면이 있다. 프론트와 소통하는 게 정말 중요한 건 잘 아는데 처음에는 어떤 식으로 소통해야 하는지도 몰랐다. 데이터를 어떤 식으로 전달해야 하나요 물으면 프론트와 이야기해서 정하시면 됩니다 라는 답변을 받았는데 프론트도 백도 뭘 해본 적이 없어 물음표만 잔뜩 떠다닌 채로 최소 일주일을 보냈다. 시간이 흐르면서 프론트는 프론트대로 목데이터 넣고 콘솔에 찍어보며 이런 식으로 받으면 되겠구나 감을 잡고 백은 백대로 쉘에 찍어보며 감을 잡았다. 이후에는 말모. 리스트형으로 주세요~ 키 이름은 이거로 하겠습니다~ 척하면 척이지. 2차 때는 프론트도 백도 많이 수월하겠지 싶다.

잘한 점

  • 이름값
    우스개로 우리는 닥터마틴이 아니라 닥터막차팀이라고 이야기했는데, 팀원 모두 우직하게 엉덩이 붙이고 앉아서 코드 쳤던 건 동네방네 자랑하고 싶다. 늦은 밤에도 주말에도 도무지 비워질줄 몰랐던 우리의 책상을 잊지 못할 것이다.
  • 웃음
    이건 다른 팀원 덕을 많이 봤다. 지치고 힘든 와중에도 웃음을 잃지 않았고 오히려 힘듦을 웃음으로 승화시켰다. 그것이 자조적인 웃음일지언정 쒹쒹 성 내는 것보단 낫지 않나.
  • 우리 애가 성실하긴 해요
    일간 스탠드업 미팅을 빼먹지 않았다. 프론트가 어떤 일을 하는지 완벽하게 이해하지는 못해도 나와 맞춰볼 팀원이 어느 정도로 일을 진행했는지 파악하기 좋았다.
  • 하면 된다
    아쉬운 점의 되면 한다와 상충하는 항목이지만 하니까 되더라. 백엔드 세 명 다 아무것도 모르는 상태였는데 어쨌든 돌아가는 사이트를 만들어냈다. 끝까지 포기하지 않은 우리가 대견하다.
  • 백지장 맞들기
    업로더를 짜고 데이터베이스에 업로드 하는 건 어쩌면 한 사람이 하는 게 더 빠를 수도 있었다. 워낙 테이블이 복잡하게 얽혀있어 칼로 무 자르듯 일을 나누기 힘들었다. 하지만 어설플지언정 모두가 조금이라도 경험해보고 배우는 것이 프로젝트 취지이므로 사이좋게 나눠서 업로더를 짰다.

아쉬운 점

  • 프리미엄 회원 서비스, 장바구니
    내가 구현한 장바구니는 회원만 이용할 수 있다. 이런 프리미엄 서비스가 다 있다니. 시간이 없어서 비회원 장바구니 기능을 어떻게 구현하는지 구글링조차 못 했는데, 다음 프로젝트에서 또 커머스 사이트를 하게 된다면 그땐 꼭 비회원도 장바구니를 이용할 수 있게 하고 싶다.
  • 지저분한 코드
    자주 하는 말이 있다. 내 코드는 너무 못생겼다고. 이번에는 기능 구현에만 집중하느라 코드 리팩토링 근처에도 못 갔다. 컨벤션만 간신히 맞췄지. 다른 메소드에서 반복되는 코드는 함수로 처리할 수 있을텐데 다음에는 간결한 코드에 대해서도 고민하고 싶다.
  • 되면 한다
    class, prefetch_related() 등 깊이 이해하지 못한 채 쓴 코드가 많다. 쉘에서 쳐봤더니 원하는 데이터가 나와서 그대로 붙여넣기도 했고 남들이 쓰니까 쓰기도 했다. 어떤 함수나 기능을 쓰기 전 장고 공식 문서를 꼭 정독하고 꼭꼭 씹어서 소화하자.
  • 휘발된 기억
    앞만 보고 달리느라 그날그날 힘들었던 점, 해냈던 것을 제대로 정리하지 못했다. 이제 와 정리하려니 벌써 기억이 희미하다. 내 생체 데이터베이스는 저장용량이 크지 않은 모양인지 새로운 지식이 들어오면 기존에 있던 지식이 밀려나버린다. 아주 간단하게라도 매일 데일리 로그를 써야겠다.
  • 완벽해야 한다는 강박
    중간 발표 때 멘탈이 산산조각 났었다. 잘하는 사람은 이미 내가 따라잡을 수 없는 경지에 있다는 생각이 들자 모든 것을 놓아버리고 싶었다. 하나라도 망하면 하기 싫어지는 강박이 나를 집어삼키려 했지만 프로젝트 일정이 너무 빡빡해서 억지로 극복할 수밖에 없었다. 난 우울할 때 코드를 쳐. 다음 프로젝트에서는 멘탈 관리가 최우선이다.

gamsung

gamsung에 취한다

기억하고 싶은 코드

  1. 장바구니 delete 메소드

장바구니 뷰 모든 코드 한 줄 한 줄이 소중하지만 가장 남기고 싶은 것은 delete 메소드다. 다들 금방금방 하셨다길래 post나 get에 비하면 무지 쉽구나! 하면서 느즈막히 구현하기 시작했는데, 쌓인 피로를 고려하지 못한 것이 패착이었다. 결국 발표 전까지 구현하지 못하고 푹 쉬고 온 오늘 마침내 정상 작동하는 메소드를 만들었다.

최종 가격 산출 부분과 오더 테이블 업데이트 부분이 잘 안 돼서 애먹었는데 맑은 정신으로 다시 보니까 후루룩 코드가 풀렸다. 끝까지 물고 늘어지길 잘했다. 칭찬 백 개~

# 로그인 인가
@login_check
def delete(self, request):
  # 프론트에서 쏴주는 토큰 정보로 유저 특정
  user_id = request.user.id
  # 장바구니 안 각 물건은 고유 id값을 가지고 있으므로 삭제하는 물건 id를 프론트로부터 받음
  data    = json.loads(request.body)
  cart_id = data['cartId']
  # 받은 id값에 맞는 데이터를 carts 테이블에서 삭제
  Cart.objects.get(id = cart_id).delete()
  # order status id값이 1이면 '주문 중'(결제 전) 상태
  # order_status_id = 1이라고 표현하는 대신 의미를 알 수 있게 상수로 처리
  # 즉, 아래에서 order_status_id = PENDING 이라고 표현 가능
  PENDING = 1

  try:
    # 로그인한 유저에 해당하는 주문 정보, 카트 정보를 다시 불러옴
    order_id = Order.objects.get(user_id = user_id, order_status_id = PENDING).id
    cart_items = Cart.objects.filter(order_id = order_id).all()

    # 장바구니에 담긴 품목
    product_list = [
      {
        "cartId"            : item.id,
        "productName"       : item.product_color.product.name,
        "productImg"        : item.product_color.detailimage_set.first().image_url,
        "color"             : item.product_color.color.name,
        "size"              : item.size,
        "singleOriginPrice" : item.product_color.product.price * item.quantity,
        "singleSalePrice"   : item.product_color.discount_price * item.quantity,
        "quantity"          : item.quantity,
        "like"              : item.order.user.userproductcolor_set.count()
      }
      for item in cart_items]

    # 최종 가격 산출  
    total_price            = 0
    total_discounted_price = 0
    for item in cart_items:
      total_price            += item.product_color.product.price * item.quantity
      total_discounted_price += (item.product_color.product.price
                                 - item.product_color.discount_price) * item.quantity 
    final_price = total_price - total_discounted_price

    # 품목을 삭제하면서 달리진 주문 정보를 orders 테이블에 업데이트
    Order(
          id              = order_id,   
          user_id         = user_id,
          total_price     = total_price,    
          final_price     = final_price,
          order_status_id = PENDING
         ).save()

    # 딕셔너리 형태로 프론트에 전달. 여러 개일 수 있는 장바구니 안 품목은 딕셔너리를 리스트 안에 넣어서 전달     
    return JsonResponse(
      {
        "products"             : product_list,
        "totalPrice"           : total_price,
        "totalDiscountedPrice" : total_discounted_price,
        "finalPrice"           : final_price
      }, status=200)

  # 예외 처리는 최대한 구체적으로
  except ObjectDoesNotExist:
    return HttpResponse(status=400)
  except ValidationError:
    return HttpResponse(status=400)
  except FieldDoesNotExist:
    return HttpResponse(status=400)
  except KeyError:
    return HttpResponse(status=400)

처음에는 가격 산출 부분을 이렇게 작성했었다.

# 코드 전략
order_id     = Order.objects.get(user_id = user_id, order_status_id = PENDING)
cart_items   = Cart.objects.prefetch_related(
              'order__product_color__product__color_set'
              ).prefetch_related(
              'order__user__userproductcolor_set'
              ).filter(order_id = order_id)
# 코드 중략
total_price            = 0
total_discounted_price = 0
final_price            = 0
for item in cart_items:
  total_price            += item.order.total_price
  total_discounted_price += (item.order.final_price - item.order.total_price)
  final_price            += item.order.final_price
# 코드 후략

문제점이 뭘까. 그것은 final_price가 for문 안에 있다는 점이다. 사실 그것만이 문제는 아니다. 도대체 왜 각 물건 정보가 담긴 carts가 아닌 유저 당 주문 정보가 통으로 담긴 orders 테이블에서 가격을 불러온 다음에 계속 더해줬는지 모르겠다. 피로는 사람을 이상하게 만든다.

아무튼 저 값을 이상한 곳에서 끌어온 게 문제였지 로직 자체가 이상 있는 것은 아니라 다행이었다. 저 로직을 짜는 데에도 애를 먹다가 동기 도움을 받았는데, 사람은 알고리즘 퀴즈를 열심히 풀고 볼 일이다.

  1. prefetch_related() 활용

장바구니 뷰를 짜면서 가장 신경썼던 부분은 역시 prefetch_related()를 잘 활용하는 것이었다. 배웠던 것을 쓰는 것도 의미있지만 도전과제를 하나씩 깨는 맛도 있기 때문에 멘토님이 써보라고 한 기술은 최대한 써보고 싶었다.

order_id = Order.objects.get(user_id = user_id, order_status_id = PENDING).id
cart_items   = Cart.objects.prefetch_related(
              'order__product_color__product__color_set'
              ).prefetch_related(
              'order__user__userproductcolor_set'
              ).filter(order_id = order_id)
  product_list = [
    {
      "cartId"            : item.id,
      "productName"       : item.product_color.product.name,
      "productImg"        : item.product_color.detailimage_set.first().image_url,
      "color"             : item.product_color.color.name,
      "size"              : item.size,
      "singleOriginPrice" : item.product_color.product.price * item.quantity,
      "singleSalePrice"   : item.product_color.discount_price * item.quantity,
      "quantity"          : item.quantity,
      "like"              : DEFAULT_LIKES + item.order.user.userproductcolor_set.count()
    }
  for item in cart_items]

이것보다 더 효율적으로 쓸 수도 있겠지만 며칠 전까지도 몰랐던 방식으로 데이터를 불러올 수 있다는 게 신기했다. 쉘에서 하나씩 찍어보면서 데이터가 출력될 때마다 ‘이게 된다고?’ 싶어서 놀랐다. 아무래도 깊이 이해하고 쓴 것은 아니다보니 다음에 어떤 것을 찍어야 원하는 데이터가 나올지 막힐 때도 많았다. 그럴 땐 탭탭 신공이나 dir 신공을 썼다. 진짜 짱. 만세.

총평

완주한 내가, 우리가 대견하다.