1. 개념

9월7일·2022년 12월 16일
0

목차

  1. 개요
  2. 프론트엔드 영역
    • html에서 반복문과 조건문 사용하기
    • html에서 중복 코드의 블럭처리
    • html에서 Javascript 사용하기
  3. 백엔드 영역
    • Flask란?
    • Flask 설치 방법
    • Flask 프로젝트 기본 세팅
    • Flask에서의 정적 라우팅
    • Flask에서의 동적 라우팅
  4. 데이터베이스 영역
    • Firebase란?
    • Pyrebase란?
    • Flask에서 Pyrebase 사용하기

개요

본 게시글에서는 2022년 2학기 오픈SW플랫폼 '9월7일' 조의 개발 과정에서 사용된 기술들을 소개하고 설명합니다.
프론트엔드에서는 별도의 프레임워크 없이 순수 html과 css만을 사용하였습니다.
백엔드에서는 Python 기반의 프레임워크 Flask를 사용하였습니다.
데이터베이스로는 Firebase를 이용하였고, Firebase 사용을 편리하게 해주는 Python wrapper인 Pyrebase로 개발을 진행하였습니다.

프론트엔드 영역

html에서 반복문과 조건문 사용하기

Flask에서는 {% %}로 문자열을 감싸면, Python에서 사용하던 문법(반복문과 조건문 등)을 html에서도 사용할 수 있다.

1) 반복문 사용하기

반복문의 기본적인 구조는 다음과 같다.

{% for 변수이름 in 순회 가능한 객체 %}
{% 변수이름 %} 이 포함된 코드
{% endfor %}

순회 가능한 객체는 함수 등을 통해 이 html 파일에게 미리 전달되어야 한다.
순회 가능한 객체의 이름은 전달된 이름과 동일하게 적어주어야 한다. 하지만 그 안에 들어있는 각각의 요소를 가리키는 변수이름은 아무 것이나 편한 대로 적어도 된다.

예를 들면 다음과 같다.
names 라는 리스트 안에 사람들의 이름이 저장되어있다고 하자.
Amy, Betty, Cindy 라는 이름이 각각 names[0], names[1], names[2]에 저장되어있다.
화면에 Amy, Betty, Cindy를 한 줄에 하나씩 순서대로 출력하는 반복문을 작성해보자.

함수를 통하여 html 파일에 names라는 객체가 전달된 상황이다.
names 안에 있는 각각의 요소는 name이라고 부르기로 하자.
그럼 다음과 같이 적어주면 된다.

{% for name in names %}
<p>{% name %}</p>
{% endfor %}

Flask는 위의 코드를 다음과 같이 해석한다.

<p>Amy</p>
<p>Betty</p>
<p>Cindy</p>

이 html 파일을 실행시키면 화면은 다음과 같이 출력된다.

2) 조건문 사용하기

조건문의 기본적인 구조는 다음과 같다.

{% if 조건문A %}
조건문A가 참일 경우에 실행될 코드
{% elif 조건문B %}
조건문A가 거짓이고 조건문B가 참일 경우에 실행될 코드
{% else %}
나머지 경우에 실행될 코드
{% endif %}

예를 들면 다음과 같다.
check라는 boolean 타입 변수가 있다.
이 변수가 True일 때는 화면에 yes를 출력하고, 이 변수가 False일 때에는 화면에 no를 출력하는 html 파일을 작성해보자.
check는 함수 등을 통해 html 파일에게 전달되어있는 상황이다.

다음과 같이 작성하면 된다.

{% if check %}
<span>yes</span>
{% else %}
<span>no</span>
{% endif %}

만약 check가 True라면 Flask는 위의 코드를 다음과 같이 해석한다.

<span>yes</span>

반대로 check가 False라면 Flask는 위의 코드를 다음과 같이 해석한다.

<span>no</span>

아래는 본 프로젝트에서 html 내의 조건문을 사용한 예시 중 일부이다.

  1. 로그인이 되어있는지를 조건문으로 체크하여, 로그인이 되어있을 경우 로그아웃 버튼을, 로그인이 되어있지 않은 경우 로그인 버튼을 띄우는 코드
        <div class="logout_container">
            {% if session['UserId'] %}
            <div class="logout" styles="visibility:hidden"><a href="/logout">로그아웃</a></div>
            {% else %}
            <div class="logout" styles="visibility:hidden"><a href="/login">로그인</a></div>
            {% endif %}
        </div>
  1. 식당 정보를 수정할 때의 편의를 위해, 수정 전의 본래 정보를 화면에 미리 띄워놓는 코드
			<select id="select-type" name="type" style="width: 200px;">
				{% if data.type == "korean" %}
				<option value="korean" selected>한식</option>
				{% else %}
				<option value="korean">한식</option>
				{% endif %}
				{% if data.type == "chinese" %}
				<option value="chinese" selected>중식</option>
				{% else %}
				<option value="chinese">중식</option>
				{% endif %}
				{% if data.type == "japanese" %}
				<option value="japanese" selected>일식</option>
				{% else %}
				<option value="japanese">일식</option>
				{% endif %}
				{% if data.type == "western" %}
				<option value="western" selected>양식</option>
				{% else %}
				<option value="western">양식</option>
				{% endif %}
				{% if data.type == "asian" %}
				<option value="asian" selected>아시안</option>
				{% else %}
				<option value="asian">아시안</option>
				{% endif %}
				{% if data.type == "desert" %}
				<option value="desert" selected>디저트/카페</option>
				{% else %}
				<option value="desert">디저트/카페</option>
				{% endif %}
		</select>

html에서 중복 코드의 블럭처리

하나의 프로젝트에는 여러 개의 html 파일이 사용된다.
여러 html 파일에 공통적으로 들어가는 부분이 있다면, 매번 그 부분을 복사-붙여넣기 해야 한다.
Flask는 이러한 번거로움을 해결할 수 있도록 중복 코드의 블럭처리 기능을 제공한다.
블럭처리를 하면, 블럭 안에 들어있는 내용을 제외한 모든 내용이 복사되어서 다른 html 파일에 자동 붙여넣기된다.

예를 들면 다음과 같다.
다음과 같은 main.html 파일이 있다고 하자.

<!DOCTYPE html>
<html lang="en">
    <head>
    		<meta charset = "UTF-8" />
    </head>
    <body>
    	<h1>Hello</h1>
        <span>This is the main page</span>
    </body>
</html>

화면에 Hello라는 제목과 'This is the main page'라는 문자열이 출력되는 코드이다.
그런데, 여기서 'This is the main page'라는 문자열의 내용이 각 페이지마다 달라지도록 하고 싶다.

list.html 파일의 경우에는 Hello라는 제목과 'This is the list page'라는 문자열을 출력하고 싶고,
login.html 파일의 경우에는 Hello라는 제목과 'This is the login page'라는 문자열을 출력하고 싶다.

그럼 먼저, main.html을 다음과 같이 수정한다.

<!DOCTYPE html>
<html lang="en">
    <head>
    		<meta charset = "UTF-8" />
    </head>
    <body>
    	<h1>Hello</h1>
        {% block content %}
        <span>This is the main page</span>
        {% endblock content %}
    </body>
</html>

화면마다 내용이 달라야 하는 부분만 content라는 이름의 블럭으로 감쌌다.

list.html은 다음과 같이 적어주면 된다.

{% extends 'main.html' %}
{% block content %}
<span>This is the list page</span>
{% endblock content %}

다른 코드는 아무것도 적지 않아도 된다.
Flask는 위의 코드를 다음과 같이 해석한다.

<!DOCTYPE html>
<html lang="en">
    <head>
    		<meta charset = "UTF-8" />
    </head>
    <body>
    	<h1>Hello</h1>
        {% block content %}
        <span>This is the list page</span>
        {% endblock content %}
    </body>
</html>

마찬가지로, login.html도 다음과 같이 적기만 하면 된다.

{% extends 'main.html' %}
{% block content %}
<span>This is the login page</span>
{% endblock content %}

이처럼 블럭처리를 하면, 중복되는 코드를 직접 복사-붙여넣기하는 수고를 줄일 수 있다.

본 프로젝트에서는 Addmenu.html의 내용을 다른 html 파일들이 extend하여 중복 코드를 줄이고 있다.

html에서 Javascript 사용하기

웹페이지가 사용자와 상호작용하도록 하기 위해서는 Javascript가 필요하다.
Javascript 사용에는 두 가지 방법이 있다.
하나는 별도의 Javascript 파일을 작성한 뒤 html 파일에 연결시키는 방법, 다른 하나는 html 파일 내부에 Javascript 코드를 직접 적는 방식이다.

다음과 같이 코드를 작성하면 된다.

별도의 Javascript 파일을 사용하는 경우, html의 head 안에 다음과 같은 코드를 추가하여 연결한다.

<head>
	...
	<script type="text/javascript" defer src="{{ url_for('static', filename='js파일까지의 경로 및 js파일의 이름') }}"></script>
	...
</head>

filename에는 js파일의 이름만 적는 것이 아니라 js파일까지 도달하는 경로까지 적어야 한다.
예를 들어, js파일이 js라는 이름의 디렉토리 안에 들어있다면 'js/파일이름.js'와 같이 적어야 한다.

html 파일 내부에 Javascript 코드를 적어넣는 경우, html의 헤더 부분에 다음과 같이 Javascript 코드를 작성하면 된다.

<head>
  	...
	<script>
      Javascript 작성
	</script>
  	...
</head>

이렇게 두 방법 중 한 가지를 선택하여 Javascript 작성을 끝냈다면, 어떤 요소에 어떤 동작을 가했을 때 어떤 Javascript 함수가 실행되게 할 것인지 지정해주어야 한다.

예를 들면 다음과 같다.
hello라고 적힌 영역을 클릭하였을 때 helloFuction()이라는 Javascript 함수가 실행되게 하고 싶은 상황이다.
위의 두 가지 방법 중 하나로 Javascript 내용을 작성 완료하여, helloFuction() 함수가 이미 정의되어있다고 가정하고, hello가 적힌 영역과 helloFuction() 함수를 짝지어주자.
아래와 같이 hello가 적힌 영역의 태그 안에 onclick 어트리뷰트를 추가해주면 된다.

<div onclick=helloFuction()>hello</div>

본 프로젝트에서 Javascript가 사용된 예시 중 일부는 다음과 같다.


  1. 로그인을 하지 않은 상태에서 맛집 등록을 시도할 경우, 화면에 로그인을 요청하는 메시지를 띄우는 함수 fail_mapage()를 실행시키는 코드
  • Javascript 파일에 정의된 함수:
function fail_mypage(){
    var result = confirm("로그인 후 이용가능한 페이지입니다. 로그인하시겠습니까?");
        
    if(result)
    {
        location.href='/login';
    }
    else
    {
        location.href='/index';
    }
        
}
  • html과 Javascript 파일을 연결하는 코드:
    <script type="text/javascript" defer src="{{ url_for('static', filename='js/index.js') }}"></script>
  • html 파일 안의 요소와 함수를 짝짓는 코드:
        {% if session['UserId'] %}
          <li><a href="/registerpage">맛집 등록</a></li>
          {% else %}
          <li><a style="cursor:pointer;" onclick=fail_mypage()>맛집 등록</a></li>
        {% endif %}

  1. 위치/종류별 버튼의 오른쪽에 위치한 삼각형 버튼을 클릭하면 드롭다운을 표시하는 코드
  • Javascript 파일에 정의된 함수:
var clicked=0;
function show_drop(){
    if(clicked==0){
        document.getElementById("dropdown").style.display="flex";
        document.getElementById("down_btn").innerHTML="▲";
        clicked=1;
    }
    
    else{
        document.getElementById("dropdown").style.display="none";
        document.getElementById("down_btn").innerHTML="▼";
        clicked=0;
       
    }
}
  • html과 Javascript 파일을 연결하는 코드:
    <script type="text/javascript" defer src="{{ url_for('static', filename='js/index.js') }}"></script>
  • html 파일 안의 요소와 함수를 짝짓는 코드:
        <li class="dropdowns">
          <a href="/typespage">위치/종류별</a><button id="down_btn" class="down_btn" onclick=show_drop()></button></li>

백엔드 영역

Flask란?

본 프로젝트의 백엔드는 Flask를 사용하여 개발되었다.
Flask는 Python 기반의 백엔드 프레임워크이다. 초보자도 웹페이지 개발을 쉽고 편하게 할 수 있어 웹 백엔드를 처음 공부하는 학생들에게 적합하다.

Flask 설치 방법

Python과 pip가 설치되어있는 상태라고 가정한다.
명령 프롬프트(cmd)를 열어, pip install flask 를 입력하면 설치가 진행된다.

Flask 프로젝트 기본 세팅

Flask 프로젝트는 일반적으로 application.py 또는 app.py라는 이름을 가진 Python 파일을 중심으로 한다. 이 파일은 Flask 프로젝트 폴더의 최상위에 위치한다.
application.py 파일의 가장 앞부분에서는 필요한 라이브러리를 import하고, 시스템이 구동되는 데 필요한 기본 객체들을 선언한다.

본 프로젝트의 application.py 파일의 앞부분은 다음과 같다.

# import문들
from flask import Flask, render_template, request, flash, redirect, url_for, session
import math
from database import DBhandler
import hashlib
import sys

application = Flask(__name__)
application.secret_key = 'eatwha_secret'

DB = DBhandler() #데이터베이스 접근을 위한 객체

Flask에서의 정적 라우팅

Flask에서의 라우팅을 하려면 함수에 @application.route() 라는 어노테이션을 붙여야 한다.
만약 Flask 기본 세팅 때

application = Flask(__name__)

위와 같은 코드 부분에서,

app = Flask(__name__)

위와 같이 application이 아닌 다른 이름을 사용하였다면 그 이름을 사용하여 @app.route() 와 같이 적어주어야 한다.

다음과 같이 적으면 된다.

@application.route("/")
	def index():
    	return render_template("index.html")

"/" 라는 url, 즉 이 웹사이트의 도메인으로 접속하였을 때 index()라는 함수가 실행되도록 라우팅하고 있는 코드이다. index() 함수는 render_template() 함수를 사용하여 index.html 이라는 html 파일이 화면에 출력되게 하고 있다.

Flask에서의 동적 라우팅

만약 그때그때 상황에 따라 동적으로 라우팅을 해야 한다면 어떻게 해야 할까?
예를 들어 게시판에 있는 게시글들을 조회하는 기능을 구현한다고 가정하자.
게시글 id에 따라 접속하는 url이 달라진다. 1번 게시글을 조회하려면 'homepage.com/view/1'에 접속하고, 2번 게시글을 조회하려면 'homepage.com/view/2'에 접속하는 상황이다.
그렇다면 아래와 같이, 가능한 모든 url에 대하여 하나하나 라우팅을 해주어야 하는 것일까?

@application.route("/view/1")
	def viewOne():
    	return render_template("viewOne.html")

@application.route("/view/2")
	def viewTwo():
    	return render_template("viewTwo.html")

@application.route("/view/3")
	def viewThree():
    	return render_template("viewThree.html")
        
.
.
.

그렇지 않다. 아래와 같이 동적 라우팅을 해주면 한다.

@application.route("/view/<id>")
	def view(id):
    	return render_template("view.html", id=id)

이렇게 하면 그때그때 다르게 url에 제공되는 id값을 반영하여 함수가 동적으로 반응할 수 있게 된다.
또한, id 값을 view.html 입장에서 id라는 이름으로 가져다 쓸 수 있도록 넘겨주고 있다. 이렇게 하면 view.html 파일이 id 값에 맞추어 알맞은 행동을 그때그때 다르게 취할 수 있다.

데이터베이스 영역

Firebase란?

본 프로젝트에서 사용한 데이터베이스는 Firebase이다.
Firebase는 구글에서 지원하는 개발 플랫폼으로, 순수하게 코드만으로 서버 개발을 할 때에 거쳐야 하는 매우 복잡한 과정들을 생략할 수 있게 해주는 편리한 서비스이다.
개발 과정에서 별도의 서버 구매 없이 사용 가능한 실시간 데이터베이스를 제공하기 때문에 웹/앱 서비스를 개발할 때에 사용하기 좋다.

Pyrebase란?

Pyrebase는 Python 기반으로 코딩을 할 때 Firebase를 더 편리하게 사용할 수 있게 해주는 Wrapper이다.
Wrapper란, 기존에 존재하는 객체를 한 번 감싸서 그 객체에 접근하는 방식 및 객체를 사용하는 방식을 바꾸어주는 것을 가리킨다.

본 프로젝트의 모든 데이터베이스 관련 코드는 Pyrebase를 사용하고 있다.

Pyrebase의 사용법 및 코드는 아래의 깃허브에 공개되어있다.
https://github.com/thisbejim/Pyrebase

1) Pyrebase 기본 세팅

Firebase를 사용하려면 먼저 Firebase 계정이 준비되어있어야 한다.
다음의 Firebase 홈페이지에 접속하여 회원가입을 한다.

준비되었다면 최상위 폴더에 authentication이라는 폴더를 만든 뒤, 그 안에 다음의 내용이 담긴 firebase_auth.json이라는 JSON 파일을 만든다.
그 안에는 Firebase 계정마다 고유하게 주어지는 인증 코드를 복사하여 넣는다.


Flask에서 Pyrebase 사용하기

1) Pyrebase 설치

명령 프롬프트(cmd)를 열어 다음과 같은 커맨드를 실행시키면 Pyrebase가 설치된다.

pip install pyrebase

2) init함수 정의

database.py의 DBhandler 클래스의 첫 부분에 다음과 같은 함수를 준비해두어야 한다.

    def __init__(self ):
        with open('./authentication/firebase_auth.json') as f:
            config=json.load(f )
        firebase = pyrebase.initialize_app(config)
        self.db = firebase.database()

3) 새로운 데이터 삽입

이제 데이터를 삽입, 삭제, 수정하는 방법에 대하여 설명하겠다.

먼저 삽입할 데이터를 준비해야 한다. 예를 들어서 살펴보자.
만약 사람의 이름, 나이, 직업에 대한 정보를 담은 데이터를 person이라는 목록 안에 넣고자 한다면 다음과 같이 작성해주면 된다.

tom_info={
    "name":"Tom",
    "age":"20",
    "job":"student"
    }

다음으로는 원하는 key 값 부여 방식에 따라 다르게 데이터를 삽입해주어야 한다.

Firebase의 모든 데이터는 고유한 key 값을 통해 구분된다.
새 데이터를 삽입할 때에는 Firebase가 자동으로 생성하는 랜덤 key 값을 부여할 수도 있고, 직접 key 값을 정하여 부여할 수도 있다.

데이터에 자동 key 값을 부여하여 저장하려면 다음과 같은 코드를 작성하면 된다.

데이터베이스 객체.child("목록 이름").push(넣을 데이터)

init 함수에서

self.db = firebase.database()

이렇게 데이터베이스 객체를 self.db로 정의하였으므로, 데이터베이스 객체 부분에는 self.db를 적어준다.
목록 이름에는 person, 데이터 이름에는 tom_info를 적는다.

self.db.child("person").push(tom_data)

이렇게 해주면 person이라는 목록 안에 Tom의 인적사항이 고유한 랜덤 key 값과 함께 저장된다.


이번에는 자동 key 값이 아닌, 내가 마음대로 정한 값을 key로 부여해볼 것이다.
Tom의 이름인 "Tom"을 key로 설정해보자.
우리의 데이터베이스에 절대로 동명이인이 없어서, 사람의 이름을 key 값으로 써도 되는 상황이라고 가정하자.

작성해야 하는 코드의 구조는 다음과 같다.

데이터베이스 객체.child("목록 이름").child("key 값").set(데이터)

여기에 우리가 다루는 변수들을 올바르게 넣어주면 다음과 같은 코드가 된다.

self.db.child("person").child("Tom").set(tom_info)

이렇게 해주면 Tom의 인적사항이 Tom이라는 key 값과 함께 person 목록에 저장된다.


4) 데이터 조회

데이터 조회에는 기본적으로 get()을 사용한다.
예를 들어, person 안에 들어있는 모든 데이터를 가져오려면 다음과 같은 코드를 작성한다.

self.db.child("person").get() #person 안에 있는 모든 데이터를 리턴

특정 기준에 부합하는 데이터만 조회하고 싶다면 다음과 같이 추가적인 표현을 덧붙인다.

db.child("person").order_by_child("name").equal_to("Tom").get() #이름이 "Tom"인 객체만을 리턴한다.

5) 기존의 데이터 삭제

기존의 데이터를 삭제할 때에는 remove() 메소드를 사용한다.

key 값을 기준으로, 해당 key 값을 가지고 있는 데이터를 삭제할 수 있다.
다음과 같이 코드를 작성하면 된다.

데이터베이스 객체.child("목록 이름").child("key 값").remove()

만약 key 값이 아닌 다른 값을 기준으로 데이터를 삭제하고 싶다면, 먼저 목록 내의 모든 객체를 쭉 조회하면서 특정 값을 가진 객체를 조건문으로 찾아내어야 한다. 그 후 해당 객체의 key를 알아내어 삭제해야 한다.
예를 들어, 본 프로젝트에서 메뉴의 이름을 기준으로 특정 메뉴 데이터를 삭제하는 코드를 살펴보면 다음과 같다.

    #메뉴 데이터 삭제 함수
    def delete_menu (self, name):
        menus = self.db.child("menu").get()
        for menu in menus.each():
            value = menu.val()
            if value['menuname']==name:
                key = str(menu.key())
                self.db.child("menu").child(key).remove()

설명하자면 이렇다.

삭제하려는 메뉴의 이름이 name이라는 변수에 담겨있다.
먼저 menus라는 변수에 모든 메뉴 데이터 리스트를 받아온다.
그 후 for문을 돌려 모든 메뉴 데이터를 순서대로 살핀다.
살피던 중 메뉴의 이름 ( value['menuname'] )이 name 과 동일한 경우가 발견되면,
key() 메소드를 이용하여 해당 메뉴의 key 값을 알아온 뒤,
self.db.child("menu").child(key).remove() 를 통해 삭제하고 있다.


6) 기존의 데이터 수정

기존의 데이터를 수정할 때에는 update() 메소드를 사용한다.

코드의 구조는 다음과 같다.

데이터베이스 객체.child("목록 이름").child("key 값").update({"업데이트할 어트리뷰트": "데이터 값"})

위에서 만들었던 Tom의 인정사항 데이터를 수정해보자.
현재 데이터베이스에는 Tom의 데이터가 다음과 같이 저장되어 있고, key 값은 "Tom"인 상황이다.

tom_info={
    "name":"Tom",
    "age":"20",
    "job":"student"
    }

Tom의 나이를 30세로 수정하려면 다음과 같은 코드를 작성한다.

self.db.child("person").child("Tom").update({"age":"30"})

Tom의 직업을 요리사로 수정하려면 다음과 같은 코드를 작성한다.

self.db.child("person").child("Tom").update({"job":"cook"})

본 프로젝트에서 사용한, 식당 정보 수정 코드를 살펴보면 다음과 같다.

    #식당정보 수정 함수
    def edit_resinfo(self, key, data, img_path):
		#data와 img_path에는 사용자가 입력한 새 값들이 담겨있음
        #key에는 수정 대상인 식당의 Key 값이 담겨있음
        #해당 Key로 DB를 탐색하여, 그 데이터의 값들을 하나하나 update
        self.db.child("restaurant").child(key).update({"name": data.get("name")})
        self.db.child("restaurant").child(key).update({"type": data.get("type")})
        self.db.child("restaurant").child(key).update({"location": data.get("location")})
        self.db.child("restaurant").child(key).update({"locatedetail": data.get("locatedetail")})
        self.db.child("restaurant").child(key).update({"phone": data.get("phone")})
        self.db.child("restaurant").child(key).update({"monday": [data.get("start1"),data.get("end1"),data.get("bstart1"),data.get("bend1")]})
        self.db.child("restaurant").child(key).update({"tuesday": [data.get("start2"),data.get("end2"),data.get("bstart2"),data.get("bend2")]})
        self.db.child("restaurant").child(key).update({"wednesday": [data.get("start3"),data.get("end3"),data.get("bstart3"),data.get("bend3")]})
        self.db.child("restaurant").child(key).update({"thursday": [data.get("start4"),data.get("end4"),data.get("bstart4"),data.get("bend4")]})
        self.db.child("restaurant").child(key).update({"friday": [data.get("start5"),data.get("end5"),data.get("bstart5"),data.get("bend5")]})
        self.db.child("restaurant").child(key).update({"saturday": [data.get("start6"),data.get("end6"),data.get("bstart6"),data.get("bend6")]})
        self.db.child("restaurant").child(key).update({"sunday": [data.get("start7"),data.get("end7"),data.get("bstart7"),data.get("bend7")]})
        self.db.child("restaurant").child(key).update({"img_path": img_path})

0개의 댓글