#Gemini 활용 꿀팁
'''
코드 붙여넣고,
"이 코드 내부 실행 순서를 시뮬레이터로 만들어줘. 단, html로"
설정은 Canvas로
'''
MVP
#MVP 패턴 적용
'''
MVP는 Model-View-Presenter의 약자로, GUI 프로그램의 코드를 역할에 따라 세 부분으로
나누는 아키텍처입니다.
핵심 목적은 데이터, 화면, 로직을 분리하여 규모가 커져도 코드를 관리할 수 있게 만드는 것입니다.
MVP의 세가지 역할
구성 요소 역할 tkinter에서
Model 데이터와 비즈니스 로직 순수 파이썬 클래스 (tkinter 모름)
View ` 화면 표시와 사용자 입력 받기 위젯 배치, 이벤트 전달
Presenter Model과 View를 연결하는 중재자 입력 처리 후 Model 갱신, View 업데이트
핵심 규칙과 데이터 흐름
핵심 규칙은 "View와 Model은 서로 직접 대화하지 않는다" 입니다.
모든 흐름은 Presenter를 거칩니다.
[사용자] → View → Presenter → Model
↓
[사용자] ← View ← Presenter
View는 스스로 판단하지 않고 사용자 동작을 Presenter에게 "전달"만 합니다.
Model은 tkinter를 전혀 모르는 순수한 파이썬일 뿐입니다.
Presenter는 Model과 View를 둘 다 알고 있으며, 흐름을 조율합니다.
MVP 적용 시 장점
화면(View) 없이도 데이터(Model)와 로직(Presenter)을 독립적으로 단위 테스트할 수 있습니다.
화면을 변경해도 로직은 그대로, 로직을 수정해도 화면은 그대로 둘 수 있어 유지보수가 편리합
니다.
디자이너는 화면에, 개발자는 로직에 집중할 수 있어 명확한 업무 분장과 협업이 가능합니다.
프로그램 기능이 늘어나도 역할별로 코드가 체계적으로 정리되어 확장성이 뛰어납니다.
'''
# MVP 패턴으로 앱 만들기
# Model
#Model은 import tkinter 가 없는 순수한 파이썬일 뿐입니다.
class DistanceModel:
KM_PER_MILE = 1.609344
def km_to_miles(self, km):
return km / self.KM_PER_MILE
def miles_to_km(self, miles):
return miles * self.KM_PER_MILE
#View
# 위젯을 만들고, 사용자 동작을 Presenter에게 "전달"만 합니다. 스스로 판단 X
import tkinter as tk
class DistanceView(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.pack(padx=15, pady=15)
self.entry = tk.Entry(self, width=15)
self.entry.grid(row=0, column=0, columnspan=2, pady=5)
self.km_btn = tk.Button(self, text="km → 마일")
self.km_btn.grid(row=1, column=0, padx=2)
self.mile_btn = tk.Button(self, text="마일 → km")
self.mile_btn.grid(row=1, column=1, padx=2)
self.result = tk.Label(self, text="값을 입력하세요", width=25)
self.result.grid(row=2, column=0, columnspan=2, pady=10)
def get_input(self):
return self.entry.get()
def show_result(self, text):
self.result.config(text=text)
#Presenter
#Presenter는 Model과 View를 둘 다 알고 있으며, 흐름을 조율합니다.
class DistancePresenter:
def __init__(self, model, view):
self.model = model
self.view = view
self.view.km_btn.config(command=self.on_km_to_miles)
self.view.mile_btn.config(command=self.on_miles_to_km)
def _read_value(self):
"""입력값을 숫자로 변환. 실패하면 None과 함께 안내."""
try:
return float(self.view.get_input())
except ValueError:
self.view.show_result("숫자를 입력하세요")
return None
def on_km_to_miles(self):
value = self._read_value()
if value is not None:
miles = self.model.km_to_miles(value)
self.view.show_result(f"{value} km = {miles:.2f} 마일")
def on_miles_to_km(self):
value = self._read_value()
if value is not None:
km = self.model.miles_to_km(value)
self.view.show_result(f"{value} 마일 = {km:.2f} km")
#조립과 실행
if __name__ == "__main__":
root = tk.Tk()
presenter = DistancePresenter(DistanceModel(), DistanceView(root))
root.mainloop()
km-마일 변환 프로그램이다.

스레드
'''
스레드란
프로그램을 실행하면 프로세스가 생기고, 그 안에서 실제로 코드를 실행하는 흐름이 스레드
(Thread) 입니다.
기본적으로 스레드는 하나(메인 스레드)지만, 필요하면 여러 개를 만들어 일을 나눌 수 있습니다.
스레드는 왜 필요한가?
스레드가 하나면 모든 일을 순서대로 처리합니다. 그래서 오래 걸리는 작업(읽기, 네트워크 등)을
만나면 끝날 때까지 다른 일이 멈춥니다.
time.sleep , 파일
GUI에서 이 문제가 바로 '응답 없음' 입니다. 메인 스레드가 대기 작업에 묶이면 창 이동·버튼 클
릭에 반응하지 못합니다.
그래서 해결책은 역할을 나누는 것입니다.
메인 스레드 : 화면을 그리고 사용자 입력에 반응하는 일에 집중합니다.
작업 스레드 : 오래 걸리는 작업을 따로 맡아 처리합니다.
이렇게 하면 작업이 도는 동안에도 화면이 멈추지 않아 '응답 없음'을 피할 수 있습니다.
파이썬에서 스레드 구성 방법
파이썬은 표준 라이브러리인
threading 모듈로 스레드를 다룹니다. 스레드를 만드는 방식은 크게 두 가지입니다.
함수를 넘기는 방식
가장 간단한 방법은 Thread 객체를 만들면서 실행할 함수를 target 으로 지정하는 것입니다.
'''
# 스레드 예제
import threading
import time
def work():
for i in range(5):
print(f"작업 중 {i}")
time.sleep(1)
t = threading.Thread(target=work)
t.start()
1초동안 대기하고 print하는 스레드를 만든 코드이다.

'''
Thread를 상속하는 방식
좀 더 구조적인 방법은 threading.Thread 를 상속한 클래스를 만들고 run 메서드를 재정의하
는 것입니다.
스레드가 다룰 데이터나 상태가 많아 객체지향적으로 설계할 때 적합합니다.
'''
#스레드 상속 예제
import threading
import time
class Worker(threading.Thread):
def __init__(self, name):
super().__init__()
self.worker_name = name
def run(self):
for i in range(5):
time.sleep(1)
print(f"{self.worker_name}: {i}")
w = Worker("작업자1")
w.start()
threading.Thread를 상속받고, run함수를 오버라이딩한 모습이다.


'''
이제 같은 카운트다운 프로그램을 두 가지 방식으로 만들어, 화면이 멈추는 경우와 멈추지 않는
경우를 직접 비교해 보겠습니다.
두 코드는 10부터 0까지 1초 간격으로 숫자를 줄여 표시하고, 마지막에 "끝!"을 보여준다는 점에
서 동일합니다.
차이는 그 작업을 누가 처리하느냐에 있습니다.
'''
#메인 스레드에서 처리
import tkinter as tk
import time
class CountdownApp:
def __init__(self, root):
self.root = root
self.label = tk.Label(root, text="10", font=("Arial", 60))
self.label.pack(expand=True)
tk.Button(root, text="시작", command=self.start).pack(pady=10)
def start(self):
for i in range(10, -1, -1):
self.label.config(text=str(i))
self.root.update_idletasks()
time.sleep(1)
self.label.config(text="끝!")
if __name__ == "__main__":
root = tk.Tk()
CountdownApp(root)
root.mainloop()
time.sleep(1) 이 메인 스레드를 붙잡아 두어 화면 갱신이나 다른 입력을 막는다.
카운트다운 중 창을 움직이면 반응하지 않는다.
오래 걸리는 작업을 메인 스레드에서 처리하면 결국 '응답 없음' 상태가 됨을 보여준다.

#작업 스레드로 분리
import tkinter as tk
import threading
import time
class CountdownApp:
def __init__(self, root):
self.root = root
self.label = tk.Label(root, text="10", font=("Arial", 60))
self.label.pack(expand=True)
tk.Button(root, text="시작", command=self.start).pack(pady=10)
def start(self):
threading.Thread(target=self.count, daemon=True).start()# 작업스레드에 짬떄리기
def count(self):
for i in range(10, -1, -1):
self.label.config(text=str(i))
time.sleep(1)
self.label.config(text="끝!")
if __name__ == "__main__":
root = tk.Tk()
CountdownApp(root)
root.mainloop()
시작 버튼을 누르면 카운트다운 작업(count)을 새로운 작업 스레드에 온전히 맡깁니다.
무거운 대기 시간이 작업 스레드에서 흐르므로 메인 스레드는 자유로운 상태가 됩니다.
메인 스레드가 멈추지 않아 카운트다운 중에도 창 이동, 버튼 클릭 등에 정상적으로 반응합니다.
#after()로 안전하게 화면 갱신하기
'''
위젯은 메인 스레드에서만 다루어야 함
앞서 작업 스레드로 분리한 코드는 화면이 멈추지 않아 잘 동작하는 것처럼 보입니다.
작업 스레드에서 self.label.config(...) 처럼 위젯을 직접 건드리면,
당장은 문제가 없어 보여도 상황에 따라 화면이 깨지거나 예기치 않게 프로그램이 멈출 수 있습니다.
그래서 필요한 것이 after()
after(밀리초, 함수)는 "지정한 시간이 흐른 뒤 이 함수를 메인 스레드에서 실행하라"고 예약하는
메서드입니다.
즉 위젯을 바꾸는 일은 메인 스레드에 맡기고, 작업 스레드는 "언제 무엇을 할지"만 예약하는 방
식으로 역할을 다시 나눌 수 있습니다.
after()만으로 카운트다운 구현하기
사실 카운트다운처럼 "1초마다 한 번씩"인 단순한 반복은 스레드 없이
after() 만으로도 깔끔하게 만들 수 있습니다.
time.sleep 으로 흐름을 멈추는 대신, 다음 숫자를 1초 뒤에 실행하도록 예약하고 함수를
빠져나오는 것이 핵심입니다.
'''
#after 예제
import tkinter as tk
class CountdownApp:
def __init__(self, root):
self.root = root
self.label = tk.Label(root, text="10", font=("Arial", 60))
self.label.pack(expand=True)
tk.Button(root, text="시작", command=self.start).pack(pady=10)
def start(self):
self.count(10)
def count(self, i):
if i < 0:
self.label.config(text="끝!")
return
self.label.config(text=str(i))
self.root.after(1000, self.count, i - 1)
if __name__ == "__main__":
root = tk.Tk()
CountdownApp(root)
root.mainloop()
# 큐 통신
'''
tkinter 위젯은 메인 스레드에서만 안전하게 조작할 수 있으므로,
백그라운드 스레드가 위젯을 직접 바꾸면 간헐적으로 충돌합니다.
그래서 큐(queue)를 사이에 둡니다. 백그라운드 스레드는 결과를 큐에 넣기만 하고,
메인 스레드는 큐에서 결과를 꺼내 화면을 바꿉니다. 역할이 나뉘므로 위젯은 언제나
메인 스레드에서만 조작됩니다
큐(Queue)란
큐는 먼저 넣은 것이 먼저 나오는(FIFO, First In First Out) 자료구조입니다.
넣기(put) → [ 1 ][ 2 ][ 3 ] → 꺼내기(get)
뒤 앞
리스트로도 비슷하게 흉내 낼 수 있지만, 파이썬의 queue.Queue에는 두 가지 중요한 장점이 있
습니다.
-스레드 안전(thread-safe): 여러 스레드가 동시에 넣고 꺼내도 데이터가 깨지지 않도록 내부
적으로 잠금(lock)을 처리해 줍니다. 그래서 스레드 사이의 데이터 전달 통로로 안성맞춤입니
다.
-대기 기능: 큐가 비어 있을 때 값이 들어올 때까지 기다리게 하거나, 기다리지 않고 즉시 넘어
가게 하는 등 상황에 맞게 선택할 수 있습니다.
기본 사용법
큐 만들기
import queue
q = queue.Queue()
# 크기 제한 없는 큐
q = queue.Queue(maxsize=5) # 최대 5개까지만 담는 큐
값 꺼내기 ( get , get_nowait )
n = q.get()
n = q.get_nowait()
get() 은 값이 올 때까지 멈춰 서서 기다립니다. GUI 메인 스레드에서 이걸 쓰면 화면이
얼어붙으므로 주의해야 합니다.
get_nowait() 는 기다리지 않으므로 GUI에 적합합니다. 대신 큐가 비어 있으면 예외가
나므로 try/except로 감싸 처리합니다.
상태 확인
q.empty() # 비어 있으면 True
q.qsize() # 대략적인 크기 (참고용)
'''
#스레드와 큐 활용하기
#'스레드 → 큐 → 화면' 예제코드
#백그라운드에서 1초마다 숫자를 세어 큐에 넣고, 메인 스레드가 이를 꺼내 라벨에 표시합니다.
import tkinter as tk
import threading
import queue
import time
root = tk.Tk()
label = tk.Label(root, text="대기 중", font=("", 20))
label.pack(padx=40, pady=40)
q = queue.Queue()
def work():
for i in range(1, 10):
time.sleep(1)
q.put(i)
def poll():
try:
while True:
n = q.get_nowait()
label.config(text=f"{n} 초")
except queue.Empty:
pass
root.after(100, poll)
threading.Thread(target=work, daemon=True).start() #daemon은 윈도우가 꺼지면, 이 스레드도 멈춰라
root.after(100, poll)
root.mainloop()
'''
카운트 다운 코드 큐 기능 추가
앞서 스레드 강좌를 위한 카운트다운 예제 코드는 백그라운드 스레드에서 GUI를 조작하는 문제
점이 있었습니다.
이를 큐 기능을 추가해 안전한 코드로 아래와 같이 개선했습니다.
'''
#큐 기능이 추가된 코드
import tkinter as tk
import threading
import queue
import time
class CountdownApp:
def __init__(self, root):
self.root = root
self.running=False # 카운트다운 중
self.queue = queue.Queue()
self.label = tk.Label(root, text="10", font=("Arial", 60))
self.label.pack(expand=True)
tk.Button(root, text="시작", command=self.start).pack(pady=10)
self.poll()
def start(self):
if self.running:# 카운트다운 중일땐 스레드를 더이상 만들지 않음
return
self.running=True
threading.Thread(target=self.count, daemon=True).start()
def count(self):
for i in range(10, -1, -1):
self.queue.put(str(i))
time.sleep(1)
self.queue.put("끝!")
def poll(self):
try:
while True:
self.label.config(text=self.queue.get_nowait())
except queue.Empty:
pass
self.root.after(100, self.poll)
if __name__ == "__main__":
root = tk.Tk()
CountdownApp(root)
root.mainloop()
카운트다운 모델을 직접 MVP로 구현해보았다.
import tkinter as tk
import threading
import queue
import time
class CountdownModel:
def __init__(self):
self.queue = queue.Queue()
def start(self):
threading.Thread(target=self.count, daemon=True).start()
def count(self):
for i in range(10, -1, -1):
self.queue.put(str(i))
time.sleep(1)
self.queue.put("끝!")
class CountdownView(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.label = tk.Label(root, text="10", font=("Arial", 60))
self.label.pack(expand=True)
self.btn=tk.Button(root, text="시작")
self.btn.pack(pady=10)
def schedule(self, delay, callback):
self.after(delay, callback)
class CountdownPresenter:
def __init__(self,model,view):
self.model = model
self.view = view
self.view.btn.config(command=self.start)
def start(self):
threading.Thread(target=self.model.count, daemon=True).start()
self.poll()
def poll(self):
try:
while True:
self.view.label.config(text=self.model.queue.get_nowait())
except queue.Empty:
pass
self.view.schedule(100,self.poll)
if __name__ == "__main__":
root = tk.Tk()
CountdownPresenter(CountdownModel(),CountdownView(root))
root.mainloop()
원본(MVP없이 구현된 코드) 코드를 보았을때, poll 함수에서 root를 사용한다.
def poll(self):
try:
while True:
self.label.config(text=self.queue.get_nowait())
except queue.Empty:
pass
self.root.after(100, self.poll)
-CountdownApp 클래스의 poll 함수
그래서 poll함수를 CountdownPresenter클래스에 넣을지, CountdownView 클래스에 넣을지 고민을 했는데,
GPT한테 물어본 결과
Presenter에 root(Tk 객체)를 넘기는 것보다, View가 after()를 관리하도록 두는 쪽이 더 MVP스럽습니다.
다만 poll() 자체는 View보다 Presenter에 두는 게 더 자연스럽습니다.
라고 했다. 그래서 view쪽에 schedule이라는 함수를 만들고, (after 실행) 해당 코드를 Presenter클래스의
poll함수에서 호출하는 쪽으로 결정하게 되었다.
JSON 파일 이해하기
'''
JSON이란 무엇인가?
JSON(JavaScript Object Notation)은 데이터를 텍스트로 표현하는 형식입니다.
읽기 쉽고 구조도 단순해서, 프로그램끼리 데이터를 주고받거나 파일에 데이터를 저장할 때 널리 쓰입니다.
JSON의 생김새
연락처 하나를 JSON으로 표현하면 아래와 같습니다.
{
"name": "홍길동",
"company": "한빛상사",
"phone": "010-1234-5678"
}
중괄호 { }로 묶인 이 덩어리를 객체라고 부릅니다. 안에는 "이름": 값 형태의 쌍이 들어 있습니다.
왼쪽의 "name" 은 항목의 이름(키), 오른쪽은 그 값입니다. 키는 반드시 큰따옴표로 감싸고, 쌍과
쌍 사이는 쉼표로 구분합니다.
연락처는 여러 개가 필요하므로, 객체들을 대괄호 [ ] 로 묶어 배열(목록)로 만듭니다.
파이썬에서 JSON 파일 읽고 쓰기
파이썬에는 JSON을 다루는 json 모듈이 기본으로 들어 있어 따로 설치할 필요가 없습니다.
핵심 함수는 두 개입니다.
json.load(f) 는 파일에서 JSON을 읽어 파이썬 자료형으로 바꿉니다. 파일 속 배열은 리스트
로, 객체는 딕셔너리로 변환되어 돌아옵니다.
json.dump(데이터, f) 는 반대로 파이썬 자료형을 JSON 텍스트로 바꿔 파일에 씁니다.
'''
#JSON 저장/불러오기 코드
#앱에서 사용할 저장/불러오기 코드를 보면 이 두 함수가 어떻게 쓰이는지 알 수 있습니다.
import os, json
FILE = "contacts.json"
def load():
if os.path.exists(FILE):
with open(FILE, encoding="utf-8") as f:
return json.load(f)
return []
def save():
with open(FILE, "w", encoding="utf-8") as f:
json.dump(contacts, f, ensure_ascii=False, indent=2)
contacts = load()
save()
'''
알아두기
with open(...) 구문과 인코딩 지정
with open(...) 구문을 쓰면 작업이 끝났을 때 파일이 자동으로 닫혀 안전합니다.
encoding="utf-8" 은 한글이 깨지지 않도록 문자 인코딩을 지정하는 부분입니다.
'''
실행시 디렉토리에 contacts.json이 생성됨
#입력폼 생성하고 연락처 추가하기
#목표
#contacts.json 파일에 '이름','회사','연락처'를 추가할 수 있는 최소한의 GUI 폼을 작성합니다.
#연락처 v0.1
import tkinter as tk
import os, json
FILE = "contacts.json"
def load():
if os.path.exists(FILE):
with open(FILE, encoding="utf-8") as f:
return json.load(f)
return []
def save():
with open(FILE, "w", encoding="utf-8") as f:
json.dump(contacts, f, ensure_ascii=False, indent=2)
def add():
contacts.append({
"name": e_name.get(),
"company": e_company.get(),
"phone": e_phone.get(),
})
save()
contacts = load()
root = tk.Tk()
root.title("연락처 v0.1")
e_name = tk.Entry(root)
tk.Label(root, text="이름").grid(row=0, column=0)
tk.Label(root, text="회사").grid(row=0, column=1)
tk.Label(root, text="전화").grid(row=0, column=2)
e_company = tk.Entry(root)
e_phone = tk.Entry(root)
e_name.grid(row=1, column=0)
e_company.grid(row=1, column=1)
e_phone.grid(row=1, column=2)
tk.Button(root, text="추가", command=add).grid(row=3, column=1)
root.mainloop()


#Treeview 위젯으로 연락처 보기
#목표
#ttk.Treeview 위젯으로 'contacts.json' 파일 내 연락처 정보를 엑셀 시트처럼 출력합니다.
#연락처 삭제 기능을 추가합니다.
#연락처 v0.2
import tkinter as tk
from tkinter import ttk, messagebox
import os, json
FILE = "contacts.json"
def load():
if os.path.exists(FILE):
with open(FILE, encoding="utf-8") as f:
return json.load(f)
return []
def save():
with open(FILE, "w", encoding="utf-8") as f:
json.dump(contacts, f, ensure_ascii=False, indent=2)
def refresh():
tree.delete(*tree.get_children())
for c in contacts:
tree.insert("", "end", values=(c["name"], c["company"], c["phone"]))
def add():
name, company, phone = e_name.get(), e_company.get(), e_phone.get()
if not name:
messagebox.showwarning("확인", "이름을 입력하세요.")
return
contacts.append({"name": name, "company": company, "phone": phone})
save()
refresh()
for e in (e_name, e_company, e_phone):
e.delete(0, "end")
def delete():
sel = tree.selection()
if not sel:
return
idx = tree.index(sel[0])
contacts.pop(idx)
save()
refresh()
contacts = load()
root = tk.Tk()
root.title("연락처 v0.2")
input_frame = tk.LabelFrame(root, text="입력", padx=10, pady=10)
input_frame.pack(padx=10, pady=10, fill="x")
tk.Label(input_frame, text="이름").grid(row=0, column=0)
tk.Label(input_frame, text="회사").grid(row=0, column=1)
tk.Label(input_frame, text="전화").grid(row=0, column=2)
e_name = tk.Entry(input_frame)
e_company = tk.Entry(input_frame)
e_phone = tk.Entry(input_frame)
e_name.grid(row=1, column=0, padx=2)
e_company.grid(row=1, column=1, padx=2)
e_phone.grid(row=1, column=2, padx=2)
tk.Button(input_frame, text="추가", command=add).grid(row=1, column=3, padx=5)
tk.Button(input_frame, text="삭제", command=delete).grid(row=1, column=4)
list_frame = tk.LabelFrame(root, text="목록", padx=10, pady=10)
list_frame.pack(padx=10, pady=(0, 10), fill="both")
tree = ttk.Treeview(list_frame, columns=("name", "company", "phone"), show="headings", height=8)
tree.heading("name", text="이름")
tree.heading("company", text="회사")
tree.heading("phone", text="전화")
tree.pack()
refresh()
root.mainloop()

#연락처 v0.3
위 코드를 MVP형식으로 재구성.
#model
'''
모델
설명
데이터를 담당하는 모델부터 만듭니다.
절차적 코드에서 흩어져 있던 load, save, 그리고 연락처 리스트를 다루던 부분이 하나의 클래스
로 모입니다.
모델은 데이터만 다루고 화면이나 입력은 전혀 알지 못한다는 점이 핵심입니다
'''
import os, json
class ContactModel:
def __init__(self, file_path="contacts.json"):
self.file_path = file_path
self.contacts = self._load()
def _load(self):
if os.path.exists(self.file_path):
with open(self.file_path, encoding="utf-8") as f:
return json.load(f)
return []
def _save(self):
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.contacts, f, ensure_ascii=False, indent=2)
def get_all(self):
return self.contacts
def add(self, name, company, phone):
self.contacts.append({"name": name, "company": company, "phone": phone})
self._save()
def delete(self, idx):
self.contacts.pop(idx)
self._save()
'''
이름 앞에 밑줄이 붙은 _load 와 get_all 는 클래스 내부에서만 쓰는 메서드라는 신호입니다.
바깥에서는 , add ,_save, delete 처럼 밑줄 없는 메서드만 호출하고, 파일을 실제로 읽고
쓰는 세부 동작은 클래스 안에 감춰 둔 것입니다.
add 나 delete 가 끝날 때마다 스스로 _save 를 호출하므로,호출하는 쪽은 저장을 신경 쓸
필요가 없습니다
'''
#view
'''
뷰
설명
화면을 담당하는 뷰입니다. 입력창·버튼·목록을 그리고 입력값을 넘겨주거나 목록을 다시 그립니다.
저장이나 검증은 하지 않고, 버튼을 눌렀을 때의 처리는 프리젠터에게 맡깁니다
'''
import tkinter as tk
from tkinter import ttk, messagebox
class ContactView:
def __init__(self, root):
self.root = root
self.presenter = None
self._setup_ui()
def set_presenter(self, presenter):
self.presenter = presenter
def _setup_ui(self):
input_frame = tk.LabelFrame(self.root, text="입력", padx=10, pady=10)
input_frame.pack(padx=10, pady=10, fill="x")
tk.Label(input_frame, text="이름").grid(row=0, column=0)
tk.Label(input_frame, text="회사").grid(row=0, column=1)
tk.Label(input_frame, text="전화").grid(row=0, column=2)
self.e_name = tk.Entry(input_frame)
self.e_company = tk.Entry(input_frame)
self.e_phone = tk.Entry(input_frame)
self.e_name.grid(row=1, column=0, padx=2)
self.e_company.grid(row=1, column=1, padx=2)
self.e_phone.grid(row=1, column=2, padx=2)
tk.Button(input_frame, text="추가",
command=lambda: self.presenter.add()).grid(row=1, column=3, padx=5)
tk.Button(input_frame, text="삭제",
command=lambda: self.presenter.delete()).grid(row=1, column=4)
list_frame = tk.LabelFrame(self.root, text="목록", padx=10, pady=10)
list_frame.pack(padx=10, pady=(0, 10), fill="both", expand=True)
self.tree = ttk.Treeview(list_frame, columns=("name", "company", "phone"),
show="headings", height=8)
self.tree.heading("name", text="이름")
self.tree.heading("company", text="회사")
self.tree.heading("phone", text="전화")
self.tree.pack(fill="both", expand=True)
def get_inputs(self):
return self.e_name.get(), self.e_company.get(), self.e_phone.get()
def clear_inputs(self):
for e in (self.e_name, self.e_company, self.e_phone):
e.delete(0, "end")
def get_selected_index(self):
sel = self.tree.selection()
if not sel:
return None
return self.tree.index(sel[0])
def render(self, contacts):
self.tree.delete(*self.tree.get_children())
for c in contacts:
self.tree.insert("", "end", values=(c["name"], c["company"], c["phone"]))
def show_warning(self, title, message):
messagebox.showwarning(title, message)
'''
버튼의 command 가 lambda: self.presenter.add() 로 되어 있는 데 주목합니다.
이전에는 버튼이 직접 add 함수를 불렀지만, 이제는 프리젠터에게 "추가해 달라"고 요청만 합
니다.get_inputs , clear_inputs , render 처럼 뷰가 제공하는 메서드들은 모두 화면을 읽거나 다
시 그리는 창구 역할을 하며, 프리젠터는 이 창구를 통해서만 화면과 소통합니다.
view.py 코드 하단에 아래 코드를 추가하면 UI만 확인할 수 있습니다.
if __name__ == "__main__":
root = tk.Tk()
view = ContactView(root)
root.mainloop()
'''
#presenter
'''
프리젠터
설명
서로 모르는 모델과 뷰 사이를 오가며 흐름을 조율하는 것이 프리젠터입니다
절차적 코드에서 add , delete , refresh 함수가 하던 "판단과 조율"의 로직이 이곳으로 모입
니다
'''
class ContactPresenter:
def __init__(self, view, model):
self.view = view
self.model = model
self.view.set_presenter(self)
self.refresh()
def refresh(self):
self.view.render(self.model.get_all())
def add(self):
name, company, phone = self.view.get_inputs()
if not name:
self.view.show_warning("확인", "이름을 입력하세요.")
return
self.model.add(name, company, phone)
self.refresh()
self.view.clear_inputs()
def delete(self):
idx = self.view.get_selected_index()
if idx is None:
return
self.model.delete(idx)
self.refresh()
'''
add의 흐름을 따라가 보면 세 부분이 어떻게 협력하는지 잘 드러납니다.
프리젠터는 먼저 뷰에서 입력값을 받아 오고(get_inputs ), 이름이 비었는지 검증한 뒤, 문제가
없으면 모델에 저장을 지시하고(model.add ), 마지막으로 화면을 다시 그리고 입력창을 비웁니
다.
이름 검증 같은 "규칙"은 뷰도 모델도 아닌 프리젠터가 쥐고 있습니다.
생성자에서 set_presenter 로 자신을 뷰에 등록하고 곧바로
refresh 를 호출해, 앱이 켜지자
마자 저장돼 있던 연락처가 화면에 나타나게 합니다.
mvp는 이해만 하고 넘어가면 안됨.
스스로 설계할 줄 알아야한다.
'''
#main
#세 파일을 모두 만들었으니, 이제 이들을 실제로 조립해 실행하는 진입점만 남았습니다.
import tkinter as tk
from model import ContactModel
from view import ContactView
from presenter import ContactPresenter
def main():
root = tk.Tk()
root.title("연락처 v0.3")
view = ContactView(root)
model = ContactModel()
ContactPresenter(view, model)
root.mainloop()
if __name__ == "__main__":
main()
'''
main 함수가 하는 일은 세 조각을 만들어 연결하는 것뿐입니다.
뷰와 모델을 각각 생성한 뒤, 이 둘을 프리젠터에 넘겨 주면 나머지 연결은 프리젠터가 알아서 처
리합니다.
조립 과정이 단순하다는 것 자체가 MVP 패턴이 역할을 잘 나눠 놓았다는 증거입니다.
'''
네트워크
'''
HTTP(HyperText Transfer Protocol)/ HTTPS(HyperText Transfer Protocol Secret)
인터넷에서 우리가 매일 보는 웹 페이지(HTML),이미지, 텍스트 등의 데이터를 주고받기 위해
사용하는 가장 대표적인 웹 통신 규약
ICMP(Internet Control Message Protocol): ping test
데이터를 주고받기 위한 목적보다는, 네트워크의 상태를 진단하고
통신 오류를 알려주기 위해 만들어진 프로토콜
TCP(Transmission Control Protocol)
인터넷상에서 데이터를 빠짐없이, 순서대로, 정확하게 주고 받기 위해
상호 간에 신뢰할 수 있는 연결을 맺는 프로토콜
UDP(User Datagram Protocol)
인터넷에서 데이터를 주고 받을 때 정확성이나 순서 보다는 '속도'를 최우선으로
하는 통신 규약
FTP()
파일을 주고받는 프로토콜
==============================================
모듈
socket : 네트워크의 가장 낮은 수준에서 TCP 연결을 직접 맺어 포트 상태를 확인할 때 사용
subprocess : 파이썬 코드 안에서 운영체제의 셸(CMD, 터미널) 명령어를 실행하고 결과를 받아온다.
urllib.request : HTTP/HTTPS/ 웹 페이지를 요청하고 서버의 응답(상태 코드 등)을 파싱할 때 사용
==============================================
cheker.py(위의 각 네트워크 점검에 필요한 함수들을 보유하고 있는 기본 클래스)
Checker 클래스 : run()를 정의만..
HttpChecker 클래스 : 웹 사이트 점검
서버에서 반환한 reaponse코드가 200인지 확인 200이면 True
urllib.request.urlopen()를 이용하여 설정된 URL로 접속을 시도.
IcmpChecker 클래스 : ping 점검
returncode가 0인지를 확인
platform.system()를 이용하여 Window, Mac, Linux인지를 판별 ping을 1회 보내고 경과를 확인
subprocess.run()를 이용하여 파이썬 내부 터미널 명령어를 직접 실행. 그 결과를 캡처.
내부 터미널 명령어가 오류없이 정상적으로 종료(returncode==0)이면 True
TcpChecker 클래스 : port 개방 점검
지정한 port가 열려 있으면 True
socket.create_connection()를 이용하여 IP의 해당 port로 직접 TCP연결을 시도
'''
# -*- coding: utf-8 -*-
"""
교재 예제 파일: 네트워크 점검 스레드 적용
"""
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import queue
from checker import HttpChecker, IcmpChecker, TcpChecker
TYPES = {"HTTP/HTTPS": HttpChecker, "ICMP": IcmpChecker, "TCP": TcpChecker}
class App:
def __init__(self, root):
self.root = root
root.geometry("700x450")
self.checkers = {}
self.result_queue = queue.Queue()
top = ttk.Frame(root, padding=10)
top.pack(fill="x")
self.type_cb = ttk.Combobox(top, values=list(TYPES), state="readonly", width=12)
self.type_cb.current(0)
self.type_cb.pack(side="left")
self.target_entry = ttk.Entry(top, width=30)
self.target_entry.pack(side="left", padx=5)
ttk.Label(top, text="포트:").pack(side="left")
self.port_entry = ttk.Entry(top, width=8)
self.port_entry.pack(side="left", padx=(2, 5))
ttk.Button(top, text="추가", command=self.add).pack(side="left")
cols = ("type", "target", "result", "detail")
self.tree = ttk.Treeview(root, columns=cols, show="headings")
for c, t in zip(cols, ("유형", "대상", "결과", "상세")):
self.tree.heading(c, text=t)
self.tree.tag_configure("ok", foreground="green")
self.tree.tag_configure("fail", foreground="red")
self.tree.pack(fill="both", expand=True, padx=10)
bottom = ttk.Frame(root, padding=10)
bottom.pack(fill="x")
self.check_btn = ttk.Button(bottom, text="전체 점검", command=self.check_all)
self.check_btn.pack(side="left")
ttk.Button(bottom, text="전체 삭제", command=self.clear).pack(side="left", padx=5)
self.root.after(100, self.process_queue)
def add(self):
ctype = self.type_cb.get()
cls = TYPES[ctype]
target = self.target_entry.get().strip()
port = self.port_entry.get().strip()
if not target:
messagebox.showwarning("입력 오류", "대상을 입력하세요.")
return
if cls is TcpChecker:
if not port.isdigit():
messagebox.showwarning("입력 오류", "TCP는 숫자 포트가 필요합니다.")
return
checker = cls(target, port)
shown = f"{target}:{port}"
else:
checker = cls(target)
shown = target
iid = self.tree.insert("", tk.END, values=(ctype, shown, "-", ""))
self.checkers[iid] = checker
self.target_entry.delete(0, tk.END)
def check_all(self):
if not self.checkers:
return
self.check_btn.config(state="disabled")
for iid in self.checkers:
self.tree.set(iid, "result", "점검 중…")
self.tree.set(iid, "detail", "")
self.tree.item(iid, tags=())
targets = list(self.checkers.items())
thread = threading.Thread(target=self.run_checks, args=(targets,), daemon=True)
thread.start()
def run_checks(self, targets):
for iid, checker in targets:
try:
ok, detail = checker.run()
self.result_queue.put((iid, "정상" if ok else "실패", detail,
"ok" if ok else "fail"))
except Exception as e:
self.result_queue.put((iid, "오류", str(e), "fail"))
self.result_queue.put(("__done__", None, None, None))
def process_queue(self):
try:
while True:
iid, result, detail, tag = self.result_queue.get_nowait()
if iid == "__done__":
self.check_btn.config(state="normal")
continue
if self.tree.exists(iid):
self.tree.set(iid, "result", result)
self.tree.set(iid, "detail", detail)
self.tree.item(iid, tags=(tag,))
except queue.Empty:
pass
finally:
self.root.after(100, self.process_queue)
def clear(self):
for iid in self.tree.get_children():
self.tree.delete(iid)
self.checkers.clear()
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()
# ==================================================
# 참고 예제 코드 (Reference Code)
# ==================================================
# [코드 조각]
# self.result_queue = queue.Queue()
# [코드 조각]
# self.root.after(100, self.process_queue)
# [코드 조각]
# def check_all(self):
# if not self.checkers:
# return
#
# self.check_btn.config(state="disabled")
# for iid in self.checkers:
# self.tree.set(iid, "result", "점검 중…")
# self.tree.set(iid, "detail", "")
# self.tree.item(iid, tags=())
#
# targets = list(self.checkers.items())
# thread = threading.Thread(target=self.run_checks, args=(targets,), daemon=True)
# thread.start()
# [코드 조각]
# def run_checks(self, targets):
# for iid, checker in targets:
# try:
# ok, detail = checker.run()
# self.result_queue.put((iid, "정상" if ok else "실패", detail,
# "ok" if ok else "fail"))
# except Exception as e:
# self.result_queue.put((iid, "오류", str(e), "fail"))
# self.result_queue.put(("__done__", None, None, None))
# [코드 조각]
# def process_queue(self):
# try:
# while True:
# iid, result, detail, tag = self.result_queue.get_nowait()
# if iid == "__done__":
# self.check_btn.config(state="normal")
# continue
# if self.tree.exists(iid):
# self.tree.set(iid, "result", result)
# self.tree.set(iid, "detail", detail)
# self.tree.item(iid, tags=(tag,))
# except queue.Empty:
# pass
# finally:
# self.root.after(100, self.process_queue)
자세한 설명은 pdf 04에..
'데이터 분석' 카테고리의 다른 글
| 0721 아키텍처/데이터파이프라인 +GUI (0) | 2026.07.21 |
|---|---|
| 0720 (0) | 2026.07.20 |