6강 실습 중심 ⏱ 약 35분

 

0. 학습 목표

→ users 테이블을 SELECT로 조회하고, 결과를 QTableWidget에 표로 표시합니다.

더보기

0.1 이번 글에서 다룰 내용

 

이번 글은 실습 중심 강의입니다.

 

5강에서 저장한 데이터를 SELECT로 꺼내 화면에 보여 줍니다.

결과는 여러 행이므로, 표 형태로 표시하는 QTableWidget을 새로 사용합니다.

 

지금까지의 INSERT와 달리 조회는 데이터를 바꾸지 않으므로 commit()이 필요 없다는 차이도 함께 확인합니다.

 

구분 내용
이해할 것 SELECTfetchall()의 동작, 조회에 commit()이 필요 없는 이유
만들 것 조회 버튼과 QTableWidget, 결과를 표로 채우는 load_users 메서드

 

 

 

 

0.2 이번 강의 실습 내용

 

이번 강의에서 직접 해보는 것은 다음과 같습니다.

  1. 조회 버튼과 QTableWidget을 창에 추가합니다.
  2. 테이블을 읽기 전용·행 단위 선택으로 설정합니다.
  3. load_users 메서드에서 SELECT 결과를 표에 채웁니다.
  4. 전체 조회를 실행해 저장한 사용자들이 표에 뜨는지 확인합니다.

 

1. 무엇을 추가하는가

→ 조회 결과를 담을 QTableWidget과 관련 import 위치를 먼저 확인합니다.

더보기

여러 행을 표로 보여 주기 위해

QTableWidget 관련 클래스를 새로 가져옵니다.

셀 정렬에 쓸 Qt도 함께 추가합니다.

[화면 캡처: 아래쪽에 전체 조회 버튼과 빈 표(QTableWidget)가 추가된 창]

 

 

실제 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 표를 채우는 순서

 

가져온 rowsQTableWidget에 옮기는 순서는 간단합니다.

먼저 결과 개수만큼 표의 행 수를 정하고, 각 행을 돌면서 셀에 값을 하나씩 넣습니다.

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은 문자열을 받기 때문입니다.

 

조회 결과가 바뀌어도 이 setRowCountsetItem 패턴만 알면 표를 다시 채울 수 있어,

다음 강의의 검색에서도 그대로 재사용합니다.

 

▶ 지금 해보세요

  1. 1에서 본 ①~⑥ 위치에 import·표·버튼·시그널을 __init__에 반영합니다.
  2. ⑦ 위치에 load_users 메서드를 작성합니다.
  3. 결과를 채우는 반복문이 setRowCountsetItem 순서인지 확인합니다.

 

4. 실행하고 결과 확인하기

→ 전체 조회를 실행해, 저장한 사용자들이 표에 나타나는지 확인합니다.

더보기

main.py를 실행하고 접속 정보와 pyqtdb를 입력한 뒤 users 테이블 전체 조회 버튼을 누릅니다.

5강에서 저장한 사용자들이 id 순서로 표에 채워지고, 하단 라벨에 조회된 행 수가 표시되면 정상입니다.

 

💡 팁: 표에 password가 그대로 보이는 것은 학습 확인을 위한 것입니다. 실제 앱에서는 비밀번호를 화면에 표시하지 않으며, 애초에 평문으로 저장하지도 않습니다. 이 문제는 8강에서 정리합니다.

▶ 지금 해보세요

  1. users 테이블 전체 조회 버튼을 눌러 표가 채워지는지 확인합니다.
  2. 표의 한 행을 클릭해 행 전체가 선택되는지 확인합니다.
  3. 표의 셀을 더블클릭해 수정이 안 되는지(읽기 전용) 확인합니다.
  4. 5강에서 사용자를 하나 더 추가한 뒤 다시 조회해, 새 행이 나타나는지 확인합니다.

✔ 정리: 이제 저장한 데이터를 화면에 표로 볼 수 있습니다. 조회는 commit() 없이 fetchall()로 결과를 받고, setRowCountsetItem으로 표를 채우는 패턴을 익혔습니다.

 

5. 이번 강의 완성본

→ 2~6강까지 이어 붙인 전체 코드를 한 번에 확인합니다.

더보기

본문에서는 변경분만 다뤘습니다.

아래는 위치를 대조하거나 코드가 어긋났을 때 기준으로 삼을 mainwindow.py 전체입니다.

main.py는 2강에서 작성한 것과 동일하므로 여기서는 mainwindow.py만 싣습니다.

6강.zip
0.00MB
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로 바인딩합니다.