Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Flask 공식 튜토리얼 따라하기 #1

Flask 개발환경을 구성했으므로 이제 Flask 애플리케이션을 개발할 차례가 되었다. 문서를 보면서 대충 눈치껏 만들어 보려고 했지만, 막상 만들려고 하니까 모르는 부분이 너무 많았다.

  • 프로젝트 폴더/파일 구조는 어떻게 가져가야 하는가?
  • 웹 애플리케이션의 진입점이 되는 라우팅 처리와 HTTP 메서드 처리는 어떻게 하는가?
  • 뷰 템플릿인 Jinja2는 어떻게 사용해서 페이지를 제공할 수 있는가?
  • HTML을 반환할 때와 JSON을 응답할 때는 어떻게 처리해야 하는가?(content negotiation?)
  • DB 연동을 어떻게 해야 하는가?(추가로 SQL Alchemy?)
  • 정적 파일은 어떻게 제공하는가?
  • Flask를 설치할 때 Werkzeug, itsdangerous, click 패키지를 함께 설치하는데 어떤 패키지일까?
  • 테스트는 어떻게 작성하는가?

예제 코드를 보고 해보다가 안 되겠다는 느낌이 들어서 Python 환경에 대해서 궁금한 걸 하나하나 정리하고 나니 이해하기가 한결 편해져서 Flask도 튜토리얼부터 차근차근히 해보기로 했다. 중간중간 자꾸 대충 이해할 것 같은 기분에 건너뛰고 싶은 충동이 느껴져서 이번에도 모르는 부분을 다 찾아보면서 일단 Flask 애플리케이션 개발 및 Python 개발에 대해서 좀 더 이해해 보기로 했다.

튜토리얼은 Flask로 간단한 블로그 애플리케이션 flaskr을 만드는 과정을 설명한다. 이 블로그는 설정해 놓은 사용자로 로그인을 할 수 있고 사용자는 제목과 내용(HTML)으로 글을 올릴 수 있고 첫 페이지에서 올려진 글 목록을 보여준다.

0 단계: 폴더 생성

└── flaskr/
    └── flaskr/
        ├── static/
        └── templates/

튜토리얼에서는 위와 같은 폴더 구조를 제안하고 있다. 이게 Flask의 관례인지 Python의 관례인지는 아직 잘 모르겠지만 프로젝트명으로 된 루트 디렉터리 아래 같은 이름에 디렉터리가 또 있다는 부분 그러니까 flaskr/flaskr의 형태가 좀 어색하게 느껴졌다. src같은 게 더 자연스럽지 않나 생각하는데 느낌상 배포 등을 할 때 이름을 지정하는 것과 관련이 있지 않을까 상상 정도를 해본다. 그러고 보니 회사에서 보던 Django 프로젝트도 저런 형태의 폴더구조를 본 기억이 났다. FLASK_APP=flaskr.factory:create_app() 처럼 환경변수를 지정할 때 FLASK_APP=src.factory:create_app() 같은 형태가 되면 이상하니까 그런 게 아닐까 정도의 생각이다.

여기서 static/ 폴더는 .js.css 같은 정적 파일을 모아두는 폴더고 templates/Jinja2의 뷰 파일을 두는 폴더다.

1 단계: 데이터베이스 스키마

drop table if exists entries;
create table entries (
  id integer primary key autoincrement,
  title text not null,
  'text' text not null
);

위 내용으로 flaskr/flaskr/schema.sql 파일을 생성한다. 블로그에서 사용할 간단한 테이블이다. Alembic같은 마이그레이션 도구를 사용하는 것은 아니지만 drop 테이블이 있는 것으로 보아 마이그레이션 도구와 비슷하게 사용하려는 것으로 보인다. 참고로 이 튜토리얼에서는 SQLite를 쓰는데 나는 SQLite로 애플리케이션을 만드는 경우가 거의 없어서 크게 관심 없지만 일단 튜토리얼이라서 그대로 따라 해보고 있다.

2 단계: 애플리케이션 설정 코드

여기서는 애플리케이션을 설정하는 코드를 작성한다. flaskr/flaskr 아래 flaskr.py 파일을 다음의 내용으로 만든다.

import os
import sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, \
     render_template, flash


app = Flask(__name__) # create the application instance :)
app.config.from_object(__name__) # load config from this file , flaskr.py

# Load default config and override config from an environment variable
app.config.update(dict(
    DATABASE=os.path.join(app.root_path, 'flaskr.db'),
    SECRET_KEY='development key',
    USERNAME='admin',
    PASSWORD='default'
))
app.config.from_envvar('FLASKR_SETTINGS', silent=True)

import 다음에는 2줄을 띄우는 게 관례인 것 같다. PEP 8에는 최상위 함수와 클래스 정의는 2줄을 띄운다고 나와 있는데 최상위 함수의 기분이 좀 헷갈리지만 일단 import 구문과 코드를 구분하는데 2줄을 띄우는 정도는 이해했다. 난 2줄을 띄우는 습관이 없어서 아직 엄청 어색하지만... 관례가 익숙지 않으므로 flake8 같은 도구를 사용해서 작성한 코드를 계속 검색할 수 있게 해야할텐데 이 방법은 이후 더 찾아봐야 할 것 같다.

app = Flask(__name__)는 주석 설명대로 Flask 인스턴스를 생성한 것이다. python에서 __name__은 모듈의 이름을 뜻한다. 모듈의 이름이란 게 애매한 의미로 느껴지는 데 테스트해보면 파일명이 바로 모듈의 이름이 되는 것으로 보인다. 그래서 이 튜토리얼은 flaskr를 만들기 때문에 시작 파일이 flaskr.py가 되는 것 같다. 여기서 모듈 이름이란 의미는 flaskr.pyimport해서 사용하면 __name__의 값이 flaskr이 된다. 추가로 python flaskr.py처럼 직접 실행할 때는 __name__의 값이 __main__가 된다. 파이썬 코드에서 많이 본 if __name__ == "__main__":import해서 사용하는 경우인지 직접 실행한 경우인지를 구분하기 위한 것이다.

그다음은 app.config.update()에 딕션어리를 넣고 있다. 코드를 보면 데이터베이스 관련 설정값을 추가하는 것으로 충분히 예측할 수 있다. app.conf를 출력해 보면 다음과 같이 출력된다.

<Config {
  'DEBUG': False,
  'TESTING': False,
  'PROPAGATE_EXCEPTIONS': None,
  'PRESERVE_CONTEXT_ON_EXCEPTION': None,
  'SECRET_KEY': None,
  'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31),
  'USE_X_SENDFILE': False,
  'LOGGER_NAME': 'flaskr',
  'LOGGER_HANDLER_POLICY': 'always',
  'SERVER_NAME': None,
  'APPLICATION_ROOT': None,
  'SESSION_COOKIE_NAME': 'session',
  'SESSION_COOKIE_DOMAIN': None,
  'SESSION_COOKIE_PATH': None,
  'SESSION_COOKIE_HTTPONLY': True,
  'SESSION_COOKIE_SECURE': False,
  'SESSION_REFRESH_EACH_REQUEST': True,
  'MAX_CONTENT_LENGTH': None,
  'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200),
  'TRAP_BAD_REQUEST_ERRORS': False,
  'TRAP_HTTP_EXCEPTIONS': False,
  'EXPLAIN_TEMPLATE_LOADING': False,
  'PREFERRED_URL_SCHEME': 'http',
  'JSON_AS_ASCII': True,
  'JSON_SORT_KEYS': True,
  'JSONIFY_PRETTYPRINT_REGULAR': True,
  'JSONIFY_MIMETYPE': 'application/json',
  'TEMPLATES_AUTO_RELOAD': None}>

지금 당장은 각 값의 사용 용도는 알 수 없지만 대충 예상할 수는 있다. 앞의 코드처럼 딕션어리로 업데이트한 뒤의 값을 보면 SECRET_KEY처럼 이미 있던 키는 갱신되고 새로운 키는 추가되는 것을 확인할 수 있다.

app.config를 업데이트하는 값을 보면 DATABASE=os.path.join(app.root_path, 'flaskr.db')부분이 있다. os.path는 경로와 관련된 모듈이다. os.path.join()은 전달한 값을 연결한 경로로 반환한다. app.root_pathFlask 애플리케이션 객체에 있는 속성인데 문서를 보면 자동으로 애플리케이션의 루트 경로로 잡힌다. 그래서 flaskr/flaskr/flaskr.py 파일이 있는 /Users/outsider/flaskr/flaskr/가 이 값이 되고 flaskr.db와 연결했으므로 최종적으로 /Users/outsider/flaskr/flaskr/flaskr.db가 된다.

인스턴스 폴더

튜토리얼의 설명을 보면 OS가 프로세스의 워킹 디렉터리를 할 수 있지만 보통 한 프로세스에서 여러 애플리케이션을 띄울 수 있으므로 app.root_path를 사용한다고 나와 있고 데모가 아닌 실제 애플리케이션에서는 Instance Folder를 추천한다고 나와 있다. 그래서 인스턴스 폴더가 궁금해졌다.

인스턴스 폴더는 Flask 애플리케이션을 생성할 때 app = Flask(__name__, instance_path='/path/to/instance/folder')처럼 지정할 수 있다. 실제로 어떤 상황에 유용한지는 Flask를 더 써봐야겠지만 문서를 보면 app.root_path는 패키징되지 않은 경우에만 잘 동작한다고 되어 있다. 모듈로 PyPI에 올리는 경우가 아닌 Flask처럼 웹 애플리케이션을 작성하는 상황에서 Python에서 말하는 패키징이라는 개념을 아직 제대로 이해하지 못했지만, Flask 애플리케이션을 다른 곳에서 불러다 사용하는 경우 root_path는 패키지의 루트 경로가 될 것이므로 불러올 구성 파일 같은 경우도 패키지에 포함되어야 한다. 이때 instance_path를 다른 경로의 파일을 참조하도록 할 수 있고 패키지에 포함하지 않아도 되므로 시크릿 정보가 있어서 설정 파일을 다른 곳에 두거나 외부에서 구성파일을 받고자 하는 경우 사용하는 것으로 이해했다. 아니면 서버 배포를 하는 경우도 스테이지나 프로덕션 서버별로 파일을 다르게 지정해서 배포할 수 있으니 이때도 사용할 수 있다.

지정하지 않으면 루트 경로의 /instance가 값이 되고 설치한 모듈이나 패키지일 때 /path/to/site-packages/myapp 등이 된다. 지정할 때는 반드시 절대 경로로 지정해야 하고 상대경로를 사용해야 하는 경우는 app = Flask(__name__, instance_relative_config=True)처럼 설정해 주어야 한다.

설정 파일 분리

튜토리얼에서는 flaskr.py 파일에 직접 설정하고 있지만, 보통은 별도의 .ini.py 파일로 분리한 뒤 가져와서 사용하는 것을 추천한다. 보통 이 파일은 외부에서 제어할 수 있는 게 좋으므로 튜토리얼에서도 환경변수에서 가져오는 방법도 설명하고 있다. app.config.from_envvar('FLASKR_SETTINGS', silent=True)를 사용하면 FLASKR_SETTINGS라는 환경변수의 경로에 있는 파일에서 값을 가져와서 Config 객체에 설정할 수 있다. 여기서 파일의 종류가 문서에 명확하게 나와 있지 않아서 헷갈리는데 찾아보니 보통은 cfg 파일을 사용하는 것 같다.

그래서 위와 똑같은 설정을 하려면 아래와 같이 config.cfg를 만들고

DATABASE='/Users/outsider/flaskr/flaskr/flaskr.db'
SECRET_KEY='development key'
USERNAME='admin'
PASSWORD='default'

FLASKR_SETTINGS=/Users/outsider/flaskr/config.cfg 환경 변수를 지정하면 Config 객체에 위의 내용이 들어가게 된다. 이때 silent=True를 하면 해당 환경변수가 없어도 오류가 나지 않는다.

JSON 파일을 사용하고 싶다면 app.config.from_json(os.environ['FLASKR_SETTINGS'], silent=True)처럼 사용할 수 있다. 이 외에도 파이썬 객체에서 가져오는 from_object 등 여러 가지가 있어서 필요 한대로 설정 파일을 가져와서 사용할 수 있다.

데이터베이스 접속

이번 튜토리얼의 마지막으로 flaskr/flaskr/flaskr.py 파일에 데이터베이스(여기서는 SQLite3) 접속에 사용한 함수를 추가한다.

def connect_db():
    """Connects to the specific database."""
    rv = sqlite3.connect(app.config['DATABASE'])
    rv.row_factory = sqlite3.Row
    return rv

튜토리얼에 아직 이 부분을 사용하는 코드가 나오지 않아서 정확한 위치를 모르겠는데 그냥 flaskr.py에 추가했다.

2017/11/12 22:32 2017/11/12 22:32