4강. GUI에서 파일 쓰기/읽기/복사 기능 연결하기

0. 학습 목표
→ 콘솔에서 익힌 QFile 기능을 GUI 버튼에 연결해 파일 쓰기, 읽기, 복사를 구현합니다.
이번 글에서 다룰 내용
이번 글에서는 QFile큐파일, QFileDialog큐파일다이얼로그, QMessageBox큐메시지박스를 사용해서 GUI 파일 처리 기능을 만듭니다.
1강에서는 QFile로 파일을 쓰고 읽는 가장 작은 실습을 했습니다.
2강에서는 QFile.open()과 QIODevice 플래그를 비교했습니다.
3강에서는 QTextStream으로 텍스트 파일을 다루는 방법을 익혔습니다.
이번 4강에서는 그 코드를 GUI 버튼에 연결합니다.
| 구분 | 내용 |
| 핵심 개념 | GUI 버튼을 클릭했을 때 QFile로 파일을 쓰고, 읽고, 복사하는 흐름을 이해합니다. |
| 실습 준비 | Python, PySide6가 설치된 환경에서 GUI 프로그램을 실행합니다. |
| 최종 목표 | 텍스트 편집기, 파일 저장/읽기 버튼, 파일 복사 기능을 가진 간단한 GUI 프로그램을 만듭니다. |
이번 단계의 핵심: 콘솔에서 사용한 QFile 코드는 거의 그대로 사용할 수 있고, GUI에서는 파일 경로를 QFileDialog로 받아오고 결과를 QMessageBox로 알려줍니다.
1. 콘솔 코드를 GUI에 바로 넣으면 무엇이 달라질까?
→ 콘솔 파일 처리 코드와 GUI 파일 처리 코드의 차이를 먼저 확인합니다.
1.1 콘솔에서는 파일 경로를 코드에 직접 적었다
이전 강의에서는 파일 경로를 코드에 직접 적었습니다.
예를 들면 아래처럼 sample.txt를 바로 사용했습니다.
file = QFile("sample.txt")
콘솔 실습에서는 이 방식이 단순해서 좋습니다.
하지만 GUI 프로그램에서는 사용자가 직접 저장할 위치나 읽을 파일을 선택하는 것이 더 자연스럽습니다.
| 구분 | 파일 경로 처리 방식 |
| 콘솔 실습 | 코드 안에 "sample.txt"처럼 직접 적습니다. |
| GUI 프로그램 | QFileDialog로 사용자가 직접 파일 경로를 선택합니다. |
1.2 GUI에서는 결과를 print()로만 보여주기 어렵다
콘솔에서는 print()로 결과를 확인했습니다.
하지만 GUI 프로그램에서는 사용자가 콘솔을 보지 않을 수 있습니다.
그래서 성공, 실패, 경고 메시지를 화면에 보여주는 QMessageBox를 사용합니다.
| 상황 | GUI에서 사용할 도구 |
| 파일 저장 성공 | QMessageBox.information() |
| 입력값 누락 | QMessageBox.warning() |
| 파일 열기 실패 | QMessageBox.critical() |
| 덮어쓰기 확인 | QMessageBox.question() |
1.3 이번 강의에서 만들 흐름
이번 강의에서는 아래 흐름으로 GUI 기능을 연결합니다.
QTextEdit에 글 입력
↓
[파일에 쓰기] 버튼 클릭
↓
QFileDialog로 저장 경로 선택
↓
QFile + QTextStream으로 저장
↓
QMessageBox로 결과 안내
읽기와 복사도 같은 방식으로 진행합니다.
버튼 클릭
↓
경로 선택 또는 경로 입력
↓
QFile 기능 실행
↓
QMessageBox로 성공/실패 안내
문제의 핵심: GUI에서는 파일 처리 코드 자체보다, 사용자가 파일 경로를 선택하고 결과를 화면에서 확인할 수 있게 연결하는 과정이 중요합니다.
2. 파일 쓰기, 읽기, 복사 버튼 직접 연결하기
→ 하나의 GUI 클래스 안에서 파일 저장, 불러오기, 복사 기능을 직접 구현합니다.
2.1 프로젝트 구조 만들기
먼저 아래와 같은 구조로 파일을 만듭니다.
QFileGuiDemo/
├─ main.py
└─ file_editor_window.py
main.py는 프로그램 실행 진입점입니다.
file_editor_window.py는 화면과 버튼 기능을 담당합니다.
이번 강의에서는 아직 Model/View 구조로 분리하지 않고, 하나의 화면 클래스 안에 기능을 직접 연결합니다.
2.2 main.py 작성하기
먼저 프로그램을 실행하는 main.py를 작성합니다.
# main.py
import sys
from PySide6.QtWidgets import QApplication
from file_editor_window import FileEditorWindow
def main():
app = QApplication(sys.argv)
window = FileEditorWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
이 코드는 QApplication을 만들고, FileEditorWindow를 화면에 띄웁니다.
2.3 file_editor_window.py 전체 코드
이제 화면과 파일 기능을 담당하는 file_editor_window.py를 작성합니다.
코드가 길지만, 크게 보면 화면 만들기와 버튼 기능 연결로 나뉩니다.
# file_editor_window.py
from PySide6.QtCore import QFile, QIODevice, QTextStream
from PySide6.QtWidgets import (
QFileDialog,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QMessageBox,
QPushButton,
QTextEdit,
QVBoxLayout,
QWidget,
)
class FileEditorWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QFile GUI 파일 도구")
self.resize(700, 500)
self._setup_ui()
self._connect_signals()
def _setup_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText("파일에 저장할 내용을 입력하거나, 파일에서 읽은 내용이 표시됩니다.")
main_layout.addWidget(self.text_edit)
file_button_layout = QHBoxLayout()
self.btn_write = QPushButton("파일에 쓰기")
self.btn_read = QPushButton("파일에서 읽기")
file_button_layout.addWidget(self.btn_write)
file_button_layout.addWidget(self.btn_read)
main_layout.addLayout(file_button_layout)
src_layout = QHBoxLayout()
src_label = QLabel("원본 파일:")
self.line_src = QLineEdit()
self.line_src.setPlaceholderText("복사할 원본 파일 경로")
self.btn_find_src = QPushButton("원본 찾기")
src_layout.addWidget(src_label)
src_layout.addWidget(self.line_src)
src_layout.addWidget(self.btn_find_src)
main_layout.addLayout(src_layout)
dst_layout = QHBoxLayout()
dst_label = QLabel("복사 대상:")
self.line_dst = QLineEdit()
self.line_dst.setPlaceholderText("복사될 파일 경로")
self.btn_find_dst = QPushButton("대상 선택")
dst_layout.addWidget(dst_label)
dst_layout.addWidget(self.line_dst)
dst_layout.addWidget(self.btn_find_dst)
main_layout.addLayout(dst_layout)
copy_layout = QHBoxLayout()
self.btn_copy = QPushButton("파일 복사")
copy_layout.addStretch()
copy_layout.addWidget(self.btn_copy)
main_layout.addLayout(copy_layout)
def _connect_signals(self):
self.btn_write.clicked.connect(self.on_write_clicked)
self.btn_read.clicked.connect(self.on_read_clicked)
self.btn_find_src.clicked.connect(self.on_find_src_clicked)
self.btn_find_dst.clicked.connect(self.on_find_dst_clicked)
self.btn_copy.clicked.connect(self.on_copy_clicked)
def on_write_clicked(self):
text = self.text_edit.toPlainText()
if not text:
QMessageBox.information(self, "알림", "저장할 텍스트가 없습니다.")
return
file_path, _ = QFileDialog.getSaveFileName(
self,
"텍스트 파일 저장",
"",
"텍스트 파일 (*.txt);;모든 파일 (*.*)",
)
if not file_path:
return
file = QFile(file_path)
if not file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text):
QMessageBox.critical(
self,
"오류",
f"파일을 쓰기 모드로 열 수 없습니다.\\n경로: {file_path}\\n오류: {file.errorString()}",
)
return
stream = QTextStream(file)
stream << text
file.close()
QMessageBox.information(self, "저장 완료", f"파일이 저장되었습니다.\\n경로: {file_path}")
def on_read_clicked(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"텍스트 파일 열기",
"",
"텍스트 파일 (*.txt);;모든 파일 (*.*)",
)
if not file_path:
return
file = QFile(file_path)
if not file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
QMessageBox.critical(
self,
"오류",
f"파일을 읽기 모드로 열 수 없습니다.\\n경로: {file_path}\\n오류: {file.errorString()}",
)
return
stream = QTextStream(file)
content = stream.readAll()
file.close()
self.text_edit.setPlainText(content)
QMessageBox.information(self, "읽기 완료", f"파일 내용을 불러왔습니다.\\n경로: {file_path}")
def on_find_src_clicked(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"원본 파일 선택",
"",
"모든 파일 (*.*)",
)
if file_path:
self.line_src.setText(file_path)
def on_find_dst_clicked(self):
file_path, _ = QFileDialog.getSaveFileName(
self,
"복사 대상 파일 선택",
"",
"모든 파일 (*.*)",
)
if file_path:
self.line_dst.setText(file_path)
def on_copy_clicked(self):
src_path = self.line_src.text().strip()
dst_path = self.line_dst.text().strip()
if not src_path:
QMessageBox.warning(self, "경고", "원본 파일 경로를 입력하거나 선택하세요.")
return
if not dst_path:
QMessageBox.warning(self, "경고", "복사 대상 파일 경로를 입력하거나 선택하세요.")
return
if not QFile.exists(src_path):
QMessageBox.critical(self, "오류", f"원본 파일이 존재하지 않습니다.\\n경로: {src_path}")
return
if QFile.exists(dst_path):
result = QMessageBox.question(
self,
"파일 덮어쓰기 확인",
f"대상 파일이 이미 존재합니다.\\n덮어쓰시겠습니까?\\n\\n경로: {dst_path}",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result == QMessageBox.StandardButton.No:
return
if not QFile.remove(dst_path):
QMessageBox.critical(self, "오류", f"기존 대상 파일을 삭제할 수 없습니다.\\n경로: {dst_path}")
return
src_file = QFile(src_path)
if src_file.copy(dst_path):
QMessageBox.information(
self,
"복사 완료",
f"파일 복사가 완료되었습니다.\\n원본: {src_path}\\n대상: {dst_path}",
)
else:
QMessageBox.critical(
self,
"오류",
f"파일 복사에 실패했습니다.\\n원본: {src_path}\\n대상: {dst_path}\\n오류: {src_file.errorString()}",
)
이제 main.py를 실행하면 파일 쓰기, 읽기, 복사 기능을 가진 GUI 창이 열립니다.
2.4 실행 결과 확인
프로그램을 실행하면 다음 기능을 사용할 수 있습니다.
| 버튼 | 동작 |
| 파일에 쓰기 | QTextEdit의 내용을 사용자가 선택한 파일에 저장합니다. |
| 파일에서 읽기 | 사용자가 선택한 텍스트 파일을 읽어서 QTextEdit에 표시합니다. |
| 원본 찾기 | 복사할 원본 파일 경로를 선택합니다. |
| 대상 선택 | 복사될 대상 파일 경로를 선택합니다. |
| 파일 복사 | 원본 파일을 대상 경로로 복사합니다. |
핵심 확인: 이번 단계에서는 기능을 분리하기보다, 버튼을 클릭했을 때 QFile 코드가 실제로 실행되는 흐름을 경험하는 것이 중요합니다.
3. GUI 파일 처리 코드 흐름 자세히 살펴보기
→ 파일 쓰기, 읽기, 복사 버튼이 어떤 순서로 동작하는지 분석합니다.
3.1 파일에 쓰기 버튼 흐름
파일에 쓰기 버튼을 누르면 아래 순서로 동작합니다.
[파일에 쓰기] 버튼 클릭
↓
QTextEdit에서 텍스트 가져오기
↓
텍스트가 비어 있는지 확인
↓
QFileDialog로 저장 경로 선택
↓
QFile을 WriteOnly + Text 모드로 열기
↓
QTextStream으로 텍스트 저장
↓
QMessageBox로 저장 완료 안내
| 코드 | 역할 |
| self.text_edit.toPlainText() | 입력된 텍스트를 가져옵니다. |
| QFileDialog.getSaveFileName() | 저장할 파일 경로를 사용자에게 선택하게 합니다. |
| QFile(file_path) | 선택한 경로의 파일을 다룰 QFile 객체를 만듭니다. |
| WriteOnly | Text | 파일을 쓰기 전용 텍스트 모드로 엽니다. |
| stream << text | 텍스트를 파일에 저장합니다. |
3.2 파일에서 읽기 버튼 흐름
파일에서 읽기 버튼을 누르면 파일 선택 창이 열리고, 선택한 파일 내용이 QTextEdit에 표시됩니다.
[파일에서 읽기] 버튼 클릭
↓
QFileDialog로 읽을 파일 선택
↓
QFile을 ReadOnly + Text 모드로 열기
↓
QTextStream.readAll()로 전체 내용 읽기
↓
QTextEdit에 읽은 내용 표시
↓
QMessageBox로 읽기 완료 안내
| 코드 | 역할 |
| QFileDialog.getOpenFileName() | 읽을 파일을 선택합니다. |
| ReadOnly | Text | 파일을 읽기 전용 텍스트 모드로 엽니다. |
| stream.readAll() | 파일 전체 내용을 읽습니다. |
| self.text_edit.setPlainText(content) | 읽은 내용을 화면에 표시합니다. |
3.3 파일 복사 버튼 흐름
파일 복사 기능은 쓰기와 읽기보다 확인할 조건이 조금 더 많습니다.
원본 경로가 있는지, 대상 경로가 있는지, 대상 파일이 이미 존재하는지 확인해야 합니다.
[파일 복사] 버튼 클릭
↓
원본 경로와 대상 경로 가져오기
↓
입력값이 비어 있는지 확인
↓
원본 파일이 존재하는지 확인
↓
대상 파일이 이미 있으면 덮어쓰기 여부 확인
↓
필요하면 기존 대상 파일 삭제
↓
QFile.copy()로 복사
↓
QMessageBox로 복사 결과 안내
| 코드 | 역할 |
| self.line_src.text().strip() | 원본 파일 경로를 가져옵니다. |
| self.line_dst.text().strip() | 복사 대상 파일 경로를 가져옵니다. |
| QFile.exists(src_path) | 원본 파일이 실제로 있는지 확인합니다. |
| QFile.remove(dst_path) | 덮어쓰기를 위해 기존 대상 파일을 삭제합니다. |
| src_file.copy(dst_path) | 원본 파일을 대상 경로로 복사합니다. |
주의할 점: QFile.copy()는 대상 파일이 이미 있으면 실패할 수 있으므로, 덮어쓰기를 허용할 때는 기존 파일을 먼저 삭제한 뒤 복사합니다.
4. 콘솔 방식과 GUI 방식 비교하며 정리하기
→ 콘솔에서 직접 실행하던 파일 처리 코드를 GUI 버튼에 연결한 방식으로 비교합니다.
4.1 콘솔 방식과 GUI 방식 비교
이번 강의에서는 콘솔에서 사용하던 파일 처리 코드를 GUI로 옮겼습니다.
파일 처리의 핵심 코드는 비슷하지만, 경로 선택과 사용자 안내 방식이 달라졌습니다.
| 구분 | 콘솔 방식 | GUI 방식 |
| 파일 경로 | 코드에 직접 작성합니다. | QFileDialog로 사용자가 선택합니다. |
| 텍스트 입력 | 코드 안에 문자열을 직접 작성합니다. | QTextEdit에서 입력합니다. |
| 결과 확인 | print()로 콘솔에 출력합니다. | QMessageBox로 화면에 표시합니다. |
| 파일 처리 | QFile, QIODevice, QTextStream을 사용합니다. | 동일하게 QFile, QIODevice, QTextStream을 사용합니다. |
| 복사 처리 | 경로를 코드에 직접 작성합니다. | QLineEdit과 QFileDialog로 경로를 입력하거나 선택합니다. |
4.2 최종 정리
이번 글에서는 콘솔에서 익힌 QFile 코드를 GUI 버튼에 연결했습니다.
| 핵심 내용 | 정리 |
| 문제 파악 | GUI에서는 파일 경로 선택과 결과 안내가 필요하다는 것을 확인했습니다. |
| 문제 해결 | 파일 쓰기, 읽기, 복사 버튼을 직접 구현했습니다. |
| 코드 분석 | QFileDialog, QMessageBox, QFile, QTextStream이 각각 어떤 역할을 하는지 살펴봤습니다. |
| 비교 정리 | 콘솔 방식과 GUI 방식은 사용자 입력과 결과 표시 방식이 다르지만, 파일 처리 핵심 코드는 비슷합니다. |
기억할 문장: GUI에서는 QFile 기능을 버튼에 연결하고, 파일 경로는 QFileDialog로 받고, 결과는 QMessageBox로 알려줍니다.
참고. 공식 문서로 확인하기
→ GUI 파일 처리와 관련된 자세한 내용은 공식 문서에서 확인합니다.
참고 문서
이번 글에서 사용한 클래스는 PySide6의 QtWidgets와 QtCore 모듈에 포함되어 있습니다.
- PySide6 QFile 공식 문서
- PySide6 QIODevice 공식 문서
- PySide6 QTextStream 공식 문서
- PySide6 QFileDialog 공식 문서
- PySide6 QMessageBox 공식 문서
- PySide6 QTextEdit 공식 문서
참고: 다음 강의에서는 이번 코드에서 파일 처리 로직을 따로 분리해 FileModel과 FileEditorView 구조로 정리합니다.