생애 첫 팀 프로젝트 돌아보기

Chris-Yang·2021년 10월 16일

Project

목록 보기
4/4

> 프로젝트 준비

▶︎ 프로젝트 대상 선정과 목표 설정

목표는 이케아 웹페이지 클론이었습니다.

클론 대상을 이케아로 결정한 이유는 다음과 같습니다.

  1. 기본적인 회원가입/로그인, 단일제품 정보 로딩, 단순한 css 등 비교적 난이도가 낮은
    구현을 목표로 시작하여 필요 시 해당 홈페이지가 지닌 더욱 난이도 있는 기술들을
    추가구현할 수 있음.
  2. FE 측면에서 hover, 이미지 modal / Carousel, 좋아요 표시, 더보기 버튼을
    통한 추가 데이터로딩, popup navigator 등 다양한 기술들이 예시되어
    구현되고 있음.
  3. BE 측에서도 DB관련 모든 관계설정을 다룰 수 있고, 검색기능과 경우에 따라
    구분되는 다수의 제품 정보 로딩 등을 통해 조금 더 심화 과정의 API를
    구축해 볼 수 있을 것 같음.
  4. 다뤄지는 기술이 다양하고 거의 모든 핵심기능들이 있기 때문에 1차 난이도와
    2차 난이도를 나눠 프로젝트를 진행해도 좋을 것 같음.

그리고 구현 목표를 필수구현과 추가구현 사항으로 분류했습니다.

✔️ 필수구현:

  1. 로그인, 로그아웃, 회원가입 기능
  2. 상품 목록 (+ 필터)
  3. 상품 상세페이지
  4. 장바구니 기능
  5. 검색 및 카테고리 분류 기능

✔️ 추가구현:

  1. 마이페이지 기능
  2. 디지털 쇼룸 기능
  3. 배송 조회 기능
  4. 결제 기능
  5. 상품평 작성 기능


▶︎ 팀 구성과 나의 역할

팀원은 FE 3명, BE 2명 총 5명으로 저는 BE 포지션에 포함되어 있습니다.

개발기간은 2021.10.05 ~ 15일로 주말 포함 11일이었습니다.

BE에서 담당한 공동작업은 크게 DataBase 설계/ 데이터 수집/ 데이터 삽입이었으며
저는 회원가입, 로그인, 상품 필터, 상품 검색 부분을 담당하였습니다.


▶︎ 첫 팀 프로젝트에 대한 마음가짐

저의 부사관 복무, 부동산업에 대한 경험은 직접적으로 개발과는 상관이 없지만
사람이 사람과 엮여있고 한사람의 독단적인 행동이 전체 분위기를 어지럽힐 수 있다는
무시할 수 없는 공통점이 있다음을 상기했습니다.

이기심은 누구에게나 있지만 누군가 그 정도가 과했을때 집단의 목표는
늘 결과가 좋지 못하게 됨을 알기 때문에 주장보다는
배려를 우선시 하기로 정했습니다.

따라서 우선 나 자신부터 지치거나 껄끄러움이 있을 경우에도 유머와 긍정을 잃지 말고
정 못참겠으면 숨어서 울자라는 마음을 세팅하고 프로젝트에 뛰어들었습니다.
(너무너무너무 힘들때면 마음의 안식처인 분들에게 달려가즈아ㅏㅏ!!)

이미지에 '파도가 어떻게 널 치던지 그냥 서있어라'는 표어는 프로젝트를 끝내고 난
소감에 개인적으로는 가장 어울리는 문장이 아닌가 하는 생각이 듭니다..





> 개발환경

▶︎ 공동작업 도구

✔️ Trello:
트렐로는 주제가 있는 박스에 카드를 만들고 진행사항에 따라 알맞는 주제의 박스로
카드를 옮기는 방식의 단순한 도구이며 하나의 공간에서 여럿이 함께 작업할 수 있습니다.

단순하지만 프로젝트의 진척도 확인만이 아닌 그 자체가 의사소통의 역할도 해주는
신박하고 멋진 도구였다고 생각합니다.

아이언맨의 자비스를 만든다던가 하는 큰 꿈이 아닌 트렐로나 퀴즐렛같은
작은 아이디어로 세상에 널리 유익한 무언가를 만드는게 목적이기에
개인적으로 정말 인상깊은 도구였습니다.

https://trello.com



✔️ google docs:
데이터베이스에 넣기 위해 데이터를 수집해 놓을 csv파일을 지원하는 도구가 필요했는데
팀원 모두 강제로 엑셀을 구매하는 것은 최선이 아니었기에 선택한 친구입니다.

https://docs.google.com



✔️ erdcloud:
이알디클라우드는 투박하지만 아주 손쉽게 ERD를 만들수 있는 도구입니다.

보통 many to one과 같은 관계설정 라인이 그냥 선으로 되어있다던가 삼지창 뿐인데
디테일하게 관계표시를 할 수 있고 공동작업도 가능하며 채팅, 메모까지 되는
제가 아는 가장 좋은 무료 ERD었고 감사하게 사용하였습니다.

https://www.erdcloud.com



✔️ git&github:
설명이 필요한 도구인가 싶습니다.

왜이리 널리 사용되는지 알게 되는가 동시에 주변에서 종종 터져나오는 탄성때문에
예전에 엘리라는 배테랑 개발자분조차 시니어임에도 실수해서 프로젝트를
다 날리셨었다는 말씀이 자꾸 떠올라 늘 두려움으로 push했었습니다.



▶︎ 개인 도구

✔️ Python:
파이썬을 배운지 얼마 되지 않아서 기본적인 문법만 사용하였었는데도
어느정도 프로젝트를 진행할 수 있어서 일단 감사했습니다.

지인 개발자분께 부탁해서 똑같은 코드를 한 번 자바스크립트로
만들어줄 수 있냐고 부탁해서 결과물을 본 후 더 감사하기도 했었습니다.

쉬운 문법으로 복잡한 연산을 해주시는 파이썬님 고맙습니다.


✔️ Django:
장고를 처음 접했을때는... SQL row query도 잘 모르는데 장고의 ORM을
익히면서 그와 대응되는 query까지 알아야한다는 상황이 당황스러웠었습니다..

여러가지 사정으로 API를 만들 수 있는 날이 2일밖에 없었을 때
참고자료를 찾아보며 나오는 코드를 해석할 수 없었던 1등공신이면서도
사용법을 익히고 알맞게 사용했을 때 그 감사함은 조울증 체험을 하기 좋았습니다.


✔️ 몸뚱이:

어쨌거나 저 도구들을 모두 사용할 수 있었던 것은 육신이 있기 때문이었습니다.

원래 체중 50키로 중반에서 70키로 중반으로 만들어준 운동을 개발을 접하고
거의 하지 못해 나약해져 있었기에 평균수면 세시간은 지옥같았습니다.

예전에 봤던 영화 imitation game에서 튜링님이 조깅을 왜그렇게 열심히
하셨는지 체감할 수 있는 부분이었습니다.

몸뚱이는 도구들의 도구이므로 건강한 코딩을 위해 꼭 건강을 잘 챙겨야 하겠습니다.





> 구현 내용

▶︎ 회원가입

✔️ 데이터 통신과 유효성검사:
기획의도에 부합하는 적절한 정규표현식을 통해 입력받은 데이터에 대한 유효성 검사로
에러가 어떠한 사유로 발생하였는지 FE측에게 명확히 제시하면서 합의된 정상적인
데이터가 서버에 저장될 수 있도록 하였습니다.

이와 관련하여 테스트를 하며 정말 많은 시행착오가 있었는데, date 등
데이터베이스에 입력되는 데이터타입/형식에 대해 미숙한 상태임을 파악했고
httpie에서는 int type으로 데이터를 전송해서 성공했는데 postman에서는
str type으로 전송해야하는 등 수많은 돌발상황들이 많이 발생하는 진귀한
경험을 했었습니다.


✔️ 보안:
유저 비밀번호에 bcrypt를 적용하여 암호화된 상태로 서버에 저장/통신함으로써
유저정보 유출에 대한 취약성을 개선하고 개인정보보호 법률을 준수하였습니다.

정말 간단하게 복잡한 암호화/복호화를 해보며 라이브러리의
감사함을 느낄 수 있었습니다.


✔️ 트랜잭션:
설계상 유저정보 생성 시 두개의 테이블에 각각 데이터를 동시에
저장해야 했음으로 데이터베이스 트랜잭션에 대한 이해가 필요했었습니다.

장고의 트랜잭션 관리 도구를 통해 에러처리 및 commit위치 지정 등을
구현하려 했으나 현재의 단순한 로직에서는 전혀 필요치 않다고 판단하여 관련
데코레이터 하나만 적용하게되었습니다.

테스트시 고의적으로 에러를 발생시킴으로 트랜잭션 처리되어 테이블이 생기다 말아서
데이터베이스에 남는 흔적인 ‘해당 row 없는 상태(예: id 1, 2, 4)’을 보며
단전에서부터 올라오는 희열을 느꼈습니다.

개념만 알고 있던 트랜잭션을 경험해보고 싶었는데 우연치않게 다뤄볼 수 있었고
그과정에서 비록 관련내용의 이해보다 구현의 난이도가 월등히 낮아 코드 자체는
화려하거나 복잡하지는 않은 모양이지만 다시 관련 내용을 마주했을 때 바로
해결책으로 떠올릴 수 있을거라는 자신감과 대응능력을 습득한 귀한
경험이었다고 생각합니다.


import json
import re
import bcrypt
import jwt

from django.http  import JsonResponse
from django.db    import transaction
from django.views import View
from django.conf.global_settings import SECRET_KEY

from user.models import User, Address

class SignUp(View):
    @transaction.atomic
    def post(self, request):
        data              = json.loads(request.body)
        REGX_EMAIL        = '^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
        REGX_PASSWORD     = '^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$'
        REGX_BIRTHDAY     = "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
        REGX_MOBILE_PHONE = '\d{10,11}$'
        REGX_ZIP_CODE     = '\d{5}$'

        try:
            if User.objects.filter(email=data['email']).exists():
                return JsonResponse({'message': 'EXIST_EMAIL'}, status=400)

            if not re.match(REGX_EMAIL, data['email']):
                return JsonResponse({'message': 'INVALID_EMAIL_FORM'}, status=400)

            if not re.match(REGX_PASSWORD, data['password']):
                return JsonResponse({'message': 'INVALID_PASSWORD_FORM'}, status=400)

            if not re.match(REGX_MOBILE_PHONE, data['mobile_phone']):
                return JsonResponse({'message': 'INVALID_MOBILE_PHONE_FORM'}, status=400)

            if not re.match(REGX_BIRTHDAY, data['birthday']):
                return JsonResponse({'message': 'INVALID_BIRTHDAY_FORM'}, status=400)

            if not re.match(REGX_ZIP_CODE, data['zip_code']):
                return JsonResponse({'message': 'INVALID_ZIP_CODE_FORM'}, status=400)

            password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
            
            create_user = User(
                last_name      = data['last_name'],
                first_name     = data['first_name'],
                gender         = data['gender'],
                email          = data['email'],
                password       = password,
                mobile_phone   = data['mobile_phone'],
                favorite_store = data['favorite_store'],
                birthday       = data['birthday']
            )
            
            create_user.save()

            create_adderss = Address(
                user_id         = create_user.id,
                name_of_street  = data['name_of_street'],
                detail_address  = data['detail_address'],
                zip_code        = data['zip_code'],
                default_address = data['default_address'],
            )
            
            create_adderss.save()

            return JsonResponse({'message': 'SUCCESS'}, status=201)
        
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)


▶︎ 상품 목록 sorting / searching

✔️ sort, search:
특정 카테고리의 제품들만 출현하는 페이지를 불러오고 그 안에서 가격,
이름 순과 같은 조건에 의해 제품들을 재정렬하는 기능을 구현하였습니다.


✔️ query parameter:
query prameter가 무엇인지 직접 다뤄보며 새로운 경험을 할 수 있었고
URI만으로 데이터를 다룰 수 있다는 점이 인상깊었습니다.

쿼리로 받은 데이터가 힌트가 되어 내가 만든 로직을 통해 데이터베이스에서
필요한 데이터를 꺼내오고 그것을 요리하여 FE측에서 원하는 정보를 만들어 다시
되돌려준다는 전개가 낮설었고 FE와 데이터의 구조를 맞춰야 한다는 점 등으로
고난의 행군이 계속되었었습니다.

개인적으로 일정조절 실패와 함께 장고 ORM과 장고 특유의 자료형인
QuerySet에 대한 이해가 부족했기에 뇌절과 탈모의 원인이 되었던 부분입니다.

** sort 기능은 발표 2시간 전 구현하여 시연만 하였고, search 기능은
ㅤ발표 직전 구현하여 둘 다 merge시키지 않았습니다.
ㅤㅤ


from django.http                  import JsonResponse
from django.db.models.query_utils import Q

from django.views import View

from product.models import MainCategory, Product

class SortItem(View):
    def get(self, request):
        sub_category_products = request.GET.get('products_list')
        sort                  = request.GET.get('sort')
        search                = request.GET.get('search')

        if not sub_category_products and not sort and not search:
            return JsonResponse({"message": "fuck"}, status=400)
        sort_set = {
            'best'            : 'id',
            'price_ascending': 'price',
            'price_descending': '-price',
            'name_ascending'   : 'korea_name',
            'name_descending'  : '-korea_name',
            'created_ascending': 'created_at',
            'created_decending': '-created_at',
        }

        check_products = Q()

        if sub_category_products:
            check_products.add(Q(sub_category=sub_category_products), Q.AND)

        if search:
            check_products.add(Q(korea_name__icontains=search)\ 
                |Q(sub_category__name__icontains=search), Q.AND)

        products = Product.objects.filter(check_products).order_by(sort_set.get(sort, 'id'))

        results = [{
            'product_id'  : product.id,
            'korea_name'  : product.korea_name,
            'foreign_name': product.foreign_name,
            'price'       : product.price,
            'information' : product.information,
            'is_deleted'  : product.is_deleted,
            'sizes' : [
                {
                    "id"    : size.id,
                    "width" : size.width,
                    "length": size.length
                } for size in product.product_sizes.all()
            ],
            'images': [
                {
                    "product_image": product_images.product_image,
                } for product_images in product.product_images.all()
            ] 
        } for product in products]
            
        return JsonResponse({'results': results }, status = 200)




> 반성과 아쉬움..

▶︎ Agile process를 매우 실천하지 못함.

10일정도의 프로젝트 기간 중 6일을 ERD 하나 PR하는데 소모하였습니다.

돌이켜 생각해보니 어느정도 틀이 잡혔을 때 PR을 보내고 큰 변화가 없을만한
API를 구현하며 ERD를 조금씩 수정해나갔어야 했는데 정말 너무 후회되고
이후 목표했던 API들을 구현하는데 큰 치명타가 되었습니다.

또한 그렇게 공을 들였었음에도 수정은 역시나 빈번히 발생했었습니다.

한번의 완벽함을 추구하기 보다는 빠르게 피드백을 받으며 진행했더라면
이후 갑작스레 한번에 찾아온 API구현 과정에서의 에로사항들 역시 나누어
발생했을 것이기에 압박감이 훨씬 덜했고 프로젝트의 완성도도 나았을 것이라
생각이 됩니다.



▶︎ 마인드컨트롤

ERD 설계/작성에 6일, 필요 데이터 수집 및 데이터베이스 삽입에 1일,
그리고FE와의 통신테스트 과정에서 10분이면 끝날 작업을 서로의 미흡함으로 인해
예상치 못하게 소모된 1일을 포함하여 2일의 시간만이 API구현에 주어졌을 때
남은건 절망뿐이었습니다.

틈틈히 장고 ORM과 구현 예정인 API와 관련된 자료들을 눈으로 학습했지만
백문이 불허일타라고 막상 코드한줄 의도한대로 쳐지지 않았습니다.

또한 애자일의 실패로 API설계와 관련된 피드백을 받아보지도 못해 이해도가
떨어져 한개의 로직에 담을 수 있는 API도 쪼개서 분담해 구현하게 되었습니다.

그러다보니 중복된 내용이 각자의 API에 녹아있게 되기도 하였습니다.

이 상황이 너무 괴로웠고 정신적인 압박에 의해 지쳐보이는 나의 상태가
적어도 팀 분위기에 긍정적인 도움은 주지 못했을 것이라는 생각이 듭니다.

그때와 같은 스트레스를 받지 않았어도 결과물이 크게 좋아졌을거라고는
생각되지 않기에 가장 큰 후회로 남았습니다.

과오를 받아들이며 전방에 힘찬함성!! 흐아아아앙!!!!!😭🤤

profile
sharing all the world

0개의 댓글