포스트

[Python 100일 챌린지] Day 30 - 미니 프로젝트 - 계산기 프로그램

[Python 100일 챌린지] Day 30 - 미니 프로젝트 - 계산기 프로그램

🎉 Phase 3 완료! 지금까지 배운 함수의 모든 것을 활용해서 실용적인 계산기 프로그램을 만들어봅시다! (40분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

  1. Phase 3 내용 복습하기
  2. 계산기 프로그램 설계하기
  3. 함수를 활용하여 계산기 구현하기
  4. 프로젝트 완성하고 Phase 3 마무리

📚 사전 지식


🎯 학습 목표 1: Phase 3 내용 복습하기

Phase 3에서 배운 내용

Day 21-23: 제어문 심화

  • if-elif-else 완벽 이해
  • while문 고급 패턴
  • break, continue, pass
  • 중첩 조건문과 반복문

Day 24-26: 함수 기초

  • 함수 정의와 호출
  • 매개변수와 반환값
  • 기본값 매개변수
  • 키워드 전용 인수

Day 27-29: 함수 심화

  • *args와 **kwargs
  • 람다 함수와 내장 함수
  • 전역/지역 스코프
  • LEGB 규칙

오늘 만들 프로젝트

계산기 프로그램 - Phase 3의 모든 함수 개념을 활용합니다!


🎯 학습 목표 2: 계산기 프로그램 설계하기

프로젝트 목표

고급 계산기 구현:

  • 함수로 구조화된 계산기
  • 메뉴 시스템과 사용자 인터랙션
  • 에러 처리와 입력 검증
  • 계산 기록 관리

사용할 개념

  • ✅ Day 24: 함수 정의 (def)
  • ✅ Day 25: 매개변수와 반환값
  • ✅ Day 26: 기본 매개변수와 키워드 인자
  • ✅ Day 27: 가변 인자 (*args, **kwargs)
  • ✅ Day 28: 람다 함수
  • ✅ Day 29: 스코프와 전역 변수

프로젝트 개요

기능 목록

  1. 기본 연산: 덧셈, 뺄셈, 곱셈, 나눗셈
  2. 고급 연산: 제곱, 제곱근, 나머지
  3. 계산 기록: 최근 계산 내역 저장
  4. 연속 계산: 이전 결과를 다음 계산에 사용
  5. 에러 처리: 잘못된 입력 처리

🎯 학습 목표 3: 함수를 활용하여 계산기 구현하기

버전 1: 기본 계산기

기본 연산 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def add(a, b):
    """두 숫자를 더합니다"""
    return a + b

def subtract(a, b):
    """두 숫자를 뺍니다"""
    return a - b

def multiply(a, b):
    """두 숫자를 곱합니다"""
    return a * b

def divide(a, b):
    """두 숫자를 나눕니다"""
    if b == 0:
        return "0으로 나눌 수 없습니다"
    return a / b

# 테스트
print(add(10, 5))       # 15
print(subtract(10, 5))  # 5
print(multiply(10, 5))  # 50
print(divide(10, 5))    # 2.0
print(divide(10, 0))    # 0으로 나눌 수 없습니다

기본 계산기 메인 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def calculator_v1():
    """간단한 계산기 v1"""
    print("=== 계산기 v1 ===")

    # 숫자 입력
    num1 = float(input("첫 번째 숫자: "))
    num2 = float(input("두 번째 숫자: "))

    # 연산자 선택
    print("\n연산자를 선택하세요:")
    print("1. + (덧셈)")
    print("2. - (뺄셈)")
    print("3. * (곱셈)")
    print("4. / (나눗셈)")

    choice = input("\n선택 (1-4): ")

    # 계산 수행
    if choice == '1':
        result = add(num1, num2)
        operator = '+'
    elif choice == '2':
        result = subtract(num1, num2)
        operator = '-'
    elif choice == '3':
        result = multiply(num1, num2)
        operator = '*'
    elif choice == '4':
        result = divide(num1, num2)
        operator = '/'
    else:
        print("잘못된 선택입니다")
        return

    # 결과 출력
    print(f"\n결과: {num1} {operator} {num2} = {result}")

# 실행
calculator_v1()

💡 버전 2: 메뉴 시스템 추가

고급 연산 함수 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def power(a, b):
    """a의 b제곱을 계산합니다"""
    return a ** b

def square_root(a):
    """제곱근을 계산합니다"""
    if a < 0:
        return "음수의 제곱근을 계산할 수 없습니다"
    return a ** 0.5

def modulo(a, b):
    """나머지를 계산합니다"""
    if b == 0:
        return "0으로 나눌 수 없습니다"
    return a % b

메뉴 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def show_menu():
    """메뉴를 표시합니다"""
    print("\n" + "=" * 40)
    print("           계산기 v2")
    print("=" * 40)
    print("1. 덧셈 (+)")
    print("2. 뺄셈 (-)")
    print("3. 곱셈 (*)")
    print("4. 나눗셈 (/)")
    print("5. 제곱 (^)")
    print("6. 제곱근 (√)")
    print("7. 나머지 (%)")
    print("0. 종료")
    print("=" * 40)

def get_numbers(count=2):
    """숫자를 입력받습니다"""
    numbers = []
    for i in range(count):
        while True:
            try:
                num = float(input(f"{i+1}번째 숫자: "))
                numbers.append(num)
                break
            except ValueError:
                print("올바른 숫자를 입력하세요")

    return numbers if count > 1 else numbers[0]

def calculator_v2():
    """메뉴 시스템이 있는 계산기"""
    while True:
        show_menu()
        choice = input("\n선택 (0-7): ")

        if choice == '0':
            print("계산기를 종료합니다")
            break

        if choice in ['1', '2', '3', '4', '5', '7']:
            # 두 개의 숫자 필요
            numbers = get_numbers(2)
            a, b = numbers[0], numbers[1]

            if choice == '1':
                result = add(a, b)
                print(f"\n결과: {a} + {b} = {result}")
            elif choice == '2':
                result = subtract(a, b)
                print(f"\n결과: {a} - {b} = {result}")
            elif choice == '3':
                result = multiply(a, b)
                print(f"\n결과: {a} * {b} = {result}")
            elif choice == '4':
                result = divide(a, b)
                print(f"\n결과: {a} / {b} = {result}")
            elif choice == '5':
                result = power(a, b)
                print(f"\n결과: {a} ^ {b} = {result}")
            elif choice == '7':
                result = modulo(a, b)
                print(f"\n결과: {a} % {b} = {result}")

        elif choice == '6':
            # 제곱근: 한 개의 숫자만 필요
            a = get_numbers(1)
            result = square_root(a)
            print(f"\n결과: √{a} = {result}")

        else:
            print("잘못된 선택입니다")

        input("\nEnter를 눌러 계속...")

# 실행
calculator_v2()

🚀 버전 3: 계산 기록 추가

전역 변수로 기록 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
history = []  # 계산 기록 저장

def add_to_history(expression, result):
    """계산 기록에 추가"""
    global history
    history.append({
        "expression": expression,
        "result": result
    })

def show_history():
    """계산 기록 표시"""
    if not history:
        print("\n계산 기록이 없습니다")
        return

    print("\n" + "=" * 40)
    print("           계산 기록")
    print("=" * 40)
    for i, record in enumerate(history, 1):
        print(f"{i}. {record['expression']} = {record['result']}")
    print("=" * 40)

def clear_history():
    """계산 기록 삭제"""
    global history
    history = []
    print("\n계산 기록이 삭제되었습니다")

확장된 메뉴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def show_menu_v3():
    """메뉴를 표시합니다 (v3)"""
    print("\n" + "=" * 40)
    print("           계산기 v3")
    print("=" * 40)
    print("1. 덧셈 (+)        5. 제곱 (^)")
    print("2. 뺄셈 (-)        6. 제곱근 (√)")
    print("3. 곱셈 (*)        7. 나머지 (%)")
    print("4. 나눗셈 (/)")
    print("-" * 40)
    print("8. 계산 기록 보기")
    print("9. 기록 삭제")
    print("0. 종료")
    print("=" * 40)

def calculate(choice, a, b=None):
    """연산을 수행하고 기록을 저장합니다"""
    operations = {
        '1': (add, lambda: f"{a} + {b}"),
        '2': (subtract, lambda: f"{a} - {b}"),
        '3': (multiply, lambda: f"{a} * {b}"),
        '4': (divide, lambda: f"{a} / {b}"),
        '5': (power, lambda: f"{a} ^ {b}"),
        '6': (square_root, lambda: f"{a}"),
        '7': (modulo, lambda: f"{a} % {b}")
    }

    if choice not in operations:
        return None

    func, expr_func = operations[choice]

    # 계산 수행
    if choice == '6':
        result = func(a)
    else:
        result = func(a, b)

    # 기록 추가
    expression = expr_func()
    add_to_history(expression, result)

    return result

def calculator_v3():
    """계산 기록 기능이 있는 계산기"""
    while True:
        show_menu_v3()
        choice = input("\n선택 (0-9): ")

        if choice == '0':
            print("계산기를 종료합니다")
            break

        if choice == '8':
            show_history()
        elif choice == '9':
            clear_history()
        elif choice in ['1', '2', '3', '4', '5', '7']:
            # 두 개의 숫자 필요
            numbers = get_numbers(2)
            result = calculate(choice, numbers[0], numbers[1])
            print(f"\n결과: {result}")
        elif choice == '6':
            # 제곱근: 한 개의 숫자만 필요
            a = get_numbers(1)
            result = calculate(choice, a)
            print(f"\n결과: {result}")
        else:
            print("잘못된 선택입니다")

        input("\nEnter를 눌러 계속...")

# 실행
calculator_v3()

🎯 버전 4: 연속 계산 모드

연속 계산 기능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
last_result = None  # 마지막 결과 저장

def use_last_result():
    """마지막 결과를 사용할지 묻습니다"""
    global last_result

    if last_result is None:
        return False

    print(f"\n마지막 결과: {last_result}")
    choice = input("마지막 결과를 사용하시겠습니까? (y/n): ")
    return choice.lower() == 'y'

def get_number_with_option(prompt="숫자: "):
    """숫자를 입력받거나 마지막 결과를 사용합니다"""
    if use_last_result():
        return last_result

    while True:
        try:
            return float(input(prompt))
        except ValueError:
            print("올바른 숫자를 입력하세요")

def calculator_v4():
    """연속 계산 모드가 있는 계산기"""
    global last_result

    while True:
        show_menu_v3()
        choice = input("\n선택 (0-9): ")

        if choice == '0':
            print("계산기를 종료합니다")
            break

        if choice == '8':
            show_history()
        elif choice == '9':
            clear_history()
        elif choice in ['1', '2', '3', '4', '5', '7']:
            # 두 개의 숫자 입력
            a = get_number_with_option("첫 번째 숫자: ")
            b = get_number_with_option("두 번째 숫자: ")

            result = calculate(choice, a, b)
            print(f"\n결과: {result}")

            if isinstance(result, (int, float)):
                last_result = result

        elif choice == '6':
            # 제곱근
            a = get_number_with_option("숫자: ")
            result = calculate(choice, a)
            print(f"\n결과: {result}")

            if isinstance(result, (int, float)):
                last_result = result
        else:
            print("잘못된 선택입니다")

        input("\nEnter를 눌러 계속...")

# 실행
calculator_v4()

🎨 최종 버전: 완성된 계산기

모든 기능을 통합한 최종 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
"""
이제와서 시작하는 Python - Phase 3 프로젝트
다기능 계산기 프로그램
"""

# 전역 변수
history = []
last_result = None

# ========== 기본 연산 함수 ==========

def add(a, b):
    """두 숫자를 더합니다"""
    return a + b

def subtract(a, b):
    """두 숫자를 뺍니다"""
    return a - b

def multiply(a, b):
    """두 숫자를 곱합니다"""
    return a * b

def divide(a, b):
    """두 숫자를 나눕니다"""
    if b == 0:
        return "오류: 0으로 나눌 수 없습니다"
    return a / b

def power(a, b):
    """a의 b제곱을 계산합니다"""
    return a ** b

def square_root(a):
    """제곱근을 계산합니다"""
    if a < 0:
        return "오류: 음수의 제곱근을 계산할 수 없습니다"
    return a ** 0.5

def modulo(a, b):
    """나머지를 계산합니다"""
    if b == 0:
        return "오류: 0으로 나눌 수 없습니다"
    return a % b

# ========== 기록 관리 함수 ==========

def add_to_history(expression, result):
    """계산 기록에 추가"""
    global history
    if len(history) >= 10:  # 최대 10개까지만 저장
        history.pop(0)
    history.append({
        "expression": expression,
        "result": result
    })

def show_history():
    """계산 기록 표시"""
    if not history:
        print("\n계산 기록이 없습니다")
        return

    print("\n" + "=" * 50)
    print("              📜 계산 기록")
    print("=" * 50)
    for i, record in enumerate(history, 1):
        print(f"{i:2d}. {record['expression']:30s} = {record['result']}")
    print("=" * 50)

def clear_history():
    """계산 기록 삭제"""
    global history
    history = []
    print("\n✅ 계산 기록이 삭제되었습니다")

# ========== UI 함수 ==========

def show_menu():
    """메뉴를 표시합니다"""
    global last_result

    print("\n" + "=" * 50)
    print("              🔢 Python 계산기")
    print("=" * 50)
    print("  [기본 연산]")
    print("    1. 덧셈 (+)          5. 제곱 (^)")
    print("    2. 뺄셈 (-)          6. 제곱근 (√)")
    print("    3. 곱셈 (*)          7. 나머지 (%)")
    print("    4. 나눗셈 (/)")
    print("-" * 50)
    print("  [기타]")
    print("    8. 계산 기록 보기    9. 기록 삭제")

    if last_result is not None:
        print(f"\n  💾 마지막 결과: {last_result}")

    print("-" * 50)
    print("    0. 종료")
    print("=" * 50)

def get_number(prompt="숫자 입력: "):
    """숫자를 입력받습니다"""
    while True:
        try:
            return float(input(prompt))
        except ValueError:
            print("❌ 올바른 숫자를 입력하세요")

def use_last_result_prompt():
    """마지막 결과를 사용할지 묻습니다"""
    global last_result

    if last_result is None:
        return False

    choice = input(f"마지막 결과 ({last_result})를 사용하시겠습니까? (y/n): ")
    return choice.lower() == 'y'

def get_number_with_option(prompt="숫자 입력: "):
    """숫자를 입력받거나 마지막 결과를 사용합니다"""
    if use_last_result_prompt():
        return last_result
    return get_number(prompt)

# ========== 계산 수행 함수 ==========

def perform_calculation(choice, a, b=None):
    """연산을 수행하고 결과를 반환합니다"""
    operations = {
        '1': (add, "+", True),
        '2': (subtract, "-", True),
        '3': (multiply, "*", True),
        '4': (divide, "/", True),
        '5': (power, "^", True),
        '6': (square_root, "", False),
        '7': (modulo, "%", True)
    }

    if choice not in operations:
        return None, None

    func, operator, needs_two = operations[choice]

    # 계산 수행
    if needs_two:
        result = func(a, b)
        expression = f"{a} {operator} {b}"
    else:
        result = func(a)
        expression = f"{operator}{a}"

    return result, expression

# ========== 메인 계산기 함수 ==========

def calculator():
    """메인 계산기 함수"""
    global last_result

    print("\n🎉 Python 계산기에 오신 것을 환영합니다!")
    print("Phase 3에서 배운 함수를 활용한 프로젝트입니다.")

    while True:
        show_menu()
        choice = input("\n선택 (0-9): ")

        if choice == '0':
            print("\n👋 계산기를 종료합니다. 감사합니다!")
            break

        if choice == '8':
            show_history()
        elif choice == '9':
            clear_history()
        elif choice in ['1', '2', '3', '4', '5', '7']:
            # 두 개의 숫자 필요
            print("\n📝 두 개의 숫자를 입력하세요")
            a = get_number_with_option("첫 번째 숫자: ")
            b = get_number_with_option("두 번째 숫자: ")

            result, expression = perform_calculation(choice, a, b)

            if isinstance(result, str):  # 오류 메시지
                print(f"\n{result}")
            else:
                print(f"\n{expression} = {result}")
                last_result = result
                add_to_history(expression, result)

        elif choice == '6':
            # 제곱근: 한 개의 숫자만 필요
            print("\n📝 숫자를 입력하세요")
            a = get_number_with_option("숫자: ")

            result, expression = perform_calculation(choice, a)

            if isinstance(result, str):  # 오류 메시지
                print(f"\n{result}")
            else:
                print(f"\n{expression} = {result}")
                last_result = result
                add_to_history(expression, result)
        else:
            print("\n❌ 잘못된 선택입니다")

        input("\n⏎ Enter를 눌러 계속...")

# ========== 프로그램 실행 ==========

if __name__ == "__main__":
    calculator()

💡 실전 팁 & 주의사항

1. 함수는 하나의 역할만 (Single Responsibility Principle)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ❌ 나쁜 예: 하나의 함수가 여러 일을 함
def calculate_and_save_and_print(a, b, operation):
    if operation == "+":
        result = a + b
    # ... (계산)
    history.append(result)  # 기록 저장
    print(f"결과: {result}")  # 출력
    return result

# ✅ 좋은 예: 역할 분리
def calculate(a, b, operation):
    """계산만 담당"""
    if operation == "+":
        return a + b
    # ... (다른 연산)

def save_history(result):
    """기록 저장만 담당"""
    history.append(result)

def display_result(result):
    """출력만 담당"""
    print(f"결과: {result}")

이유: 함수가 하나의 역할만 하면 테스트, 디버깅, 재사용이 쉬워집니다.

2. 입력 검증은 함수 시작 부분에

1
2
3
4
5
6
7
8
9
def divide(a, b):
    # ✅ 함수 시작 부분에서 입력 검증
    if b == 0:
        return "오류: 0으로 나눌 수 없습니다"
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        return "오류: 숫자만 입력 가능합니다"

    # 메인 로직
    return a / b

이유: 잘못된 입력을 조기에 처리하면 함수 내부 로직이 간결해집니다 (Early Return 패턴).

3. 매직 넘버 대신 상수 사용

1
2
3
4
5
6
7
8
# ❌ 매직 넘버 (무슨 의미인지 불명확)
if len(history) > 10:
    history = history[-10:]

# ✅ 상수로 의미 명확화
MAX_HISTORY_SIZE = 10
if len(history) > MAX_HISTORY_SIZE:
    history = history[-MAX_HISTORY_SIZE:]

이유: 코드의 의도가 명확해지고, 값을 변경할 때 한 곳만 수정하면 됩니다.

4. 반환값 타입 일관성 유지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ❌ 나쁜 예: 경우에 따라 다른 타입 반환
def calculate(a, b, op):
    if op == "+":
        return a + b  # 숫자
    else:
        return "잘못된 연산자"  # 문자열

# ✅ 좋은 예: 항상 같은 타입 (튜플)
def calculate(a, b, op):
    if op == "+":
        return (True, a + b)
    else:
        return (False, "잘못된 연산자")

# 사용
success, result = calculate(5, 3, "+")
if success:
    print(f"결과: {result}")
else:
    print(result)  # 오류 메시지

이유: 함수 사용자가 예측 가능한 결과를 받을 수 있어 안전합니다.

5. Docstring으로 함수 문서화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def power(base, exponent=2):
    """
    밑(base)을 지수(exponent)만큼 제곱합니다.

    Args:
        base (float): 밑
        exponent (float, optional): 지수. 기본값은 2.

    Returns:
        float: base의 exponent 제곱 값

    Examples:
        >>> power(2, 3)
        8.0
        >>> power(5)
        25.0
    """
    return base ** exponent

이유: 함수 사용법을 명확히 전달하고, help(power)로 설명을 볼 수 있습니다.

6. 전역 변수 대신 반환값 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 전역 변수 남용
history = []

def add_to_history(item):
    global history
    history.append(item)

# ✅ 반환값 활용 (더 안전)
def add_to_history(history, item):
    """새 항목이 추가된 리스트 반환"""
    return history + [item]

# 사용
history = []
history = add_to_history(history, "2 + 3 = 5")

이유: 전역 변수를 수정하는 함수는 예측하기 어렵고 테스트하기 어렵습니다.

7. 메뉴 시스템에 딕셔너리 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# ❌ 반복적인 if-elif 체인
def handle_menu(choice):
    if choice == "1":
        add()
    elif choice == "2":
        subtract()
    elif choice == "3":
        multiply()
    elif choice == "4":
        divide()
    else:
        print("잘못된 선택")

# ✅ 딕셔너리로 깔끔하게
MENU_ACTIONS = {
    "1": ("덧셈", add),
    "2": ("뺄셈", subtract),
    "3": ("곱셈", multiply),
    "4": ("나눗셈", divide),
}

def handle_menu(choice):
    if choice in MENU_ACTIONS:
        name, func = MENU_ACTIONS[choice]
        func()
    else:
        print("잘못된 선택")

이유: 새로운 메뉴 항목 추가가 쉽고, 코드가 간결하며 확장 가능합니다.

8. 에러 메시지는 구체적으로

1
2
3
4
5
6
7
8
9
10
11
# ❌ 모호한 에러 메시지
def divide(a, b):
    if b == 0:
        return "오류"

# ✅ 구체적인 에러 메시지
def divide(a, b):
    if b == 0:
        return "❌ 오류: 0으로 나눌 수 없습니다. 다른 숫자를 입력해주세요."
    if not isinstance(b, (int, float)):
        return f"❌ 오류: '{b}'는 숫자가 아닙니다."

이유: 사용자가 무엇이 잘못되었는지, 어떻게 해결해야 하는지 명확히 알 수 있습니다.

9. 함수명은 동사로 시작

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 명사형 함수명
def calculation(a, b):
    return a + b

# ✅ 동사형 함수명 (의도가 명확)
def calculate_sum(a, b):
    return a + b

def get_user_input():
    return input("숫자 입력: ")

def display_result(result):
    print(f"결과: {result}")

이유: 함수는 행동을 나타내므로 동사로 시작하면 코드의 의도가 명확합니다.

10. 기본값 매개변수로 유연성 확보

1
2
3
4
5
6
7
8
9
# ✅ 기본값을 활용한 유연한 함수
def format_result(result, precision=2, currency=""):
    """결과를 포맷팅하여 출력"""
    return f"{result:.{precision}f}{currency}"

# 사용 예시
print(format_result(1234.5))           # "1234.50원"
print(format_result(1234.5, 0))        # "1235원"
print(format_result(1234.5, 2, "$"))   # "1234.50$"

이유: 기본값을 제공하면 간단한 사용 케이스에서 편리하면서도, 필요시 세밀한 제어가 가능합니다.

11. 반복 코드는 함수로 추출 (DRY 원칙)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ❌ 코드 중복
num1 = input("첫 번째 숫자: ")
while not num1.replace(".", "").replace("-", "").isdigit():
    print("숫자를 입력하세요!")
    num1 = input("첫 번째 숫자: ")

num2 = input("두 번째 숫자: ")
while not num2.replace(".", "").replace("-", "").isdigit():
    print("숫자를 입력하세요!")
    num2 = input("두 번째 숫자: ")

# ✅ 함수로 추출 (Don't Repeat Yourself)
def get_valid_number(prompt):
    """유효한 숫자를 입력받을 때까지 반복"""
    while True:
        num = input(prompt)
        if num.replace(".", "").replace("-", "").isdigit():
            return float(num)
        print("⚠️ 숫자를 입력하세요!")

num1 = get_valid_number("첫 번째 숫자: ")
num2 = get_valid_number("두 번째 숫자: ")

이유: 같은 코드를 반복하지 않으면 유지보수가 쉽고, 버그 수정 시 한 곳만 고치면 됩니다.

12. 프로그램 종료 조건은 명확하게

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ❌ 모호한 종료 조건
while True:
    choice = input("선택: ")
    if choice == "quit" or choice == "q" or choice == "exit":
        break

# ✅ 명확한 종료 조건
EXIT_COMMANDS = {"quit", "q", "exit", "종료"}

def is_exit_command(user_input):
    """종료 명령어인지 확인"""
    return user_input.lower() in EXIT_COMMANDS

while True:
    choice = input("선택 (종료: q): ").strip()
    if is_exit_command(choice):
        print("👋 계산기를 종료합니다.")
        break

이유: 종료 조건을 함수로 분리하면 다양한 종료 명령어를 쉽게 추가할 수 있습니다.

13. 코드 블록은 함수로 분리하여 가독성 향상

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ❌ 긴 메인 로직
def main():
    print("=== 계산기 ===")
    num1 = float(input("첫 번째 숫자: "))
    num2 = float(input("두 번째 숫자: "))
    op = input("연산자 (+, -, *, /): ")
    if op == "+":
        result = num1 + num2
    elif op == "-":
        result = num1 - num2
    # ... (계속 이어짐)
    print(f"결과: {result}")
    history.append(f"{num1} {op} {num2} = {result}")

# ✅ 논리적 단위로 함수 분리
def main():
    """메인 실행 흐름"""
    show_welcome_message()
    num1, num2 = get_two_numbers()
    operator = get_operator()
    result = calculate(num1, num2, operator)
    display_result(num1, operator, num2, result)
    save_to_history(num1, operator, num2, result)

이유: 각 함수가 하나의 작업만 수행하면 코드를 읽기 쉽고, 테스트와 디버깅이 간편합니다.

14. 프로젝트 시작 전 설계 단계 거치기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""
계산기 프로젝트 설계

1. 필요한 기능 (Requirements)
   - 사칙연산 (+, -, *, /)
   - 계산 기록 저장
   - 연속 계산 모드

2. 함수 목록 (Functions)
   - get_number(): 숫자 입력
   - add/subtract/multiply/divide(): 연산
   - show_history(): 기록 출력
   - main(): 메인 루프

3. 데이터 구조 (Data Structures)
   - history: list (계산 기록)
   - last_result: float (마지막 결과)

4. 개발 순서 (Development Steps)
   - v1: 기본 계산기
   - v2: 메뉴 시스템
   - v3: 기록 기능
   - v4: 연속 계산
"""

이유: 코드를 작성하기 전에 설계하면 필요한 기능을 파악하고, 체계적으로 개발할 수 있습니다.

15. 버전 관리로 점진적 개발

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Version 1: 기본 계산기
def calculate_v1():
    """가장 단순한 계산기 - 한 번만 계산"""
    num1 = float(input("첫 번째 숫자: "))
    num2 = float(input("두 번째 숫자: "))
    print(f"결과: {num1 + num2}")

# Version 2: 메뉴 추가
def calculate_v2():
    """연산자 선택 기능 추가"""
    # ... 메뉴 시스템 구현

# Version 3: 기록 기능 추가
def calculate_v3():
    """계산 기록 저장 기능 추가"""
    # ... 기록 기능 구현

# Version 4: 연속 계산 모드
def calculate_v4():
    """이전 결과를 활용한 연속 계산"""
    # ... 최종 버전

이유: 처음부터 완벽한 프로그램을 만들려고 하지 말고, 작동하는 단순한 버전부터 시작하여 점진적으로 기능을 추가하면 실패 위험이 줄어듭니다.


❓ FAQ - 자주 묻는 질문

Q1: 계산기 함수를 어떻게 구조화하는 것이 좋나요?

A: 계산기 함수는 단일 책임 원칙(SRP)에 따라 구조화하는 것이 좋습니다:

✅ 좋은 구조 - 각 함수가 하나의 역할만

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_number_input(prompt):
    """숫자 입력을 받아서 검증"""
    while True:
        try:
            return float(input(prompt))
        except ValueError:
            print("⚠️ 숫자를 입력해주세요!")

def add(a, b):
    """덧셈 연산만"""
    return a + b

def display_result(operation, num1, num2, result):
    """결과 출력만"""
    print(f"{num1} {operation} {num2} = {result}")

구조화 원칙:

  • 입력 처리: 별도 함수로 분리하여 검증 로직 중앙화
  • 연산 함수: 순수 함수로 구현 (부작용 없이 계산만)
  • 출력 함수: 결과 표시 로직 분리
  • 메뉴 시스템: 사용자 인터페이스 담당

Q2: 전역 변수를 사용해야 하나요, 아니면 반환값을 사용해야 하나요?

A: 가능하면 반환값을 사용하는 것이 좋지만, 계산 기록 같은 상태는 전역 변수가 적절할 수 있습니다:

✅ 연산 함수 - 반환값 사용 (권장)

1
2
3
def multiply(a, b):
    """순수 함수 - 입력만 의존하고 반환값으로 결과 전달"""
    return a * b

✅ 상태 관리 - 전역 변수 (제한적 사용)

1
2
3
4
5
6
history = []  # 계산 기록

def add_to_history(calculation):
    """전역 상태 변경 시 명시적인 함수 사용"""
    global history
    history.append(calculation)

❌ 나쁜 예 - 불필요한 전역 변수

1
2
3
4
5
result = 0  # 전역 변수로 계산 결과 저장 (피할 것)

def bad_add(a, b):
    global result  # 전역 변수 변경 - 추적 어려움
    result = a + b

선택 기준:

  • 연산 로직: 반환값 사용 (테스트 용이, 예측 가능)
  • 애플리케이션 상태: 전역 변수 허용 (history, last_result)
  • 임시 계산: 지역 변수 사용

Q3: 계산기에서 에러 처리는 어디서 해야 하나요?

A: 계층별 에러 처리 전략을 사용하세요:

1단계: 입력 검증 (가장 바깥 계층)

1
2
3
4
5
6
7
8
def get_operation():
    """잘못된 입력을 받지 않도록 입력 단계에서 검증"""
    valid_ops = ['+', '-', '*', '/']
    while True:
        op = input("연산자를 선택하세요 (+, -, *, /): ")
        if op in valid_ops:
            return op
        print("⚠️ 올바른 연산자를 선택해주세요!")

2단계: 연산 함수 (비즈니스 로직)

1
2
3
4
5
def divide(a, b):
    """연산 함수에서 비즈니스 규칙 검증"""
    if b == 0:
        return None  # 또는 특별한 값 반환
    return a / b

3단계: 메인 로직 (결과 처리)

1
2
3
4
5
6
7
def calculate():
    """결과 처리 단계에서 최종 검증"""
    result = divide(num1, num2)
    if result is None:
        print("❌ 0으로 나눌 수 없습니다!")
    else:
        print(f"결과: {result}")

계층별 역할:

  • 입력 계층: 잘못된 타입, 포맷 검증
  • 비즈니스 로직: 도메인 규칙 검증 (0으로 나누기 등)
  • 출력 계층: 사용자에게 적절한 메시지 표시

Q4: 계산 기록 기능을 효율적으로 구현하려면?

A: 리스트와 딕셔너리를 조합하여 구조화된 기록을 저장하세요:

✅ 구조화된 기록 저장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
history = []

def add_calculation(num1, operator, num2, result):
    """계산 정보를 딕셔너리로 저장"""
    calculation = {
        'num1': num1,
        'operator': operator,
        'num2': num2,
        'result': result,
        'timestamp': datetime.now().strftime('%H:%M:%S')
    }
    history.append(calculation)

def show_history():
    """기록을 보기 좋게 출력"""
    print("\n📜 계산 기록")
    print("-" * 40)
    for i, calc in enumerate(history, 1):
        print(f"{i}. [{calc['timestamp']}] "
              f"{calc['num1']} {calc['operator']} {calc['num2']} = {calc['result']}")

# 고급: 기록 제한 (최근 N개만)
MAX_HISTORY = 10

def add_calculation_limited(num1, operator, num2, result):
    """최근 10개만 유지"""
    calculation = {...}
    history.append(calculation)
    if len(history) > MAX_HISTORY:
        history.pop(0)  # 가장 오래된 기록 삭제

구현 팁:

  • 딕셔너리로 계산 정보 구조화
  • 시간 정보 포함 고려
  • 메모리 관리 (기록 개수 제한)

Q5: 계산기를 테스트하려면 어떻게 해야 하나요?

A: 함수를 순수 함수로 작성하면 테스트가 쉬워집니다:

✅ 테스트하기 쉬운 순수 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        return None
    return a / b

# 간단한 테스트 함수
def test_calculator():
    """계산기 함수 테스트"""
    print("🧪 테스트 시작...")

    # 덧셈 테스트
    assert add(2, 3) == 5, "덧셈 테스트 실패"
    assert add(-1, 1) == 0, "음수 덧셈 테스트 실패"

    # 나눗셈 테스트
    assert divide(10, 2) == 5, "나눗셈 테스트 실패"
    assert divide(5, 0) is None, "0으로 나누기 테스트 실패"

    print("✅ 모든 테스트 통과!")

# 테스트 실행
if __name__ == "__main__":
    test_calculator()

테스트 전략:

  • 정상 케이스: 일반적인 입력
  • 경계 케이스: 0, 음수, 큰 수
  • 에러 케이스: 0으로 나누기, 잘못된 입력
  • 자동화: 간단한 assert문으로 검증

🎯 학습 목표 4: 프로젝트 완성하고 Phase 3 마무리

배운 내용 체크리스트

  • 함수 정의: def 키워드로 재사용 가능한 코드 블록 생성
  • 매개변수와 반환값: 입력과 출력으로 함수 간 데이터 전달
  • 기본 매개변수: 선택적 인자로 함수 유연성 향상
  • 키워드 인자: 명확한 인자 전달로 가독성 향상
  • 가변 인자: *args, **kwargs로 개수 제한 없는 인자 처리
  • 람다 함수: 간단한 일회성 함수를 한 줄로 정의
  • 스코프: 변수 유효 범위 이해 (LEGB 규칙)
  • 전역 변수: global 키워드로 전역 변수 수정

프로젝트에서 사용한 개념

  1. 함수 정의: 모든 연산과 기능을 함수로 구현
  2. 매개변수: 숫자와 연산자를 함수에 전달
  3. 반환값: 계산 결과를 반환
  4. 전역 변수: 계산 기록과 마지막 결과 저장
  5. 에러 처리: 0으로 나누기 등 예외 상황 처리
  6. 함수 모듈화: 기능별로 함수를 분리하여 유지보수성 향상
  7. 입력 검증: 사용자 입력의 유효성 검사

Phase 3 핵심 개념 정리

Day 주제 핵심 개념 프로젝트 적용
Day 21 if-elif-else 조건 분기 처리 연산자 선택, 메뉴 처리
Day 22 while 심화 무한 루프, break 메인 루프, 종료 조건
Day 23 break/continue 반복 제어 메뉴 재시작, 오류 건너뛰기
Day 24 함수 정의 def로 함수 만들기 모든 연산 함수화
Day 25 매개변수/반환값 입력과 출력 숫자 전달, 결과 반환
Day 26 기본값 매개변수 선택적 인자 연산 기본값 설정
Day 27 *args/**kwargs 가변 인자 여러 숫자 평균 계산
Day 28 람다 함수 익명 함수 연산자 매핑
Day 29 스코프 변수 범위 전역 변수 관리
Day 30 종합 프로젝트 모든 개념 통합 완성된 계산기

🚀 확장 아이디어

더 발전시킬 수 있는 기능

  1. 과학 계산기: 삼각함수, 로그, 지수함수
  2. 단위 변환: 온도, 길이, 무게 등
  3. 파일 저장: 계산 기록을 파일로 저장/불러오기
  4. 그래프 계산기: matplotlib으로 그래프 그리기
  5. 수식 파싱: 문자열 수식을 직접 입력받아 계산

예제: 과학 계산 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import math

def sin_deg(angle):
    """각도(degree)의 사인 값"""
    return math.sin(math.radians(angle))

def cos_deg(angle):
    """각도(degree)의 코사인 값"""
    return math.cos(math.radians(angle))

def log(a, base=10):
    """로그 계산 (기본 밑: 10)"""
    if a <= 0:
        return "오류: 양수만 가능합니다"
    return math.log(a, base)

def factorial(n):
    """팩토리얼 계산"""
    if n < 0:
        return "오류: 음수는 불가능합니다"
    return math.factorial(int(n))

📝 과제

기본 과제

계산기 프로그램에 다음 기능을 추가해보세요:

  1. 절댓값 계산 기능
  2. 평균 계산 기능 (여러 숫자 입력)
  3. 최댓값/최솟값 찾기

도전 과제

  1. 계산 기록을 파일(txt)로 저장하고 불러오기
  2. 괄호를 포함한 수식 계산 (예: (2 + 3) * 4)
  3. GUI 계산기 만들기 (tkinter 사용)

🎉 Phase 3 완료!

축하합니다!

Phase 3에서 배운 내용:

  • ✅ Day 21-23: 조건문과 반복문 심화
  • ✅ Day 24-27: 함수의 모든 것
  • ✅ Day 28: 람다 함수
  • ✅ Day 29: 스코프와 변수
  • ✅ Day 30: 종합 프로젝트

다음 단계

Phase 4: 객체지향 프로그래밍에서는 클래스와 객체를 배우고, 더욱 체계적인 프로그램을 작성하는 방법을 익힙니다!


🔗 관련 자료


📚 이전 학습

Day 29: 변수 스코프

어제는 변수의 스코프(범위)를 이해했습니다!


📚 다음 학습

Phase 4 - Day 31: 객체지향 프로그래밍 시작!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 30/100 Phase 3 완료! 🎉 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.