본문 바로가기
Python/Flask

Flask + PostgreSQL + React - 1

by 쿠리의일상 2024. 3. 5.

https://youtu.be/RcQwcyyCOmM?si=4LxZ80Ezl3Wts3yG

 

프로젝트 구성상 해당 조합을 사용하게 되어 우연히 발견한 강의이다. 플라스크는 아직도 어떻게 설계해야할지 막막해서 강의를 들으며 찬찬히 내용을 정리해볼 예정이다.

 

pipenv

파이썬은 기본적으로 가상환경에 패키지를 다운받거나 전역으로 다운 받는 방법만 사용했었다. 그런데 해당 라이브러리가 있다는 것을 알게 됐고 패키지를 프로젝트 단위로 관리할 수 있도록 도와주는 패키지 관리 도구라고 한다. 파이썬에서 공식으로 권장하는 패키지 관리 툴이라니까 사용 안할 이유가 없을 것이다.

마치 우리가 리액트든 넥스트든 프로젝트 안에 package.json 파일로 여러 라이브러리들을 관리하듯 해당 라이브러리가 있으면 비슷하게 가능해진다.

pip install pipenv

다운 받은 다음 프로젝트가 실행될 폴더에 들어가서 아래의 명령어를 실행해주면 Pipfile 이라는 파일이 생기면서 해당 프로젝트용 가상환경이 생성된다.

pipenv shell

 

해당 Pipfile 파일이 바로 package.json 같은 프로젝트의 메타 정보가 담기는 파일이 된다. packages 항목 아래에 이제부터 다운 받게 될 패키지들이 나열될 것이다.

 

그럼 env를 사용하여 파이썬 파일을 실행해주고자 한다면 어떻게 해야할까?

pipenv run python

위의 명령어를 사용하여 실행이 가능하다.

 

프로젝트 가상환경 비활성화

exit

 

패키지 다운로드

pipenv install 패키지명

추가적으로 --dev 옵션을 주어 개발용 패키지와 프로덕션 패키지를 구분해줄 수 있다. 개발용 패키지의 경우 Pipfile에서 [dev-packages] 항목에 들어가게 된다.

 

Pipfile.lock 파일은 pipenv 가 설치된 패키지의 버전과 의존하는 다른 패키지들의 버전을 기억하기 위해 사용되는 파일로, 직접 수정하면 안된다. (package-lock.json과 유사)

 

작업 중이던 프로젝트를 받았다면

리액트 프로젝트의 npm install 명령어와 마찬가지로 아래의 명령으로 협업을 위한 개발 환경을 설정할 수 있다.

pipenv install

Pipfile 과 Pipfile.lock 파일로 누구나 동일한 가상 환경과 버전의 패키지를 설치할 수 있다.

 

해당 강의에서 필요한 패키지를 다운 받고 난 후의 모습이다.

기존의 파이썬 프로젝트에 패키지들의 버전을 작성했던 requeirements.txt 대신 pipenv 패키지를 사용한 Pipfile 파일을 사용하면 더 좋을 것 같다.

 

SQLAlchemy 와 Flask 연동

기존의 강의에선 SQLALCHEMY_DATABASE_URI 키를 postgres 로 시작했지만 postgresql 이어야 한다.

sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres

이런 에러가 발생한다. 관련 내용은 링크를 확인해보자. https://stackoverflow.com/questions/62688256/sqlalchemy-exc-nosuchmoduleerror-cant-load-plugin-sqlalchemy-dialectspostgre

 

sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres

I'm trying to connect to a Postgres database with SQLAlchemy. I've installed psycopg2. However, I get the error sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres. Ho...

stackoverflow.com

 

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://유저네임:비밀번호@호스트:포트번호/DB명'
db = SQLAlchemy(app)

이렇게 하면 현재 나의 포스트그리와 플라스크가 정상적으로 연동될 것이다.

 

flask-dotenv

그럼 당연히 DB 관련 내용은 암호화를 해줘야 할 것이다. flask-dotenv 를 위한 .flaskenv 파일을 생성해준다. 해당 파일은 실행시켜줄 app.py 와 동일한 위치에 생성해준다.

FLASK_APP = app
FLASK_ENV = development

기본적으로 FLASK_APP 과 FLASK_ENV 를 지정해준다. FLASK_APP 은 flask run 으로 실행했을 때 app.py 가 아니고, 특정 파일명을 넣어주면 찾아줄 수 있게끔 이름을 지정해준다.

FLASK_ENV 는 현재 서버가 실행되는 환경을 개발 환경인지 프로덕션 환경인지 선택해줄 수 있다.

그리고 암호화를 위한 DB 정보를 입력해주고 아래처럼 사용하기 위해 dotenv, os 를 import 해준다.

import dotenv
import os

dotenv.load_dotenv()
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = f'postgresql://{os.getenv('PG_USER')}:{os.getenv('PG_PW')}@{os.getenv('PG_HOST')}:{os.getenv('PG_PORT')}/{os.getenv('PG_DBNAME')}'
db = SQLAlchemy(app)

os.getenv() 함수를 통해 .flaskenv 파일에서 키를 통해 읽어와준다.

 

SQLAlchemy 로 모델 만들기

class Event(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    description = db.Column(db.String(100), nullable= False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    
    def __repr__(self):
        return f'Event: {self.description}'
    
    def __init__(self, description):
        self.description = description

위처럼 3개의 컬럼으로 이루어진 테이블을 SQLAlchemy 를 통해 설정해주었고, 이를 포스트그리에 만들어주고자 한다면 강의 내용대로 python 인터프리터에 접근하여 from app import db 후 db.create_all() 형식으로 해줘도 되겠지만...

일단 나는 정상적인 작동이 되질 않아서 app.py 파일의 모델 선언 아래쪽에 아래와 같이 코드를 추가했고, 정상적으로 포스트그리에 테이블이 생겼다. (참고: https://stackoverflow.com/questions/20744277/sqlalchemy-create-all-does-not-create-tables)

class Event(db.Model):
    // ...

with app.app_context():
    db.create_all()

매번 SQL 을 만들고 -> Flask 를 작동시켜줬는데 이는 유용한 방법인 것 같다.

 

만든 모델로 테이블에 레코드 추가하기

이렇게 만들어준 Event 모델에 description 을 넣어주면 자동적으로 해당 테이블에 레코드가 작성되게 된다. 이를 이용하여 강의와는 조금 다르지만(Postman 을 쓰기 귀찮았다) 간단한 폼을 만들고 이벤트를 직접 입력해준 것을 테이블에 넣어주는 라우트를 작성해보았다.

@app.route('/')
def hello():
    return render_template('/event_input.html') // 여기서 이벤트 작성

@app.route('/event', methods=["Post"])
def create_event():
    description = request.form.get('description') // 작성된 input의 name이 description 을 찾아옴
    event = Event(description) // 튜플 생성
    
    db.session.add(event)
    db.session.commit()
    return 'OK'

정상적으로 튜플이 생성된 것을 알 수 있다.

 

모델 정보로 테이블의 튜플들 전체를 읽어오기

@app.route('/events', methods=['GET'])
def get_events():
    events = Event.query.order_by(Event.id.asc()).all()
    event_list = []
    
    for event in events:
        event_list.append(format_event(event))
        
    return {'events': event_list}

Event 모델에 query 를 통해 SQL 문을 실행해줄 수 있다. select 의 방식으로는 다양한 함수가 존재하지만 모든 정보를 읽어오는 방법으론 all() 메서드를 사용해준다. order_by()는 매개변수로 담기는 기준으로 정렬을 해준다. 이때 불러오는 쿼리 정보를 확인해주기 위해 임의의 배열에 담아주었다.

 

특정 이벤트 조회

@app.route('/events/<id>', methods=["get"])
def get_event(id):
    event = Event.query.filter_by(id = id).one()
    formatted_event = format_event(event)
    return {'event': formatted_event}

위에서 전체 조회를 했듯이 약간의 변형만 해주면 된다. SQLAlchemy 에선 where 절을 사용할 때 filter 와 filter_by 함수를 사용해준다. 강의에선 filter_by 를 사용해주었고, 찾아줄 id를 <id> 에서 동적으로 받아줘서 같은 id를 one() 으로 하나만 찾아주게 된다(결과값이 2개 이상이라면 오류가 발생할 수 있다)

 

filter 함수를 사용하게 되면 아래처럼 id부분이 Event.id == id 가 되어야할 것이다.

event = Event.query.filter(Event.id == id).one()

 

튜플 삭제 & 수정하기

@app.route('/events/<id>', methods=["get", 'DELETE'])
def get_event(id):
    if request.method == 'GET':
        event = Event.query.filter(Event.id == id).one()
        formatted_event = format_event(event)
        return {'event': formatted_event}
    elif request.method == 'DELETE':
        event = Event.query.filter_by(id = id).one()
        db.session.delete(event)
        db.session.commit()
        return f'Event ID: {id} deleted'

동일한 주소를 사용하되, 메서드만 다르게 쓰고 싶다면 위처럼 묶어줄 수 있다. request.method 로 어떤 요청 메서드를 사용하고 있는지 검사하고 db.session.delete() 를 통해 아이디를 삭제해준다.

@app.route('/events/<id>', methods=["POST"])
def update_event(id):
    event = Event.query.filter_by(id = id)
    description = request.form.get('description')
    event.update(dict(description=description, created_at = datetime.utcnow()))
    db.session.commit()
    return f'EventID: {id} updated'

기존의 update 는 PUT 으로 해주는게 정석이지만 html의 form 태그에선 PUT 메서드가 통하지 않는다. 그래서 POST로 임시 처리해줘서 변경해주었다.