17강 / 20강
폼과 POST — 사용자의 입력이 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"))
네 단계가 전부입니다. 받고 · 검사하고 · 넣고 · 돌려보낸다. 세상의 거의 모든 '등록' 버튼이 이 구조예요.
🎮 폼 실험실 — 브라우저를 믿으면 안 되는 이유
두 가지를 실험해 보세요.
- 브라우저 검사를 끄고 빈 이름이나
-100원을 등록해 보세요. 개발자 도구를 여는 것과 같은 상황입니다. 그래도 서버가 막습니다. - 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()를 하는 가장 큰 이유는?해설 PRG 패턴입니다. POST 응답을 화면에 그대로 그리면 새로고침 때 같은 INSERT가 다시 실행돼요. redirect로 GET 화면에 넘겨야 안전합니다.