All Articles

직방 웹사이트 클론 프로젝트 회고

프로젝트 요약

  • 프로젝트명 : 직방 프로젝트
  • 프로젝트 소개 : 대한민국 대표 주거 정보 웹사이트 직방 클론
  • 팀명 : 죽방
  • 기간 : 7/6(월) ~ 7/17(금), 12일
  • 인원 : 프론트엔드 3명, 백엔드 3명
  • 역할 : 백엔드
  • 담당 업무

    • 장고 프로젝트 초기 세팅
    • API 이용해 데이터 크롤링
    • MySQL DB 구축
    • 데이터베이스 업로더 작성
    • 모델링
    • 매물 리스트 기능 구현
    • 매물 상세 정보 기능 구현
    • 검색 기능 구현
    • Docker와 AWS EC2를 이용해 사이트 배포
    • AWS RDS에 DB 세팅
    • Git rebase로 버전 관리
    • Unit test
  • 사용 기술

    • Python
    • Django
    • MySQL
    • CORS headers
    • Git
    • AWS EC2, RDS
    • Docker
  • 백엔드 GitHub / 프론트엔드 GitHub
  • POSTMAN API 문서
  • 데모 영상

프로젝트 목적

  • 스크럼 등 실무에서 사용하는 개발 방법론을 통해 협업 방식을 익힌다.
  • 데이터 양이 많은 서비스를 클론하면서 데이터를 다루는 방식을 익힌다.
  • 위치 정보(좌표) 기반 기능, 필터링, 검색 등 타겟 사이트 핵심 기능을 구현하며 백엔드 개발자로서 역량을 키운다.

무주택 세대원의 여정

1차 프로젝트에서 커머스 사이트를 클론해봤으니 2차에는 커머스가 아닌 서비스를 구현해보고 싶었다. 내가 투표한 사이트 팀에 배정될 확률이 높았다고는 해도, 사이트 20개에 투표했기 때문에 어느 팀으로 배정될지 예상할 수 없었다. 운이 좋게도 위치 기반 기능 구현이 가능하고 + 다뤄야 할 데이터 양이 많은 직방팀에 배정되었다.

단순하게 ‘오 지도 좋은디’, 그리고 ‘오 커머스 사이트 아니라 좋은디’라고 생각했으나 30년 넘게 무주택 세대’원’으로 산 데다가 자취 경력 2년 중 1개월은 학교 앞 고시원, 나머지 기간은 변방의 외국인1로 지낸 터라 직방 사이트 구조나 규모를 잘 모르고 한 소리였다. 이내 모델링에게 두들겨맞고 누더기가 되었다.

안녕하세요 백엔드소년단의 모델링몬스터입니다

modeling

직방은 겁나 덩치가 크고, 크며, 또한 커다란 몬스터 같은 느낌이다. 부동산 서비스의 신뢰도는 제공하는 정보 양에 비례하기 때문에 이런 것까지 알려준다고? 싶은 것까지 알려줘야 한다. 제한시간 12일짜리 프로젝트라 원룸을 제외한 아파트, 오피스텔 매물 데이터에 맞는 모델링을 하기로 했다. 프로젝트 전 실습에서도, 1차 프로젝트에서도 나름 복잡한 모델링을 해봤기 때문에 이번에는 개미 눈물만한 자신감을 가지고 모델링을 시작

rip-image

그렇다. 내 얘기다.

…했으나, 엄청난 양에 압도당했다. 한 테이블을 다 했다고 생각하고 넘어가면 추가해야 할 필드가 어디선가 툭툭 튀어나와서 다시 돌아가고 다시 돌아가고. 현역 개발자분에 따르면 실제 회사에서는 최소 기능에 관한 모델링으로 시작해서 기능이 하나씩 추가될 때마다 테이블을 추가한다고 한다. 따라서 한 번에 엄청난 양을 모델링 하는 일은 잘 없을 거라고 하는데, 이것이 나의 마지막 희망이다. 모델링. 정말 힘들었다.

크롤링 대참사

사실 아파트와 오피스텔을 타겟으로 모델링을 짠 이유가 있다. 처음에는 아파트를 하기로 프론트와 이야기하고 약 이틀 동안 모델링을 끝냈다. 하지만 크롤링을 시작하면서 설마 했던 난관을 마주했다. 다행스럽게도 어느 카테고리든 api가 있었지만 그곳에서 긁을 수 있는 아파트 관련 데이터는 몹시 적어서 상세 페이지로 직접 들어가서 크롤링을 해야했다. 그런데 각 페이지에서 긁어야 할 정보의 class 이름이 불규칙하고 추상적이어서 손크롤링을 할 수밖에 없는 상황이었다. 이건 아니지. 물론 하면 어떻게든 할 수는 있었겠지만(하면 된다) 이번에도 한 주 내내 크롤링과 db 업로드를 하고 있을 수 없었다.

갈팡질팡 하던 와중 원룸과 오피스텔 쪽 api가 아주 깔끔하게 된 것을 보고 긴급 팀회의에 들어갔다. 프론트에서 이미 어느 정도 작업을 진행한 아파트 카테고리와 유사한 것이 원룸보다는 오피스텔이라 오피스텔을 다루기로 결정했다.

유닛 테스트는 인내심 테스트라는 것이 학계의 정설

이번 프로젝트 복병은 유닛 테스트였다. 우리 팀은 모델링 때문에 다른 팀보다 뷰를 늦게 짜기 시작해서, 다들 곡소리를 내며 괴로워하는 이유도 뒤늦게 알았다. 특히 리턴할 정보가 가장 많은 매물 상세 뷰를 내가 맡았기 때문에 인생 첫 유닛 테스트부터 아주 고역이었다. 사람이 성장하려면 끊임없이 자기를 comfort zone 밖으로 밀어내야 한다고 하는데, 개발 병아리인 나에게 유닛 테스트란 stretch zone을 넘어선 panic zone이었다.

유닛 테스트 강의 영상을 계속 돌려봐도 내 코드에 적용시키려니 여전히 어려웠지만 일단 해보자는 마음으로 한 자 한 자 써내려갔다. 마시면서 배우고 해보면서 배우고 뭐 그러는 거지… 유닛 테스트를 팀에서 가장 먼저 시작하기도 했고 매물 상세 정보가 가입 관련 뷰를 제외하고는 어느 뷰에서나 쓰는 데이터라 나 하나 고생하면 다른 팀원이 그나마 편하게 유닛 테스트를 할 수 있어서 뿌듯했다. 그리고 말도 안 되는 목데이터를 넣는 재미도 있었다. 부동산 소개 문구를 ’기다렸습니다. 제대로 모시겠습니다.‘라고 한다든지.

unit-test

유닛 테스트 세계관에는 18평 포룸 깔세 매물이 존재한다.

걸어서 광기 속으로

1차 프로젝트에서는 중간 발표 때 멘탈 와르르맨션이었다면 2차에서는 최종 발표 때 조금 위기가 왔다. 멘탈붕괴까지는 아니고 멘탈휘청 정도? 이번 프로젝트에서도 할 수 있는 만큼 최선을 다했으나 모르는 것, 알아야 할 것이 산더미라는 것을 다시 느꼈다. 사실 그동안 자만했는지도 모른다. 그러니까 충격을 받은 거겠지. 역시 어설프게 아는 놈이 제일 설친다더니 어른들 말이 딱이다. 많이 공부하고 많이 배우자.

crazy

이제 돌이킬 수 없다. 난 코딩 광인이ㄷr.

잘한 점

  • 근성
    이번 프로젝트에서 꼭 도커를 사용해보고 싶었으나 마지막 날 수정해야 할 게 생각보다 많이 생기는 바람에 AWS 단계까지 가지도 못했다. 결국 발표 전까지 나의 작고 사랑스러운 고래를 띄우지 못해서 다른 팀원이 미리 작업해놓은 도커를 사용했다. 뒷맛이 영 아쉬워서 발표 마치고 뒷풀이 하고 다시 돌아와서 AWS부터 차근차근 해보고 도커로 배포하는 것까지 성공했다. 이렇게 공부했으면 하버드도 갔을텐데.
  • 코드 리뷰
    지난 프로젝트는 일단 굴러가는 것을 만드는 데에 초점을 맞췄다면 이번에는 코드를 알고 쓰는 것이 목표였다. 그래서 오늘 백엔드 팀원끼리 모여서 각자 쓴 코드를 설명하고 모르는 것을 물어보고 개선할 수 있는 것은 수정하기도 했다. 프로젝트가 끝났다고 끝이 아니라 그 코드를 밑거름으로 무럭무럭 클 거니까 되짚어보는 시간이 꼭 필요했다.
  • 기록
    이번에는 꼬박꼬박 데일리로그를 썼다. 12일 동안 고군분투 했던 기억이 생생하게 떠오른다. 나중에 읽어보면 정말 재미있겠지?
  • 절대적인 시간 투자
    막차 시간까지 코드를 쳤어도 시간이 부족했다. 매일 급하게 가방을 챙기면서 코로나만 아니었어도 더 늦게까지 지하철이 있었을텐데!!! 하면서 아쉬워했지. 차 사고싶다. (갑자기요?)

아쉬운 점

  • 체력
    부트캠프 시작 이후 약 두 달 간 새벽 1시 반~2시에 자서 7시에 일어나는 생활을 반복하니 2차 프로젝트부터 기력이 급격하게 떨어졌다. 해야할 것도, 하고싶은 것도 많아서 고농축 비타민을 먹으며 정말 겨우 연명했다. 이제 시간 여유가 나면 다시 운동을 시작해야겠다.
  • 코드 리팩토링
    ‘잘 쓴 코드’란 무엇인지 항상 고민한다. 문제는 고민만 한다는 것이지만… 리팩토링 책을 사서 봐야겠다.

기억하고 싶은 코드

  1. 검색 기능

아주 기본적인 검색 기능이지만 여태까지 구현했던 기능 중 가장 낯설었던 것이라 기억하고 싶다. 그리고 멘토님 도움을 받아 코드 리팩토링도 조금 해봤는데, 리팩토링은 코드 중복을 줄이는 것에서부터 시작이라는 것을 배웠다.

class SearchView(View):
  def post(self, request):
    data = json.loads(request.body)
      try:
        # body에 'searchTerm'이라는 키 이름으로 검색어가 담겨옴
        search_term = data['searchTerm']
        # 오피스텔 건물 이름과 지하철 역 이름 중 띄어쓰기가 한 칸 있는 것이 있어서 로직에서 예외 처리 해주고
        # 검색어가 아무것도 입력되지 않았을 때 또한 예외 처리를 해줌
        if search_term == ' ' or search_term == '':
          return JsonResponse({'message' : 'No results.'}, status = 400)

        # 검색 결과로 리턴해주어야 할 데이터가 담긴 테이블을 연결해줌
        # 데이터를 가져오기 위한 준비 단계
        complexes = Complex.objects.prefetch_related('province__district__city')
        provinces = Province.objects.select_related('district__city')
        districts = District.objects.select_related('city')
        subways = Subway.objects.prefetch_related('line')
        schools = School.objects.select_related(
          'school_establishment_type',
          'school_gender',
          'school_type',
          'school_category'
        )

        # 각 테이블에서 불러올 데이터가 모두 'name'이라는 필드에 들어있는 값이라
        # 변수 하나를 선언해서 코드 반복을 줄여줌
        # 검색어가 포함된 것을 모두 불러와야 하기 때문에 contains 사용
        search = Q(name__contains = search_term)
        complex_search_result = complexes.filter(search)
        city_search_result = City.objects.filter(search)
        district_search_result = districts.filter(search)
        province_search_result = provinces.filter(search)
        subway_search_result = subways.filter(search)
        school_search_result = schools.filter(search)

        # 위에서 filter한 query set에서 원하는 값만 불러옴
        complex_search_list = [
          {
            'type'      : '오피스텔',
            'name'      : word.name,
            'city'      : word.province.district.city.name,
            'district'  : word.province.district.name,
            'province'  : word.province.name,
            'longitude' : word.longitude,
            'latitude'  : word.latitude
          } for word in complex_search_result]
            
        # 코드 중략

        # 프론트에 아래 형태로 리턴함
        return JsonResponse(
          {
            'complexes' : complex_search_list,
            'cities'    : city_search_list,
            'districts' : district_search_list,
            'provinces' : province_search_list,
            'subways'   : subway_search_list,
            'schools'   : school_search_list
          }, status = 200)
      except KeyError:
        return JsonResponse({'message' : 'Invalid key.'}, status = 400)

  1. Django ORM

호출할 정보 종류가 많다보니 필연적으로 장고 orm을 많이 주무를 수밖에 없었다. 프론트에서도 또한 다룰 데이터가 많을테니 어떤 자료구조가 나을까를 많이 고민했다. 내가 전달한 자료구조가 프론트 입장에서 베스트는 아니기 때문에 몇 번 대화를 나눈 뒤 최초 자료 형태에서 조금씩 수정해나갔다. 테이블을 넘나들면서 데이터를 끌어오는 것도 재미있었고 db에 저장된 필드값 그대로가 아니라 그것을 다시 가공해서 리턴하는 작업이 재미있었다.

예를 하나 들자면, db에 보증금을 만 원 단위 decimal 속성으로 저장해놓았다. 유저가 브라우저에서 보는 전세금 항목 값이 ’3억 5,000‘이리고 하면 db에 저장된 값은 ’35000.00‘이다. 이것을 프론트에 전달할 때 integer로 변환하거나 float로 변환해서 ’35000‘이라고 전달할 수도 있었지만 커뮤니케이션 비용을 줄이기 위해 애초에 ’3억 5,000‘이라는 string으로 리턴했다. 수십만 수백만 데이터를 다룰 때는 백엔드에서 연산해서 내보내는 방법이 오히려 효율적이지 않을 수 있으니 이 부분은 좋은 예시를 보고 배우고 싶다.

views

뷰 길이만큼 유닛 테스트 길이도 길어진다…

총평

인생에서 가장 빨리 지나간 두 달이었다. 정말 많이 성장했다. 같이 고생한 팀원들한테도 그저 고맙다.

reward

고생한 나에게 주는 상