
0. 학습 목표
→ users 테이블에 데이터를 넣고, 사용자 입력을 안전하게 전달하는 파라미터 바인딩을 익힙니다.
0.1 이번 글에서 다룰 내용
이번 글은 실습 중심 강의입니다.
4강에서 만든 users 테이블에 실제 데이터를 넣습니다.
사용자가 입력한 이름과 비밀번호를 INSERT로 저장하는데,
이때 값을 문자열로 직접 이어 붙이지 않고 %s로 전달하는 파라미터 바인딩을 사용합니다.
3강에서 "식별자는 검증"을 배웠다면, 이번에는 그 규칙의 나머지 절반인 "값은 바인딩"을 구현합니다.
| 구분 | 내용 |
| 이해할 것 | INSERT 문의 구조, 문자열 조합이 위험한 이유, 파라미터 바인딩이 막아 주는 것 |
| 만들 것 | 이름·비밀번호를 입력받아 users에 저장하는 입력 폼과 insert_user 메서드 |
0.2 이번 강의 실습 내용
이번 강의에서 직접 해보는 것은 다음과 같습니다.
- 사용자 이름·비밀번호 입력 폼과 추가 버튼을 창에 넣습니다.
insert_user메서드에서 파라미터 바인딩으로INSERT를 실행합니다.- 저장 후 입력칸을 비워 연속 입력을 준비합니다.
- 실제로 사용자를 여러 명 추가하고 저장을 확인합니다.
1. 무엇을 추가하는가
→ 접속 영역과 분리된 '사용자 입력' 영역을 새로 추가합니다.
지금까지는 접속 정보(host·user·password·db) 하나만 다뤘습니다.
이번에는 테이블에 저장할 사용자 데이터를 위한 입력칸이 따로 필요합니다.
접속용 비밀번호와 혼동하지 않도록,
저장할 사용자 정보는 username_edit·password_user_edit라는 별도 입력칸으로 만듭니다.

실제 diff 코드를 보기 전에, 이번에 손대는 자리를 먼저 지도로 확인합니다.
class MySqlDemoWindow(QWidget):
def __init__(self):
... 접속 정보 입력 필드 (2강) ...
... 접속 버튼 3개 (2·3·4강) ...
... 사용자 데이터 입력칸 생성 ← ① username_edit·password_user_edit·버튼 만들기
... main_layout 배치 ← ② addWidget 으로 입력칸·버튼 넣기
... 시그널 연결 ← ③ clicked.connect 연결
def connect_to_server_or_db(self): # 2강 메서드 (그대로)
...
def create_database(self): # 3강 메서드 (그대로)
...
def write_log_with_qt(self, message): # 4강 메서드 (그대로)
...
def create_users_table(self): # 4강 메서드 (그대로)
...
# ④ insert_user 메서드를 여기(클래스 안)에 새로 추가
①~③은 __init__ 안에서 동시에 바뀌는 자리이고,
④는 클래스 안에 새로 추가되는 메서드입니다. 아래는 ①~③의 실제 코드입니다.
# ── ① 사용자 데이터 입력칸 생성 부분 (접속 정보와 별개) ──
self.username_edit = QLineEdit() # 추가
self.password_user_edit = QLineEdit() # 추가
self.password_user_edit.setEchoMode(QLineEdit.Password) # 추가
self.btn_insert_user = QPushButton("users 테이블에 사용자 추가") # 추가
# ── ② main_layout 배치 부분 ──
main_layout.addWidget(self.btn_create_table) # (4강)
main_layout.addWidget(self.result_label) # (2강)
main_layout.addWidget(self.username_edit) # 추가 (결과 라벨 바로 아래)
main_layout.addWidget(self.password_user_edit) # 추가
main_layout.addWidget(self.btn_insert_user) # 추가
# ── ③ 시그널 연결 부분 ──
self.btn_create_table.clicked.connect(self.create_users_table) # (4강)
self.btn_insert_user.clicked.connect(self.insert_user) # 추가
접속 영역은 그대로 두고, 그 아래에 사용자 데이터 입력 영역만 얹는 방식입니다. 화면이 두 영역으로 나뉘는 셈입니다.
2. INSERT와 파라미터 바인딩 이해하기
→ INSERT 문의 구조와, 값을 %s로 넘겨야 하는 이유를 정리합니다.
2.1 INSERT 문의 기본 구조
INSERT는 테이블에 새 행을 추가하는 SQL입니다. 어떤 컬럼에 어떤 값을 넣을지 짝지어 씁니다.
id는 AUTO_INCREMENT라 자동으로 채워지므로, 우리가 넣는 것은 username과 password 두 값뿐입니다.
INSERT INTO users (username, password) VALUES (%s, %s)
여기서 값 자리에 실제 데이터 대신 %s 두 개를 두었습니다. 이 자리에 나중에 파이썬이 값을 안전하게 채워 넣습니다.
2.2 문자열 조합은 왜 위험한가
만약 값을 %s 없이 문자열로 직접 이어 붙이면 어떻게 될까요. 아래는 절대 쓰면 안 되는 방식입니다.
# 위험한 예 — 이렇게 하지 마세요
query = f"INSERT INTO users (username, password) VALUES ('{username}', '{password}')"
사용자가 이름 대신 '); DROP TABLE users; -- 같은 문자열을 넣으면,
이어 붙인 SQL이 예상치 못한 명령으로 바뀔 수 있습니다.
이렇게 입력값으로 SQL의 구조 자체를 조작하는 공격을 SQL Injection이라고 합니다.
| 방식 | 결과 |
| 문자열 직접 조합 (f-string) | 입력값이 SQL 구조에 섞여 들어감 — SQL Injection에 취약 |
파라미터 바인딩 (%s + values) |
입력값을 순수한 '값'으로만 취급 — 구조를 바꿀 수 없어 안전 |
✔ 참고: 3강에서 데이터베이스 이름(식별자)은 %s로 못 넘겨 isidentifier()로 검증했습니다. 반대로 여기 사용자 이름·비밀번호는 값이므로 %s로 바인딩합니다. '식별자는 검증, 값은 바인딩'이 하나로 맞물립니다.
3. 삽입 기능 구현하기
→ insert_user 메서드를 작성해, 입력값을 파라미터 바인딩으로 저장합니다.
클래스에 insert_user 메서드를 추가합니다.
위치 지도의 ④번 자리, 즉 create_users_table 메서드 아래입니다.
접속 정보와 저장할 데이터를 각각 읽고,
4강처럼 database를 지정해 연결한 뒤 파라미터 바인딩으로 INSERT를 실행합니다.

# ④ create_users_table 메서드 아래에 새로 추가
def insert_user(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text() # 접속 비밀번호는 strip하지 않음
db_name = self.db_edit.text().strip()
# 저장할 사용자 데이터
username = self.username_edit.text().strip()
user_password = self.password_user_edit.text() # 사용자 비밀번호도 strip하지 않음
# 접속 정보 검증
if not host or not user or not db_name:
QMessageBox.warning(self, "입력 오류", "먼저 접속 정보와 데이터베이스 이름을 모두 입력해야 합니다.")
return
# 저장할 데이터 검증
if not username or not user_password:
QMessageBox.warning(self, "입력 오류", "사용자 이름과 비밀번호를 모두 입력해야 합니다.")
return
conn = None
try:
conn = mc.connect(host=host, user=user, password=password, database=db_name)
cursor = conn.cursor()
# 값 자리에 %s를 두고, 실제 값은 별도로 전달한다. (파라미터 바인딩)
query = "INSERT INTO users (username, password) VALUES (%s, %s)"
values = (username, user_password)
cursor.execute(query, values)
conn.commit()
msg = f"사용자 {username} 정보가 저장되었습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "저장 완료", msg)
self.write_log_with_qt(f"사용자 추가 성공: {db_name}.users, username={username}")
# 연속 입력을 위해 입력칸을 비우고 이름칸으로 커서 이동
self.username_edit.clear()
self.password_user_edit.clear()
self.username_edit.setFocus()
except mc.Error as e:
self.result_label.setText("데이터 저장 중 오류가 발생했습니다.")
QMessageBox.critical(self, "저장 오류", f"데이터 저장에 실패했습니다.\n오류 내용: {e}")
self.write_log_with_qt(f"사용자 추가 실패: username={username}, 오류: {e}")
finally:
if conn and conn.is_connected():
conn.close()
핵심은 cursor.execute(query, values) 한 줄입니다.
SQL 문(query)과 값(values)을 따로 넘기면,
드라이버가 값을 순수한 데이터로만 처리해 SQL 구조에 섞이지 않게 합니다.
저장에 성공하면 입력칸을 비워, 다음 사용자를 곧바로 입력할 수 있게 한 점도 눈여겨보세요.
▶ 지금 해보세요
- 1에서 본 ①~③ 위치에 입력 폼·버튼·시그널을 __init__에 반영합니다.
- ④ 위치에
insert_user메서드를 작성합니다. query와values를 따로 넘기는 부분을 2.2의 위험한 예와 비교해 봅니다.
4. 실행하고 결과 확인하기
→ 사용자를 여러 명 저장하고, 로그와 테이블에서 결과를 확인합니다.
main.py를 실행하고 접속 정보와 pyqtdb를 입력한 상태에서,
아래쪽 사용자 입력칸에 이름과 비밀번호를 넣고 users 테이블에 사용자 추가 버튼을 누릅니다.
성공하면 저장 완료 팝업이 뜨고 입력칸이 비워집니다. 이어서 다른 사용자도 몇 명 더 추가해 봅니다.

USE pyqtdb;
SELECT * FROM users; -- 저장한 사용자들이 id 순서로 보이는지 확인
💡 팁: 저장된 password가 입력한 그대로(평문) 들어가 있는 것이 보입니다. 학습용으로는 괜찮지만, 실무에서는 비밀번호를 절대 평문으로 저장하지 않습니다. 이 부분은 로그인을 다루는 8강에서 다시 짚습니다.
▶ 지금 해보세요
- 이름·비밀번호를 입력하고 추가 버튼을 눌러 저장 완료 메시지를 확인합니다.
- 서로 다른 사용자를 3명 이상 추가합니다. (예:
testuser,john,kim) - MySQL 클라이언트에서
SELECT * FROM users;로 저장된 데이터를 확인합니다. db_log_all_steps.txt에 사용자 추가 로그가 남았는지 확인합니다.
✔ 정리: 이제 테이블에 데이터를 안전하게 넣을 수 있습니다. 값은 %s로 바인딩해 SQL Injection을 막고, cursor.execute(query, values) 패턴을 앞으로 조회·검색·로그인에서 그대로 재사용합니다.
5. 이번 강의 완성본
→ 2~5강까지 이어 붙인 전체 코드를 한 번에 확인합니다.
본문에서는 변경분만 다뤘습니다.
아래는 위치를 대조하거나 코드가 어긋났을 때 기준으로 삼을 mainwindow.py 전체입니다.
main.py는 2강에서 작성한 것과 동일하므로 여기서는 mainwindow.py만 싣습니다.
import sys
from PySide6.QtWidgets import (
QApplication, QWidget, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QFormLayout, QMessageBox,
)
from PySide6.QtCore import QFile, QIODevice, QTextStream
import mysql.connector as mc
class MySqlDemoWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("MySQL 연결 설정")
self.resize(420, 400)
# 접속 정보 입력 필드
self.host_edit = QLineEdit()
self.user_edit = QLineEdit()
self.password_edit = QLineEdit()
self.password_edit.setEchoMode(QLineEdit.Password)
self.db_edit = QLineEdit()
self.host_edit.setText("localhost")
self.user_edit.setText("root")
form_layout = QFormLayout()
form_layout.addRow("호스트", self.host_edit)
form_layout.addRow("사용자", self.user_edit)
form_layout.addRow("비밀번호", self.password_edit)
form_layout.addRow("데이터베이스 이름", self.db_edit)
self.btn_connect = QPushButton("연결 테스트")
self.btn_create_db = QPushButton("데이터베이스 생성")
self.btn_create_table = QPushButton("users 테이블 생성")
self.result_label = QLabel("연결 결과가 여기에 표시됩니다.")
# 저장할 사용자 정보 입력칸 (접속 정보와 별개)
self.username_edit = QLineEdit()
self.password_user_edit = QLineEdit()
self.password_user_edit.setEchoMode(QLineEdit.Password)
self.btn_insert_user = QPushButton("users 테이블에 사용자 추가")
main_layout = QVBoxLayout(self)
main_layout.addLayout(form_layout)
main_layout.addWidget(self.btn_connect)
main_layout.addWidget(self.btn_create_db)
main_layout.addWidget(self.btn_create_table)
main_layout.addWidget(self.result_label)
main_layout.addWidget(self.username_edit)
main_layout.addWidget(self.password_user_edit)
main_layout.addWidget(self.btn_insert_user)
self.btn_connect.clicked.connect(self.connect_to_server_or_db)
self.btn_create_db.clicked.connect(self.create_database)
self.btn_create_table.clicked.connect(self.create_users_table)
self.btn_insert_user.clicked.connect(self.insert_user)
def connect_to_server_or_db(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
db_name = self.db_edit.text().strip()
if not host or not user:
QMessageBox.warning(self, "입력 오류", "호스트와 사용자 이름은 반드시 입력해야 합니다.")
return
conn = None
try:
if db_name:
conn = mc.connect(host=host, user=user, password=password, database=db_name)
else:
conn = mc.connect(host=host, user=user, password=password)
if conn.is_connected():
if db_name:
msg = f"MySQL 서버에 연결되었습니다.\n사용 중인 데이터베이스: {db_name}"
else:
msg = "MySQL 서버에 연결되었습니다.\n데이터베이스 이름은 지정되지 않았습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "연결 성공", msg)
except mc.Error as e:
self.result_label.setText("연결 중 오류가 발생했습니다.")
QMessageBox.critical(self, "연결 오류", f"MySQL 연결에 실패했습니다.\n오류 내용: {e}")
finally:
if conn and conn.is_connected():
conn.close()
def create_database(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
db_name = self.db_edit.text().strip()
if not host or not user or not db_name:
QMessageBox.warning(self, "입력 오류", "호스트, 사용자, 데이터베이스 이름을 모두 입력해야 합니다.")
return
if not db_name.isidentifier():
QMessageBox.warning(self, "입력 오류", "데이터베이스 이름은 영문·숫자·밑줄만 사용할 수 있습니다.")
return
conn = None
try:
conn = mc.connect(host=host, user=user, password=password)
cursor = conn.cursor()
cursor.execute(f"CREATE DATABASE {db_name}")
conn.commit()
msg = f"{db_name} 데이터베이스가 생성되었습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "생성 완료", msg)
except mc.Error as e:
self.result_label.setText("데이터베이스 생성에 실패했습니다.")
QMessageBox.critical(self, "생성 오류", f"데이터베이스 생성에 실패했습니다.\n오류 내용: {e}")
finally:
if conn and conn.is_connected():
conn.close()
def write_log_with_qt(self, message):
file = QFile("db_log_all_steps.txt")
if not file.open(QIODevice.Append | QIODevice.Text):
return
stream = QTextStream(file)
stream << message << "\n"
file.close()
def create_users_table(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
db_name = self.db_edit.text().strip()
if not host or not user or not db_name:
QMessageBox.warning(self, "입력 오류", "테이블을 만들 데이터베이스 이름까지 모두 입력해야 합니다.")
return
create_table_sql = """
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(150) NOT NULL,
password VARCHAR(150) NOT NULL
)
"""
conn = None
try:
conn = mc.connect(host=host, user=user, password=password, database=db_name)
cursor = conn.cursor()
cursor.execute(create_table_sql)
conn.commit()
msg = f"{db_name} 데이터베이스에 users 테이블이 생성되었습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "테이블 생성 완료", msg)
self.write_log_with_qt(f"테이블 생성: {db_name}.users")
except mc.Error as e:
self.result_label.setText("테이블 생성 중 오류가 발생했습니다.")
QMessageBox.critical(self, "테이블 생성 오류", f"users 테이블 생성에 실패했습니다.\n오류 내용: {e}")
self.write_log_with_qt(f"테이블 생성 실패: {db_name}.users, 오류: {e}")
finally:
if conn and conn.is_connected():
conn.close()
def insert_user(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
db_name = self.db_edit.text().strip()
username = self.username_edit.text().strip()
user_password = self.password_user_edit.text()
if not host or not user or not db_name:
QMessageBox.warning(self, "입력 오류", "먼저 접속 정보와 데이터베이스 이름을 모두 입력해야 합니다.")
return
if not username or not user_password:
QMessageBox.warning(self, "입력 오류", "사용자 이름과 비밀번호를 모두 입력해야 합니다.")
return
conn = None
try:
conn = mc.connect(host=host, user=user, password=password, database=db_name)
cursor = conn.cursor()
query = "INSERT INTO users (username, password) VALUES (%s, %s)"
values = (username, user_password)
cursor.execute(query, values)
conn.commit()
msg = f"사용자 {username} 정보가 저장되었습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "저장 완료", msg)
self.write_log_with_qt(f"사용자 추가 성공: {db_name}.users, username={username}")
self.username_edit.clear()
self.password_user_edit.clear()
self.username_edit.setFocus()
except mc.Error as e:
self.result_label.setText("데이터 저장 중 오류가 발생했습니다.")
QMessageBox.critical(self, "저장 오류", f"데이터 저장에 실패했습니다.\n오류 내용: {e}")
self.write_log_with_qt(f"사용자 추가 실패: username={username}, 오류: {e}")
finally:
if conn and conn.is_connected():
conn.close()
→ 다음 강의 (6강): 방금 저장한 데이터를 SELECT로 꺼내 QTableWidget에 표 형태로 보여 줍니다. INSERT와 달리 조회는 commit()이 필요 없다는 차이도 함께 확인합니다.