[REAL Python – Flask] – “블로그 웹 애플리케이션 개발(0) – 프로젝트 생성, 패키지 설치, 기본 작업”
[REAL Python – Flask] – “블로그 웹 애플리케이션 개발(0) – 프로젝트 생성, 패키지 설치, 기본 작업”
https://youtu.be/GQcM8wdduLI 을 보고 정리/추가한 것입니다.
PyCharm 으로 프로젝트 생성
실제로 무언가를 만들어가면서 알아가 보겠습니다. 아래와 PyCharm 에서는 아래와 같이 프로젝트를 생성합니다.
이후, Templates 폴더와 Static 폴더를 삭제하고 파이썬 패키지를 하나 만들어 줍니다. (VSCode에서는, ‘blog’ 라는 디렉토리를 하나 만들고 그 안에 아무런 내용이 없는 __init__.py 파일을 하나 만들어 주면 됩니다.)
그리고, 우리는 플라스크 프로젝트에서 ORM을 사용할 것이므로 Flask-SQLAlchemy, Flask-Login을 아래의 명령어를 터미널에 입력해 설치해 줍니다. (venv) 처럼, 가상환경이 활성화되어 있는지 꼭 확인하세요!
pip install Flask-SQLAlchemy
pip install Flask-Login
__init__.py로 애플리케이션 팩토리를 만들어 보자!
먼저, __init__.py 에 관한 설명은 https://www.gdsanadevlog.com/13769 를 참고하는 것을 추천합니다.
__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__) # Flask app 만들기
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에 아래의 코드를 작성해 봅시다. 위에서 만든 create_app() 함수로 앱을 하나 만들고, 그것을 실행한 것을 볼 수 있네요.
맨 위의 from blog import create_app
이 있습니다. app.py 를 실행하면, import 된 create_app() 가 불러와집니다. 만약, create_app에 print(“안녕하세요”) 와 같은 문구를 추가하고 app.py를 실행하면 “안녕하세요” 가 터미널에 찍힐 겁니다.
아무튼, 중요한 건 __init__.py 에 작성한 create_app 함수를 app.py에서 불러와 사용했다는 겁니다.
from blog import create_app
if __name__ == "__main__":
app = create_app()
app.run(debug=True)
그리고, 터미널에서 python ./app.py 로 앱을 실행해 보겠습니다.
서버는 성공적으로 잘 열렸습니다. 지금은 홈 페이지, 자기소개 페이지 2개의 페이지만 존재합니다. 그런데, 만약에 자신만의 쇼핑몰 페이지도 /shop으로 추가하고 싶고, 쇼핑몰의 제품 페이지들도 /shop/products 으로 추가하고 싶고, 쇼핑몰의 로그인 페이지도 /shop/login 으로 추가하고 싶다고 가정해 봅시다. 물론 이러한 경우 말고도 추가하고 싶은 경우의 수는 엄청 많을 겁니다.
(실습하지 않아도 되는 코드입니다!) 첫 번째로 생각해 볼 수 있는 방법은, 위의 상황처럼 create_app() 에 우리가 원하는 경우의 수를 모조리 추가해 버리는 것입니다.(물론 함수명은 달라야 하겠죠..) 아직은 위의 예시에서는 5개의 라우팅만 존재하지만, 만약 웹 페이지 규모가 커지고 복잡해진다면 create_app은 엄청 복잡해질 겁니다. 플라스크에서는, 이러한 문제를 해결하기 위해서 BluePrint라는 것을 활용합니다.
__init__.py 를 아래의 내용으로 수정하고, 새로운 파일을 몇 개 만들어 주겠습니다.
views.py
, auth.py
가 바로 그것들입니다. __init__.py
와 같은 레벨에 만들어 주면 됩니다.
- auth.py 에서는 로그인, 로그아웃 등의 로그인 관련 기능들을 처리할 겁니다.
- views.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..."
이번에는 만들어준 Blueprint를 __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__) # Flask app 만들기
app.config['SECRET_KEY'] = "IFP"
from .views import views
# blueprint 등록, '/blog' 를 기본으로 한다.
app.register_blueprint(views, url_prefix="/blog")
return app
그리고, 서버를 실행하고 나서 http://127.0.0.1:5000/blog/ 에, 그리고 http://127.0.0.1:5000/blog/about_me 에 접속해 봅시다. 원하던 대로 잘 분류되어 나타나는 것을 알 수 있을 겁니다.
url_prefix="/blog"
코드 덕분에, /blog 에 “/” 가 붙으면 views의 blog_home() 함수가 수행되고, /about_me
가 붙으면 about_me()
함수가 수행되는 것을 볼 수 있네요. 플라스크에서는 이와 같이 블루프린트를 이용해서 url을 관리합니다.
플라스크의 render_template()
지금까지는 어떤 HTML 파일을 사용하는 대신 return “<h1>This is Home page.</h1>” 와 같이 문자열을 리턴했습니다. 이번에는 HTML 템플릿을 사용해서 페이지를 구성해 볼 것입니다.
먼저, views.py 와 auth.py에 아래와 같이 render_template를 임포트해 줍니다.
그리고, blog 패키지 아래에 templates 라는 디렉토리를 만들고, 그 아래 home.html 이라는 임의의 html 파일을 작성해 줍니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog Home Page!</title>
</head>
<body>
<h1>WELCOME!</h1>
<P>this is blog home page...</P>
</body>
</html>
그리고, views.py를 아래와 같이 수정해 보겠습니다.
그리고, 우리가 설정해 둔 url로 들어가 보면 실제로 html 파일이 적용된 것을 볼 수 있네요! 플라스크는 기본적으로 “templates” 폴더에서 템플릿을 찾습니다.
그런데 render_template() 는 어떻게 생겼을까요..? 실제로 플라스크에서 render_template() 가 어떻게 구성되어 있는지, 함수가 구현되어 있는 곳으로 가 보겠습니다.
render_template() 함수는 flask 아래의 templating.py 안에 정의되어 있습니다. 주석 부분을 보면 template_name_or_list 와 context를 인자로 받는다고 설명되어 있네요.
그러면, context도 한번 인자로 넣어서 사용해 보겠습니다.
결과로는 위처럼, 우리가 만든 context가 화면에 잘 적용된 것을 볼 수 있네요. 이는 플라스크의 jinja 템플릿 엔진 덕분입니다. {{}}
처럼 중괄호 2개가 있으면, jinja 템플릿 엔진은 그것을 변수로 인식하고, 전달된 context에서 적절한 값을 찾아 렌더링하게 됩니다.
jinja 템플릿 엔진에서는 for문, if문과 같은 문법들도 사용할 수 있는데, 그것들은 사용하면서 천천히 다뤄 보겠습니다.
Bootstrap 사용하기 : flask가 정적 파일을 다루는 방법
https://startbootstrap.com/theme/clean-blog 에서 템플릿을 다운로드받아 줍니다. 필자는 디자인에는 정말 젬병입니다. ㅋ
다운로드 받아 보면, JS, CSS 파일들이 같이 들어있는 것을 확인할 수 있습니다. 플라스크에서는 정적 파일들을 어떻게 관리할까요? 플라스크 공식 문서에서는 다음과 같이 소개하고 있네요.
첫째로는 받은 4개의 html 파일들을 templates 폴더에 넣어 줍니다. 아쉽지만(?) 지금까지 있던 파일들을 모두 삭제해 줍니다.
그리고 나서, home에 템플릿 이름을 “index.html” 로 바꾸어 실행해 봅시다.
서버를 실행해 보면 무언가 깨져 있죠? CSS가 적용되지 않아서 그렇습니다.
그 전에 잠시 다운로드받은 템플릿의 구조를 볼까요?
세 개의 페이지들을 보면 무언가 비슷한 점이 보일 겁니다.
샘플 포스트 탭을 클릭해서 들어가 보면,
다른 것은 몰라도 왼쪽의 Start bootstrap 부분은 모든 페이지가 공유한다는 것을 알 수 있습니다. 그 말은, 모든 템플릿에서 같은 html 코드를 공유한다는 것이죠!
그 점을 활용해 봅시다. templates 폴더 아래에 base.html 을 만들고, 아래의 코드를 넣어 봅시다.
{# <nav>, <footer> 만 있는 파일 #}
<!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="{{ url_for('static', filename='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="{{ url_for('static', filename='js/scripts.js') }}"></script>
</body>
</html>
모든 템플릿들은 위의 base.html을 상속받아 만들어질 겁니다. 기본이 되는 부분들은 그대로 두고, 변화하는 부분만 작성하자는 것이죠!
위의 코드에서 어떻게 정적파일을 처리하는지가 나와 있네요. 먼저, 전제는 템플릿들의 모든 js,css,assets를 static 폴더 아래에 넣는 것입니다.
그리고, <script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
처럼 url_for을 활용해서 정적 파일들을 불러와 줍니다.
jinja 템플릿 엔진의 상속
위의 코드를 보면 {# 변화하는 부분 #}
이라는 주석과 함께 {%block 어쩌고저쩌고 %}{%endblock %}
과 같은 정체불명의 코드가 있죠. 먼저 변화된 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_detail.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_detail.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_detail.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_detail.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 %}
먼저 달라진 것은 <html> <nav> <title> 과 같은 태그들이 과감하게 생략되었습니다. 그것은, 맨 위의 {% extends 'base.html' %}
이라는 코드 덕분입니다. base.html
을 상속받아 사용하겠다는 것이죠. 앞서 base.html
에 {# 변화하는 부분 #} {% block header %}{% endblock %}
와 같은 코드들이 있던 것, 기억하시나요?
그리고 나서 위의 index.html
을 또 보면, {% block header %}
와 같은 코드들이 또 들어있는 것을 확인할 수 있습니다. 아~ index.html
의 {%block header%}{%endblock%}
안에 들어가는 내용들이, base.html
의 {%block header%}{%endblock%}
부분에 들어가는 겁니다. 그렇기에 또 변화하는 부분인 content, header, title 등도 {%block %}
을 통해서 처리되었습니다.
서버를 실행하고 접속해 보면 원했던 대로 잘 동작하는 것을 볼 수 있네요. 이미지 또한 정적 파일이므로, url_for을 통해서 처리할 수 있겠죠?
저는 아래와 같은 템플릿 규칙들을 만들었습니다.
그렇기에, 각각의 디렉토리에 다음과 같은 템플릿들을 작성하였습니다.
현재까지의 라우팅 코드들은, 다음과 같습니다.
# auth.py
from flask import Blueprint, render_template, redirect
auth = Blueprint("auth", __name__)
@auth.route("/login")
def login():
return render_template("login.html")
@auth.route("/logout")
def logout():
return redirect("views.blog_home") # 로그아웃하면 views의 blog_home으로 리다이렉트됨
@auth.route("/sign-up")
def signup():
return render_template("signup.html")
# views.py
from flask import Blueprint, render_template
views = Blueprint("views", __name__)
@views.route("/")
def home():
return render_template("index.html")
@views.route("/about")
def about():
return render_template("about.html")
@views.route("/categories-list")
def categories_list():
return render_template("categories_list.html")
@views.route("/post-list")
def post_list():
return render_template("post_list.html")
@views.route('posts/<int:id>')
def post_detail():
return render_template("post_detail.html")
@views.route("/contact")
def contact():
return render_template("contact.html")
다음에는, 로그인을 다뤄 볼 겁니다!