ORM 이란?
객체 관계 매핑(Object-relational mapping)의 약자로
객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 것을 말합니다.
자세하게는, 객체지향 프로그래밍에서 쓰이는 객체 라는 개념을 구현한 클래스와
관계형 데이터베이스에서 쓰이는 데이터인 테이블을 자동으로 매핑하는 것을 말합니다.
클래스와 테이블은 서로가 호환되는 것을 고려하고 만들어진 것이 아니기 때문에
객체 모델과 관계형 모델 간의 불일치가 생기게되는데
ORM을 이용하면 데이터베이스 접근을 프로그래밍 언어의 관점에서 맞출 수 있고
객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 불일치를 해결할 수 있습니다.
덕분에 SQL 쿼리(query)라는 구조화된 질의를 작성하고 실행하는 등의 복잡한 과정 없이
파이썬 문법만으로도 데이터베이스를 다룰 수 있게 됩니다.
SQLAlchemy
ORM 중 하나로 파이썬에서 사용할 수 있는 라이브러리 중 하나로
일반적인 관계형 데이터베이스(mysql, sqlite, oracle 등)를 지원합니다.
Flask-Login
Flask 프레임워크를 위한 사용자 세션관리 기능을 제공하는 라이브러리로
사용자 정보를 세션에 저장할 수 있습니다.
작업을 하기에 앞서 위에 두 라이브러리를 먼저 설치해줄 겁니다.
> pip install Flask-SQLAlchemy
> pip install Flask-Login
가상환경이 잘 활성화된걸 확인했다면 준비를 모두 마친 것입니다.
'blog' 라는 디렉토리를 만들고 그 안에 __init__.py 파일을 하나 생성해줍니다.
그 다음 __init__.py에 아래 코드를 입력해주세요.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from os import path
from flask_login import LoginManager
# app을 만들어주는 함수를 지정해 주자.
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = "IFP"
@app.route("/")
def home():
return "home"
@app.route("/about_me/")
def about_me():
return "introduce about myself..."
return app
그 다음 app.py에 아래 코드를 입력해주세요.
from blog import create_app
if __name__ == "__main__":
app = create_app()
app.run(debug=True)
입력이 끝났다면 두 코드의 연결 지점을 찾아보겠습니다.
app.py에 form blog import create_app부분을 보시면
blog라는 디렉토리에서 create_app() 함수를 import한 것을 알 수 있습니다.
이를 통해 __init__.py에 작성한 create_app() 함수를 app.py에서 불러와 사용했다는 것을 알 수 있습니다.
한 번 실행해보겠습니다.
> python ./app.py
두 가지 페이지 모두 잘 작동하는 걸 확인할 수 있습니다.
이제 여기서 한 가지 생각해 볼 수 있습니다.
예를 들어 소개 페이지 /blog/about, 회원가입 페이지 /blog/sign_up, 로그인 페이지 /blog/login를 추가하고 싶습니다.
그렇다고 가정했을 때 아마 생각나는 코드는 아래와 같을 것입니다.
def create_app():
app = Flask(__name__) # Flask app 만들기
app.config['SECRET_KEY'] = "IFP"
@app.route("/")
def home():
return "home"
@app.route("/blog/about/")
def about():
return "introduce about myself..."
@app.route("/blog/sign_up/")
def sign_up():
return "sign up page..."
@app.route("/blog/login/")
def login():
return "login page..."
근데 여기서 보면 동일한 경로에 해당하는 URL이 많습니다.
지금과 같이 간단한 구조와는 다르게
구조가 복잡해질수록 새로운 URL 매핑이 필요할 때마다 라우팅 함수를 계속 추가해야 하기 때문에
코드가 길어지면서 더욱더 크고 복잡한 함수가 될 수 있습니다.
하지만 블루프린트(Blueprint)를 사용한다면 이 문제를 해결할 수 있습니다.
BluePrint
큰 application을 단순화시키는 역할을 하고, 공통적인 패턴을 지원하는 목적으로
라우트 함수들을 보다 구조적으로 관리할 수 있게 됩니다.
app.py에서 blog로 매핑될 데코레이터 및 함수는 views.py에 가져와 정의하도록 할 겁니다.
views.py 파일을 생성해줍니다.
여기서 views.py 에는 홈페이지, 소개 페이지, 포스트 생성, 삭제, 조회, 리스트 페이지 등에 관한 것들을 다룰 예정입니다.
추가로 로그인, 로그아웃 등의 로그인 관련 기능들을 처리할 auth.py 파일도 생성해주고 아래 코드를 입력해줍니다.
views.py
from flask import Blueprint
views = Blueprint("views", __name__)
@views.route("/")
def blog_home():
return "This is Blog home."
@views.route("/about_me")
def about_me():
return "Introduce about myself..."
auth.py
from flask import Blueprint
auth = Blueprint("auth", __name__)
@auth.route("/login")
def login():
return "Login page."
@auth.route("/logout")
def logout():
return "Logout page."
@auth.route("/sign-up")
def signup():
return "sign up!"
그 다음 __init__.py에 등록을 해줍니다.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from os import path
from flask_login import LoginManager
def create_app():
app = Flask(__name__) # Flask app 만들기
app.config['SECRET_KEY'] = "IFP"
from .views import views
app.register_blueprint(views, url_prefix="/blog")
return app
url_prefix
URL 프리픽스는 views.py 파일에 있는 함수들의 URL 앞에 항상 붙게 되는 프리픽스 URL을 의미합니다.
바로 "/blog" 이것으로 뒤에 "/" 가 붙거나 "/about_me"가 붙으면 해당 함수가 수행되는 것을 알 수 있습니다.
이를 통해 blueprint는 큰 어플리케이션을 단순화시키고 확장 프로그램과 라이브러리 등록을 위한 중심 수단으로도 사용됨을 짐작할 수 있습니다.
플라스크의 render_template()
웹 브라우저에서 웹 페이지를 볼 때에는 HTML 이라는 마크업 언어를 사용하게 됩니다.
Flask에서도 HTML 파일들을 제공하면서 웹 페이지를 클라이언트에게 보여줄 수 있습니다.
그중 하나로 파이썬 코드에서 직접적으로 쓰는 방식으로 HTML을 리턴하도록 하는 작업이 있습니다.
def templete(context,name):
return f'''<html>
<body>
{context}
<form action = "/login" method = "post">
<p>Enter Name: {name}</p>
<p><input type = "text" name = "myName" /></p>
<p><input type = "submit" value = "submit" /></p>
</form>
</body>
</html>'''
하지만 이 방식은 코드의 가독성이 떨어지고 관리하기 어렵습니다.
또 다른 방식으로 render_template 이라는 메소드로 html 파일들을 불러올 수 있습니다.
프로젝트 내에 "templates"라는 이름의 폴더를 기본 경로로 설정하도록 설계되어 있는데,
그럼으로써 폴더 내부에 있는 html 파일들을 관리할 수 있습니다.
'templates' 라는 디렉토리를 만들고 그 안에 home.html 파일을 하나 생성해줍니다.
그 다음 home.html에 아래 코드를 입력해주세요.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>render_template test</title>
</head>
<body>
<h1>Works well!</h1>
<P>GOOD!</P>
</body>
</html>
그 다음 view.py에 코드를 아래와 같이 수정해주세요.
from flask import Blueprint, render_template
views = Blueprint("views", __name__)
@views.route("/")
def blog_home():
return render_template("home.html")
@views.route("/about_me")
def about_me():
return "Introduce about myself..."
render_template 모듈을 import 하고, 적용할 html 파일을 설정합니다.
그 다음 설정해 둔 "/" URL로 들어가면 home.html 파일이 적용된 것을 확인할 수 있습니다.
render_template() 함수의 인수를 살펴보겠습니다.
첫 번째 인수 - 템플릿의 파일 이름
추가 인수 - 템플릿에서 참조하는 변수들에 대한 실제값을 표현하는 키/값
방금까지는 첫 번째 인수 만을 추가해줬지만
추가 인수 부분도 추가해보겠습니다.
views.py
@views.route("/")
def blog_home():
return render_template("home.html", name_A="123", name_B="456")
home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>render_template test</title>
</head>
<body>
<h1>Works well! {{name_A}}</h1>
<P>GOOD! {{name_B}}</P>
</body>
</html>
jinja2 템플릿 엔진
플라스크에서 제공하는 render_template()는 어플리케이션과 jinja2 이라는 템플릿 엔진과 통합되어있습니다.
여기서템플릿 엔진이란 템플릿과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어를 말합니다.
즉, 웹사이트 화면을 어떤 형태로 만들지 도와주는 양식을 말합니다.
home.html에 <body> 부분을 보시면
{{name_A}}, {{name_B}} 이런 식으로 중괄호가 2개로 둘러 쌓여진 것을 볼 수 있습니다.
이것은 받아들일 변수로 인식 됩니다.
render_template() 괄호 안에 렌더링 할 HTML 파일 이름에 이어 변수명을 키워드 인수를 추가해주고
템플릿 내에서 변수를 사용하려면 {{ 변수명 }} 이런 식으로 중괄호로 표현하면 됩니다.
이런 식으로 다룰 수 있도록 해주는게 바로 jinja 템플릿 엔진 입니다.
정적 파일
이제 부트스트랩을 통해 정적 파일을 다뤄보겠습니다.
아래 링크를 통해 템플릿을 다운받습니다.
그 다음 받은 html 파일들을 templates 폴더에 넣어줍니다.
그 다음 views.py에 home 템플릿 이름을 "index.html" 로 수정해줍니다.
from flask import Blueprint, render_template
views = Blueprint("views", __name__)
@views.route("/")
def blog_home():
return render_template("index.html")
근데 실행해보면, 아래 화면과 같이 CSS가 적용이 되지 않은 것을 확인 할 수 있습니다.
근데 여기서 위에 템플릿 구조를 봤을 때 동일한 부분이 보입니다.
바로 맨 윗부분과 맨 아랫부분입니다.
실제 코드를 비교해보면 완전히 동일한 코드가 존재함을 알 수 있습니다.
그렇다면 여기서 생각해볼 수 있습니다.
동일하게 중복되는 코드 base.html을 만들고
그것을 가지고 모든 템플릿 들을 관리할 수 없을 것 인지 말입니다.
근데 하나의 base.html을 가지고 어떻게 여러 템플릿들을 연결시킬지 고민입니다.
여기서 유용한 것이 바로 템플릿 상속 입니다.
템플릿 상속(Inheritance)
웹 사이트 레이아웃의 일관성을 유지하거나, header와 footer를 여러곳에 쓰기위해서 사용되는
jinja 템플릿 엔진의 또 하나의 예시이기도 합니다.
상속방법은 아래와 같습니다.
base.html
<html>
<head>
<title> Inheritance </title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
index.html
{% extends "base.html" %}
{% block content %}
<p> hello </p>
{% endblock %}
부모 문서 base.html을 만들고 자식문서가 들어갈 부분에 {% block content %} {% endblock %} 라고 작성합니다.
그 다음 자식 문서 index.html에는 가장 윗부분에 {%extends base.html(부모문서이름) %} 라고 작성하고
{% block content %}와 {% endblock %} 사이에 들어갈 내용을 작성해주면 끝입니다.
이제 실제로 base.html과 index.html을 만들어보겠습니다.
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<meta name="description" content=""/>
<meta name="author" content=""/>
{# ******************** #}
<title>{% block title %}{% endblock %}</title>
{# ******************** #}
<link rel="icon" type="image/x-icon" href="assets/favicon.ico"/>
<!-- Font Awesome icons (free version)-->
<script src="https://use.fontawesome.com/releases/v6.1.0/js/all.js" crossorigin="anonymous"></script>
<!-- Google fonts-->
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet"
type="text/css"/>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800"
rel="stylesheet" type="text/css"/>
<!-- Core theme CSS (includes Bootstrap)-->
<link href="css/styles.css" rel="stylesheet" />
</head>
<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="index.html">Start Bootstrap</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="index.html">Home</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="about.html">About</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="post_detail.html">Sample Post</a>
</li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="contact.html">Contact</a></li>
</ul>
</div>
</div>
</nav>
{# ******************** #}
{% block header %}{% endblock %}
<div class="content-wrapper">
{% block content %}{% endblock %}
{# ******************** #}
</div>
<!-- Footer-->
<footer class="border-top">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<div class="small text-center text-muted fst-italic">Copyright © Your Website 2022</div>
</div>
</div>
</div>
</footer>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="js/scripts.js"></script>
</body>
</html>
index.html
{% extends 'base.html' %}
{% block title %}This is home.{% endblock %}
{% block header %}
<!-- Page Header-->
<header class="masthead" style="background-image: url('assets/img/home-bg.jpg')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="site-heading">
<h1>Clean Blog</h1>
<span class="subheading">A Blog Theme by Start Bootstrap</span>
</div>
</div>
</div>
</div>
</header>
{% endblock %}
{% block content %}
<!-- Main Content-->
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<!-- Post preview-->
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">Man must explore, and this is exploration at its greatest</h2>
<h3 class="post-subtitle">Problems look mighty small from 150 miles up</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on September 24, 2022
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="post.html"><h2 class="post-title">I believe every human has a finite number of heartbeats. I don't intend to waste any of mine.</h2></a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on September 18, 2022
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">Science has not yet mastered prophecy</h2>
<h3 class="post-subtitle">We predict too much for the next year and yet far too little for the next ten.</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on August 24, 2022
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">Failure is not an option</h2>
<h3 class="post-subtitle">Many say exploration is part of our destiny, but it’s actually our duty to future generations.</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on July 8, 2022
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Pager-->
<div class="d-flex justify-content-end mb-4"><a class="btn btn-primary text-uppercase" href="#!">Older Posts →</a></div>
</div>
</div>
</div>
{% endblock %}
이렇게 잘 작동하는 것을 확인 할 수 있습니다.
근데 아직도 CSS가 적용되지 않는 것을 확인 할 수 있는데
url_for을 이용해 css,js 를 연결 시켜줄 수 있는 방법이 있습니다.
url_for
<link href="css/styles.css" rel="stylesheet"/>
<script src="js/scripts.js"></script>
현재 base.html코드에 나와있는 부분입니다.
하지만 주소가 제대로 입력되어있지 않기 때문에 적용이 되지 않는 상황입니다.
실제 경로를 보시면 static이란 폴더 안에 css와 js가 위치한 걸 확인할 수 있습니다.
url_for에 첫 번째 값에는 지정하고자 하는 파일에 속한 폴더 'static'을 입력해줍니다.
그 다음 추가적인 경로는 filename=''주소" 아래와 같이 입력해주면 됩니다.
<link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet"/>
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
추가적으로 배경화면도 바꾸고 싶다면 index.html 코드도 수정해줍니다.
<header class="masthead" style="background-image: url('{{ url_for('static', filename='assets/img/background.jpg')}}')">
다시 실행해보면 아래와 같이 잘 실행되는 모습입니다.
이제 다른 템플릿들도 수정 / 연결해주고, 추가적으로 URL도 생성해줍니다.
from flask import Blueprint, render_template
views = Blueprint("views", __name__)
@views.route("/")
def blog_home():
return render_template("index.html")
@views.route("/about")
def about_me():
return render_template("about.html")
@views.route("/contact")
def contact():
return render_template("contact.html")
@views.route("/categories-list")
def categories_list():
return render_template("categories-list.html")
@views.route("/login")
def login():
return render_template("login.html")
@views.route("/sign-up")
def sing_up():
return render_template("sign-up.html")
아래는 모두 완료됐을 때 화면입니다.
여기까지 라이브러리 설치부터 정적 파일까지 다뤄봤습니다!
'flask' 카테고리의 다른 글
Flask - 테스트 코드 / 관리자 페이지 / 카테고리, 게시물 (0) | 2022.08.10 |
---|---|
Flask - 회원가입/로그인/로그아웃 처리 [블로그 웹 애플리케이션 개발] (0) | 2022.07.27 |
Flask - python으로 쉽게 데이터베이스 다루기 (0) | 2022.07.07 |
Flask - HTTP Methods / URL Building / 데코레이터 / 변수 규칙 / 후행 슬래시에 관해 (0) | 2022.07.06 |
FLask 개발 환경의 첫 단계 (0) | 2022.07.03 |