Project/[약올림] Final Project

[Milestone Week 1] Home, Calendar관련 API & CountTimer 구현

HJChung 2021. 1. 22. 09:01

이번에 Full Stack 포지션을 맡게 되면서 Server와 Client의 작업을 동시 진행하였다.

 

프로젝트를 진행하면서 개발적인 역량을 키울 수 있었던 것도 정말 큰 성장이지만 무엇보다

1. 필요한 기능이 무엇인지 정확하게 파악하고(문제 정의 능력),

2. 그 기능을 구현하기 위해 정확하고 효율적인 질문/검색을 하고,

3. 찾은 해답을 어떻게 내 상황에 맞게 적용하는 

능력을 기를 수 있어서 정말 값진 시간이었다는 것이다. 

 

Milestone 기준 본격적인 기능구현 1주차에 Server와 Client side에서 한 일을 적어보고자 한다. 

1. Server side

로그인 완료 후 처음 앱에 들어가자마자 보이는 Home 페이지와 약올림 서비스의 핵심 페이지라 할 수 있는 Calendar 페이지에 필요한 API를 구현하였다. 

Flask로 MVC 패턴을 맞춰서 구현해보기는 처음이라 간단한 API였음에도 불구하고 시간이 꽤 걸렸던 것 같다. 

 

구현한 API는 Get Monthly Checked API, Get Today Checked API, Get AlarmList API등이 있는데, Get Monthly Checked API를 구현하면서 Flask-restx를 사용하여 MVC 패턴을 맞춰서 구현하는 것에 익숙해 지고,이후에는 감을 잡게 되었으므로 대표적으로 Get Monthly Checked API구현 당시를 적어보고자 한다.

 

Get Monthly Checked API

 

 

여기 달력에 해당 월의 날짜마다 등록되어 있는 알람을 시간 순서대로, 약 복용여부(check)에 따라 색을 달리 보여주기 위해 해당 데이터를 응답으로 보내주데 사용될 Get Monthly Checked API이다.

 

calendar page에 들어가자마자 GET 요청을 보내어 복용현황을 보내주어 달력에 색으로 표시해주기 위해 해당 월에 등록된 모든 알람의 check(bool) 값 데이터를 응답해주는 API이다.(default는 당월)

  • 클라에서 오늘 날짜를 보내줌

1) 파일 구성

앞서 '초기세팅'에서 논의되었던 것 처럼

Flask-RESTX: MVC 폴더 구성 및 Flask REST API 구현하기의 방식으로 구현하기로 하고, 파일을 구성했다. 

 

2) manage.py

이 파일이 우리가 구현할 서버코드 라우팅의 시작인 최상위 실행 폴더이다.

여기서 Blueprint로 모든 것을 묶어준다.

 

 

※ manage.py에 있는 app이라는건 뭘까?

Flask는 Flask class로 app 이라는 객체를 만들고 app객체에 웹서비스 기능을 추가하는 형태로 구현하게 되어있다. (자세한 설명은 1. 파이썬 flask 시작하기에 <2. flask 객체 생성>에 정리해 두었다.)

 

그래서 app = Flask(__name__)으로 객체를 만들어주어야 하는데, 우리는 이것 뿐만 아니라 환경변수 설정, DB적용, bcrypt암호화 적용 등 초기세팅시 해줘야할 것이 몇 개 더 있다. 그래서 이걸 아에 create_app()이라는 함수로 따로 만들어준 후 이를 import해와서 사용해주는 것으로 파일을 나누었다.

 

 

3) 이제 controller와 model 쿼리문을 작성해주어야 한다. 그 전에 해당 API의 controller파일을 특정 상위 uri로 등록시켜주는 작업이 필요하다.

그래서 app/__init__.py에서 add_namespace()를 이용하여 해당controller를 특정 경로로 연결을 시켜준다.

※ Routing은 어떻게 정의되고 및 설정하는가?

형식)'서비스 주소/Routing 주소' 이면, @[웹 서비스를 구현한 객체].route([Routing경로 이름])
                                                              (↑@로 시작하는 코드는 데코레이터 라고 한다.)

예를 들어 http://www.helloworld.com/Main이면, @app.route('/Main')
:그러면http://www.helloworld.com서버에서 Main이라는 엔드포인트에 맞는 바로 아래의 함수를 호출한다.

 

4) 이제 controller를 작성해주자.

app/main/controller/schedules_date.py

여기서 controller는 클라이언트와 서버의 모델 접근을 연결시켜주는 정말 중간다리역할 까지 하도록 짰다.

그리고 실제 모델에 접근하여 쿼리문을 날리고, 그 결과를 응답해주는 것은 /service 폴더 내의 schedules_date.py에서 진행해주도록 하였다.

그래서app/main/controller/schedules_date.py에서,

app/main/service/schedules_date.py의 코드 중 GET monthly checked API에 필요한 쿼리문와 응답 처리를 해주는 코드(함수)인 get_monthly_checked를 import해와서 사용하였다.

 

 

5) 이제 쿼리문과 응답처리에 해당하는 service코드를 작성해주자.

app/service/schedules_date.py

class TimeFormat(fields.Raw):
    def format(self, value):
        return time.strftime(value, "%H:%M")

def sorting_date_time(data):
  """ date로 오름차순 정렬 후 time으로 오름차순 정렬하는 함수 """
  data = sorted(data, key=itemgetter('date', 'time'))
  return data
# 
def get_monthly_checked(data): 
  """ Get monthly checked API for calendar"""
  try: 
    parsing = data['today'].split('-')  #1. 아까 controller에서 보내준 client에서 전달해준 request params. 그걸 쿼리문에 활용가능한 형태로 처리해줍니다. 
    year = parsing[0] 
    month = parsing[1] 

    token = request.headers.get('Authorization')
    decoded_token = jwt.decode(token, jwt_key, jwt_alg)
    user_id = decoded_token['id'] 

    if decoded_token: #토큰이 있는 사용자. 
      topic_fields = {
        'date': fields.Integer(required=True),
        'time': TimeFormat(readonly=True, description='Time in HH:MM', default='HH:MM'),
        'check': fields.Boolean(required=True),
      }
      data = [marshal(topic, topic_fields) for topic in Schedules_date.query.filter(and_(Schedules_date.year==year, Schedules_date.month==month, Schedules_date.user_id==user_id)).all()]
      #2. Schedules_date 모델에서 select해올거고,(.all()) where로 client가 해당 사용자의 요청한 년, 월에 해당하는 데이터만 필터링해서 가져올 겁니다. (.filter)
      results = sorting_date_time(data) #3. 이 데이터들을 date, time기준 오름차순으로 정렬하여 응답해주어야 하기 때문에 정렬을 해주고요 (정렬을 위한 sorging_date_time함수는 위에서 선언해줬습니다.)
      response_object = { #4-1. status code가 200으로 잘 받아오면 이런 형식으로 응답을 보내줍니다. 
        'status': 'OK',
        'message': 'Successfully get monthly checked.',
        'results': results
      }
      return response_object, 200
    else:#토큰이 없는 사용자 . 
      response_object = {#4-2. 토큰이 없는 사용자는 권한 없음 에러인 401 statuus code를 아래의 형식으로 응답해줍니다. 
        'status': 'fail',
        'message': 'Provide a valid auth token.',
      }
      return response_object, 401

  except Exception as e: #4-3. 아에 서버 에러가 발생했을 경우 500 statuus code를 아래의 형식으로 응답해줍니다.
    response_object = {
      'status': 'Internal Server Error',
      'message': 'Some Internal Server Error occurred.',
    }
    return response_object, 500

※ marshal이란???

marshal은 flask-restx에서 제공하는 데이터 제어 방법중 하나로, fields를 통해 select해올

1. 데이터 타입이라던지,

2.어떤 것을 어떻게 가져올건지(필터링)등을 명확하게 정해놓고 model에 쿼리문을 던질 수 있다.

제가 marshal을 쓰고자 한 것은 time의 데이터 타입 때문이었다.
자체적으로 제공하는 fields 데이터타입에는 Datetime( yyyy-mm-dd hh:mm:ss) 뿐인데 shedules_date의 'time'필드의 데이터 타입은 time(hh:mm:ss)일 뿐만 아니라 ss 정보는불필요하다 생각되어 응답으로 hh:mm만 주고자 하니 고민이었기 때문이다.
다행히도 flask-restx의 marshal 장점은 fields 커스터마이징이 가능하다.

그래서 'TimeFormat'이라는 class를 만들어서 time의 데이터 타입을 " hh:mm"으로 select 해오는 fields를 커스터마이징 해 주고, 이를 사용하였다.

그러면서 이왕 데이터 타입 정해줄거 나머지(date, check)의 타입도 한번 더 명시해줘서 확실히 해주자! 라고 생각해서 topic_fields라는 것을 만들었고, 그 안에 해당하는 것을 가져오라는 의미로
data = [marshal(topic, topic_fields) for topic in Schedules_date.query.filter(and_(Schedules_date.year==year, Schedules_date.month==month, Schedules_date.user_id==user_id)).all()] 로 쿼리문을 짰다.

 

6) 이제 끝!!

postman으로 확인해보면 응답이 잘 나오는 것을 확인할 수 있다!


2. Client side

Client에서는 CountTimer 기능을 구현했다.

 

 

 

어려운 기능일 줄 알고 겁먹고 있었는데, 웬걸 참고 자료와 블로그 글들을 보고 너무나 쉽게 구현 할 수 있었다.

이를 통해 깨달은 것은

프로젝트를 진행하면서 개발적인 역량을 키울 수 있었던 것도 정말 큰 성장이지만 무엇보다

1. 필요한 기능이 무엇인지 정확하게 파악하고(문제 정의 능력),

2. 그 기능을 구현하기 위해 정확하고 효율적인 질문/검색을 하고,

3. 찾은 해답을 어떻게 내 상황에 맞게 적용하는 

능력을 기를 수 있어서 정말 값진 시간이었다는 것이다. 

 

또한 해당 기능을 구현하면서 UX적으로도 많이 생각해보는 계기가 되었다.

날짜, 시간, 분, 초 어느 단위까지 보여드려야 디자인적으로 괜찮으면서 사용자에게 직관적인 시간 정보를 인지시켜줄 수 있을까?

알람 시간이 되었을 때 우리 서비스 화면에 들어와있을 경우도 별로 없고, 어짜피 push 로 알람이 갈 것이다 그럼에도 불구하고 1초 정도는 약 먹을 시간이라고 띄워주는게 좋지 않을까?

당일 배정된 알람이 다 울리면 그 다음 날짜 알람을 countdown하는게 좋을까 아니면 오늘 할당된 약은 다 복용하셨단는 문구가 고정으로 나오게 하는게 좋을까?

하는 질문들을 스스로 해보며 여러 테스트를 해 보았다.

 

그리고 여러 테스트 작동 화면들을 동영상으로 남겨 팀원분들의 의견을 구하기도 하며, 최선의 결과로 이 기능은 구현 마무리가 되었다. 

 

이 과정에서 moment.js를 처음 사용해 봤는데, python에 datetime이 있다면 JavaScript에는 moment.js가 있다! 고 할 만큼 시간과 날짜 데이터타입이 많이 사용되는 우리 서비스에서 가장 유용하게 사용한 라이브러리가 아닐까 한다. 

 

이 과정을 모두 담고 있는 PR 기록을 남기면서 Milestone 1주차 회고는 마치겠다.🙂

 

 

 

 

 

 

reference

전체적인 파일 구성 및 라우팅

https://github.com/cosmic-byte/flask-restplus-boilerplate

Flask-RESTX: MVC 폴더 구성 및 Flask REST API 구현하기

flask-restx를 이용한 API 구현

API — Flask-RESTPlus 0.13.0 documentation

Response marshalling

Response marshalling — Flask-RESTPlus 0.13.0 documentation

python list에서 dictionary 정렬

inma.tistory.com/138

moment.js 사용

How to compare only date in moment.js