본문 바로가기

dev

[wecode] project 회고록

짧고 길었던 2번의 스프린트가 끝났다.(보통 이런 말들로 회고를 시작하긴 하더라 잘 팔리는 데엔 이유가 있겠지)

 

전혀 새로운 분야에서의 첫 번째 협업 프로젝트였기에 너무 얻은 것이 많은 2주였다

 

프로젝트에 관한 글은 블로그에 올리지도 않았는데 회고록부터 작성하는 게 좀 웃기긴 하지만

블로그에 글 올릴 시간도 아껴가며 프로젝트에 몰두했었다고 작은 합리화를 해볼까 한다 ㅎㅎ

 

나는 골든 서클 이론을 사랑하기 때문에 why?로 시작하는 내용으로 차례차례 글을 써 내려가 보겠다

(사이먼 시넥의 테드 강의로 알게 된 이론인데 오랜 시간이 지난 지금도 내 생각과 언동의 기준이 되고 있다)

혹여 후배 위코더가 이 글을 본다면 프로젝트에 관해 최대한 많은 궁금증을 해소하고 또 얻어가길 바란다


나는 왜 프로젝트에 참여했는가?

 

난 그냥 개발자들의 문화나 그들이 문제에 접근하는 방식이 너무 좋았기에 막연하게 개발자가 되고 싶었었고

그래서 막연하게 공부를 시작했고 목표를 좀 더 구체화하기 위해 부트캠프에 들어왔고

근데 4주가 지나니까 프로젝트를 시키길래 그냥 시켜서 했다-

 

와 같은 이유는 절대 아니긴 하다 (제발 위의 세줄만 읽고 뒤로 가기를 누르는 사람이 없길 바란다)

 

그렇다고 뭐 거창한 이유가 있는 건 아니고.. 그냥 해보고 싶었다

여러 사람들과 함께 협업으로 무엇인가 만드는 게 재밌을 것 같았고

이제 개발자가 된 지 4주밖에 안 된 개린이에겐 너무 값진 경험이 될 것이 확실했기 때문이다

 

그래 그냥 머리와 가슴이 동시에 시켰다

근데 안 할 이유가 없지 않은가?


그럼 어떻게 팀에 기여할 것인가?

 

프로젝트 시작 전 세션에서 Scrum과 Sprint, agile, mvp 등등 프로젝트에서 중요하게 다룰 키워드들에 대해 알게 되었다

 

사실 내겐 단어만 익숙하지 않았지 방식은 익숙했다

내가 성인이 된 이후에 대부분의 일들을 그렇게 처리하려고 노력했으니까

물론 요리와 개발은 문제를 해결하는 도구가 완전히 다르고 환경도 다르기 때문에 차이가 다소 있겠지만!

 

좌우지간, 이번 프로젝트에서 나는 다음의 요소에 집중하기로 했다

  1. Scrum에 대해 이해하고 이행했는가
  2. 왜 소통이 필요한지 이해하고 어떻게 소통할 것인가
  3. Sprint를 잘 지키며 일하는가 sprint마다 mvp 모델이 나오는가
  4. agile 하게 일하는가
  5. 문서화를 잘하는가
  6. 일정관리를 잘하는가

물론 API를 만들고 코드를 이쁘게 짜고 성능을 개선하는 등

개발자가 신경 써야 하는 부분을 게을리하겠다는 건 아니었고

양자택일 해야 하는 순간이 온다면 가급적 프로젝트 전체의 흐름에 도움이 되는 선택지를 택해야겠다고 생각했다

 

내가 무엇을 해야 할지도 해야 할지도 어떻게 해야 할지도 정했으니

이제 무엇을 해야 할지 정할 차례였다


성공적인 프로젝트를 위해 우리는 무엇을 해야 하는가?

잠깐 내가 요리를 했던 시절을 얘기해보자면

주방 인원 나아가서 매장 직원 전원이 1개의 팀으로 일해야 매장이 돌아갈 수 있었고

새로운 메뉴를 개발할 때는 선정된 일부가 개발하고 직원과 손님의 피드백을 받아서 점차 개선해 나가는 형태였다

물론 매장의 환경에 따라 사정은 달랐지만... 난 이미 Scrum 없이 살 수 없는 몸이 되어버렸다

 

일단 나는 이 프로젝트가 1주 단위의 Sprint를 agile 하게 잘 굴리는.... 그러니까 간단히 말해서

Scrum을 기반으로 한 방식으로 얼마나 잘 진행되는가에 집중하기로 했다

 

앞치마를 두르고 칼을 쥘 때와는 도구가 다르지만 방식 자체는 익숙했기에 잘할 자신이 있었고

마침 내가 고른 사이트가 우리 프로젝트의 target site이기도 했으니, PM의 역할을 내가 하는 것이 제일 적절하다 판단했다

 

그래서 일단 Team Notion도 만들고 Trello도 세팅한 뒤 슬랙 채널에 공유했다

노션엔 각종 미팅의 회의록과 참고할만한 레퍼런스, 그리고 ERD 링크와 이미지, target site의 depth 등을 기록했다

팀원들이 회의에 집중할 수 있게 회의록을 내가 작성하고 Trello 관리도 내가 했다

개발에 집중할 수 있게 중간발표 자료나 최종 발표 PPT 도 내가 만들었다

daily meeting 도 내가 주도했고 나름 멘토님에게 미팅 방식에 관하여 칭찬도 받았다(칭 찬 조 아)

다만, 노션과 트렐로를 함께 사용하는 것은 혼선을 줄 수 있으니 편의를 위해 다음부터는 하나만 사용해야겠다

 

물론 내가 프로젝트에 PM의 역할로만 기여하진 않았다

나를 새벽에 재우고 아침에 깨운 건 바로 파이썬, 장고, DB였으니까 ㅎㅎ


Target site : 설로인

https://www.sirloin.co.kr/

 

과정, 구조, 경험의 디자인을 통해 '고기의 기준이 되다'라는 슬로건을 내세우는 브랜드

사료-사육-도축-발골-숙성-가공 전 과정을 설계해서

기존 한우 시장의 파편적 유통구조로 인해 저하된 사용자의 미식 경험을 개선하고 있다

내가 고기를 좋아하기도 하고 요리를 좋아하기도 하고

무엇보다 팀 프로젝트이기 때문에 난이도가 프론트, 백 모두에게 적절한 수준이라고 생각했었다

(내가 프론트엔드 지식이 부족해서 사실 구현하기 어려운 것 투성이라는 것을 안 것은 불과 며칠 뒤였지만 ㅠㅠ)

나열한 이유와 뭐 몇 가지 부가적인 이유와 함께 사이트 후보로 내게 됐고 운이 좋게도 선정돼서 팀이 꾸려졌다

 

 

Project Chickin

사랑하게 된지 10년째인 웹디자이너분이 프로젝트에 쓰라고 만들어주신 로고와 배너..감사합니다 흑흑

 

여러 가지 아이디어가 나왔지만 사이트 이름을 위트 있게 바꾸고 싶었고

설로인의 in을 살려서 어린 닭을 뜻하는 Chick과 합쳐 Chickin이라는 이름으로 프로젝트를 시작했다

소고기를 파는 브랜드에서 한국인의 소울푸드인 닭을 파는 브랜드로 바꿨고

여러 카테고리 중 '구매' 카테고리만 구현하는 것으로 결정했다

 

백엔드는 django를 이용해서 회원가입, 로그인, 상품 리스트, 상품 상세페이지, 장바구니, 구매와 관련한 API를 만들어야 했고

mysql로 database와 table을 관리하고 csv 파일을 이용해 테이블에 데이터를 집어넣어야 했다

프론트엔드와 각 API를 사용해서 원하는 정보를 통신하면 프로젝트 목표 달성이었다

 

필수적으로 [회원가입-로그인-상품리스트페이지-상품상세페이지-옵션선택-장바구니추가-구매]의 필수 구현 사항이 있었고

리스트 페이지의 filter와 sort기능, 장바구니의 수정, 삭제 기능 등은 우선순위가 뒤로 좀 밀렸다


우리는 상품과 옵션의 관계를 다음과 같이 정의하고 싶었다

  1. 한 가지의 상품에 다수의 옵션을 추가할 수 있음
  2. 한 가지의 상품에 2개 이상의 서로 다른 옵션을 한 번에 추가할 수 있음
  3. 한 가지의 옵션은 여러 종류의 상품에 추가될 수 있음

한 상품에 서로 다른 옵션이 여러 개 추가될 수 있어야 하기 때문에

products, carts, order_items에 각각 options와 이어진 중간 테이블을 넣었다

 

크게 구현해야 할 앱은 user, product, cart , order 4개였고

 

그중에서 나는 user와 cart 앱을 담당하게 됐다


users

 

로그인 API

회원가입 API (이메일, 비밀번호 유효성 검사, 비밀번호 암호화)

로그인 데코레이터

 

이상 3개의 기능이 들어가 있어야 했고 난이도는 이미 한번 만들어봤던지라 어렵게 다가오진 않았다

 

그래도 좀 고민해 본 부분이 있다면

# validators.py

import re

def validate_email(email):
    return re.match('\w+@\w+\.\w', email)

def validate_password(password):
    return re.match('(?=.*[a-zA-Z])(?=.*\d)(?=.*[?!@#$%^&*-]).{8,}', password)
# users.views.py

if not validate_email(email):
	return JsonResponse({'message':'INVALID EMAIL'}, status=400)
            
if not validate_password(password):
	return JsonResponse({'message':'INVALID PASSWORD'}, status=400) 
            
if User.objects.filter(email = email).exists():
	return JsonResponse({'message':'E-MAIL ALREADY EXISTED'}, status=400)

views 파일의 if 문이 지금은 3개지만 나중에 조건이 더 추가될 것을 감안하여

validators에 함수를 만들고 관련이 있는 조건문을 함수에 추가시킨 다음

views에서 함수만 호출해서 검사하는 식으로 코드 길이를 좀 줄이고 기능을 좀 더 세분화할 수 있지 않을까 싶었다

 

다만, 고민을 시작한 게 이미 프로젝트 마감 2일 전이라 리팩터링을 하진 않았는데

위코드 과정이 끝나고 취업준비에 들어갈 때 리팩토링을 좀 진행해보면 어떨까 싶다

 

그리고 정규표현식에 대해 좀 더 공부해야겠다는 생각도 들었다

편리한 기능인데 너무 짧게 구성하는 것에만 집중한 나머지 놓치는 부분이 많았다


cart 

 

장바구니 조회(GET), 추가(POST), 수정(PATCH), 삭제(DELETE) 기능

 

다중 옵션을 처리해야 하는 것이 진짜 어려웠던 부분

이번 프로젝트에서 cart view가 제일 시간을 많이 잡아먹었고

제일 다양하고 깊은 고민을 하게 만들었다

 

다 어려웠지만 한 가지 고민을 제일 많이 했던 건 POST 부분이었다

# 수정하기 전 carts.views.py

def post(self, request):
        try:
            data         = json.loads(request.body)
            user         = request.user
            product_id   = data['product_id']
            quantity     = int(data['quantity'])

            if quantity < 1:
                return JsonResponse({'message': 'INVALID_QUANTITY'}, status=400)
            
            cart, created_cart = Cart.objects.get_or_create(
                user_id    = user.id,
                product_id = product_id,
                defaults   = {"quantity" : quantity},
            )
            
            cart_get = Cart.objects.get(user_id = user.id)
            options  = Option.objects.getlist(user_id = request.user.id)
            
            cartoption, created_option = CartOption.objects.get_or_create(
            	cart_id    = cart_get.id,
            	option_id  = [{
              		'option_id' : option.id
           		}for option in options]
            	)
           	 
            if created_cart and created_option:
                cart.quantity = quantity
            else:
                cart.quantity += quantity
            
            cart.save()
            cartoption.save()

            return JsonResponse({"message" : "SUCCESS"}, status=201) 
        except KeyError:
            return JsonResponse({"message" : "KEY ERROR"}, status=400)

get_or_create를 사용해서 새로운 객체를 생성하거나  그게 아니라면 Flase값을 반환받아

if문을 사용해서 이미 있는 장바구니 목록의 수량에 추가만 해주는 방식을 택했는데 치명적인 문제가 있었다

 

이렇게 짜게 되면 상품의 이름과 구매자만 같고 옵션은 다른 상품을 걸러내지 못한다....

 

get_or_create를 발견하고 이거다! 싶어서 신나게 써 내려갔지만 우리 사이트 기획에는 맞지 않는 방식이었다

# 수정 후 carts.views.py

def post(self, request):
        try:
            data       = json.loads(request.body)
            user       = request.user
            product_id = data['product_id']
            option_ids = data['option_ids'] 
            options    = Option.objects.filter(id__in=option_ids)
            quantity   = int(data['quantity'])
            is_added   = False

            if quantity < 1:
                return JsonResponse({'message': 'INVALID_QUANTITY'}, status=400)
            
            with transaction.atomic():
                for cart in Cart.objects.filter(product_id=product_id, user=user):
                    cart_options = Option.objects.filter(cartoption__cart=cart)
                    
                    if set(cart_options) == set(options):
                        is_added = True
                        break
                    
                if is_added:
                    cart.quantity += quantity
                    cart.save()
                    return JsonResponse({"message" : "SUCCESS"}, status=200)
                
                cart = Cart.objects.create(
                    user_id    = user.id,
                    product_id = product_id,
                    quantity   = quantity,
                )
                
                cart_options =[CartOption(
                    cart_id   = cart.id,
                    option_id = option.id,
                )for option in options]
                
                CartOption.objects.bulk_create(cart_options)

            return JsonResponse({"message" : "SUCCESS"}, status=201) 
        except KeyError:
            return JsonResponse({"message" : "KEY ERROR"}, status=400)

수정한 코드

성능은 조금 떨어지지만 전체 탐색을 사용해서 기능 구현하고 bulk_create를 사용해보라는 멘토링을 받았다

멘토님이 알려주신 코드를 이해한 뒤에 뒤이어 마무리를 했고 성공적으로 bulk_create를 적용했고

 

 쿼리셋끼리 비교하면 뜨는 오류는 쿼리셋을 set으로 감싸면 비교할 수 있다고 배울 수 있었다

 

추가적으로 transaction도 걸어서 원자성과 일관성을 부여했다!

이렇게 코드를 수정하니 flow를 이해하기도 쉽고 원하는 다중 옵션을 처리할 수 있게 되었다

 

    @login_required
    def delete(self, request):
        try:
            user     = request.user
            cart_ids = request.GET.get('cart_ids').split(',')
            carts    = Cart.objects.filter(id__in=cart_ids, user=user)
            
            if not carts:
                return JsonResponse({"message" : "CARTS DOES NOT EXIST"}, status=404)
            
            carts.delete()
            
            return JsonResponse({"message" : "SUCCESS"}, status=200)
        except Cart.DoesNotExist:
            return JsonResponse({"message" : "NOT FOUND"}, status=404)

장바구니 삭제 기능은 실제로 코드를 치는 시간은 길지 않았다

다만, 조금 고민이 있었던 건 어느 정도 수준까지 만드냐였다

 

  • 상품의 개별 삭제만 가능한 기획
  • 상품의 전체 삭제만 가능한 기획
  • 상품의 선택 삭제가 가능한 기획

 

이때 프로트엔드 담당 팀원들이 일정을 소화하는 데에 엄청 힘들어하고 있었기에

추가 구현 사항인 삭제 기능을 위해 체크박스까지 구현하라는 부담을 주고 싶진 않았다

하지만 3번째 기획은 나머지 2개의 기획도 처리할 수 있으므로 3번째 기획으로 확장성을 고려하고

프론트 단에서는 할 수 있는 만큼의 기능만 구현해달라 부탁했다

 

처음에는 request의 body에서 정보를 가져오기로 했는데

delete 메소드에는 body가 없다는 충격적인 정보를 듣고 path parameter를 이용하라는 멘토링을 받았다

기껏 path parameter를 적용했는데 다수의 cart_id를 받아오는 방식에는 부적절하다는 판단으로

Query string을 이용해서 받은 1,2,3,4,5 형태의 cart_id 문자열을 split 하는 방법으로 해결했다


프로젝트를 마치며

팀보다 위대한 개인은 없다

 

우리 팀 노션 페이지 맨 위에 내가 써놓은 글이다

가장 좋은 동료는 가장 좋은 스승이 될 수 있다

우리는 우리의 멘토고 우리의 멘티다

 

누군가는 결과물에 만족하지 않을 수도

누군가는 내가 민폐를 끼치고 있다고 생각할 수도

누군가는 남과 나의 결과물과 성취도를 비교하며 절망할 수도 있다

 

남과 비교하지 않는 사람이 있기에 비교하는 사람이 있을 수 있다

비교는 절대 나쁜 것이 아니지만 우리는 그 비교를 하나의 도구로써 사용해야 한다

 

나보다 뒤처진 사람과 비교하면 오만해지고

나와 같은 사람과 비교하면 나태해지고

나보다 앞서 나가는 사람과 비교하면 비참해진다

 

나와 남을 힐난하고 채찍질하는 도구로 비교를 이용하지 않았으면 한다

내가 발전할 수 있는 초석으로 내가 노력할 수 있는 자원으로 이용했으면 한다

 

나는 wecode에 '개발자 커리어'라는 이름의 건물을 짓기 위해 들어왔다

그중에서도 설계도를 그리러 온 것이다

 

건물은 얼마나 빨리 짓느냐가 중요한 것이 아니다

얼마나 튼튼하고 안전하게 지었냐가 중요하다

빨리 짓는 것에만 몰두한 건물은 머지않아 금이 가고 무너질 것이다

튼튼한 개발자 커리어를 쌓기 위해 설계도를 조급하게 그릴 필요는 없다


나의 가장 좋은 스승들에게

발표 PPT에 적어놓았던 롤링페이퍼

어느 하나 뾰족한 사람 없이 둥글둥글한 모습들로 힘들 텐데도 프로젝트에 임해주신 여러분들께 정말 감사드리고 싶습니다!

 

사실 제가 이끌어 갔던 Scrum이나 회의의 진행방식이 모두 마음에 찰 수는 없다고 생각하지만

세상에 완벽한 사람은 없고 완벽하지 않은 사람들이 보여 완벽한 것을 만드는 법 아니겠어요?

마음에 안 들고 화가 나는 순간에도 내색하지 않고 묵묵히 함께 해준 여러분들에게 너무 감사하고 있습니다

 

제 개발자 커리어 첫 번째 프로젝트 팀이 여러분들이라 너무 다행스럽고 영광이었습니다

월요일이면 각자 또 다른 프로젝트로 다른 곳에서 다른 분들과 협업하시겠지만

저는 그 누구 하나 가서 뒤쳐지거나 분쟁을 일으킬 거라고 생각하지 않습니다

 

남은 주말 정리 잘하시면서 심신을 좀 챙기시고

월요일부터 다시 즐거운 개발 인생을 함께 하길 바랍니다!