Skip to main content
Summary

HTML 페이지와 API는 무엇이 다를까

지금까지 만든 Flask 앱은 주로 HTML 페이지를 돌려주었습니다. 사용자가 /posts/에 접속하면 서버는 posts/list.html 템플릿을 렌더링하고, 브라우저는 그 HTML을 화면으로 보여 주었습니다.

API는 조금 다릅니다. API는 사람에게 바로 보여 줄 화면보다, 다른 프로그램이 읽기 쉬운 데이터를 돌려주는 입구입니다. 예를 들어 모바일 앱, React 같은 프론트엔드, 자동화 스크립트는 HTML보다 JSON 데이터를 받는 편이 다루기 쉽습니다.

간단히 비교하면 이렇습니다.

구분HTML 응답JSON 응답
주 사용자사람프로그램
응답 예<h1>게시글 목록</h1>[{"id": 1, "title": "첫 글"}]
Flask에서 자주 쓰는 함수render_template()jsonify()
사용 상황웹페이지 표시모바일 앱, 프론트엔드, 자동화 도구와 데이터 교환

JSON은 데이터를 표현하는 가벼운 형식입니다. 파이썬의 딕셔너리와 리스트를 문자열 형태로 바꾼 것과 비슷하게 생겼습니다.

{
  "id": 1,
  "title": "첫 번째 게시글",
  "author_email": "[email protected]"
}

파이썬 딕셔너리와 비슷하지만 JSON에서는 문자열을 반드시 큰따옴표로 감쌉니다. 또한 JSON은 파이썬 코드가 아니라 여러 언어가 함께 이해할 수 있는 데이터 형식입니다.

최근 Flask에서는 뷰 함수가 딕셔너리를 반환해도 JSON 응답으로 처리할 수 있습니다. 하지만 초보 단계에서는 응답이 JSON이라는 점을 분명히 드러내기 위해 jsonify()를 사용하는 방식으로 연습하겠습니다. 리스트처럼 여러 값을 한꺼번에 반환할 때도 jsonify()를 쓰면 의도가 더 명확합니다.1

REST API 기초와 JSON 응답 만들기AI 생성 이미지

API 블루프린트 만들기

앞에서 우리는 기능별로 코드를 나누기 위해 Blueprint를 사용했습니다. 게시글 화면은 posts.py, 인증 화면은 auth.py처럼 나누었습니다. 이번에는 API 전용 파일을 하나 더 만들겠습니다.

현재 구조를 다음처럼 생각하겠습니다.

flask-study/
└─ board/
   ├─ __init__.py
   ├─ db.py
   ├─ posts.py
   ├─ auth.py
   ├─ api.py        ← 새로 만들 파일
   ├─ templates/
   └─ static/

board/api.py 파일을 만들고 다음 코드를 작성합니다.

from flask import Blueprint, jsonify, request

from .db import get_db

bp = Blueprint('api', __name__, url_prefix='/api')

한 줄씩 보겠습니다.

Blueprint는 API 라우트를 하나의 기능 묶음으로 관리하기 위해 가져옵니다.
jsonify는 파이썬 딕셔너리나 리스트를 JSON 응답으로 바꾸는 함수입니다.
request는 사용자가 보낸 요청 데이터를 읽기 위해 필요합니다. 이번 장에서는 JSON 요청 본문을 읽을 때 사용합니다.
get_db는 앞에서 만든 데이터베이스 연결 함수입니다. 게시글을 SQLite에서 조회하고 저장하기 위해 사용합니다.
url_prefix='/api'는 이 파일에 있는 모든 주소 앞에 /api를 붙이겠다는 뜻입니다.

이제 board/__init__.py의 create_app() 안에서 API 블루프린트를 등록해야 합니다.

from . import api

app.register_blueprint(api.bp)

이미 posts, auth 블루프린트를 등록한 코드가 있다면 그 근처에 추가하면 됩니다.

def create_app():
    app = Flask(__name__, instance_relative_config=True)

    # 설정, DB 초기화 코드 생략

    from . import posts
    from . import auth
    from . import api

    app.register_blueprint(posts.bp)
    app.register_blueprint(auth.bp)
    app.register_blueprint(api.bp)

    return app

파일을 만들기만 하면 Flask가 자동으로 알지 못합니다. 반드시 register_blueprint()로 앱에 붙여야 실제 URL이 동작합니다.

게시글 데이터를 JSON으로 바꾸는 함수

앞에서 만든 데이터베이스 테이블은 작성자 컬럼 이름을 author_email로 사용했습니다. API에서도 같은 이름을 사용하겠습니다. 데이터베이스 컬럼 이름과 API 코드가 서로 다르면 초보자가 그대로 따라 했을 때 no such column 같은 오류를 만날 수 있기 때문입니다.

게시글을 JSON으로 돌려줄 때마다 같은 딕셔너리를 반복해서 만들면 코드가 길어집니다. 그래서 먼저 작은 변환 함수를 하나 만들겠습니다.

def post_to_dict(post):
    return {
        'id': post['id'],
        'title': post['title'],
        'author_email': post['author_email'],
        'content': post['content'],
        'created_at': post['created_at'],
    }

이 함수는 SQLite에서 가져온 한 줄의 게시글 데이터를 JSON으로 바꾸기 쉬운 파이썬 딕셔너리로 정리합니다. 나중에 목록 조회, 상세 조회, 작성 결과, 수정 결과에서 모두 다시 사용할 수 있습니다.

게시글 목록을 JSON으로 반환하기

먼저 게시글 목록 API를 만들어 보겠습니다.

주소는 다음처럼 정하겠습니다.

GET /api/posts

HTML 화면에서는 /posts/가 게시글 목록 페이지였습니다. API에서는 /api/posts가 게시글 목록 데이터를 반환합니다.

board/api.py에 다음 코드를 추가합니다.

@bp.route('/posts', methods=['GET'])
def post_list():
    db = get_db()
    posts = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        ORDER BY id DESC
        '''
    ).fetchall()

    result = []
    for post in posts:
        result.append(post_to_dict(post))

    return jsonify(result)

@bp.route('/posts', methods=['GET'])는 /api/posts 주소로 들어온 GET 요청을 이 함수에 연결합니다. /api는 블루프린트의 url_prefix에서 이미 붙었습니다.

db.execute(...).fetchall()은 데이터베이스에서 게시글 여러 개를 가져옵니다. 그다음 각 게시글을 post_to_dict()로 딕셔너리 형태로 바꿔 result 리스트에 넣습니다.

마지막의 return jsonify(result)는 게시글 리스트를 JSON 응답으로 반환합니다.

브라우저에서 http://127.0.0.1:5000/api/posts에 접속하면 HTML이 아니라 다음과 비슷한 데이터가 보입니다.

[
  {
    "id": 2,
    "title": "두 번째 글",
    "author_email": "[email protected]",
    "content": "Flask API 연습 중입니다.",
    "created_at": "2026-07-21 10:30:00"
  },
  {
    "id": 1,
    "title": "첫 번째 글",
    "author_email": "[email protected]",
    "content": "안녕하세요.",
    "created_at": "2026-07-21 10:00:00"
  }
]

이 응답은 사람에게 예쁘게 보이기 위한 화면이 아닙니다. 다른 프로그램이 게시글 목록을 읽고 사용할 수 있도록 만든 데이터입니다.

게시글 하나 조회하기

목록 다음에는 상세 조회 API를 만들겠습니다.

GET /api/posts/1

1 부분은 게시글 ID입니다.

@bp.route('/posts/<int:post_id>', methods=['GET'])
def post_detail(post_id):
    db = get_db()
    post = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        WHERE id = ?
        ''',
        (post_id,)
    ).fetchone()

    if post is None:
        return jsonify({
            'error': 'not_found',
            'message': '게시글을 찾을 수 없습니다.',
        }), 404

    return jsonify(post_to_dict(post))

여기서 중요한 부분은 오류 응답입니다.

return jsonify({
    'error': 'not_found',
    'message': '게시글을 찾을 수 없습니다.',
}), 404

API에서는 오류가 나도 HTML 오류 페이지보다 JSON 오류를 돌려주는 편이 좋습니다. API를 호출한 프로그램이 오류 내용을 읽고 다음 행동을 정할 수 있기 때문입니다.

404는 "요청한 대상을 찾을 수 없다"는 상태 코드입니다. 상태 코드는 HTTP 응답의 결과를 숫자로 알려 주는 약속입니다. 성공이면 보통 200, 새로 생성되면 201, 잘못된 요청이면 400, 찾을 수 없으면 404를 사용합니다.

HTTP 메서드로 기능 설계하기

REST API에서는 같은 게시글 자원이라도 HTTP 메서드에 따라 다른 일을 하도록 설계하는 경우가 많습니다.

REST는 "주소는 자원을 나타내고, 메서드는 행동을 나타낸다"는 방식으로 API를 정리하는 설계 스타일입니다. 여기서 자원은 게시글, 사용자, 댓글처럼 서버가 다루는 대상입니다.

게시글 API를 예로 들면 다음처럼 정리할 수 있습니다.

기능HTTP 메서드주소의미
게시글 목록 조회GET/api/posts게시글 여러 개를 가져온다
게시글 하나 조회GET/api/posts/11번 게시글을 가져온다
게시글 작성POST/api/posts새 게시글을 만든다
게시글 수정PUT/api/posts/11번 게시글을 수정한다
게시글 삭제DELETE/api/posts/11번 게시글을 삭제한다

GET은 가져오기, POST는 만들기, PUT은 바꾸기, DELETE는 지우기라고 먼저 기억하면 됩니다.

실무에서는 PATCH라는 메서드도 자주 사용합니다. PATCH는 일부 필드만 고칠 때 쓰는 경우가 많습니다. 이번 장에서는 초보자가 전체 흐름을 잡기 쉽도록 PUT만 사용하겠습니다.

JSON 요청 본문 읽기

HTML 폼에서는 사용자가 입력한 데이터를 request.form으로 읽었습니다. API에서는 보통 JSON 본문을 보냅니다.

예를 들어 새 게시글을 만들 때 클라이언트가 다음 JSON을 보낸다고 생각해 보겠습니다.

{
  "title": "API로 작성한 글",
  "author_email": "[email protected]",
  "content": "HTML 폼 없이 JSON으로 보낸 게시글입니다."
}

Flask에서는 이 JSON 본문을 request.get_json()으로 읽습니다. 이번 장의 실제 코드에서는 잘못된 JSON이 들어와도 우리가 정한 오류 형식으로 응답하기 위해 silent=True를 함께 사용하겠습니다.

data = request.get_json(silent=True)

읽은 결과는 보통 파이썬 딕셔너리처럼 사용할 수 있습니다.

title = data.get('title')
author_email = data.get('author_email')
content = data.get('content')

다만 사용자가 항상 올바른 JSON을 보내리라고 믿으면 안 됩니다. 본문이 비어 있을 수도 있고, JSON이 객체가 아니라 리스트일 수도 있고, 제목 자리에 문자열이 아닌 숫자가 들어올 수도 있습니다. 그래서 API에서도 검증이 필요합니다.

API용 검증 함수 만들기

폼 검증을 했던 것처럼 API 요청도 검증하겠습니다. 같은 규칙을 여러 함수에서 반복하지 않도록 작은 함수를 만들겠습니다.

board/api.py에 다음 함수를 추가합니다.

def validate_post_json(data):
    errors = {}

    if data is None:
        errors['body'] = 'JSON 본문이 필요합니다.'
        return errors

    if not isinstance(data, dict):
        errors['body'] = 'JSON 본문은 객체 형태여야 합니다.'
        return errors

    title = data.get('title', '')
    author_email = data.get('author_email', '')
    content = data.get('content', '')

    if not isinstance(title, str):
        errors['title'] = '제목은 문자열로 입력하세요.'
    else:
        title = title.strip()
        if not title:
            errors['title'] = '제목을 입력하세요.'
        elif len(title) > 100:
            errors['title'] = '제목은 100자 이하로 입력하세요.'

    if not isinstance(author_email, str):
        errors['author_email'] = '이메일은 문자열로 입력하세요.'
    else:
        author_email = author_email.strip()
        if not author_email:
            errors['author_email'] = '이메일을 입력하세요.'
        elif len(author_email) > 100:
            errors['author_email'] = '이메일은 100자 이하로 입력하세요.'

    if not isinstance(content, str):
        errors['content'] = '내용은 문자열로 입력하세요.'
    else:
        content = content.strip()
        if not content:
            errors['content'] = '내용을 입력하세요.'

    return errors

data is None은 JSON 본문을 읽지 못했거나 본문이 없는 경우를 확인합니다. not isinstance(data, dict)는 JSON 본문이 {}처럼 객체 형태가 아닌 경우를 막습니다.

각 값에 대해 문자열인지도 확인합니다. 이 검사를 하지 않고 바로 .strip()을 호출하면, 클라이언트가 숫자나 리스트를 보냈을 때 서버에서 AttributeError가 발생할 수 있습니다. API에서는 사용자의 잘못된 입력 때문에 서버가 500 오류를 내기보다, 어떤 값이 잘못되었는지 JSON으로 알려 주는 편이 좋습니다.

errors는 필드별 오류 메시지를 담는 딕셔너리입니다. 예를 들어 제목과 내용이 빠졌다면 다음처럼 응답할 수 있습니다.

{
  "error": "validation_error",
  "message": "입력값을 확인하세요.",
  "errors": {
    "title": "제목을 입력하세요.",
    "content": "내용을 입력하세요."
  }
}

API 오류 응답은 형식을 일정하게 만드는 것이 중요합니다. 어떤 오류는 msg, 어떤 오류는 message, 어떤 오류는 문자열만 반환하면 API를 쓰는 쪽에서 처리하기 어렵습니다.

이번 장에서는 오류 응답을 다음 구조로 맞추겠습니다.

{
  "error": "오류_종류",
  "message": "사람이 읽을 수 있는 설명",
  "errors": {
    "field": "필드별 오류 메시지"
  }
}

errors는 검증 오류처럼 필드별 설명이 필요할 때만 넣겠습니다.

게시글 작성 API 만들기

이제 POST 요청으로 게시글을 만들겠습니다.

POST /api/posts

board/api.py에 다음 코드를 추가합니다.

@bp.route('/posts', methods=['POST'])
def create_post():
    data = request.get_json(silent=True)
    errors = validate_post_json(data)

    if errors:
        return jsonify({
            'error': 'validation_error',
            'message': '입력값을 확인하세요.',
            'errors': errors,
        }), 400

    title = data.get('title').strip()
    author_email = data.get('author_email').strip()
    content = data.get('content').strip()

    db = get_db()
    cursor = db.execute(
        '''
        INSERT INTO posts (title, author_email, content)
        VALUES (?, ?, ?)
        ''',
        (title, author_email, content)
    )
    db.commit()

    post_id = cursor.lastrowid
    post = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        WHERE id = ?
        ''',
        (post_id,)
    ).fetchone()

    return jsonify(post_to_dict(post)), 201

request.get_json(silent=True)는 요청 본문을 JSON으로 읽습니다. silent=True를 사용하면 JSON 파싱에 실패했을 때 Flask가 바로 오류를 발생시키는 대신 None을 돌려줍니다. 그러면 우리가 준비한 일정한 오류 형식으로 응답할 수 있습니다.

errors = validate_post_json(data)는 입력값을 검사합니다.

오류가 있으면 JSON과 함께 400 상태 코드를 반환합니다. 400 Bad Request는 요청이 잘못되었다는 뜻입니다.

게시글 저장에는 앞에서 배운 SQL INSERT를 사용합니다. 여기서도 ? 자리표시자를 사용합니다. 사용자가 보낸 값을 SQL 문자열에 직접 끼워 넣지 않기 위해서입니다.

저장이 끝나면 201 상태 코드를 반환합니다. 201 Created는 새 자원이 만들어졌다는 뜻입니다.

게시글 수정 API 만들기

수정은 다음 주소와 메서드를 사용하겠습니다.

PUT /api/posts/1

1번 게시글을 요청 본문에 담긴 값으로 바꾸겠다는 뜻입니다.

@bp.route('/posts/<int:post_id>', methods=['PUT'])
def update_post(post_id):
    db = get_db()
    post = db.execute(
        'SELECT id FROM posts WHERE id = ?',
        (post_id,)
    ).fetchone()

    if post is None:
        return jsonify({
            'error': 'not_found',
            'message': '게시글을 찾을 수 없습니다.',
        }), 404

    data = request.get_json(silent=True)
    errors = validate_post_json(data)

    if errors:
        return jsonify({
            'error': 'validation_error',
            'message': '입력값을 확인하세요.',
            'errors': errors,
        }), 400

    title = data.get('title').strip()
    author_email = data.get('author_email').strip()
    content = data.get('content').strip()

    db.execute(
        '''
        UPDATE posts
        SET title = ?, author_email = ?, content = ?
        WHERE id = ?
        ''',
        (title, author_email, content, post_id)
    )
    db.commit()

    updated_post = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        WHERE id = ?
        ''',
        (post_id,)
    ).fetchone()

    return jsonify(post_to_dict(updated_post))

수정에서는 먼저 게시글이 실제로 있는지 확인합니다. 없는 게시글을 수정할 수는 없으므로 404를 반환합니다.

그다음 JSON 본문을 읽고 검증합니다. 작성 API와 같은 검증 함수를 사용하므로 규칙이 흔들리지 않습니다.

마지막으로 UPDATE SQL을 실행하고, 수정된 결과를 JSON으로 반환합니다.

게시글 삭제 API 만들기

삭제는 다음처럼 설계합니다.

DELETE /api/posts/1

코드는 다음과 같습니다.

@bp.route('/posts/<int:post_id>', methods=['DELETE'])
def delete_post(post_id):
    db = get_db()
    post = db.execute(
        'SELECT id FROM posts WHERE id = ?',
        (post_id,)
    ).fetchone()

    if post is None:
        return jsonify({
            'error': 'not_found',
            'message': '게시글을 찾을 수 없습니다.',
        }), 404

    db.execute('DELETE FROM posts WHERE id = ?', (post_id,))
    db.commit()

    return '', 204

204 No Content는 요청은 성공했지만 응답 본문은 없다는 뜻입니다. 삭제 요청에서는 "삭제된 게시글 데이터"를 굳이 다시 돌려주지 않아도 되므로 자주 사용됩니다.

처음에는 return jsonify({'message': '삭제되었습니다.'})처럼 응답해도 됩니다. 하지만 REST API에서는 삭제 성공 시 204를 사용하는 방식도 흔합니다. 중요한 것은 팀이나 프로젝트 안에서 일관되게 정하는 것입니다.

API를 테스트하는 간단한 방법

GET 요청은 브라우저 주소창으로도 확인할 수 있습니다.

http://127.0.0.1:5000/api/posts

하지만 POST, PUT, DELETE는 브라우저 주소창만으로 테스트하기 어렵습니다. 이때는 curl 같은 명령줄 도구를 사용할 수 있습니다.

새 게시글 작성 요청은 다음처럼 보낼 수 있습니다.

curl -X POST http://127.0.0.1:5000/api/posts -H "Content-Type: application/json" -d '{"title":"API 글","author_email":"[email protected]","content":"curl로 작성했습니다."}'

-X POST는 POST 메서드로 요청하겠다는 뜻입니다.

-H "Content-Type: application/json"은 요청 본문이 JSON 형식이라고 알려 줍니다. 서버는 이 정보를 보고 본문을 JSON으로 해석할 수 있습니다.

-d 뒤에는 실제로 보낼 JSON 데이터를 적습니다.

수정은 PUT을 사용합니다.

curl -X PUT http://127.0.0.1:5000/api/posts/1 -H "Content-Type: application/json" -d '{"title":"수정된 제목","author_email":"[email protected]","content":"내용을 수정했습니다."}'

삭제는 DELETE를 사용합니다.

curl -X DELETE http://127.0.0.1:5000/api/posts/1

Windows PowerShell에서는 따옴표 처리 방식이 다를 수 있습니다. 명령이 잘 동작하지 않으면 Postman, Insomnia 같은 API 테스트 도구를 사용해도 좋습니다. 중요한 것은 요청 메서드, 주소, 헤더, 본문을 구분해서 보는 습관입니다.

전체 API 코드 정리

지금까지 만든 board/api.py의 핵심 코드를 한곳에 모으면 다음과 같습니다.

from flask import Blueprint, jsonify, request

from .db import get_db

bp = Blueprint('api', __name__, url_prefix='/api')


def post_to_dict(post):
    return {
        'id': post['id'],
        'title': post['title'],
        'author_email': post['author_email'],
        'content': post['content'],
        'created_at': post['created_at'],
    }


def validate_post_json(data):
    errors = {}

    if data is None:
        errors['body'] = 'JSON 본문이 필요합니다.'
        return errors

    if not isinstance(data, dict):
        errors['body'] = 'JSON 본문은 객체 형태여야 합니다.'
        return errors

    title = data.get('title', '')
    author_email = data.get('author_email', '')
    content = data.get('content', '')

    if not isinstance(title, str):
        errors['title'] = '제목은 문자열로 입력하세요.'
    else:
        title = title.strip()
        if not title:
            errors['title'] = '제목을 입력하세요.'
        elif len(title) > 100:
            errors['title'] = '제목은 100자 이하로 입력하세요.'

    if not isinstance(author_email, str):
        errors['author_email'] = '이메일은 문자열로 입력하세요.'
    else:
        author_email = author_email.strip()
        if not author_email:
            errors['author_email'] = '이메일을 입력하세요.'
        elif len(author_email) > 100:
            errors['author_email'] = '이메일은 100자 이하로 입력하세요.'

    if not isinstance(content, str):
        errors['content'] = '내용은 문자열로 입력하세요.'
    else:
        content = content.strip()
        if not content:
            errors['content'] = '내용을 입력하세요.'

    return errors


@bp.route('/posts', methods=['GET'])
def post_list():
    db = get_db()
    posts = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        ORDER BY id DESC
        '''
    ).fetchall()

    result = []
    for post in posts:
        result.append(post_to_dict(post))

    return jsonify(result)


@bp.route('/posts/<int:post_id>', methods=['GET'])
def post_detail(post_id):
    db = get_db()
    post = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        WHERE id = ?
        ''',
        (post_id,)
    ).fetchone()

    if post is None:
        return jsonify({
            'error': 'not_found',
            'message': '게시글을 찾을 수 없습니다.',
        }), 404

    return jsonify(post_to_dict(post))


@bp.route('/posts', methods=['POST'])
def create_post():
    data = request.get_json(silent=True)
    errors = validate_post_json(data)

    if errors:
        return jsonify({
            'error': 'validation_error',
            'message': '입력값을 확인하세요.',
            'errors': errors,
        }), 400

    title = data.get('title').strip()
    author_email = data.get('author_email').strip()
    content = data.get('content').strip()

    db = get_db()
    cursor = db.execute(
        '''
        INSERT INTO posts (title, author_email, content)
        VALUES (?, ?, ?)
        ''',
        (title, author_email, content)
    )
    db.commit()

    post_id = cursor.lastrowid
    post = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        WHERE id = ?
        ''',
        (post_id,)
    ).fetchone()

    return jsonify(post_to_dict(post)), 201


@bp.route('/posts/<int:post_id>', methods=['PUT'])
def update_post(post_id):
    db = get_db()
    post = db.execute(
        'SELECT id FROM posts WHERE id = ?',
        (post_id,)
    ).fetchone()

    if post is None:
        return jsonify({
            'error': 'not_found',
            'message': '게시글을 찾을 수 없습니다.',
        }), 404

    data = request.get_json(silent=True)
    errors = validate_post_json(data)

    if errors:
        return jsonify({
            'error': 'validation_error',
            'message': '입력값을 확인하세요.',
            'errors': errors,
        }), 400

    title = data.get('title').strip()
    author_email = data.get('author_email').strip()
    content = data.get('content').strip()

    db.execute(
        '''
        UPDATE posts
        SET title = ?, author_email = ?, content = ?
        WHERE id = ?
        ''',
        (title, author_email, content, post_id)
    )
    db.commit()

    updated_post = db.execute(
        '''
        SELECT id, title, author_email, content, created_at
        FROM posts
        WHERE id = ?
        ''',
        (post_id,)
    ).fetchone()

    return jsonify(post_to_dict(updated_post))


@bp.route('/posts/<int:post_id>', methods=['DELETE'])
def delete_post(post_id):
    db = get_db()
    post = db.execute(
        'SELECT id FROM posts WHERE id = ?',
        (post_id,)
    ).fetchone()

    if post is None:
        return jsonify({
            'error': 'not_found',
            'message': '게시글을 찾을 수 없습니다.',
        }), 404

    db.execute('DELETE FROM posts WHERE id = ?', (post_id,))
    db.commit()

    return '', 204

코드가 조금 길어졌지만 구조는 단순합니다.

목록 조회, 상세 조회, 작성, 수정, 삭제가 각각 하나의 라우트 함수입니다. 각 함수는 요청을 읽고, 필요한 경우 검증하고, 데이터베이스를 처리한 뒤 JSON 응답을 반환합니다.

API를 만들 때 기억할 점

API는 화면을 만드는 일이 아니라 약속을 만드는 일에 가깝습니다. 주소, 메서드, 요청 데이터, 응답 데이터, 오류 형식이 모두 약속입니다.

초보 단계에서는 다음 다섯 가지를 지키면 충분합니다.

  1. HTML 화면 주소와 API 주소를 구분합니다. 예를 들어 화면은 /posts/, API는 /api/posts처럼 나눕니다.

  2. JSON 응답은 jsonify()로 반환합니다.

  3. GET, POST, PUT, DELETE의 역할을 명확히 나눕니다.

  4. request.get_json()으로 읽은 데이터도 반드시 검증합니다.

  5. 오류 응답의 모양과 상태 코드를 일관되게 유지합니다.

Flask는 HTML 웹사이트뿐 아니라 JSON API 서버로도 사용할 수 있습니다. 같은 게시글 데이터를 HTML로 보여 줄 수도 있고, JSON으로 제공할 수도 있습니다. 이 점을 이해하면 Flask 앱은 브라우저용 웹사이트에서 모바일 앱, 프론트엔드, 자동화 도구를 위한 서버로 확장됩니다.

참고

1JavaScript, fetch, and JSON — Flask Documentation

책 목록

초보자를 위한 파이썬 Flask 학습

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기