
0. 학습 목표
→ users 테이블을 SELECT로 조회하고, 결과를 QTableWidget에 표로 표시합니다.
0.1 이번 글에서 다룰 내용
이번 글은 실습 중심 강의입니다.
5강에서 저장한 데이터를 SELECT로 꺼내 화면에 보여 줍니다.
결과는 여러 행이므로, 표 형태로 표시하는 QTableWidget을 새로 사용합니다.
지금까지의 INSERT와 달리 조회는 데이터를 바꾸지 않으므로 commit()이 필요 없다는 차이도 함께 확인합니다.
| 구분 | 내용 |
| 이해할 것 | SELECT와 fetchall()의 동작, 조회에 commit()이 필요 없는 이유 |
| 만들 것 | 조회 버튼과 QTableWidget, 결과를 표로 채우는 load_users 메서드 |
0.2 이번 강의 실습 내용
이번 강의에서 직접 해보는 것은 다음과 같습니다.
- 조회 버튼과
QTableWidget을 창에 추가합니다. - 테이블을 읽기 전용·행 단위 선택으로 설정합니다.
load_users메서드에서SELECT결과를 표에 채웁니다.- 전체 조회를 실행해 저장한 사용자들이 표에 뜨는지 확인합니다.
1. 무엇을 추가하는가
→ 조회 결과를 담을 QTableWidget과 관련 import 위치를 먼저 확인합니다.
여러 행을 표로 보여 주기 위해
QTableWidget 관련 클래스를 새로 가져옵니다.
셀 정렬에 쓸 Qt도 함께 추가합니다.

실제 diff 코드를 보기 전에, 이번에 손대는 자리를 먼저 지도로 확인합니다.
from PySide6.QtWidgets import ... ← ① QTableWidget 계열 import 추가
from PySide6.QtCore import ... ← ② Qt(정렬용) import 추가
class MySqlDemoWindow(QWidget):
def __init__(self):
... 접속 정보 입력 필드 (2강) ...
... 접속 버튼 3개 (2·3·4강) ...
... 사용자 추가 입력칸·버튼 (5강) ...
... 표(QTableWidget) 생성 및 설정 ← ③ 컬럼·읽기 전용·선택 방식 구성
... 조회 버튼 생성 ← ④ 조회 버튼 만들기
... 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강 메서드 (그대로)
...
def insert_user(self): # 5강 메서드 (그대로)
...
# ⑦ load_users 메서드를 여기(클래스 안)에 새로 추가
①②는 import 부분, ③~⑥은 __init__ 안에서 동시에 바뀌는 자리이고,
⑦은 클래스 안에 새로 추가되는 메서드입니다. 아래는 ①~⑥의 실제 코드입니다.
from PySide6.QtWidgets import (
QApplication, QWidget, QLabel, QLineEdit, # (2강)
QPushButton, QVBoxLayout, QFormLayout, QMessageBox, # (2강)
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, # 추가 (①)
)
from PySide6.QtCore import QFile, QIODevice, QTextStream, Qt # Qt 추가 (②)
# ── ③ 표(QTableWidget) 생성 및 설정 부분 ──
self.table_users = QTableWidget() # 추가
self.table_users.setColumnCount(3) # 추가
self.table_users.setHorizontalHeaderLabels(["id", "username", "password"]) # 추가
# 컬럼 폭을 창 크기에 맞춰 고르게 늘린다.
self.table_users.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) # 추가
# 한 칸이 아니라 행 전체가 선택되도록 설정
self.table_users.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # 추가
# 표 안에서 값을 직접 수정하지 못하도록 읽기 전용으로 설정
self.table_users.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # 추가
# ── ④ 조회 버튼 생성 부분 ──
self.btn_load_users = QPushButton("users 테이블 전체 조회") # 추가
# ── ⑤ main_layout 배치 부분 ──
main_layout.addWidget(self.btn_insert_user) # (5강)
main_layout.addWidget(self.btn_load_users) # 추가 (사용자 추가 버튼 바로 아래)
main_layout.addWidget(self.table_users) # 추가
# ── ⑥ 시그널 연결 부분 ──
self.btn_insert_user.clicked.connect(self.insert_user) # (5강)
self.btn_load_users.clicked.connect(self.load_users) # 추가
읽기 전용으로 두는 이유는,
화면의 표는 어디까지나 DB 내용을 보여 주는 창이기 때문입니다.
표에서 글자를 바꿔도 DB가 바뀌는 것은 아니므로, 혼란을 막기 위해 편집을 막아 둡니다.
각 클래스의 역할은 다음과 같습니다.
| 클래스 | 역할 |
QTableWidget |
조회 결과를 행과 열로 보여 주는 표 위젯 |
QTableWidgetItem |
표의 한 셀에 들어가는 값 |
QHeaderView |
컬럼 폭을 창 크기에 맞춰 늘리는 등 헤더를 제어 |
QAbstractItemView |
행 단위 선택, 편집 금지 같은 표의 동작 방식을 지정 |
2. 조회 흐름 이해하기
→ SELECT의 실행 순서와, 조회에 commit이 필요 없는 이유를 정리합니다.
2.1 조회는 commit이 필요 없다
3~5강의 CREATE·INSERT는 데이터베이스의 내용을 바꾸는 작업이라
commit()으로 변경을 확정해야 했습니다.
반면 SELECT는 데이터를 읽기만 할 뿐 바꾸지 않습니다.
그래서 조회에는 commit()이 필요 없고, 대신 결과를 가져오는 fetchall()이 등장합니다.
# 1. 연결
conn = mc.connect(host, user, password, database)
# 2. 확인
if conn.is_connected():
성공_처리()
# 3. SQL 실행 + 결과 가져오기
cursor = conn.cursor()
cursor.execute("SELECT ...")
rows = cursor.fetchall() # commit 대신 fetch로 결과 수집
# 4. 결과 사용 (표에 표시 등)
# 5. 종료
conn.close()
fetchall()은 조회된 모든 행을 한 번에 리스트로 가져옵니다.
각 행은 컬럼 값들이 묶인 형태((id, username, password))입니다.
2.2 표를 채우는 순서
가져온 rows를 QTableWidget에 옮기는 순서는 간단합니다.
먼저 결과 개수만큼 표의 행 수를 정하고, 각 행을 돌면서 셀에 값을 하나씩 넣습니다.
rows 개수만큼 표의 행 수 설정 → setRowCount(len(rows))
각 행을 반복 → for row in rows
셀에 값 배치 → setItem(행, 열, 값)
컬럼은 id·username·password 세 개로 미리 정해 두고, 조회 결과의 각 값을 순서대로 그 자리에 넣으면 됩니다.
3. 조회 기능 구현하기
→ load_users 메서드로 SELECT 결과를 표에 채웁니다.
클래스에 load_users 메서드를 추가합니다.
위치 지도의 ⑦번 자리, 즉 insert_user 메서드 아래입니다.
연결·조회까지는 익숙한 흐름이고, 새로운 부분은 fetchall()로 받은 결과를 표에 옮기는 반복문입니다.

# ⑦ insert_user 메서드 아래에 새로 추가
def load_users(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
conn = None
try:
conn = mc.connect(host=host, user=user, password=password, database=db_name)
cursor = conn.cursor()
# id 순서로 전체 조회 (조회는 commit이 필요 없다)
query = "SELECT id, username, password FROM users ORDER BY id ASC"
cursor.execute(query)
rows = cursor.fetchall()
# 결과 개수만큼 표의 행 수를 맞춘다.
self.table_users.setRowCount(len(rows))
# 각 행을 돌면서 셀에 값을 채운다.
for row_index, (id_value, username_value, password_value) in enumerate(rows):
item_id = QTableWidgetItem(str(id_value))
item_username = QTableWidgetItem(username_value)
item_password = QTableWidgetItem(password_value)
# 셀 값을 가운데 정렬
item_id.setTextAlignment(Qt.AlignCenter)
item_username.setTextAlignment(Qt.AlignCenter)
item_password.setTextAlignment(Qt.AlignCenter)
# [행, 열] 위치에 배치
self.table_users.setItem(row_index, 0, item_id)
self.table_users.setItem(row_index, 1, item_username)
self.table_users.setItem(row_index, 2, item_password)
msg = f"총 {len(rows)}개의 사용자 데이터를 조회했습니다."
self.result_label.setText(msg)
self.write_log_with_qt(f"사용자 조회: {db_name}.users, 행 수={len(rows)}")
except mc.Error as e:
self.result_label.setText("데이터 조회 중 오류가 발생했습니다.")
QMessageBox.critical(self, "조회 오류", f"데이터 조회에 실패했습니다.\n오류 내용: {e}")
finally:
if conn and conn.is_connected():
conn.close()
id_value는 숫자이므로 str()로 문자열로 바꿔 셀에 넣는 점에 주의하세요.
QTableWidgetItem은 문자열을 받기 때문입니다.
조회 결과가 바뀌어도 이 setRowCount → setItem 패턴만 알면 표를 다시 채울 수 있어,
다음 강의의 검색에서도 그대로 재사용합니다.
▶ 지금 해보세요
- 1에서 본 ①~⑥ 위치에 import·표·버튼·시그널을 __init__에 반영합니다.
- ⑦ 위치에
load_users메서드를 작성합니다. - 결과를 채우는 반복문이
setRowCount→setItem순서인지 확인합니다.
4. 실행하고 결과 확인하기
→ 전체 조회를 실행해, 저장한 사용자들이 표에 나타나는지 확인합니다.
main.py를 실행하고 접속 정보와 pyqtdb를 입력한 뒤 users 테이블 전체 조회 버튼을 누릅니다.
5강에서 저장한 사용자들이 id 순서로 표에 채워지고, 하단 라벨에 조회된 행 수가 표시되면 정상입니다.
💡 팁: 표에 password가 그대로 보이는 것은 학습 확인을 위한 것입니다. 실제 앱에서는 비밀번호를 화면에 표시하지 않으며, 애초에 평문으로 저장하지도 않습니다. 이 문제는 8강에서 정리합니다.
▶ 지금 해보세요
- users 테이블 전체 조회 버튼을 눌러 표가 채워지는지 확인합니다.
- 표의 한 행을 클릭해 행 전체가 선택되는지 확인합니다.
- 표의 셀을 더블클릭해 수정이 안 되는지(읽기 전용) 확인합니다.
- 5강에서 사용자를 하나 더 추가한 뒤 다시 조회해, 새 행이 나타나는지 확인합니다.
✔ 정리: 이제 저장한 데이터를 화면에 표로 볼 수 있습니다. 조회는 commit() 없이 fetchall()로 결과를 받고, setRowCount → setItem으로 표를 채우는 패턴을 익혔습니다.
5. 이번 강의 완성본
→ 2~6강까지 이어 붙인 전체 코드를 한 번에 확인합니다.
본문에서는 변경분만 다뤘습니다.
아래는 위치를 대조하거나 코드가 어긋났을 때 기준으로 삼을 mainwindow.py 전체입니다.
main.py는 2강에서 작성한 것과 동일하므로 여기서는 mainwindow.py만 싣습니다.
import sys
from PySide6.QtWidgets import (
QApplication, QWidget, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QFormLayout, QMessageBox,
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView,
)
from PySide6.QtCore import QFile, QIODevice, QTextStream, Qt
import mysql.connector as mc
class MySqlDemoWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("MySQL 연결 설정")
self.resize(480, 560)
# 접속 정보 입력 필드
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 테이블에 사용자 추가")
# 조회 결과를 담을 표
self.table_users = QTableWidget()
self.table_users.setColumnCount(3)
self.table_users.setHorizontalHeaderLabels(["id", "username", "password"])
self.table_users.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.table_users.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.table_users.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.btn_load_users = 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)
main_layout.addWidget(self.btn_load_users)
main_layout.addWidget(self.table_users)
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)
self.btn_load_users.clicked.connect(self.load_users)
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()
def load_users(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
conn = None
try:
conn = mc.connect(host=host, user=user, password=password, database=db_name)
cursor = conn.cursor()
query = "SELECT id, username, password FROM users ORDER BY id ASC"
cursor.execute(query)
rows = cursor.fetchall()
self.table_users.setRowCount(len(rows))
for row_index, (id_value, username_value, password_value) in enumerate(rows):
item_id = QTableWidgetItem(str(id_value))
item_username = QTableWidgetItem(username_value)
item_password = QTableWidgetItem(password_value)
item_id.setTextAlignment(Qt.AlignCenter)
item_username.setTextAlignment(Qt.AlignCenter)
item_password.setTextAlignment(Qt.AlignCenter)
self.table_users.setItem(row_index, 0, item_id)
self.table_users.setItem(row_index, 1, item_username)
self.table_users.setItem(row_index, 2, item_password)
msg = f"총 {len(rows)}개의 사용자 데이터를 조회했습니다."
self.result_label.setText(msg)
self.write_log_with_qt(f"사용자 조회: {db_name}.users, 행 수={len(rows)}")
except mc.Error as e:
self.result_label.setText("데이터 조회 중 오류가 발생했습니다.")
QMessageBox.critical(self, "조회 오류", f"데이터 조회에 실패했습니다.\n오류 내용: {e}")
finally:
if conn and conn.is_connected():
conn.close()
→ 다음 강의 (7강): 전체 조회 코드를 그대로 두고, WHERE와 LIKE로 이름의 일부만으로 사용자를 찾는 검색 기능을 더합니다. 이때 검색어도 5강처럼 %s로 바인딩합니다.