풀 스택 미니 프로젝트

저희 6조의 첫 번째 프로젝트를 소개합니다 !!!

www.notion.so

 

1. 프로젝트 주제

- 그날 마신 음료 수를 카운트하고 일주일 치를 저장하여, 회원들의 수치와 비교 랭킹을 보여주는 사이트

 

2. 프로젝트 명칭 선정

- 니가 마신 음료 "니마음"

 

3. 사용 기술 스택

- Python, Flask, MongoDB, JWT(JSON Web Token), SSE(Server Sent Event) 

 

4. 완성 화면

1. 로그인 페이지 (기능 구현 by. 서혁수)

JWT
2. 회원 가입 페이지 (기능 구현 by. 서혁수)
3. 메인화면 (화면 제작 by. 노현승)

part1. 
- 닉네임 표시
- 로그아웃 기능
- 이미지

part2.
- 음료수 카운트를 수정, 저장 (기능 구현 by. 황지성)
- 현재 음료수 카운트 출력 (기능 구현 by. 황지성)

part3.
- 실시간 입력 상태 메세지 (기능 구현 by.조소영) 

part4. 
- 음료 카운트 랭킹 (기능 구현 by. 황지성)

 

5. 맡은 기능에 대한 리뷰

* 목적)  실시간 상태 알림

이 사이트를 이용하는 사람들의 음료 카운트가 변화하면 실시간으로 바로 모두의 화면에 보여줄 수 있었으면 했다. 

 

* 사용 기능)  SSE

이 기능에 대해 여쭤봤을때 기술 매니저님이 제안해 주신 방법 중 하나로

일반적으로 이벤트가 클라이언트에서 서버로 흐르는 것과 달리 서버에서 클라이언트로 푸쉬되는 기술을 말하며 서버의 데이터를 실시간으로 Streaming하는 기술이다.

쉽게 말하자면 서버에서 데이터가 변하면 클라이언트 요청없이 클라이언트로 보내는 기술.

 

* Frontend 코드)  

클라이언트가 실시간으로 서버 데이터에 관한 이벤트를 항상 엿듣고 있는 eventSource를 생성

addEventListener로 이벤트 핸들러를 등록하여 특정 이벤트에 특정 행동을 할 수 있도록 코딩

받아온 JSON 문자열을 javascript 객체로 변환시켜 특정 식별자에 텍스트를 객체의 키값으로 정제한 문구로 출력하게 코딩했다. 

<!--중단의 실시간 카운트 멘트 추가-->
    <div class="rank-comment">
        🔥🔥 실시간으로 카운트를 확인해 보세요 !!! 🔥🔥
    </div>
    <div class="fixed" id="massage_box">
        <div class="message">
            <form id="m_box"></form>
        </div>
    </div>
    <script>
        var eventSource = new EventSource("/listen")                                                                                {# /listen url에 대한 EventSource 생성 : 서버와의 접속을 유지하고 서버로부터 이벤트를 수신 #}
        eventSource.addEventListener("online", function (e) {                                                                       {# 지정한 타입의 이벤트를 수신하게 되면 함수를 실행 그 결과값을 가지고 블록의 실행문을 수행  #}
            data = JSON.parse(e.data)
            document.querySelector("#m_box").innerText = data.nick + " 님이 " + data.comment + "마셨습니다."
        })
    </script>

 

* Backend 코드) 

몽고DB가 제공하는 데이터 베이스에 대한 변경 스트림을 지켜보고 이를 출력해 주는 함수를 사용했다.

또한 우리 서버에서도 이 스트림을 for문으로 유지시켜 데이터를 받을 준비를 한다.

실시간으로 DB가 변경되어 출력된 사항을 이전 DB값과 비교하여, 값의 차이와 함께 변경사항 문구로 정제하고 이를 Yield를 사용해 하나하나 클라이언트로 전달한다. 그렇게되면 클라이언트의 eventListener가 event 타입이 online인 것을 캐치하고 데이터를 받는다.  

 (코드 마다의 자세한 내용은 코드 뒤에 길게 주석으로 정리)

 

임포트할 사항과 기본 세팅사항

from gevent import monkey	# gevent는 동시성과 네트워크 관련 작업들을 위한 다양한 API를 제공
monkey.patch_all()      	# 몽키 패치는 실행중인 프로그램의 메모리 소스를 바꾸는 것으로서, 런타임 환경에서 프로그램의 특정 기능을 수정하여 사용하는 기법		
from pymongo import MongoClient

ca = certifi.where()
client = MongoClient('mongodb+srv://개인 MongoDB 정보',tlsCAFile=ca)
db = client.DB이름

구현 코드 : 다음 링크의 코드를 참조해 작성 (https://github.com/coderspage/flask-sse)

# 1. 기능 : DB변화를 감지하여 변한 데이터에 대한 값을 화면에 전달 하는 함수로 SSE (Server Sent Event) 기능을 한다.
# 2. 작성자 : 6조 조소영
# 3. 작성일자 : 2022-11-15
@app.route("/listen")
def listen():

    def respond_to_client(info):

        #1. DB변화 감지 스트림 생성
        stream = db.info.watch(full_document="updateLookup", full_document_before_change="whenAvailable")                               # 몽고DB의 특정 collection을 지켜보는 함수 (full_document_before_change옵션은 변경 Key값만을 알려주는 옵션)

        #2. 스트림을 유지 및 화면에 전달할 문구 정제
        for docu in stream:                                                                                                             # Stream을 유지하며 DB의 변경을 감지하기 위해 계속 도는 for문
            message = "";                                                                                                               # 화면단에서 출력할 메세지를 담는 변수
            if docu['operationType'] == 'update':                                                                                       # 기존값 업데이트와 첫 값 입력시 처리를 나눔
                docu_updates = docu['updateDescription']['updatedFields']                                                               # operation이 update일때는 update된 Key값과 value값만 출력되므로 key값을 확인해 처리
                for Key in docu_updates:
                    if Key == 'coffee_count':                                                                                           # if문
                        count, info = find_id(docu['fullDocument']['id'],info, docu_updates[Key], Key)                                  # 이전 데이터 값과 바뀐 데이터 값을 가져오기 위해 만든 find_id 함수 콜
                        now = docu_updates[Key] - count                                                                                 # 비교값 계산
                        if docu_updates[Key] - count < 0 :                                                                              # 잔 수를 줄였을 때의 처리와 잔 수를 늘렸을 때의 처리
                            message += "커피 " + str(-now) + "잔을 쏟아서 총 "   + str(docu_updates['coffee_count']) + "잔\n"
                        else:
                            message += "커피 " + str(now)  + "잔 추가해 총 "     + str(docu_updates['coffee_count']) + "잔\n"
                    elif Key == 'energy_count':
                        count, info = find_id(docu['fullDocument']['id'], info, docu_updates[Key], Key)
                        now = docu_updates[Key] - count
                        if docu_updates[Key] - count < 0:
                            message += "에너지 드링크 " + str(-now) + "잔을 쏟아서 총 " + str(docu_updates['energy_count']) + "잔\n"
                        else:
                            message += "에너지 드링크 " + str(now) + "잔 추가해 총 "    + str(docu_updates['energy_count']) + "잔\n"
                    elif Key == 'carbon_count':
                        count, info = find_id(docu['fullDocument']['id'], info, docu_updates[Key], Key)
                        now = docu_updates[Key] - count
                        if docu_updates[Key] - count < 0:
                            message += "탄산음료 " + str(-now) + "잔을 쏟아서 총 " + str(docu_updates['carbon_count']) + "잔\n"
                        else:
                            message += "탄산음료 " + str(now) + "잔 추가해 총 "    + str(docu_updates['carbon_count']) + "잔\n"
                    elif Key == 'drink_count':
                        count, info = find_id(docu['fullDocument']['id'], info, docu_updates[Key], Key)
                        now = docu_updates[Key] - count
                        if docu_updates[Key] - count < 0:
                            message += "술 " + str(-now) + "잔을 쏟아서 총 "   + str(docu_updates['drink_count']) + "잔\n"
                        else:
                            message += "술 " + str(now)  + "잔 추가해 총 "     + str(docu_updates['drink_count']) + "잔\n"
                    elif Key == 'etc_count':
                        count, info = find_id(docu['fullDocument']['id'], info, docu_updates[Key], Key)
                        now = docu_updates[Key] - count
                        if docu_updates[Key] - count < 0:
                            message += "기타음료 " + str(-now) + "잔을 쏟아서 총 " + str(docu_updates['etc_count']) + "잔\n"
                        else:
                            message += "기타음료 " + str(now) + "잔 추가해 총 "    + str(docu_updates['etc_count']) + "잔\n"

            elif docu['operationType'] == "insert":
                docu_insert = docu['fullDocument']
                for Key in docu_insert:
                    if Key == 'coffee_count' and docu_insert['coffee_count'] != 0:
                        message += "커피 "         + str(docu_insert['coffee_count'])   + "잔 추가해 총 " + str(docu_insert['coffee_count']) + "잔\n"
                    elif Key == 'energy_count' and docu_insert['energy_count'] != 0:
                        message += "에너지 드링크 " + str(docu_insert['energy_count'])   + "잔 추가해 총 " + str(docu_insert['energy_count']) + "잔\n"

                    elif Key == 'carbon_count' and docu_insert['carbon_count'] != 0:
                        message += "탄산음료 "      + str(docu_insert['carbon_count'])  + "잔 추가해 총 " + str(docu_insert['carbon_count']) + "잔\n"

                    elif Key == 'drink_count' and docu_insert['drink_count'] != 0:
                        message += "술 "          + str(docu_insert['drink_count'])    + "잔 추가해 총 " + str(docu_insert['drink_count'])  + "잔\n"

                    elif Key == 'etc_count' and docu_insert['etc_count'] != 0:
                        message += "기타음료 "      + str(docu_insert['etc_count'])     + "잔 추가해 총 " + str(docu_insert['etc_count'])    + "잔\n"

            #3. 화면으로 전달할 데이터 JSON화
            _data = json.dumps({
                "nick": docu['fullDocument']['nick'],
                "comment": message
            })
            yield f"id: 1\ndata: {_data}\nevent: online\n\n"                                                                            # yield는 for문이 돌면서 중간중간 변경 값을 출력한 값을 하나하나 전달 하기 위한 제네레이터를 생성하는 기능을 한다
                                                                                                                                        # f""는 f-string 문자열 사이에 변수를 사용할 수 있도록 하는 문법

    # DB의 이전 값을 가져오는 함수
    def find_id(id, info, update_count, key):                                                                                           # 이전 데이터 모두를 대상으로 for문을 돌려
        for i in range(len(info)):
            if info[i]['id'] == id:                                                                                                     # 해당 아이디를 찾는다
                result = info[i][key]                                                                                                   # 이전 값을 return할 변수에 대입
                info[i][key] = update_count                                                                                             # info에 들어있는 값을 갱신
                return result, info

        return                                                                                                                          # for문 안의 return값을 함수 밖으로 그대로 return, info를 다시 return하는건 info가 갱신되어 for문 안에서 돌아야하기 때문

    info = list(db.info.find({}, {'_id': False}))                                                                                       # 변화 이전에 모든 데이터를 list형태로 저장
    return Response(respond_to_client(info), mimetype='text/event-stream')                                                              # 이전 모든 데이트를 담은 info를 가지고 respond_to_clinet를 실행

+ DB 이전 값을 저장하는 방법에 대해선 지성님께서 도움을 주셨다. 

 

 

추가적으로 배운 개념) 

+ mimetype : Multipurpose Internet Mail Extensions의 약자. 간단히 말하면 파일 변환을 의미한다. 이 단어가 생길 당시엔 이메일과 함께 보내는 파일을 텍스트 문자로 변환하여 전달하기 위해 만들어졌기 때문에 Mail이란 단어가 들어갔지만 지금은 웹을 통해 여러 형태의 파일을 전달하는데 사용되어 지고 있다.

+ Python에서) yield는 for문이 돌면서 중간중간 변경 값을 출력한 값을 하나하나 전달 하기 위한 제네레이터를 생성하는 기능을 한다
+ Python에서) f""는 f-string 문자열 사이에 변수를 사용할 수 있도록 하는 문법

 

 

깃헙 주소: 

 

GitHub - 6jo-NiMaUm/NiMaUm: 오늘 마신 음료 수를 카운팅을 기록해 참여자들 간의 랭킹을 보여준다

오늘 마신 음료 수를 카운팅을 기록해 참여자들 간의 랭킹을 보여준다. Contribute to 6jo-NiMaUm/NiMaUm development by creating an account on GitHub.

github.com

 

+ Recent posts