오류는 실패가 아니라 안내문입니다
Flask 앱을 만들다 보면 오류를 자주 만납니다. 처음에는 빨간 글씨가 길게 나오면 당황하기 쉽습니다. 하지만 오류 메시지는 "어디에서, 무엇이, 왜 잘못되었는지"를 알려 주는 안내문입니다. 초보자에게 중요한 능력은 오류를 한 번도 내지 않는 것이 아니라, 오류를 읽고 원인을 좁혀 가는 능력입니다.
개발 중에는 다음처럼 디버그 모드로 서버를 실행했습니다.
flask --app board run --debug디버그 모드에서는 요청 처리 중 오류가 났을 때 브라우저와 터미널에 자세한 traceback이 표시됩니다. traceback은 프로그램이 어떤 파일과 함수를 지나오다가 오류에 도착했는지 보여 주는 기록입니다. 길게 보이지만 처음부터 끝까지 모두 외울 필요는 없습니다. 먼저 봐야 할 곳은 정해져 있습니다.
AI 생성 이미지
Traceback에서 먼저 볼 세 가지
오류가 나면 터미널에 대략 이런 형태의 메시지가 나옵니다.
Traceback (most recent call last):
File '/.../board/posts.py', line 24, in list_posts
rows = db.execute('SELECT * FROM post').fetchall()
sqlite3.OperationalError: no such table: post가장 먼저 볼 것은 세 가지입니다.
파일명: 어느 파일에서 문제가 났는가?
줄 번호: 그 파일의 몇 번째 줄인가?
마지막 오류 메시지: 실제 오류 이름과 설명은 무엇인가?
위 예에서는 board/posts.py 파일의 24번째 줄에서 문제가 났고, 마지막 줄은 sqlite3.OperationalError: no such table: post입니다. 뜻은 "post라는 테이블이 없다"입니다. 그러면 코드 전체를 뒤질 필요가 없습니다. 먼저 데이터베이스 초기화가 되었는지, 테이블 이름이 posts인지 post인지 확인하면 됩니다.
traceback에는 Flask나 Jinja, sqlite3 같은 라이브러리 내부 경로도 함께 나올 수 있습니다. 이런 줄이 많이 보이더라도 겁먹지 마세요. 우선 내가 작성한 파일 경로가 보이는 줄과 마지막 오류 메시지를 찾으면 됩니다. 특히 마지막 오류 메시지 바로 위쪽에 내 파일이 있다면, 그 줄이 원인에 아주 가까운 경우가 많습니다.
대개 원인은 내 코드의 작은 오타, 잘못된 import, 존재하지 않는 템플릿 이름, 예상과 다른 데이터입니다. 오류를 읽는다는 것은 모든 영어 문장을 완벽히 해석하는 일이 아니라, 문제를 좁힐 단서를 찾는 일입니다.
자주 만나는 오류 읽기
NameError: 이름을 찾을 수 없을 때
NameError: name 'render_templete' is not definedNameError는 파이썬이 어떤 이름을 찾지 못했다는 뜻입니다. 대표적인 원인은 세 가지입니다.
함수나 변수 이름을 잘못 썼다.
필요한 것을 import하지 않았다.
변수를 만들기 전에 먼저 사용했다.
예를 들어 다음 코드는 오타 때문에 실패합니다.
return render_templete('posts/list.html', posts=posts)올바른 이름은 render_template입니다.
from flask import render_template
return render_template('posts/list.html', posts=posts)오류 메시지의 이름을 그대로 복사해 코드에서 검색해 보세요. 초보 단계에서는 이것만으로도 많은 오류를 찾을 수 있습니다.
ImportError: 가져오기에 실패했을 때
ImportError: cannot import name 'db' from 'board'ImportError는 파이썬이 모듈이나 객체를 가져오지 못했다는 뜻입니다. 프로젝트 구조를 나눈 뒤 자주 만납니다. 예를 들어 board/db.py, board/posts.py, board/__init__.py를 사용한다면 import 경로가 서로 맞아야 합니다.
from board import db이 코드는 board/__init__.py에서 db를 바로 꺼낼 수 있을 때 자연스럽습니다. 반면 db.py 파일 안의 특정 함수가 필요하다면 다음처럼 더 명확하게 쓸 수 있습니다.
from board.db import get_dbImportError가 나면 먼저 다음을 확인합니다.
파일 이름이 실제로 존재하는가?
폴더에
__init__.py가 있는가?실행 위치가 프로젝트 루트인가?
서로가 서로를 import하는 순환 import가 생기지 않았는가?
순환 import는 A 파일이 B 파일을 가져오고, B 파일이 다시 A 파일을 가져오는 상황입니다. 두 사람이 동시에 서로의 대답을 기다리는 것과 비슷합니다. 이런 경우에는 공통 기능을 별도 파일로 빼거나, create_app() 안에서 블루프린트를 등록하는 구조로 정리합니다.
TemplateNotFound: 템플릿을 찾지 못할 때
jinja2.exceptions.TemplateNotFound: posts/detail.htmlTemplateNotFound는 Flask가 지정한 HTML 템플릿 파일을 찾지 못했다는 뜻입니다. Flask는 기본적으로 애플리케이션 패키지 안의 templates 폴더에서 템플릿을 찾습니다.
예를 들어 다음 코드가 있다면,
return render_template('posts/detail.html', post=post)파일 위치는 보통 이렇게 되어야 합니다.
board/
templates/
posts/
detail.html확인할 것은 단순합니다.
templates폴더 이름을 정확히 썼는가?posts/detail.html경로와 실제 폴더 구조가 같은가?파일 확장자가
.html인가?대소문자가 다른가?
특히 macOS나 Windows에서는 대소문자 차이가 눈에 잘 띄지 않다가, 배포 환경인 Linux에서 문제가 될 수 있습니다. 템플릿 이름은 처음부터 소문자로 통일하는 습관이 좋습니다.
KeyError: 없는 키를 꺼냈을 때
KeyError: 'title'KeyError는 딕셔너리에서 존재하지 않는 키를 꺼내려 할 때 발생합니다. 폼이나 JSON 데이터를 다룰 때도 비슷한 상황을 자주 만납니다.
title = request.form['title']이 코드는 title 값이 반드시 있다고 가정합니다. 하지만 HTML 폼의 name을 잘못 썼거나, 요청에 title 값이 없으면 오류가 납니다. Flask에서 request.form['title']처럼 폼 값을 대괄호로 꺼내다 실패하면 화면에는 단순한 KeyError가 아니라 다음처럼 400 오류나 BadRequestKeyError 형태로 보일 수도 있습니다.
werkzeug.exceptions.BadRequestKeyError: 400 Bad Request이름은 조금 다르게 보이지만, 초보자 입장에서 확인할 지점은 같습니다. "내가 꺼내려는 키가 실제 요청 안에 있는가?"를 보면 됩니다.
사용자가 보내는 값은 언제든 빠질 수 있으므로, 초보 단계에서는 get()을 쓰는 편이 안전합니다.
title = request.form.get('title', '').strip()JSON API에서도 마찬가지입니다.
data = request.get_json() or {}
title = data.get('title', '').strip()이렇게 하면 값이 없을 때 바로 서버 오류가 나는 대신, 우리가 만든 검증 코드로 "제목을 입력하세요" 같은 응답을 줄 수 있습니다.
사용자를 위한 404, 500 오류 페이지 만들기
개발자는 traceback을 봐야 하지만, 사용자는 traceback을 보면 안 됩니다. 사용자가 잘못된 주소로 들어오면 친절한 404 페이지를 보여 주고, 서버 내부 오류가 나면 자세한 내부 정보 대신 간단한 500 페이지를 보여 주어야 합니다.
Flask는 오류 처리 함수를 등록할 수 있습니다. 공식 문서에서도 @app.errorhandler()나 app.register_error_handler()를 사용해 404, 500 같은 오류에 대한 응답을 직접 만들 수 있다고 설명합니다.1
애플리케이션 팩토리 구조에서는 create_app() 안에 다음처럼 등록할 수 있습니다.
# board/__init__.py
from flask import Flask, render_template
def create_app():
app = Flask(__name__)
# 설정, DB, 블루프린트 등록 코드가 여기에 온다.
@app.errorhandler(404)
def page_not_found(error):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_server_error(error):
return render_template('errors/500.html'), 500
return app여기서 error 매개변수는 Flask가 넘겨주는 오류 객체입니다. 예제 코드에서는 직접 사용하지 않지만, 오류 처리 함수는 이 값을 받을 수 있어야 합니다. 그리고 return render_template(...), 404처럼 응답과 함께 상태 코드도 반드시 돌려주는 습관을 들이는 것이 좋습니다.
오류 페이지 템플릿은 다음 위치에 둡니다.
board/
templates/
errors/
404.html
500.html404.html은 사용자가 주소를 잘못 입력했거나 삭제된 글에 접근했을 때 보여 줍니다.
{% extends 'base.html' %}
{% block content %}
<h2>페이지를 찾을 수 없습니다</h2>
<p>주소가 잘못되었거나, 게시글이 삭제되었을 수 있습니다.</p>
<p><a href="{{ url_for('posts.list_posts') }}">게시글 목록으로 돌아가기</a></p>
{% endblock %}500.html은 서버 코드에서 예상하지 못한 오류가 났을 때 보여 줍니다.
{% extends 'base.html' %}
{% block content %}
<h2>서버에서 문제가 발생했습니다</h2>
<p>잠시 후 다시 시도해 주세요.</p>
{% endblock %}중요한 점은 500 페이지에 데이터베이스 경로, 환경 변수, 코드 일부 같은 내부 정보를 쓰지 않는 것입니다. 사용자는 해결 방법만 알면 되고, 자세한 원인은 개발자가 로그에서 확인해야 합니다.
print, logging, 디버거를 구분해서 쓰기
오류를 찾는 도구는 하나만 있는 것이 아닙니다. 상황에 따라 가볍게 시작하고, 필요할 때 더 강한 도구를 씁니다.
가장 쉬운 방법은 print()입니다.
print('form data:', request.form)
print('post_id:', post_id)print()는 값이 실제로 들어오는지 빠르게 확인할 때 좋습니다. 다만 프로젝트가 커지면 출력이 뒤섞이고, 어떤 정보가 중요한지 찾기 어려워집니다.
조금 더 실무적인 방법은 logging입니다.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info('게시글 목록 조회')
logger.warning('제목 없이 게시글 작성 시도')로그는 프로그램이 실행되는 동안 남기는 기록입니다. print()보다 단계별 중요도를 나누기 쉽습니다. 대표적인 단계는 다음과 같습니다.
debug: 아주 자세한 개발용 정보info: 정상 흐름 기록warning: 당장은 동작하지만 주의가 필요한 상황error: 실제 오류 상황
라우트 함수 안에서는 Flask가 제공하는 current_app.logger를 사용할 수도 있습니다.
from flask import current_app
current_app.logger.info('새 게시글 저장 완료')
current_app.logger.error('게시글 저장 실패')마지막으로 디버거가 있습니다. 디버거는 코드 실행을 잠시 멈추고, 그 순간의 변수 값을 직접 살펴보게 해 주는 도구입니다. VS Code, PyCharm 같은 편집기의 디버거를 사용할 수 있고, Flask의 내장 디버거도 개발 중에는 도움이 됩니다. Flask 공식 문서는 디버그 모드에서 처리되지 않은 오류가 발생하면 브라우저에 대화형 traceback이 표시된다고 설명합니다.2
단, 초보 단계에서는 모든 문제를 디버거로 해결하려고 하기보다 순서를 정하는 것이 좋습니다.
오류 메시지 마지막 줄을 읽는다.
파일명과 줄 번호를 확인한다.
그 줄에서 사용하는 변수와 함수 이름을 확인한다.
필요하면
print()나 로그로 값을 확인한다.그래도 원인이 안 보이면 디버거로 한 줄씩 실행한다.
이 순서를 반복하면 오류를 만났을 때 무작정 코드를 바꾸는 습관을 줄일 수 있습니다.
디버그 모드는 운영 서버에서 끄기
디버그 모드는 개발 중에는 매우 편리합니다. 하지만 운영 서버, 즉 실제 사용자가 접속하는 서버에서는 절대 켜 두면 안 됩니다. Flask 공식 문서는 개발 서버나 내장 디버거를 운영 환경에서 사용하지 말라고 경고합니다. 내장 디버거는 브라우저에서 임의의 파이썬 코드를 실행할 수 있는 기능을 포함하기 때문입니다.2
또한 디버그 화면에는 파일 경로, 함수 이름, 설정 정보, 코드 흐름이 노출될 수 있습니다. 공격자는 이런 정보를 보고 애플리케이션 구조를 추측할 수 있습니다. 잠긴 문을 열어 주지는 않더라도, 건물의 내부 지도를 보여 주는 것과 비슷합니다.
따라서 배포 전에는 다음을 확인합니다.
# 개발 중에만 사용
flask --app board run --debug
# 운영에서는 debug=True를 사용하지 않는다코드에도 다음과 같은 형태를 남기지 않는 편이 안전합니다.
app.run(debug=True)운영 환경에서는 Gunicorn, uWSGI 같은 WSGI 서버와 Nginx 같은 웹 서버를 조합해 실행하는 방식이 일반적입니다. 이 책의 마지막 장에서는 작은 프로젝트를 정리하고 배포 전 점검 목록을 살펴볼 것입니다. 지금은 한 가지만 확실히 기억하면 됩니다.
디버그 모드는 내 컴퓨터에서 개발할 때만 켭니다. 실제 서버에서는 끕니다.
오류를 고치는 기본 루틴
오류가 나면 다음 순서로 움직입니다.
브라우저 화면만 보지 말고 터미널 traceback을 확인합니다.
마지막 줄의 오류 이름과 메시지를 읽습니다.
내가 작성한 파일명과 줄 번호를 찾습니다.
최근에 수정한 코드와 비교합니다.
변수 이름, import, 템플릿 경로, 딕셔너리 키를 확인합니다.
값을 모르면
print()나 로그로 확인합니다.사용자가 보는 오류 페이지에는 내부 정보를 숨깁니다.
배포할 때는 디버그 모드를 끕니다.
좋은 개발자는 오류를 안 내는 사람이 아닙니다. 오류가 났을 때 차분히 읽고, 범위를 좁히고, 다시 확인하는 사람입니다. Flask를 배우는 동안 만나는 오류들은 앞으로 더 큰 프로젝트를 다룰 때 필요한 감각을 길러 줍니다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.