17강 / 20강

폼과 POST — 사용자의 입력이 DB로 들어가는 길

읽기만 하던 앱이 드디어 '쓰기'를 배웁니다

폼과 POST — 사용자의 입력이 DB로 들어가는 길
폼은 신청서 한 장입니다. 작성해서 창구(서버)에 제출하면 장부(DB)에 기록되죠.

지금까지 우리 앱은 DB → 화면 한 방향이었습니다. 이제 반대 방향, 화면 → DB를 배웁니다. 4강의 CRUD 중 C·U·D가 여기서 완성돼요.

📝폼사용자가 입력
POST →
🧪Flask검사하고 판단
→
🗄️INSERTDB에 기록
← 목록 화면으로 ←

HTML 폼 — 신청서 한 장

<form method="post" action="{{ url_for('add_product') }}" class="row g-2">
  <div class="col-md-5">
    <input name="name" class="form-control" placeholder="상품명" required>
  </div>
  <div class="col-md-3">
    <input name="price" type="number" class="form-control" placeholder="가격" required>
  </div>
  <div class="col-md-2">
    <input name="stock" type="number" class="form-control" value="0">
  </div>
  <button class="btn btn-primary col-md-2">등록</button>
</form>

중요한 건 name 속성입니다. 서버는 이 이름으로 값을 찾아요. name="price" 로 보낸 값은 서버에서 request.form["price"] 로 받습니다. 이름표가 붙은 상자 — 1강 그대로죠.

서버 쪽 — 받고, 검사하고, 넣고, 돌려보낸다

from flask import Flask, render_template, request, redirect, url_for, flash

@app.route("/products/add", methods=["POST"])       # POST만 받는 라우트
def add_product():
    # ① 받는다
    name  = request.form.get("name", "").strip()
    price = request.form.get("price", "0")
    stock = request.form.get("stock", "0")

    # ② 검사한다 — 서버가 판단한다!
    if not name:
        flash("상품명을 입력해 주세요.")
        return redirect(url_for("product_list"))
    if not price.isdigit() or int(price) <= 0:
        flash("가격은 1원 이상의 숫자여야 합니다.")
        return redirect(url_for("product_list"))

    # ③ 넣는다 — 반드시 ? 자리표시자로
    conn = db()
    conn.execute("INSERT INTO product (name, price, stock) VALUES (?, ?, ?)",
                 (name, int(price), int(stock)))
    conn.commit()
    conn.close()

    # ④ 목록 화면으로 되돌려보낸다 (POST → Redirect → GET)
    flash(f"{name} 등록 완료!")
    return redirect(url_for("product_list"))

네 단계가 전부입니다. 받고 · 검사하고 · 넣고 · 돌려보낸다. 세상의 거의 모든 '등록' 버튼이 이 구조예요.

🎮 폼 실험실 — 브라우저를 믿으면 안 되는 이유

두 가지를 실험해 보세요.

  1. 브라우저 검사를 끄고 빈 이름이나 -100 원을 등록해 보세요. 개발자 도구를 여는 것과 같은 상황입니다. 그래도 서버가 막습니다.
  2. PRG를 끄고 등록한 뒤 🔄 새로고침을 눌러 보세요. 같은 상품이 또 등록됩니다. PRG를 켜면 그런 일이 없고요.
폼 실험실 — 검증은 서버에서, 그리고 PRG
불러오는 중…

🔒 절대 잊으면 안 되는 두 줄

🚫 이렇게 쓰면 뚫립니다
name = request.form["name"]
conn.execute(
  "INSERT INTO product (name) VALUES ('" + name + "')")

누군가 이름 칸에 x'); DROP TABLE product; -- 를 넣으면 표 전체가 사라집니다. 이것이 SQL 인젝션이에요.

✅ 항상 이렇게
name = request.form["name"]
conn.execute(
  "INSERT INTO product (name) VALUES (?)", (name,))

? 는 값을 글자로만 취급합니다. 명령으로 해석되지 않아요. 예외 없이 이 방법만 쓰세요.

두 번째 원칙 — 브라우저가 보낸 값을 절대 그대로 믿지 마세요. HTML에 required나 type="number"를 써 두어도, 개발자 도구를 열면 얼마든지 우회할 수 있습니다. 그건 친절이지 보안이 아니에요. 진짜 검사는 언제나 서버에서 합니다.

🧪 서버가 되어 검사해 보기

아래 코드는 폼으로 들어온 값들을 서버가 검사하는 과정입니다. 장난스러운 입력들이 어떻게 걸러지는지 보세요. 값을 바꿔 실행해 봐도 좋습니다.

마지막 줄을 보세요 — 공격 문자열도 '그냥 긴 상품 이름'이 될 뿐입니다.
Python 브라우저에서 실행
🤔 확인 문제
상품 등록(POST) 처리가 끝난 뒤 redirect()를 하는 가장 큰 이유는?