어제까지 우리는 데이터를 Python 리스트나 딕셔너리에 저장했습니다. 😅 하지만 서버를 재시작하면? 모든 데이터가 사라집니다!
1
| users = [] # 서버 재시작 → 빈 리스트로 초기화됨!
|
실제 웹사이트는 데이터베이스(Database)를 사용합니다. 회원 정보, 게시글, 댓글… 모든 것을 영구적으로 저장하죠!
오늘은 SQLite 데이터베이스를 Flask와 연동하는 방법을 배웁니다. SQLite는 별도의 서버 없이 파일 하나로 동작하는 가벼운 데이터베이스입니다. Python에 기본 내장되어 있어 설치 없이 바로 사용할 수 있습니다! 💡
(40분 완독 ⭐⭐⭐⭐⭐)
🎯 오늘의 학습 목표
- SQLite 기초와 SQL 기본 문법
- Flask에서 데이터베이스 연결하기
- 데이터 조회하고 표시하기
- 데이터 추가, 수정, 삭제하기
📚 사전 지식
🎯 학습 목표 1: SQLite 기초와 SQL 기본 문법
한 줄 설명
데이터베이스 = 데이터를 엑셀처럼 표 형태로 저장하고, 언제든지 꺼내 쓸 수 있게 하는 저장소
실생활 비유
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 📚 도서관 시스템:
데이터베이스 = 도서관
- 체계적으로 정리된 자료 보관소
- 서버를 껐다 켜도 데이터가 남아있음
테이블 = 책장 (카테고리별로 구분)
- users 테이블 = 회원 정보 책장
- posts 테이블 = 게시글 책장
- comments 테이블 = 댓글 책장
행(Row) = 한 권의 책
- 각 사용자, 각 게시글이 한 행
열(Column) = 책의 속성
- id (책 번호), title (제목), author (저자)
SQL = 사서에게 말하는 방법
- "SELECT * FROM users" = "회원 정보 책장에서 모든 책 가져와주세요"
- "INSERT INTO posts ..." = "게시글 책장에 새 책 추가해주세요"
|
데이터베이스란?
데이터베이스(Database)는 데이터를 체계적으로 저장하고 관리하는 시스템입니다.
1
2
3
4
5
6
7
8
9
10
| 엑셀 스프레드시트와 비슷하게 생각하면 됩니다!
┌─────────┬──────────┬────────────────┬─────┐
│ id │ name │ email │ age │
├─────────┼──────────┼────────────────┼─────┤
│ 1 │ 홍길동 │ [email protected] │ 25 │
│ 2 │ 김철수 │ [email protected] │ 30 │
│ 3 │ 이영희 │ [email protected] │ 28 │
└─────────┴──────────┴────────────────┴─────┘
👆 이것이 "테이블(Table)"입니다!
|
핵심 용어:
1
2
3
4
| 테이블(Table) : 데이터를 저장하는 표 (예: users, posts)
행(Row) : 한 개의 데이터 레코드
열(Column) : 데이터의 속성 (id, name, email 등)
기본키(Primary Key) : 각 행을 고유하게 식별하는 값 (보통 id)
|
SQLite란?
1
2
3
4
5
6
7
8
9
10
11
| # SQLite의 특징:
✅ 설치 불필요 (Python 기본 내장)
✅ 파일 기반 (database.db 파일 하나만 생성)
✅ 설정 간단 (별도 서버 필요 없음)
✅ 빠르고 가벼움
✅ 소규모 프로젝트에 적합
⚠️ 단점:
- 동시 접속자가 많으면 느려질 수 있음
- 대규모 프로젝트에는 MySQL, PostgreSQL 사용 권장
|
SQL 기본 문법
SQL(Structured Query Language)은 데이터베이스와 대화하는 언어입니다.
1. 테이블 생성 (CREATE TABLE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| -- users 테이블 생성
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
/*
설명:
- id: 정수형, 기본키, 자동 증가
- username: 텍스트, 필수(NOT NULL)
- email: 텍스트, 고유값(UNIQUE), 필수
- age: 정수형, 선택
- created_at: 타임스탬프, 기본값은 현재 시간
*/
|
주요 데이터 타입:
1
2
3
4
5
| INTEGER -- 정수 (1, 2, 3, ...)
TEXT -- 문자열 ("홍길동", "hello", ...)
REAL -- 실수 (3.14, 2.5, ...)
BLOB -- 바이너리 데이터 (이미지, 파일 등)
TIMESTAMP -- 날짜/시간
|
2. 데이터 삽입 (INSERT)
1
2
3
4
5
6
7
8
9
| -- 한 개 삽입
INSERT INTO users (username, email, age)
VALUES ('홍길동', '[email protected]', 25);
-- 여러 개 삽입
INSERT INTO users (username, email, age)
VALUES
('김철수', '[email protected]', 30),
('이영희', '[email protected]', 28);
|
3. 데이터 조회 (SELECT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| -- 모든 데이터 조회
SELECT * FROM users;
-- 특정 컬럼만 조회
SELECT username, email FROM users;
-- 조건부 조회 (WHERE)
SELECT * FROM users WHERE age >= 25;
SELECT * FROM users WHERE username = '홍길동';
-- 정렬 (ORDER BY)
SELECT * FROM users ORDER BY age DESC; -- 나이 내림차순
SELECT * FROM users ORDER BY created_at ASC; -- 생성일 오름차순
-- 개수 제한 (LIMIT)
SELECT * FROM users LIMIT 10; -- 최대 10개만
|
4. 데이터 수정 (UPDATE)
1
2
3
4
5
6
7
8
9
| -- 특정 사용자의 이메일 변경
UPDATE users
SET email = '[email protected]'
WHERE id = 1;
-- 여러 컬럼 동시 수정
UPDATE users
SET email = '[email protected]', age = 26
WHERE username = '홍길동';
|
5. 데이터 삭제 (DELETE)
1
2
3
4
5
6
7
8
| -- 특정 사용자 삭제
DELETE FROM users WHERE id = 1;
-- 조건에 맞는 모든 사용자 삭제
DELETE FROM users WHERE age < 18;
-- ⚠️ 모든 데이터 삭제 (주의!)
DELETE FROM users; -- WHERE 없으면 전체 삭제!
|
Python에서 SQLite 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| # basic_sqlite.py
import sqlite3
# 1. 데이터베이스 연결 (파일이 없으면 자동 생성)
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
# 2. 테이블 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER
)
''')
# 3. 데이터 삽입
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ('홍길동', 25))
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ('김철수', 30))
# 4. 변경사항 저장 (중요!)
conn.commit()
# 5. 데이터 조회
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()
for row in rows:
print(f"ID: {row[0]}, 이름: {row[1]}, 나이: {row[2]}")
# 6. 연결 종료
conn.close()
|
실행 결과:
1
2
| ID: 1, 이름: 홍길동, 나이: 25
ID: 2, 이름: 김철수, 나이: 30
|
🎯 학습 목표 2: Flask에서 데이터베이스 연결하기
한 줄 설명
데이터베이스 연결 = Flask 앱과 SQLite 파일을 연결해서 데이터를 주고받을 수 있게 하기
실생활 비유
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| 📞 전화 연결:
get_db() = 도서관에 전화 걸기
- 요청할 때마다 연결
- conn = sqlite3.connect('database.db')
cursor = 사서와 통화 시작
- 실제로 명령을 전달하는 통로
- cursor.execute("SELECT ...") = 사서에게 요청
commit() = "네, 알겠습니다. 처리했습니다!"
- 변경사항을 실제로 저장
- INSERT, UPDATE, DELETE 후 필수!
close() = 전화 끊기
- 연결 종료
- 메모리 절약, 리소스 정리
연결 관리:
1. 전화 걸기 (get_db)
2. 요청하기 (execute)
3. 확인받기 (commit)
4. 전화 끊기 (close)
|
데이터베이스 헬퍼 함수 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| # app.py
import sqlite3
from flask import Flask, g
app = Flask(__name__)
# 데이터베이스 파일 경로
DATABASE = 'database.db'
def get_db():
"""데이터베이스 연결 가져오기"""
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row # 딕셔너리처럼 접근 가능
return db
@app.teardown_appcontext
def close_connection(exception):
"""요청 종료 시 데이터베이스 연결 닫기"""
db = getattr(g, '_database', None)
if db is not None:
db.close()
def query_db(query, args=(), one=False):
"""SQL 쿼리 실행 헬퍼 함수"""
cur = get_db().execute(query, args)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
if __name__ == '__main__':
app.run(debug=True)
|
g 객체란?
1
2
3
4
5
6
7
8
9
10
11
| # g는 Flask의 전역 객체
# 각 요청(request)마다 독립적인 저장 공간을 제공
# 요청이 끝나면 자동으로 정리됨
from flask import g
# 데이터 저장
g.user = 'alice'
# 데이터 읽기
username = g.user
|
데이터베이스 초기화 스크립트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| # init_db.py
import sqlite3
# 데이터베이스 연결
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
# 기존 테이블 삭제 (개발 중에만!)
cursor.execute('DROP TABLE IF EXISTS users')
cursor.execute('DROP TABLE IF EXISTS posts')
# users 테이블 생성
cursor.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# posts 테이블 생성
cursor.execute('''
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
user_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 테스트 데이터 삽입
cursor.execute('''
INSERT INTO users (username, email, password)
VALUES (?, ?, ?)
''', ('admin', '[email protected]', 'password123'))
cursor.execute('''
INSERT INTO users (username, email, password)
VALUES (?, ?, ?)
''', ('alice', '[email protected]', 'alice123'))
cursor.execute('''
INSERT INTO posts (title, content, user_id)
VALUES (?, ?, ?)
''', ('첫 번째 게시글', 'Flask와 SQLite는 환상의 조합!', 1))
cursor.execute('''
INSERT INTO posts (title, content, user_id)
VALUES (?, ?, ?)
''', ('두 번째 게시글', '데이터베이스 연동 완료!', 2))
# 저장하고 닫기
conn.commit()
conn.close()
print("데이터베이스 초기화 완료!")
|
실행:
🎯 학습 목표 3: 데이터 조회하고 표시하기
한 줄 설명
데이터 조회 = SELECT 문으로 데이터베이스에서 원하는 정보를 가져와서 웹페이지에 표시하기
실생활 비유
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 🔍 도서관에서 책 찾기:
SELECT * FROM users = "회원 정보 책장에서 모든 책 가져와주세요"
- * = 모든 컬럼 (책의 모든 정보)
- FROM users = users 테이블에서
WHERE = 조건부 검색
- SELECT * FROM users WHERE age >= 19
- "19세 이상 회원만 찾아주세요"
ORDER BY = 정렬
- ORDER BY created_at DESC
- "최신 가입자부터 보여주세요" (내림차순)
JOIN = 여러 책장을 동시에 보기
- posts와 users를 연결
- "게시글과 작성자 정보를 함께 가져와주세요"
예시:
사서: "무엇을 도와드릴까요?"
나: "SELECT title, author FROM books WHERE genre = '소설'"
사서: "소설 책장에서 제목과 저자 정보를 가져왔습니다!"
|
사용자 목록 조회
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # app.py
from flask import Flask, render_template
import sqlite3
app = Flask(__name__)
DATABASE = 'database.db'
def get_db():
db = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.route('/')
def home():
return '<h1>홈 페이지</h1><a href="/users">사용자 목록</a> | <a href="/posts">게시글</a>'
@app.route('/users')
def users():
db = get_db()
cursor = db.cursor()
cursor.execute('SELECT * FROM users ORDER BY created_at DESC')
users = cursor.fetchall()
db.close()
return render_template('users.html', users=users)
if __name__ == '__main__':
app.run(debug=True)
|
templates/users.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| <!DOCTYPE html>
<html>
<head>
<title>사용자 목록</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 50px auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:hover { background-color: #f5f5f5; }
</style>
</head>
<body>
<h1>사용자 목록</h1>
<p>총 {{ users|length }}명의 사용자</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>아이디</th>
<th>이메일</th>
<th>가입일</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user['id'] }}</td>
<td>{{ user['username'] }}</td>
<td>{{ user['email'] }}</td>
<td>{{ user['created_at'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<br>
<a href="/">← 홈으로</a>
</body>
</html>
|
게시글 목록과 작성자 정보 함께 조회 (JOIN)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @app.route('/posts')
def posts():
db = get_db()
cursor = db.cursor()
# JOIN을 사용해 게시글과 작성자 정보를 함께 조회
cursor.execute('''
SELECT
posts.id,
posts.title,
posts.content,
posts.created_at,
users.username
FROM posts
JOIN users ON posts.user_id = users.id
ORDER BY posts.created_at DESC
''')
posts = cursor.fetchall()
db.close()
return render_template('posts.html', posts=posts)
|
templates/posts.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| <!DOCTYPE html>
<html>
<head>
<title>게시글 목록</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 50px auto; }
.post { border: 1px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 5px; }
.post h2 { margin: 0 0 10px 0; color: #333; }
.meta { color: #666; font-size: 0.9em; }
</style>
</head>
<body>
<h1>게시글 목록</h1>
{% for post in posts %}
<div class="post">
<h2>{{ post['title'] }}</h2>
<p>{{ post['content'] }}</p>
<p class="meta">
작성자: {{ post['username'] }} |
작성일: {{ post['created_at'] }}
</p>
</div>
{% else %}
<p>게시글이 없습니다.</p>
{% endfor %}
<br>
<a href="/">← 홈으로</a>
</body>
</html>
|
특정 사용자 상세 조회
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| @app.route('/user/<int:user_id>')
def user_detail(user_id):
db = get_db()
cursor = db.cursor()
# 사용자 정보
cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))
user = cursor.fetchone()
if not user:
db.close()
return "사용자를 찾을 수 없습니다", 404
# 해당 사용자의 게시글들
cursor.execute('''
SELECT * FROM posts
WHERE user_id = ?
ORDER BY created_at DESC
''', (user_id,))
posts = cursor.fetchall()
db.close()
return render_template('user_detail.html', user=user, posts=posts)
|
templates/user_detail.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| <!DOCTYPE html>
<html>
<body>
<h1>{{ user['username'] }}님의 프로필</h1>
<div style="background: #f5f5f5; padding: 20px; margin: 20px 0;">
<p><strong>이메일:</strong> {{ user['email'] }}</p>
<p><strong>가입일:</strong> {{ user['created_at'] }}</p>
<p><strong>작성한 게시글:</strong> {{ posts|length }}개</p>
</div>
<h2>작성한 게시글</h2>
{% for post in posts %}
<div style="border: 1px solid #ddd; padding: 15px; margin: 10px 0;">
<h3>{{ post['title'] }}</h3>
<p>{{ post['content'] }}</p>
<small>{{ post['created_at'] }}</small>
</div>
{% else %}
<p>작성한 게시글이 없습니다.</p>
{% endfor %}
<br>
<a href="/users">← 목록으로</a>
</body>
</html>
|
🎯 학습 목표 4: 데이터 추가, 수정, 삭제하기
한 줄 설명
CRUD 작업 = Create(추가), Read(조회), Update(수정), Delete(삭제)로 데이터를 완벽하게 관리하기
실생활 비유
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 📝 도서관 관리 업무:
CREATE (INSERT) = 새 책 등록
- INSERT INTO books (title, author) VALUES ('파이썬', '홍길동')
- "새 책을 책장에 추가해주세요"
READ (SELECT) = 책 찾기
- SELECT * FROM books WHERE id = 1
- "1번 책 정보 알려주세요"
UPDATE = 책 정보 수정
- UPDATE books SET title = '새 제목' WHERE id = 1
- "1번 책의 제목을 바꿔주세요"
DELETE = 책 폐기
- DELETE FROM books WHERE id = 1
- "1번 책을 책장에서 빼주세요"
⚠️ 주의사항:
- UPDATE/DELETE 시 WHERE 빼먹으면 전체 수정/삭제! (위험!)
- DELETE FROM books ← 모든 책 삭제됨!
- 반드시 WHERE 조건 확인!
|
데이터 추가 (INSERT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| from flask import request, redirect, url_for, flash
app.secret_key = 'your-secret-key'
@app.route('/user/add', methods=['GET', 'POST'])
def add_user():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
db = get_db()
cursor = db.cursor()
try:
cursor.execute('''
INSERT INTO users (username, email, password)
VALUES (?, ?, ?)
''', (username, email, password))
db.commit()
flash(f'{username}님이 추가되었습니다!', 'success')
return redirect(url_for('users'))
except sqlite3.IntegrityError:
flash('이미 존재하는 아이디 또는 이메일입니다', 'error')
finally:
db.close()
return render_template('add_user.html')
|
templates/add_user.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| <!DOCTYPE html>
<html>
<head>
<title>사용자 추가</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; }
input { width: 100%; padding: 10px; margin: 10px 0; }
button { padding: 10px 20px; background: #4CAF50; color: white; border: none; }
</style>
</head>
<body>
<h1>새 사용자 추가</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<p style="color: {% if category == 'error' %}red{% else %}green{% endif %}">
{{ message }}
</p>
{% endfor %}
{% endwith %}
<form method="POST">
<input type="text" name="username" placeholder="아이디" required>
<input type="email" name="email" placeholder="이메일" required>
<input type="password" name="password" placeholder="비밀번호" required>
<button type="submit">추가하기</button>
</form>
<br>
<a href="/users">← 목록으로</a>
</body>
</html>
|
데이터 수정 (UPDATE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| @app.route('/user/<int:user_id>/edit', methods=['GET', 'POST'])
def edit_user(user_id):
db = get_db()
cursor = db.cursor()
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
try:
cursor.execute('''
UPDATE users
SET username = ?, email = ?
WHERE id = ?
''', (username, email, user_id))
db.commit()
flash('사용자 정보가 수정되었습니다', 'success')
return redirect(url_for('users'))
except sqlite3.IntegrityError:
flash('이미 존재하는 아이디 또는 이메일입니다', 'error')
finally:
db.close()
# GET: 기존 정보 표시
cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))
user = cursor.fetchone()
db.close()
if not user:
return "사용자를 찾을 수 없습니다", 404
return render_template('edit_user.html', user=user)
|
templates/edit_user.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| <!DOCTYPE html>
<html>
<body>
<h1>사용자 정보 수정</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<p style="color: {% if category == 'error' %}red{% else %}green{% endif %}">{{ message }}</p>
{% endfor %}
{% endwith %}
<form method="POST">
<input type="text" name="username" value="{{ user['username'] }}" required>
<input type="email" name="email" value="{{ user['email'] }}" required>
<button type="submit">수정하기</button>
</form>
<br>
<a href="/users">← 목록으로</a>
</body>
</html>
|
데이터 삭제 (DELETE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @app.route('/user/<int:user_id>/delete', methods=['POST'])
def delete_user(user_id):
db = get_db()
cursor = db.cursor()
# 사용자의 게시글도 함께 삭제
cursor.execute('DELETE FROM posts WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
db.commit()
db.close()
flash('사용자가 삭제되었습니다', 'success')
return redirect(url_for('users'))
|
templates/users.html (삭제 버튼 추가):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| <table>
<thead>
<tr>
<th>ID</th>
<th>아이디</th>
<th>이메일</th>
<th>작업</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user['id'] }}</td>
<td>{{ user['username'] }}</td>
<td>{{ user['email'] }}</td>
<td>
<a href="/user/{{ user['id'] }}/edit">수정</a> |
<form method="POST" action="/user/{{ user['id'] }}/delete" style="display: inline;">
<button onclick="return confirm('정말 삭제하시겠습니까?')">삭제</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
|
💻 실전 예제
예제: 간단한 방명록 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| # guestbook_app.py
from flask import Flask, render_template, request, redirect, url_for, flash
import sqlite3
from datetime import datetime
app = Flask(__name__)
app.secret_key = 'guestbook-secret'
DATABASE = 'guestbook.db'
def init_db():
"""데이터베이스 초기화"""
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def get_db():
db = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.route('/')
def index():
db = get_db()
cursor = db.cursor()
cursor.execute('SELECT * FROM messages ORDER BY created_at DESC')
messages = cursor.fetchall()
db.close()
return render_template('guestbook.html', messages=messages)
@app.route('/add', methods=['POST'])
def add_message():
name = request.form.get('name', '').strip()
message = request.form.get('message', '').strip()
if not name or not message:
flash('이름과 메시지를 모두 입력해주세요', 'error')
return redirect(url_for('index'))
db = get_db()
cursor = db.cursor()
cursor.execute('''
INSERT INTO messages (name, message)
VALUES (?, ?)
''', (name, message))
db.commit()
db.close()
flash('메시지가 등록되었습니다!', 'success')
return redirect(url_for('index'))
@app.route('/delete/<int:message_id>', methods=['POST'])
def delete_message(message_id):
db = get_db()
cursor = db.cursor()
cursor.execute('DELETE FROM messages WHERE id = ?', (message_id,))
db.commit()
db.close()
flash('메시지가 삭제되었습니다', 'success')
return redirect(url_for('index'))
if __name__ == '__main__':
init_db()
app.run(debug=True)
|
templates/guestbook.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| <!DOCTYPE html>
<html>
<head>
<title>방명록</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 50px auto; background: #f9f9f9; }
.container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #333; }
.form-box { background: #f0f0f0; padding: 20px; margin: 20px 0; border-radius: 5px; }
input, textarea { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }
button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; }
button:hover { background: #0056b3; }
.message { border: 1px solid #ddd; padding: 15px; margin: 15px 0; border-radius: 5px; background: #fafafa; }
.message-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.message-name { font-weight: bold; color: #007bff; }
.message-date { color: #666; font-size: 0.9em; }
.delete-btn { padding: 5px 10px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; }
.flash { padding: 10px; margin: 10px 0; border-radius: 5px; }
.flash-success { background: #d4edda; color: #155724; }
.flash-error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="container">
<h1>📝 방명록</h1>
{% with messages_flash = get_flashed_messages(with_categories=true) %}
{% for category, message in messages_flash %}
<div class="flash flash-{{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
<div class="form-box">
<h2>메시지 남기기</h2>
<form method="POST" action="/add">
<input type="text" name="name" placeholder="이름" required>
<textarea name="message" rows="4" placeholder="메시지를 입력하세요" required></textarea>
<button type="submit">등록하기</button>
</form>
</div>
<h2>메시지 목록 ({{ messages|length }}개)</h2>
{% for msg in messages %}
<div class="message">
<div class="message-header">
<span class="message-name">{{ msg['name'] }}</span>
<div>
<span class="message-date">{{ msg['created_at'] }}</span>
<form method="POST" action="/delete/{{ msg['id'] }}" style="display: inline;">
<button class="delete-btn" onclick="return confirm('삭제하시겠습니까?')">삭제</button>
</form>
</div>
</div>
<p>{{ msg['message'] }}</p>
</div>
{% else %}
<p>아직 메시지가 없습니다. 첫 번째 메시지를 남겨보세요!</p>
{% endfor %}
</div>
</body>
</html>
|
⚠️ 주의사항
1. SQL Injection 공격 방지
1
2
3
4
5
6
7
| # ❌ 위험: 문자열 포맷팅 사용 (SQL Injection 취약!)
username = request.form.get('username')
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")
# 공격자가 username에 "admin' OR '1'='1" 입력 시 모든 데이터 노출!
# ✅ 안전: 파라미터 바인딩 사용
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
|
2. 반드시 commit() 호출하기
1
2
3
4
5
6
7
| # ❌ 잘못됨: commit() 없으면 저장 안 됨!
cursor.execute("INSERT INTO users (name) VALUES (?)", ('홍길동',))
# 데이터가 저장되지 않음!
# ✅ 올바름
cursor.execute("INSERT INTO users (name) VALUES (?)", ('홍길동',))
conn.commit() # 반드시 필요!
|
3. 연결 종료하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # ✅ try-finally 패턴
db = get_db()
try:
cursor = db.cursor()
cursor.execute("SELECT * FROM users")
users = cursor.fetchall()
finally:
db.close() # 반드시 닫기
# ✅ 또는 with 문 사용
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
# 자동으로 commit되고 close됨
|
🧪 연습 문제
문제 1: 할 일 관리 앱
다음 기능을 가진 할 일 관리 앱을 만드세요:
- 테이블:
todos (id, task, completed, created_at) - 할 일 추가
- 할 일 목록 조회
- 완료 처리 (completed를 0에서 1로 변경)
- 삭제
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
| # todo_app.py
from flask import Flask, render_template, request, redirect, url_for, flash
import sqlite3
app = Flask(__name__)
app.secret_key = 'todo-secret-key'
DATABASE = 'todos.db'
def init_db():
"""데이터베이스 초기화"""
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def get_db():
db = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.route('/')
def index():
"""할 일 목록 조회"""
db = get_db()
cursor = db.cursor()
cursor.execute('''
SELECT * FROM todos
ORDER BY completed ASC, created_at DESC
''')
todos = cursor.fetchall()
db.close()
return render_template('todos.html', todos=todos)
@app.route('/add', methods=['POST'])
def add_todo():
"""할 일 추가"""
task = request.form.get('task', '').strip()
if not task:
flash('할 일을 입력해주세요', 'error')
return redirect(url_for('index'))
db = get_db()
cursor = db.cursor()
cursor.execute('INSERT INTO todos (task) VALUES (?)', (task,))
db.commit()
db.close()
flash('할 일이 추가되었습니다!', 'success')
return redirect(url_for('index'))
@app.route('/complete/<int:todo_id>', methods=['POST'])
def complete_todo(todo_id):
"""할 일 완료 처리"""
db = get_db()
cursor = db.cursor()
# 현재 상태 확인
cursor.execute('SELECT completed FROM todos WHERE id = ?', (todo_id,))
todo = cursor.fetchone()
if todo:
# 토글: 0이면 1로, 1이면 0으로
new_status = 0 if todo['completed'] == 1 else 1
cursor.execute('UPDATE todos SET completed = ? WHERE id = ?',
(new_status, todo_id))
db.commit()
flash('상태가 변경되었습니다', 'success')
else:
flash('할 일을 찾을 수 없습니다', 'error')
db.close()
return redirect(url_for('index'))
@app.route('/delete/<int:todo_id>', methods=['POST'])
def delete_todo(todo_id):
"""할 일 삭제"""
db = get_db()
cursor = db.cursor()
cursor.execute('DELETE FROM todos WHERE id = ?', (todo_id,))
db.commit()
db.close()
flash('할 일이 삭제되었습니다', 'success')
return redirect(url_for('index'))
if __name__ == '__main__':
init_db()
app.run(debug=True)
|
templates/todos.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| <!DOCTYPE html>
<html>
<head>
<title>할 일 관리</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; background: #f5f5f5; }
.container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #333; }
.flash { padding: 10px; margin: 10px 0; border-radius: 5px; }
.flash-success { background: #d4edda; color: #155724; }
.flash-error { background: #f8d7da; color: #721c24; }
.add-form { display: flex; gap: 10px; margin: 20px 0; }
.add-form input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
.add-form button { padding: 10px 20px; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; }
.todo-list { list-style: none; padding: 0; }
.todo-item { display: flex; align-items: center; padding: 15px; margin: 10px 0; background: #f9f9f9; border-radius: 5px; }
.todo-item.completed { opacity: 0.6; }
.todo-item.completed .task { text-decoration: line-through; }
.task { flex: 1; }
.btn { padding: 5px 10px; margin: 0 5px; border: none; border-radius: 3px; cursor: pointer; }
.btn-complete { background: #007bff; color: white; }
.btn-delete { background: #dc3545; color: white; }
</style>
</head>
<body>
<div class="container">
<h1>✅ 할 일 관리</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="flash flash-{{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
<form method="POST" action="/add" class="add-form">
<input type="text" name="task" placeholder="할 일을 입력하세요" required>
<button type="submit">추가</button>
</form>
<ul class="todo-list">
{% for todo in todos %}
<li class="todo-item {% if todo['completed'] %}completed{% endif %}">
<span class="task">
{% if todo['completed'] %}✅{% else %}⭐{% endif %}
{{ todo['task'] }}
</span>
<form method="POST" action="/complete/{{ todo['id'] }}" style="display: inline;">
<button type="submit" class="btn btn-complete">
{% if todo['completed'] %}취소{% else %}완료{% endif %}
</button>
</form>
<form method="POST" action="/delete/{{ todo['id'] }}" style="display: inline;">
<button type="submit" class="btn btn-delete"
onclick="return confirm('정말 삭제하시겠습니까?')">삭제</button>
</form>
</li>
{% else %}
<p>할 일이 없습니다. 새로운 할 일을 추가해보세요!</p>
{% endfor %}
</ul>
</div>
</body>
</html>
|
설명:
completed INTEGER DEFAULT 0: 0은 미완료, 1은 완료 상태 ORDER BY completed ASC: 미완료 할 일을 먼저 표시 - 완료 버튼: completed 값을 토글 (0↔1)
- CSS로 완료된 항목은 취소선 표시
📝 요약
이번 Day 85에서 학습한 내용:
- SQLite 기초: 테이블, SQL 기본 문법 (SELECT, INSERT, UPDATE, DELETE)
- Flask 연동:
get_db(), g 객체, row_factory - 데이터 조회:
fetchall(), fetchone(), JOIN - CRUD 작업: 생성, 읽기, 수정, 삭제
핵심 코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 연결
db = sqlite3.connect('database.db')
cursor = db.cursor()
# 조회
cursor.execute('SELECT * FROM users')
users = cursor.fetchall()
# 삽입
cursor.execute('INSERT INTO users (name) VALUES (?)', ('홍길동',))
db.commit()
# 종료
db.close()
|
📚 다음 학습
Day 86: CRUD 애플리케이션 ⭐⭐⭐⭐⭐
내일은 오늘 배운 내용을 활용해 완전한 CRUD(Create, Read, Update, Delete) 애플리케이션을 만듭니다!
“데이터베이스는 웹 애플리케이션의 심장입니다!” 💾
| Day 85/100 | Phase 9: 웹 개발 입문 | #100DaysOfPython |