๐ŸŽฎ Python (Tkinter)์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ–‰๋งจ ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ โ€” ๋‹จ๊ณ„๋ณ„

๋ฐœํ–‰: (2026๋…„ 2์›” 8์ผ ์˜คํ›„ 01:17 GMT+9)
7 ๋ถ„ ์†Œ์š”
์›๋ฌธ: Dev.to

Source: Dev.to

๐Ÿงฐ ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ๊ฒƒ

  • Python 3
  • Tkinter (GUI ํ”„๋ ˆ์ž„์›Œํฌ)
  • ttk (์Šคํƒ€์ผ์ด ์ ์šฉ๋œ ์œ„์ ฏ)
  • svโ€‘ttk (ํ˜„๋Œ€์ ์ธ ๋ผ์ดํŠธ/๋‹คํฌ ํ…Œ๋งˆ)

๐Ÿ“ฆ 1๋‹จ๊ณ„ โ€“ ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ์˜์กด์„ฑ

import sys
import os
import random
import tkinter as tk
from tkinter import ttk, messagebox
import sv_ttk

์™œ ์ด๋Ÿฌํ•œ ๊ฐ€์ ธ์˜ค๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๋‚˜์š”?

๋ชจ๋“ˆ๋ชฉ์ 
random๋ฌด์ž‘์œ„ ๋‹จ์–ด ์„ ํƒ
tkinterGUI ๊ตฌ์ถ•
ttk๋ณด๋‹ค ๊น”๋”ํ•œ ์œ„์ ฏ
messageboxํŒ์—… ๋Œ€ํ™”์ƒ์ž
sv_ttkํ˜„๋Œ€์ ์ธ ๋ผ์ดํŠธ/๋‹คํฌ ํ…Œ๋งˆ ์ง€์›

๐Ÿง  Step 2 โ€“ ํ—ฌํผ ํ•จ์ˆ˜

ํŒŒ์ผ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋กœ๋“œํ•˜๊ธฐ (ํŒจํ‚ค์ง• ํ›„์—๋„)

def resource_path(file_name):
    base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, file_name)

์ด ํ—ฌํผ๋Š” ๋‚˜์ค‘์— PyInstaller์™€ ๊ฐ™์€ ๋„๊ตฌ๋กœ ์•ฑ์„ ๋ฒˆ๋“ค๋งํ•˜๋”๋ผ๋„ ํŒŒ์ผ ๊ฒฝ๋กœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

์ƒํƒœ ํ‘œ์‹œ์ค„ ์—…๋ฐ์ดํŠธ

def set_status(msg):
    status_var.set(msg)
    root.update_idletasks()

์•ฑ ํ•˜๋‹จ์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ƒํƒœ ๋ฉ”์‹œ์ง€๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

๐ŸชŸ Step 3 โ€“ ๋ฉ”์ธ ์•ฑ ์ฐฝ ๋งŒ๋“ค๊ธฐ

root = tk.Tk()
root.title("Animated Hangman Game")
root.geometry("700x850")
sv_ttk.set_theme("light")
  • Tk()๋Š” ์ฐฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • geometry()๋Š” ์ฐฝ์˜ ํฌ๊ธฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
  • sv_ttk.set_theme()๋Š” ํ˜„๋Œ€์ ์ธ ํ…Œ๋งˆ๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๐ŸŒ Step 4 โ€“ Global Game State

dark_mode_var = tk.BooleanVar(value=False)

word_list = ["PYTHON", "HANGMAN", "COMPUTER", "AI", "PROGRAMMING", "DEVELOPER"]

current_word = ""
guessed_letters = set()
wrong_guesses = 0
max_wrong = 6

UIโ€‘bound variables

display_word_var = tk.StringVar()
status_var = tk.StringVar(value="Ready")
lives_var = tk.StringVar(value=f"Lives: {max_wrong}")

StringVar๋Š” ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ Tkinter ์œ„์ ฏ์ด ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

๐ŸŒ— Step 5 โ€“ Darkโ€‘Mode Toggle

def toggle_theme():
    bg = "#2E2E2E" if dark_mode_var.get() else "#FFFFFF"
    fg = "white" if dark_mode_var.get() else "black"

    root.configure(bg=bg)

    for w in ["TFrame", "TLabel", "TLabelframe", "TLabelframe.Label", "TCheckbutton"]:
        style.configure(w, background=bg, foreground=fg)

    guess_entry.configure(background=bg, foreground=fg)
    hangman_canvas.configure(bg=bg)

์ฒดํฌ๋ฐ•์Šค๋ฅผ ํ† ๊ธ€ํ•  ๋•Œ ๋ฐฐ๊ฒฝ ๋ฐ ํ…์ŠคํŠธ ์ƒ‰์ƒ์ด ๋™์ ์œผ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.

๐Ÿ” 6๋‹จ๊ณ„ โ€“ ์ƒˆ๋กœ์šด ๊ฒŒ์ž„ ์‹œ์ž‘

def start_new_game():
    global current_word, guessed_letters, wrong_guesses

    current_word = random.choice(word_list)
    guessed_letters = set()
    wrong_guesses = 0

    update_display_word()
    lives_var.set(f"Lives: {max_wrong}")
    draw_hangman(animated=False)

    set_status("New game started! Guess a letter.")

๋ชจ๋“  ๊ฒƒ์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค: ์ƒˆ๋กœ์šด ๋‹จ์–ด๋ฅผ ์„ ํƒํ•˜๊ณ , ์ถ”์ธก์„ ์ดˆ๊ธฐํ™”ํ•˜๋ฉฐ, ๊ต์ˆ˜ํ˜• ๊ทธ๋ฆผ์„ ๋ฆฌ์…‹ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ค 7๋‹จ๊ณ„ โ€“ ๋‹จ์–ด ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ

def update_display_word():
    display = " ".join(
        c if c in guessed_letters else "_" for c in current_word
    )
    display_word_var.set(display)

    # Win condition
    if "_" not in display:
        set_status("Congratulations! You guessed the word!")
        messagebox.showinfo("Winner!", f"You correctly guessed: {current_word}")

์˜ˆ์‹œ ์ถœ๋ ฅ:

P Y T H O N

Source:

โ“ 8๋‹จ๊ณ„ โ€“ ๋ฌธ์ž ์ถ”์ธก

def guess_letter():
    global wrong_guesses

    letter = guess_var.get().strip().upper()
    guess_var.set("")

์ž…๋ ฅ ๊ฒ€์ฆ

    if not letter or len(letter) != 1 or not letter.isalpha():
        set_status("Please enter a single letter.")
        return

์ค‘๋ณต ์ถ”์ธก ๋ฐฉ์ง€

    if letter in guessed_letters:
        set_status(f"You already guessed '{letter}'.")
        return

์ •๋‹ต๊ณผ ์˜ค๋‹ต ์ฒ˜๋ฆฌ

    guessed_letters.add(letter)

    if letter not in current_word:
        wrong_guesses += 1
        lives_var.set(f"Lives: {max_wrong - wrong_guesses}")
        draw_hangman(animated=True)

        # Gameโ€‘over logic
        if wrong_guesses >= max_wrong:
            messagebox.showinfo("Game Over", f"The word was: {current_word}")
            start_new_game()
            return

    update_display_word()

๐ŸŽจ 9๋‹จ๊ณ„ โ€“ ๊ต์ˆ˜๋Œ€ ๊ทธ๋ฆฌ๊ธฐ

๊ธฐ๋ณธ ๊ตฌ์กฐ (ํ•ญ์ƒ ๊ทธ๋ ค์ง)

def draw_hangman(animated=True):
    hangman_canvas.delete("all")

    # Scaffold
    hangman_canvas.create_line(50, 350, 250, 350, width=3)   # Ground
    hangman_canvas.create_line(150, 350, 150, 50, width=3)   # Pole
    hangman_canvas.create_line(150, 50, 300, 50, width=3)    # Top beam
    hangman_canvas.create_line(300, 50, 300, 100, width=3)   # Rope

์‹ ์ฒด ๋ถ€์œ„ (์‹ค์ˆ˜๋งˆ๋‹ค ์ถ”๊ฐ€)

    parts = []

    if wrong_guesses >= 1:
        parts.append(("oval", 275, 100, 325, 150))   # Head
    if wrong_guesses >= 2:
        parts.append(("line", 300, 150, 300, 250))   # Body
    if wrong_guesses >= 3:
        parts.append(("line", 300, 180, 260, 220))   # Left arm
    if wrong_guesses >= 4:
        parts.append(("line", 300, 180, 340, 220))   # Right arm
    if wrong_guesses >= 5:
        parts.append(("line", 300, 250, 260, 300))   # Left leg
    if wrong_guesses >= 6:
        parts.append(("line", 300, 250, 340, 300))   # Right leg

๋ Œ๋”๋ง (์„ ํƒ์  ์• ๋‹ˆ๋ฉ”์ด์…˜ ํฌํ•จ)

    for i, (shape, *coords) in enumerate(parts):
        if animated:
            # Simple animation: draw each part after a short delay
            root.after(i * 200, lambda s=shape, c=coords: draw_part(s, *c))
        else:
            draw_part(shape, *coords)

def draw_part(shape, *coords):
    if shape == "oval":
        hangman_canvas.create_oval(*coords, width=3)
    elif shape == "line":
        hangman_canvas.create_line(*coords, width=3)

๐ŸŽฌ Step 10 โ€“ ๋ชจ๋“  ๊ฒƒ์„ ์—ฐ๊ฒฐํ•˜๊ธฐ (UI ๋ ˆ์ด์•„์›ƒ)

style = ttk.Style()

# Top frame โ€“ title & theme toggle
top_frame = ttk.Frame(root, padding=10)
top_frame.pack(fill="x")

title_lbl = ttk.Label(top_frame, text="Hangman", font=("Helvetica", 24))
title_lbl.pack(side="left")

theme_chk = ttk.Checkbutton(
    top_frame,
    text="Dark mode",
    variable=dark_mode_var,
    command=toggle_theme
)
theme_chk.pack(side="right")

# Word display
word_frame = ttk.Frame(root, padding=10)
word_frame.pack(fill="x")
word_lbl = ttk.Label(word_frame, textvariable=display_word_var, font=("Consolas", 32))
word_lbl.pack()

# Canvas for the drawing
hangman_canvas = tk.Canvas(root, width=400, height=400, bg="white", highlightthickness=0)
hangman_canvas.pack(pady=10)

# Guess entry & button
guess_frame = ttk.Frame(root, padding=10)
guess_frame.pack(fill="x")
guess_var = tk.StringVar()
guess_entry = ttk.Entry(guess_frame, textvariable=guess_var, width=5, font=("Helvetica", 18))
guess_entry.pack(side="left", padx=(0, 5))
guess_btn = ttk.Button(guess_frame, text="Guess", command=guess_letter)
guess_btn.pack(side="left")

# Lives & status bar
status_frame = ttk.Frame(root, padding=5)
status_frame.pack(fill="x", side="bottom")
lives_lbl = ttk.Label(status_frame, textvariable=lives_var)
lives_lbl.pack(side="left")
status_lbl = ttk.Label(status_frame, textvariable=status_var, anchor="e")
status_lbl.pack(side="right", fill="x", expand=True)

# Start the first game
start_new_game()

root.mainloop()

๐ŸŽ‰ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์ „ํ™˜ํ•˜๋ฉฐ, ํ‘œ์ค€ Python ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ์œผ๋กœ ๋งŒ๋“  ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ–‰๋งจ์„ ์ฆ๊ฒจ๋ณด์„ธ์š”. ์ž์œ ๋กญ๊ฒŒ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋” ํฐ ๋‹จ์–ด ๋ชฉ๋ก (ํŒŒ์ผ์—์„œ ๋กœ๋“œ).
  • winsound ๋˜๋Š” pygame์„ ์‚ฌ์šฉํ•œ ์‚ฌ์šด๋“œ ํšจ๊ณผ.
  • ํ•˜์ด ์Šค์ฝ”์–ด ํŠธ๋ž˜์ปค.

์ฝ”๋”ฉ ์ฆ๊ฒ๊ฒŒ!

์• ๋‹ˆ๋ฉ”์ด์…˜ ํ–‰๋งจ โ€“ ์ฝ”๋“œ walkthrough

animate_parts โ€“ ํ–‰๋งจ์„ ๋‹จ๊ณ„๋ณ„๋กœ ๊ทธ๋ฆฌ๊ธฐ

def animate_parts(parts, idx=0):
    """Recursively draw each part of the hangman with a short delay."""
    if idx >= len(parts):
        return

    # Draw an oval (the head)
    if parts[idx][0] == "oval":
        hangman_canvas.create_oval(*parts[idx][1:], width=3)

    # Draw a line (any limb)
    else:
        x1, y1, x2, y2 = parts[idx][1:]
        hangman_canvas.create_line(x1, y1, x2, y2, width=3)

    # Wait a bit, then draw the next part
    hangman_canvas.after(400, lambda: animate_parts(parts, idx + 1))

Source:

๐ŸŽ› Stepโ€ฏ11: UI ๋ ˆ์ด์•„์›ƒ

๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ

main = ttk.Frame(root, padding=20)
main.pack(expand=True, fill="both")

์บ”๋ฒ„์Šค

hangman_canvas = tk.Canvas(main, width=400, height=400, bg="white")
hangman_canvas.grid(row=1, column=0, pady=10)

์ž…๋ ฅ ๋ฐ ๋ฒ„ํŠผ

guess_var = tk.StringVar()

guess_entry = ttk.Entry(
    main,
    textvariable=guess_var,
    font=("Segoe UI", 16),
    width=5,
)
guess_entry.grid(row=4, column=0)

ttk.Button(main, text="Guess", command=guess_letter).grid(row=5, column=0)

โ–ถ๏ธ ๋‹จ๊ณ„โ€ฏ12: ์•ฑ ์‹คํ–‰

start_new_game()
root.mainloop()

์ด๊ฒƒ์€ ๊ฒŒ์ž„ ๋ฃจํ”„๋ฅผ ์‹œ์ž‘ํ•˜๊ณ  ์ฐฝ์„ ์—ฝ๋‹ˆ๋‹ค.

๐ŸŽ‰ Final Result

You now have:

  • โœ… Animated hangman drawing โ†’ โœ… ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ–‰๋งจ ๊ทธ๋ฆผ
  • ๐ŸŒ— Light / Dark mode โ†’ ๐ŸŒ— ๋ผ์ดํŠธ / ๋‹คํฌ ๋ชจ๋“œ
  • ๐ŸŽฏ Input validation โ†’ ๐ŸŽฏ ์ž…๋ ฅ ๊ฒ€์ฆ
  • ๐Ÿง  Gameโ€‘state handling โ†’ ๐Ÿง  ๊ฒŒ์ž„ ์ƒํƒœ ์ฒ˜๋ฆฌ
  • ๐ŸŽจ Modern UI styling โ†’ ๐ŸŽจ ํ˜„๋Œ€์ ์ธ UI ์Šคํƒ€์ผ๋ง

๐Ÿš€ ๋‹ค์Œ ์•„์ด๋””์–ด

  • ๋‚œ์ด๋„ ๋ ˆ๋ฒจ ์ถ”๊ฐ€
  • ํŒŒ์ผ์—์„œ ๋‹จ์–ด ๋กœ๋“œ
  • ํ‚ค๋ณด๋“œ ์ž…๋ ฅ ์ถ”๊ฐ€
  • ์ตœ๊ณ  ์ ์ˆ˜ ์ถ”์ 
  • .exe ๋กœ ํŒจํ‚ค์ง•

๋ฏธ๋ฆฌ๋ณด๊ธฐ

์• ๋‹ˆ๋ฉ”์ด์…˜ ํ–‰๋งจ ๊ฒŒ์ž„

๋ชจ๋“  ์ฝ”๋“œ ์Šค๋‹ˆํŽซ์„ ํ”„๋กœ์ ํŠธ์— ๋ฐ”๋กœ ๋ณต์‚ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Back to Blog

๊ด€๋ จ ๊ธ€

๋” ๋ณด๊ธฐ ยป

Python ์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ: ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์ฒซ ํ”„๋กœ์ ํŠธ ๋งŒ๋“ค๊ธฐ๊นŒ์ง€

Python์€ ์˜ค๋Š˜๋‚  ๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. ๊ทธ ๋‹จ์ˆœํ•จ, ๊ฐ€๋…์„ฑ, ๊ทธ๋ฆฌ๊ณ  ๋‹ค์žฌ๋‹ค๋Šฅํ•จ์€ ์ดˆ๋ณด์ž์™€ ์ „๋ฌธ๊ฐ€ ๋ชจ๋‘์—๊ฒŒ ์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค.

ํŒŒ์ด์ฌ ์—ฐ์‚ฐ๊ณผ ํ•จ์ˆ˜

์ˆ˜์‹(Expression) โ€“ ํ”ผ์—ฐ์‚ฐ์ž์™€ ์—ฐ์‚ฐ์ž์˜ ์กฐํ•ฉ ํ”ผ์—ฐ์‚ฐ์ž(Operand): ์—ฐ์‚ฐ์˜ ๋Œ€์ƒ ์—ฐ์‚ฐ์ž(Operator): ์—ฐ์‚ฐ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ธฐํ˜ธ, ์˜ˆ: +, -, *, / ์ •ํ™•ํ•œ ์ˆ˜์‹์œผ๋กœ ํ‘œํ˜„๋œ ์—ฐ์‚ฐ์„ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋กœ ์ž‘์„ฑํ•˜๋ฉด ์ปดํ“จํ„ฐ๊ฐ€ ์ •ํ™•ํžˆ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ์—ฐ์‚ฐ ์šฐ์„  ์ˆœ์œ„ โ€“ ์†Œ๊ด„ํ˜ธ๋ฅผโ€ฆ

Quantified Self: DuckDB์™€ Streamlit์œผ๋กœ ์ดˆ๊ณ ์† ๊ฑด๊ฐ• ๋Œ€์‹œ๋ณด๋“œ ๋งŒ๋“ค๊ธฐ

Apple Health ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ๊ณ ์„ฑ๋Šฅ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ „ํ™˜ํ•˜๊ธฐ Apple Health ๋ฐ์ดํ„ฐ๋ฅผ ๋‚ด๋ณด๋‚ด๋ ค๊ณ  ์‹œ๋„ํ•ด ๋ณธ ์ ์ด ์žˆ๋‚˜์š”? ๊ทธ๋Ÿฐ๋ฐ 2 GB๊ฐ€ ๋„˜๋Š” ๊ฑฐ๋Œ€ํ•œ ํŒŒ์ผ์ด ๋‚˜์˜ค๊ณค ํ•ฉ๋‹ˆ๋‹ค....