본문 바로가기

웹/Back-End

Python과 Flask를 활용한 웹 서버 구축

Flask는 Python으로 작성된 웹 프레임워크로 간단한 기능과 빠른 설치 및 사용이 장점이다. 물론 기능을 덧붙인다면 충분히 복잡한 웹 애플리케이션을 작성할 수 있지만 다른 라이브러리나 의존성을 강요하지 않기 때문에 가볍게 시작할 수 있다.

설치

Flask를 설치하는 방법은 단순히 pip install flask처럼 pip를 이용하면 된다.

Flask는 Werkzeug와 Jinja라는 다른 파이썬 웹 프레임워크에 대한 래퍼에서 탄생했기 때문에 Flask를 설치하면 자동으로 이 두 프레임워크도 같이 설치된다. 이 모듈을 따로 임포트해서 사용할 필요는 없고 아래처럼 Flask 자체만 임포트하여 사용할 수 있다.

from flask import Flask, escape, request

app = Flask(__name__)

@app.route('/')
def hello():
    name = request.args.get("name", "World")
    return f'Hello, {escape(name)}!'

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Flask를 활용하여 간단하게 웹 서버를 구축하고자 한다면 크게 다음과 같은 3가지가 필요하다.

  • Flask 클래스의 인스턴스 생성

  • route() 데코레이터를 이용한 URL 처리 함수 지정

  • Flask 인스턴스에 호스트 주소, 포트 넘버를 지정한 후 run() 함수 호출

문서에서는 조금 다른 방법을 이용해서 웹서버를 동작시키고 있기 때문에 app.run() 호출 구문이 없지만 이 파이썬 파일을 실행시켰을 때 서버가 동작하게 만들고 싶다면 아래처럼 이 파이썬 파일이 직접 실행됐을 때, 즉 모듈로 임포트된 게 아닐 때(__name__ == "__main__") 0.0.0.0:5000 주소로 Flask 인스턴스를 실행한다.

위와 같은 요청의 경우 최상위 디렉토리('/')에 요청이 들어왔기 때문에 해당 URL에 대한 처리기인 hello() 함수에서 반환한 값이 브라우저에 출력되는 것을 볼 수 있다.

사용자 요청 수신

사용자가 웹서버에 데이터를 요청하거나 전달할 때는 GET이나 POST로 수행할 수 있다. 그렇지만 Rest API인지 뭔지 하면서 유행하는 것처럼 사용자가 URL에 어떤 값을 넣어서 요청한다면 어떨까? 이를 수신하기 위해 Flask에서는 URL 변수를 사용할 수 있다.

@app.route('/calculator/<string:operation>/<int:value1>/<int:value2>')
def calculator(operation, value1, value2):
    ret = 0
    if operation == "add":
        ret = value1 + value2
    elif operation == "sub":
        ret = value1 - value2
    elif operation == "mul":
        ret = value1 * value2
    elif operation == "div":
        ret = value1 / value2

    return f'Operation {operation} with {value1} and {value2} is {ret}'

사용자가 URL에 입력하는 부분을 '/'로 구분해서 꺽쇠(<, >)로 감싸면 변수처럼 사용할 수 있는데 route 처리 함수(calculator)에서 매개변수로 받아와야 내부 파이썬 코드에서 사용할 수 있다. 필수는 아니지만 string('/'가 없는 문자열)으로 지정된 operation, int로 지정된 value1, value2처럼 데이터 타입을 지정할 수 있는데 이는 값을 변환시키는 역할이기 때문에 만약 변환할 수 없는 값이 입력된다면 이 route에서 처리하지 않는다.

위와 같은 경우 value1, value2에 1, 2가 전달됐다면 int 자료형으로 변환됐기 때문에 calculator() 함수에서 이를 처리할 수 있었으나 hello, world가 전달된 경우 이는 int 자료형으로 변환될 수 없기 때문에 calculator() 함수로 route되지 않아 결국 처리기를 찾지 못해 404 Not Found 에러가 발생했다. 그렇기 때문에 다음과 같은 컨버터 타입을 잘 보고 route를 지정해줘야 할 것이다.

  • string: '/'를 제외한 모든 텍스트

  • int: 자연수(양수인 정수)

  • float: 양수인 실수

  • path: '/'도 받을 수 있는 string

  • uuid: UUID 문자열

유의할 것은 route할 URL 맨 마지막에 슬래시('/')가 있느냐 없느냐에 따라 처리가 달라진다는 것이다.

@app.route('/<notrail>')
def notrail(notrail):
    return "notrail"

@app.route('/<trail>/')
def trail(trail):
    return "trail"

이처럼 응답이 다르기 때문에 실수로 슬래시를 붙이거나 빼먹는 일이 없도록 하자.

 

URL 변수가 아니라 많이 보는 것처럼 URL 파라미터로 값이 넘어온다면 request 모듈을 이용해 URL 파라미터에서 해당 인자에 대해 기본값을 포함하여 값을 받아오는 것이 가능하다.

@app.route('/information')
def information():
    users = [{'name':'John Smith', 'workplace':'School', 'userid':'10011'},
              {'name':'U.N. Owen', 'workplace':'DoA', 'userid':'10021'},
              {'name':'Guest', 'workplace':'None', 'userid':'10001'}]
    
    name = request.args.get("name", "Guest")
    workplace = request.args.get("workplace", "None")

    for user in users:
        if user['name'] == name and user['workplace'] == workplace:
            return f'Hello, {name}#{user["userid"]}!'

    return 'Who are you?'

request 모듈의 args 변수는 "?var1=val1&var2=val2" 같은 URL 파라미터를 파싱한 결과값을 사전으로 가지고 있으며 이 사전에 대해 get() 메소드로 해당하는 파라미터를 찾아서 얻어올 수 있다. 해당 결과값이 없을 경우를 대비해 기본값("Guest", "None" 등)을 지정할 수 있으며 이렇게 얻은 파라미터는 내부 로직에서 사용될 수 있다.

 

위에서 설명한 접근 방법들은 모두 GET 메소드에 해당되는 것들이고 바디에 데이터를 포함해서 보내는 POST 메소드의 경우 어떻게 처리할 수 있을까? 이는 request 모듈의 form 변수에서 참조할 수 있다.

@app.route('/information', methods=['GET', 'POST'])
def information():
    users = [{'name':'John Smith', 'workplace':'School', 'userid':'10011'},
              {'name':'U.N. Owen', 'workplace':'DoA', 'userid':'10021'},
              {'name':'Guest', 'workplace':'None', 'userid':'10001'}]
    method = ''
    if request.method == 'GET':
        name = request.args.get("name", "Guest")
        workplace = request.args.get("workplace", "None")
        method = 'GET'
        
    elif request.method == 'POST':
        name = request.form['name']
        workplace = request.form['workplace']
        method = 'POST'

    for user in users:
        if user['name'] == name and user['workplace'] == workplace:
            return f'Hello, {name}#{user["userid"]} by {method}!'

    return 'Who are you?'

위의 information() 핸들러를 조금 수정하여 사용자가 이 URL에 접근하는 HTTP Method가 GET인지, POST인지에 따라 다른 처리를 수행하도록 구현하였다. 따로 폼은 구현하지 않고 Postman을 통해 POST 요청을 보냈는데 원래라면 HTML의 input 태그 등을 활용해서 form 태그에서 POST 방식으로 전송하는 것이 일반적이다.

바디에 name=John%20Smith&workplace=School처럼 전달되었을 것이며 information() 핸들러에서 POST 분기를 타서 출력 맨 마지막에 POST가 포함된 것을 통해 확인할 수 있다.

사용자 요청에 대한 응답

사용자 요청에 대한 응답을 반환하는 것이기 때문에 이를 반환하지 않을 경우 사용자에게는 아무런 출력도 돌아가지 않는다. 그래서 어떤 템플릿이든 문자열이든 json 데이터든 꼭 return으로 반환해줘야 한다.

he view function did not return a valid response. The function either returned None or ended without a return statement.

그런데 단순 문자열이나 어떤 값이 아니라 사이트 자체를 반환하려면 어떻게 해야 할까? 일일히 모든 태그와 설정값을 적어서 반환해주면 대충 HTML 문서 구조는 만들 수 있겠지만 코드 내용이 엄청 길어질것이고 가독성이나 유지보수도 불편할 것이다.

h1, p 태그를 직접 사용

그래서 사용할 수 있는 모듈이 render_template이다. 이름에서부터 알 수 있듯이 이는 HTML 문서 템플릿에 값만 넣어서 HTML 문서를 동적으로 생성하여 반환해줄 수 있는 기능이다. 이를 사용하려면 템플릿이 될 HTML 문서에 다음처럼 미리 코드가 작성되어 있어야 한다.

<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello, World!</h1>
{% endif %}

이렇게 작성된 HTML 문서는 templates라는 폴더 내부에 위치해야 한다. 그래야 render_template() 함수 호출의 매개변수로 사용될 수 있으며 필요한 매개변수(위의 코드에서는 name)들도 호출 시 같이 전달되어야 한다.

@app.route('/hello')
def template_hello():
    return render_template('hello_template.html', name='John Smith')

이외에도 {% for %} ~ {% endfor %}이나 다른 템플릿도 있으니 이는 문서를 참고해서 응용할 수 있다.

 

Template Designer Documentation — Jinja Documentation (2.11.x)

This document describes the syntax and semantics of the template engine and will be most useful as reference to those creating Jinja templates. As the template engine is very flexible, the configuration from the application can be slightly different from t

jinja.palletsprojects.com

서버 띄우기

어쨌든 이렇게 파이썬으로 작성된 코드를 AWS나 기타 호스팅을 통해서 24시간 켜놓아야 할 일이 있을 텐데 이는 문서에 나온 방법을 따라할 수도 있지만 제일 간단한 것은 리눅스 환경일 경우 nohup 명령어를 사용하는 것이다.

nohup python -u webserver.py &

이는 쉘 명령어를 사용한 방법으로 SSH로 서버에 로그인하거나 기타 방법을 통해 쉘에 접속한 후 다음과 같이 명령어를 실행해주면 서버를 끄거나 직접 프로세스를 종료하지 않는이상 로그아웃해도 파이썬 프로세스가 꺼지지 않는다. 자세한 설명은 이곳 참조.

 

Flask - nohup으로 백그라운드 실행하기

nohup과 &로 파이썬 플라스크 웹 서버를 백그라운드로 실행하기

wooiljeong.github.io

기타

이 포스트에 적힌 내용 말고도 에러 핸들링(404 에러 등), 파일 전송 및 수신, 리디렉션, URL 빌딩 등 여러 유용한 기능을 플라스크를 통해 사용할 수 있는데 이는 공식 문서를 참고하자.

 

Quickstart — Flask Documentation (1.1.x)

For web applications it’s crucial to react to the data a client sends to the server. In Flask this information is provided by the global request object. If you have some experience with Python you might be wondering how that object can be global and how

flask.palletsprojects.com

 

' > Back-End' 카테고리의 다른 글

PHP의 SQL Injection 방지 대책  (0) 2021.01.16