Step 47. ★ 프로젝트: 가위바위보 — 설명서 없이 완성하는 첫 게임
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 4시간
전제: Step 41~46을 마쳤다. 변수, 조건, 반복, 함수, 파일, 예외처리를 쓸 수 있다.
- 준비물: 파이썬이 설치된 컴퓨터, 텍스트 에디터, 메모할 종이 한 장. 새로 설치하는 것은 없습니다.
- 주의: 오늘 실습은 100% 안전합니다. 새 문법은 거의 없는 대신, 지금까지의 모든 문법이 출동하는 첫 종합 프로젝트입니다.
레고 블록을 사면 설명서대로 한두 개는 따라 만듭니다. 그런데 진짜 실력은 설명서 없이 상상의 것을 만들 때 생깁니다. Step 41~46이 블록 설명서였다면, 오늘은 상상의 성을 쌓는 날입니다. 왜 하필 가위바위보일까요? 작지만 완전하기 때문입니다 — 입력(내 패), 처리(컴퓨터 패 생성과 승부 판정), 출력(결과와 전적)이 전부 있고, 반복·조건·예외처리가 한 프로그램 안에서 만납니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 코딩 전에 다섯 줄 설계(할 일, 입력, 출력, 규칙, 사고)를 종이에 적는다
random.choice로 컴퓨터가 무작위 패를 내게 한다- 복잡한 판정을 표로 정리한 뒤
in검사로 단순하게 구현한다 - 입력 검사(strip + in + continue)로 오타에도 죽지 않는 프로그램을 만든다
- 돌아가는 코드를 함수로 나누는 리팩터링을 경험한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 (스크립트 파일 실행) |
| 오늘의 부품 | random.choice(무작위 선택), 튜플 (a, b), in 검사, continue, 카운터 변수 |
| 복습 문법 | 리스트, if/elif/else, while, f-string, 함수 정의 |
| 필요한 개념 | 다섯 줄 설계, 난수(주사위), 판정 표, 점진적 성장 작업법 |
| 오늘의 산출물 | rsp.py — 3판 2선승 가위바위보 게임 |
2-1. 설계 먼저 — 코딩 전의 다섯 줄
초보의 가장 큰 착각은 "코딩은 일단 쓰는 것"이라는 생각입니다. 고수는 쓰기 전에 설계합니다. 종이에 다섯 줄이면 됩니다.
- 프로그램이 할 일 한 문장: "사용자와 3판 2선승 가위바위보를 한다"
- 입력: 가위/바위/보 (문자)
- 출력: 판별 결과와 전적 (승/무/패 수)
- 규칙: 승리 조건 표
- 예상되는 사고: 오타 입력, 빈 입력
이 다섯 줄이 있으면 코딩은 "번역"이 되고, 없으면 "방황"이 됩니다. 오늘은 이 다섯 줄부터 시작합니다.
2-2. 난수 — 컴퓨터의 주사위
컴퓨터가 무작위 패를 내려면 주사위가 필요합니다. 파이썬의 주사위는 random 모듈입니다.
import random
hand = random.choice(["가위", "바위", "보"])
random.choice(리스트)는 리스트에서 하나를 무작위로 집어 줍니다. 실행할 때마다 다른 결과가 나오는 것이 포인트입니다. Step 43의 숫자 맞추기에서 random.randint를 써 본 적 있지요. 같은 가족입니다.
다섯 번 굴려 본 실측 (2026-09-09):
바위
가위
바위
가위
바위
같은 패가 연속으로 나와도 그게 무작위의 정상적인 모습입니다.
2-3. 판정 로직 — 표로 먼저 정리하기
가위바위보의 승패는 아홉 가지 조합입니다. if를 아홉 개 나열하기 전에, 표로 정리하면 구조가 보입니다.
- 비김: 내 패 == 컴퓨터 패 (3가지)
- 내가 이김: (가위,보), (바위,가위), (보,바위) (3가지)
- 나머지: 내가 짐 (3가지)
"이기는 경우 세 쌍"을 리스트로 만들어 두면, 판정이 in 한 번으로 줄어듭니다. 복잡한 조건을 표로 정리하는 버릇은 게임을 넘어 모든 로직 설계에 쓰입니다.
3. 따라 하기
3-1. 뼈대 — 한 판 게임
입력 (rsp.py)
import random
choices = ["가위", "바위", "보"]
me = input("가위, 바위, 보 중 하나를 내세요: ")
computer = random.choice(choices)
print(f"컴퓨터는 {computer}를 냈습니다.")
실행해 보세요. 여러 번.
읽는 법: 실행할 때마다 컴퓨터의 패가 바뀝니다. 아직 판정은 없습니다 — "입력받고, 컴퓨터가 내는" 뼈대만 세운 것입니다. 큰 것을 만들 때의 철칙: 작게 만들고, 돌아가면 키운다.
3-2. 판정 붙이기 — 표를 코드로
입력 (rsp.py에 이어서)
win_pairs = [("가위", "보"), ("바위", "가위"), ("보", "바위")]
if me == computer:
print("비겼습니다!")
elif (me, computer) in win_pairs:
print("이겼습니다!")
else:
print("졌습니다...")
읽는 법: (me, computer)는 튜플(tuple) — Step 43에서 배운, 바꿀 수 없는 묶음입니다. in win_pairs는 "그 쌍이 이기는 쌍 목록에 있니?"라는 질문입니다. if 아홉 개가 표 덕분에 세 갈래로 정리됐습니다.
예측: 컴퓨터가 "보"를 냈을 때 내가 "가위"를 내면 이기나요? win_pairs에서 ("가위","보")를 찾아보고, 실제로 그 조합이 나올 때까지 몇 번 실행해 확인해 보세요.
3-3. 입력 다듬기 — 입구에서 걸러내기
사용자는 "가위 "(뒤에 공백)나 "가위가위" 같은 것을 넣습니다.
입력
me = input("가위, 바위, 보 중 하나를 내세요: ").strip()
if me not in choices:
print("가위, 바위, 보 중에서만 골라 주세요!")
else:
# ... 판정 코드 ...
읽는 법: .strip()은 앞뒤 공백을 벗겨 냅니다. 그리고 if me not in choices:로 "세 가지 중 하나가 아니면"을 먼저 걸러 냅니다. 이상한 입력은 여기서 잡히니, 판정부는 안심하고 일할 수 있습니다. 입구에서 걸러라는 Step 46 방어적 프로그래밍의 실전형입니다.
3-4. 반복과 전적 — 3판 2선승제 완성
이제 게임 전체를 조립합니다.
입력 (rsp.py 전체)
import random
choices = ["가위", "바위", "보"]
win_pairs = [("가위", "보"), ("바위", "가위"), ("보", "바위")]
win = draw = lose = 0
print("=== 가위바위보, 3판 2선승제! ===")
while win < 2 and lose < 2:
me = input("가위, 바위, 보: ").strip()
if me not in choices:
print("가위, 바위, 보 중에서만 골라 주세요!")
continue
computer = random.choice(choices)
print(f"컴퓨터는 {computer}!")
if me == computer:
draw = draw + 1
print("비김!")
elif (me, computer) in win_pairs:
win = win + 1
print("이김!")
else:
lose = lose + 1
print("짐...")
print(f"현재 전적: {win}승 {draw}무 {lose}패")
print("=== 게임 종료 ===")
if win == 2:
print("축하합니다! 최종 승리입니다!")
else:
print("아쉽습니다. 다음 판을 노려 보세요!")
실제로 돌린 실행 (2026-09-09 실측 — 첫 입력에 일부러 "가위가위"라는 오타를 넣었습니다):
=== 가위바위보, 3판 2선승제! ===
가위, 바위, 보: 가위, 바위, 보 중에서만 골라 주세요!
가위, 바위, 보: 컴퓨터는 보!
짐...
현재 전적: 0승 0무 1패
가위, 바위, 보: 컴퓨터는 보!
이김!
현재 전적: 1승 0무 1패
가위, 바위, 보: 컴퓨터는 보!
비김!
현재 전적: 1승 1무 1패
가위, 바위, 보: 컴퓨터는 가위!
이김!
현재 전적: 2승 1무 1패
=== 게임 종료 ===
축하합니다! 최종 승리입니다!
(컴퓨터의 패는 실행할 때마다 다릅니다 — 난수이니까요.)
읽는 법: while win < 2 and lose < 2 — "둘 다 2가 되기 전까지 계속"이 3판 2선승의 수식입니다. 오타("가위가위")를 넣은 첫 입력은 continue로 무효 처리되어 카운터가 오르지 않은 것을 실측에서 확인하세요. 비기면 어느 쪽도 2에 가까워지지 않으니 판이 이어집니다.
왜: 이 스무 줄에 변수, 리스트, 튜플, in, if/elif/else, while, and, continue, 카운터, f-string이 전부 들어 있습니다. 종합선물세트입니다.
3-5. 함수로 나누기 — 리팩터링 연습
돌아가는 코드를 부품으로 나누는 연습입니다. 판정부를 함수로 빼 봅시다.
입력
def judge(me, computer):
if me == computer:
return "draw"
if (me, computer) in win_pairs:
return "win"
return "lose"
이제 게임 본체는 result = judge(me, computer) 한 줄로 판정을 시키고, result가 "win"/"draw"/"lose"인지에 따라 카운터를 올리면 됩니다. 판정 규칙이 바뀌면(예: 묵찌빠) 이 함수 하나만 고치면 됩니다 — Step 44의 "한 가지 일만 하는 함수"의 실전입니다.
함수를 단독으로 시험한 실측 (2026-09-09):
judge("바위", "바위") -> draw
judge("보", "바위") -> win
judge("가위", "보") -> win
judge("보", "가위") -> lose
예측: ("보","가위")가 왜 lose일까요? win_pairs의 쌍은 언제나 (내 패, 컴퓨터 패) 순서입니다. ("보","가위")는 이기는 쌍 목록에 없으니 else로 떨어집니다.
3-6. 일부러 버그 넣기 — 디버깅 훈련
완성한 게임에 일부러 버그를 심고, 찾아내는 연습을 합니다. 파일을 복사해 rsp_bug.py로 저장한 뒤, 아래 세 군데를 고의로 바꿔 보세요.
win_pairs의 ("가위", "보")를 ("보", "가위")로 바꾼다while win < 2 and lose < 2의 and를 or로 바꾼다- 입력 검사의
continue를 지운다
실험 방법: 바꾼 뒤 실행해 "어떤 증상이 나타나는지"를 노트에 적습니다. 참고 증상: 1은 "가위로 보를 이겼는데 졌다고 나옴", 2는 "한쪽이 2승해도 안 끝남", 3은 "오타가 패로 기록됨"입니다. 증상을 먼저 적고 나서 원인을 설명해 보세요.
왜: 버그를 만들어 보는 것이 버그를 찾는 가장 빠른 훈련입니다. "증상 → 원인"의 연결 고리를 몸에 새기는 것이며, Step 46의 트레이스백 독해와 합쳐져 진짜 디버깅력이 됩니다.
3-7. 구조 그리기 — 내 게임의 설계도
완성한 게임의 흐름을 노트에 그려 봅시다.
시작 → 전적 초기화 → [반복: 입력 → 검사 → 컴퓨터 패 → 판정 → 전적 갱신] → 2승/2패 도달 → 결과 출력 → 종료
그리고 각 칸 옆에, 그 일을 하는 코드 줄 번호를 적습니다. 그림과 코드가 일대일로 연결되면, 여러분은 이 프로그램을 "소유"한 것입니다. 이 설계도 습관은 프로그램이 커질수록 빛을 발합니다 — 백 줄짜리도 설계도가 있으면 두렵지 않습니다.
4. 미션과 연습문제
미션 — 게임 업그레이드
완성한 게임에 다음 네 가지를 스스로 추가하세요:
- 전적 기록 저장: 게임이 끝나면 최종 전적과 날짜를
rsp_log.txt에 덧붙인다 (Step 45의 파일 쓰기,datetime모듈은import datetime후datetime.date.today()로 오늘 날짜를 얻을 수 있습니다) - 재시작 메뉴: 게임 종료 후 "다시 할까요? (y/n)"를 묻고, y면 새 게임을 시작한다
- 연승 표시: 무승부 없이 2연승으로 끝나면 "완벽한 승리!"라는 특별 메시지를 출력한다
- 단서 문서: 파일 맨 위에 주석으로 게임의 규칙과 조작법을 다섯 줄로 적는다
완성하면 가족에게 시연해 보세요. 가족이 이상한 것을 마구 넣을 때(반드시 넣습니다) 게임이 죽지 않는지 지켜보세요. 죽으면 그것이 다음 고칠 일입니다.
연습문제
문제 1. 코딩 전에 적는 다섯 줄 설계의 항목을 모두 말해 보세요.
문제 2. random.choice를 while 반복문 밖에서 한 번만 호출하면 어떤 증상이 나타나고, 왜 그런가요?
문제 3. 3판 2선승제에서 계속 비기기만 하면 게임이 끝나지 않습니다. 버그일까요, 규칙의 결과일까요? 규칙의 결과라면 메우는 방법을 하나 제안해 보세요.
문제 4. win_pairs의 ("가위", "보")를 ("보", "가위")로 잘못 적으면 어떤 증상이 나타나나요? 이 사고를 예방하는 습관은 무엇인가요?
5. 모범 답안과 완료 기준
미션 모범 답안
네 가지 추가의 핵심 코드:
import random
import datetime
def save_log(win, draw, lose):
today = datetime.date.today()
with open("rsp_log.txt", "a", encoding="utf-8") as f:
f.write(f"{today} — {win}승 {draw}무 {lose}패\n")
# 게임 종료 후:
save_log(win, draw, lose)
if win == 2 and draw == 0:
print("완벽한 승리!")
again = input("다시 할까요? (y/n): ").strip().lower()
if again == "y":
# win, draw, lose를 0으로 되돌리고 게임 루프 재진입
...
재시작은 게임 루프 전체를 def play_one_game(): 함수로 감싸고, 바깥 while에서 again == "y"이면 다시 호출하는 구조가 깔끔합니다.
검증하는 법: ① 게임을 두 번 끝내고 rsp_log.txt를 열어 두 줄이 날짜와 함께 쌓였는지 확인. ② 재시작에서 "y" 외의 대답("yes", "응")은 종료로 처리되는지 확인하고, 허용하고 싶으면 again in ["y", "yes"]로 고칩니다. ③ 2연승으로 끝냈을 때만 특별 메시지가 뜨는지 확인 (비긴 판이 끼면 안 떠야 함). ④ 파일 맨 위의 다섯 줄 주석만 읽고도 남이 게임을 할 수 있는지 확인.
연습문제 해답
문제 1 해답. ① 할 일 한 문장, ② 입력, ③ 출력, ④ 규칙, ⑤ 예상되는 사고. 이 다섯 줄이 있으면 코딩은 번역이 되고, 없으면 방황이 됩니다.
문제 2 해답. 한 게임 안에서 컴퓨터가 매번 같은 패만 냅니다. 주사위를 게임 시작 전에 한 번만 굴린 셈이기 때문입니다. 패 생성 줄은 반드시 while 안에 있어야 매 판 새로 굴립니다.
문제 3 해답. 버그가 아니라 규칙의 결과입니다. 3판 2선승제는 무승부를 세지 않으니, 무승부만 나오면 원칙적으로 무한합니다. 메우는 방법의 예: round_count 변수를 두고 10판이 지나면 "무승부로 종료" 처리. 규칙의 빈틈을 스스로 발견하고 메우는 것도 설계입니다.
문제 4 해답. "가위로 보를 이겼는데 졌다"고 나옵니다. 쌍의 첫 자리는 언제나 "내 패"인데 ("보","가위")는 목록에 없는 조합이라 else(짐)로 떨어지기 때문입니다. 예방 습관: 판정 로직을 코드 전에 표로 정리하고, win_pairs를 print해 눈으로 확인한 뒤, 튜플의 자리 의미(앞=나, 뒤=컴퓨터)를 주석으로 적어 둡니다.
완료 기준 체크리스트
- [ ] 코딩 전에 다섯 줄 설계를 종이에 적었다
- [ ] random.choice로 컴퓨터 패를 매 판 새로 만들 수 있다
- [ ] 판정 로직을 표로 정리해 in으로 구현했다
- [ ] 오타 입력에도 죽지 않는 3판 2선승 게임을 완성했다
- [ ] 판정부를 함수(judge)로 분리하는 리팩터링을 해 봤다
- [ ] 일부러 버그 세 개를 넣고 증상과 원인을 연결했다
- [ ] 미션: 저장·재시작·연승 표시·단서 문서를 추가했다
6. 흔한 실수와 해결
벽 1. 컴퓨터가 매번 같은 패만 낸다
증상: 실행할 때는 다른데, 한 게임 안에서는 계속 같은 패.
원인: random.choice를 반복문 밖에서 한 번만 호출한 것입니다. 주사위를 게임 시작 전에 한 번만 굴린 셈입니다.
해결: 패 생성 줄이 while 안에 있는지 확인하세요. 매 판 새로 굴릴 것.
벽 2. 오타를 넣었는데 판 수가 올라간다
증상: "가위가위"라고 쳤는데 패로 기록됩니다.
원인: 입력 검사(not in choices)가 판정보다 뒤에 있거나, 검사 후 continue를 안 쓴 것입니다. 걸러내기만 하고 다시 묻지 않으면 흐름이 판정으로 흘러갑니다.
해결: 검사 → 안 맞으면 안내 + continue의 순서를 확인합니다. continue가 "이번 바퀴는 무효"라는 표지입니다. 완성 코드에서는 오타 입력이 카운터를 올리지 않음이 실측으로 확인됐습니다 (2026-09-09).
벽 3. 비길 때마다 무한히 반복된다
증상: 계속 비기기만 하면 게임이 안 끝납니다.
원인: 버그가 아니라 규칙의 결과입니다. 3판 2선승제는 무승부를 세지 않으니 원칙적으로 무한합니다.
해결: 진짜 게임처럼 최대 판 수를 정하세요. 예: round_count 변수를 두고 10판이 지나면 "무승부로 종료". 규칙의 빈틈을 스스로 발견하고 메우는 것도 설계입니다.
벽 4. 판정이 이상하다 (이겼는데 졌다고 나온다)
증상: 가위 vs 보에서 졌다고 나옵니다.
원인: win_pairs의 쌍 순서가 뒤집힌 경우가 대부분입니다. ("보","가위")는 "보가 가위를 이긴다"가 아니라 목록에 없는 조합이 됩니다. 쌍의 첫 자리는 언제나 "내 패"입니다.
해결: win_pairs를 출력해 눈으로 확인하고, 튜플의 자리 의미(앞=나, 뒤=컴퓨터)를 주석으로 적어 두세요. 표로 먼저 정리하는 습관이 이 사고를 예방합니다.
벽 5. 완벽하게 만들려다 시작을 못 한다
증상: 처음부터 아름다운 코드를 쓰려다 파일이 빈 채로 멈춥니다.
원인: 완벽주의입니다. 프로젝트에서 가장 흔한 실패입니다.
해결: 오늘의 순서를 기억하세요. 뼈대(한 판) → 판정 → 검사 → 반복 → 함수 분리. 매 단계 "돌아가는 것"을 확인하고 다음으로 갔습니다. 완성된 못난이가 미완성의 걸작을 이깁니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 다섯 줄 설계 | 할 일 / 입력 / 출력 / 규칙 / 사고 — 코딩 전에 종이에 |
| 난수(random) | 게임용 주사위 — 매 판 반복문 안에서 새로 굴림 |
| 판정 표 | 복잡한 조건을 표로 정리하면 in 한 번으로 끝 |
| 입구 검사 | 이상한 입력은 strip + in + continue로 앞에서 차단 |
| 점진적 성장 | 작게 만들고, 돌아가면 키운다 |
오늘의 문법·부품
| 부품 | 하는 일 |
|---|---|
random.choice(리스트) |
리스트에서 무작위로 하나 선택 |
(me, computer) in win_pairs |
쌍이 목록에 있는지 검사 |
.strip() |
입력의 앞뒤 공백 제거 |
continue |
이번 바퀴 무효, 반복 처음으로 |
win = draw = lose = 0 |
카운터 한 줄 초기화 |
명령어보다 중요한 감각
"일단 돌아가게, 그다음 좋게." 오늘의 순서 — 뼈대 → 판정 → 검사 → 반복 → 함수 분리 — 가 실무의 표준 작업법이고, 초보가 고수를 따라잡는 유일한 지름길입니다.
두 가지를 더 기억해 두세요. 첫째, 오늘 쓴 random은 게임용 주사위입니다. 정해진 규칙으로 무작위를 흉내 내기에 씨앗(seed)을 알면 예측이 가능하고, 비밀번호나 열쇠를 만들 때는 치명적입니다. 보안용으로는 secrets라는 별도 모듈이 있습니다 — "무작위에도 등급이 있다"는 사실이 오늘의 보안 한 줄입니다. 둘째, 완성한 rsp.py는 보관함에 넣어 두세요. 빈 화면에서 게임 하나를 일궈 낸 증거이며, 지치는 날 미래의 여러분이 찾아올 증거입니다.
축하합니다. 여러분은 오늘 "설명서 없이 완성한 첫 번째 프로그램"을 세상에 내놓았습니다. 비어 있는 파일에서 시작해, 설계하고, 만들고, 고치고, 완성하는 전체 순환 — 이것이 개발자의 일상이고, 오늘부터 여러분의 일상이기도 합니다.
전부 체크되면 Step 47 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.