졸업 작품 — 진짜 쇼핑몰 만들기
SQLite3 · Flask · Jinja2 · Bootstrap을 한 채의 집으로
드디어 마지막입니다. 지금까지 배운 전부를 써서 작동하는 쇼핑몰을 만듭니다.
쇼핑몰은 화려한 화면처럼 보이지만, 사실은 데이터베이스 위에 세운 집입니다. 상품·장바구니·주문이라는 표 세 개가 진짜 알맹이고, 나머지 세 층은 그 표를 사람이 쓸 수 있게 만들어 줄 뿐이에요.
카드·격자·뱃지 — 보기 좋고 휴대폰에서도 편하게
{% for p in products %} — 행 하나가 카드 한 장
라우트·검증·트랜잭션 — 판단은 전부 여기서
product · cart · orders — 유일한 진실
진짜 프로젝트의 파일 구조
실무의 Flask 프로젝트도 정확히 이 모양입니다. 파일 위치만 봐도 어느 층의 일인지 알 수 있어요.
myshop/
├── app.py ← 2층 : 라우트 · 검증 · 트랜잭션 (파이썬)
├── shop.db ← 1층 : 데이터베이스 파일 그 자체
├── templates/ ← 3층 : Jinja2 템플릿
│ ├── base.html 공통 껍데기 (navbar · footer)
│ ├── products.html 상품 목록 {% extends "base.html" %}
│ ├── cart.html 장바구니
│ └── orders.html 주문 내역
└── static/ ← 4층 : 브라우저가 그대로 받아 가는 파일
├── css/style.css Bootstrap 위에 얹는 우리 색깔
└── img/ 상품 사진
이제 첫 삽을 뜹니다. 가장 먼저 할 일은 코딩이 아니라 설계예요. "무엇에 대한 정보를 저장할 것인가"를 나누는 일이죠.
여러 번 담길 수 있다
옮겨진다
이런 그림을 ERD(개체-관계 다이어그램)라고 부릅니다. 실무에서 개발자들은 코드를 한 줄도 쓰기 전에 이 그림부터 그려요.
🎮 진짜 쇼핑몰 — 직접 주문해 보세요
아래는 이 서버의 SQLite3에 실제로 저장되는 쇼핑몰입니다. 상품을 담고 주문하면 진짜 레코드가 만들어지고, 재고가 실제로 줄어듭니다.
모든 클릭마다 서버가 실행한 진짜 SQL이 아래 로그에 나타납니다. 버튼을 누르면서 SQL이 어떻게 바뀌는지 관찰해 보세요.
서버는 무슨 일을 했을까? — 주문 버튼의 내부
여러분이 주문하기를 눌렀을 때, 서버에서는 이런 일이 벌어집니다. 11강의 트랜잭션이 여기서 진가를 발휘해요.
@app.route("/api/shop/order", methods=["POST"])
def place_order():
conn = db()
try:
conn.execute("BEGIN") # ① 트랜잭션 시작
# ② JOIN으로 장바구니 + 상품 정보를 함께 읽는다
items = conn.execute('''
SELECT c.id, c.qty, p.id AS pid, p.name, p.price, p.stock
FROM cart c JOIN product p ON c.product_id = p.id
''').fetchall()
if not items:
raise ValueError("장바구니가 비어 있습니다")
# ③ 재고 검사 — 서버가 DB를 보고 직접 확인한다!
for it in items:
if it["qty"] > it["stock"]:
raise ValueError(f"{it['name']} 재고 부족")
# ④ 가격도 서버가 DB에서 다시 계산한다 (브라우저 값을 안 믿는다)
total = sum(it["price"] * it["qty"] for it in items)
# ⑤ 주문 생성 + 재고 차감 + 장바구니 비우기
cur = conn.execute(
"INSERT INTO orders (total, created) VALUES (?, datetime('now'))", (total,))
for it in items:
conn.execute("UPDATE product SET stock = stock - ? WHERE id = ?",
(it["qty"], it["pid"]))
conn.execute("DELETE FROM cart")
conn.commit() # ⑥ 전부 성공 → 확정
return jsonify({"ok": True, "total": total})
except Exception as e:
conn.rollback() # ⑦ 하나라도 실패 → 전부 취소
return jsonify({"ok": False, "error": str(e)}), 400
그 데이터가 화면이 되는 곳 — 3층과 4층
위 함수는 주문을 DB에 기록했습니다. 그럼 사용자는 그 결과를 어떻게 볼까요? 2층이 SELECT 결과를 3층에 넘기고, 3층이 4층의 부품으로 화면을 찍어 냅니다.
# ── app.py (2층) — 주문 내역 페이지 ──────────────
@app.route("/orders")
def order_list():
conn = db()
orders = conn.execute('''
SELECT o.id, o.total, o.created, COUNT(i.id) AS 품목수
FROM orders o LEFT JOIN order_item i ON i.order_id = o.id
GROUP BY o.id
ORDER BY o.id DESC
''').fetchall()
conn.close()
return render_template("orders.html", orders=orders)
<!-- ── templates/orders.html (3층 + 4층) ────────── -->
{% extends "base.html" %}
{% block title %}주문 내역{% endblock %}
{% block content %}
<h1 class="mb-4">🧾 주문 내역</h1>
{% if orders %}
<table class="table table-striped align-middle">
<thead><tr><th>주문번호</th><th>품목</th><th class="text-end">결제금액</th><th>주문시각</th></tr></thead>
<tbody>
{% for o in orders %}
<tr>
<td><span class="badge bg-secondary">#{{ o.id }}</span></td>
<td>{{ o.품목수 }}종</td>
<td class="text-end fw-bold">{{ "{:,}".format(o.total) }}원</td>
<td class="text-secondary">{{ o.created }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info">아직 주문이 없습니다. 🛒</div>
{% endif %}
{% endblock %}
주목할 점 — 이 템플릿에는 SQL도, 가격 계산도 없습니다.
반대로 app.py 에는 <table> 태그가 한 개도 없습니다.
각 층이 자기 일만 하고 있다는 증거예요. 이 규율이 프로그램을 오래 살아남게 합니다.
🔍 데이터가 진짜로 들어갔는지 확인하기
말로만 믿을 수 없죠. 위 쇼핑몰에서 주문을 한 번 한 뒤, 아래 SQL을 실행해 여러분의 주문 기록을 직접 조회해 보세요.
🎓 여기까지의 여정을 되돌아봅시다
여러분이 지금 할 수 있는 일
무엇을 표로 나눌지 정하고 ERD를 그릴 수 있습니다
Flask 라우트로 요청을 받아 SQL을 실행할 수 있습니다
Jinja2 + Bootstrap으로 데이터를 화면으로 바꿀 수 있습니다
자리표시자·서버 검증·트랜잭션으로 데이터를 지킬 수 있습니다
다음은 무엇을 배울까요?
여기까지 온 여러분, 정말 수고했습니다. 🎉
이제 여러분은 데이터를 설계하고, 저장하고, 보여 주는 법을 압니다.
세상의 거의 모든 서비스가 이 네 층 위에 서 있습니다.
그런데 딱 하나가 비어 있습니다 — 주문 표에 "누가 주문했는지"가 없어요.
지금 이 쇼핑몰은 모두가 장바구니 하나를 함께 쓰는 가게입니다.
마지막 19강에서 회원·로그인·세션을 붙여 진짜 서비스로 완성합니다. 👉