[C언어 50강] 48강. 미니 프로젝트 1: 메뉴형 콘솔 프로그램(계산기/유틸)
C언어 50강의 48강입니다. 이번 강의부터는 문법 단원을 넘어, 지금까지 배운 요소를 하나의 실행 가능한 프로그램으로 엮는 연습을 합니다. 주제는 메뉴형 콘솔 프로그램입니다. 계산기나 간단한 유틸리티처럼, 사용자가 메뉴를 선택하고 기능을 실행한 뒤 다시 메뉴로 돌아오는 구조를 설계해 봅니다.
핵심 개념
- 기능 구현보다 먼저 **프로그램 흐름(입력 → 분기 → 실행 → 복귀)**을 설계한다.
- 메뉴형 프로그램의 안정성은 계산 로직이 아니라 입력 검증과 상태 관리에서 결정된다.
- 한 파일에 다 때려 넣지 말고, 기능별 함수 분리 + 명확한 반환 규약을 잡아야 유지보수가 가능하다.
개념 먼저 이해하기
메뉴형 콘솔 프로그램은 초보자에게는 단순해 보이지만, 실제로는 C 프로그래밍에서 매우 중요한 설계 연습입니다. 이유는 명확합니다. 이 구조 안에 C의 거의 모든 핵심이 들어 있기 때문입니다. 표준 입력 처리, 조건문과 반복문, 함수 분리, 예외 상황 처리, 에러 메시지 정책, 심지어는 향후 파일 저장이나 모듈화 확장까지 자연스럽게 연결됩니다.
많은 학습자가 “연산 기능만 잘 만들면 된다”라고 생각하는데, 실무 관점에서는 반대입니다. 덧셈·뺄셈 코드는 누구나 금방 작성합니다. 문제는 사용자가 숫자 대신 문자를 넣었을 때, 범위를 벗어난 메뉴 번호를 입력했을 때, 0으로 나누려 할 때, 혹은 입력 버퍼에 쓰레기 데이터가 남아 다음 입력을 망가뜨릴 때 프로그램이 어떻게 반응하느냐입니다. 즉, 메뉴 프로그램의 본질은 기능 시연이 아니라 입력과 상태를 안전하게 다루는 습관입니다.
또 하나 중요한 지점은 “흐름 제어의 중심”입니다. 메뉴 프로그램은 보통 무한 반복(while (1)) 위에서 동작하고, 종료는 특정 입력에서만 허용됩니다. 이때 각 기능 함수가 자기 역할을 넘어서 exit()를 남용하거나, 전역 변수를 여기저기 수정하면 흐름이 빠르게 엉망이 됩니다. 따라서 중심 루프는 메인에서 관리하고, 개별 기능 함수는 “입력값을 받아 결과를 계산하고 출력하거나 반환하는 일”에 집중시키는 것이 좋습니다.
설계 단계에서 추천하는 방식은 다음과 같습니다. 첫째, 메뉴 항목을 먼저 표로 적고(예: 1.더하기, 2.빼기, 3.곱하기, 4.나누기, 5.절댓값, 0.종료), 둘째, 각 항목에 필요한 입력과 실패 조건을 명시합니다. 셋째, 공통 입력 루틴을 둬서 정수/실수 파싱을 일관되게 처리합니다. 넷째, 실패 시 재시도 정책(한 번 더 입력받기, 메뉴로 복귀, 즉시 종료)을 미리 정합니다. 이렇게 하면 코드가 길어져도 구조가 무너지지 않습니다.
마지막으로, 이 강의의 핵심은 “완벽한 계산기”가 아니라 확장 가능한 뼈대를 만드는 것입니다. 오늘 만든 패턴은 49강의 파일 저장형 관리 프로그램으로 거의 그대로 재사용됩니다. 즉, 지금 잘 설계해 두면 다음 프로젝트 난이도가 급격히 낮아집니다.
기본 사용
예제 1) 메뉴 루프의 최소 뼈대
#include <stdio.h>
void print_menu(void) {
printf("\n===== DevLab Mini Utility =====\n");
printf("1) 덧셈\n");
printf("2) 뺄셈\n");
printf("0) 종료\n");
printf("선택: ");
}
int main(void) {
int menu = -1;
while (1) {
print_menu();
if (scanf("%d", &menu) != 1) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {}
printf("[오류] 숫자로 메뉴를 입력하세요.\n");
continue;
}
if (menu == 0) {
printf("프로그램을 종료합니다.\n");
break;
} else if (menu == 1) {
printf("덧셈 기능(준비 중)\n");
} else if (menu == 2) {
printf("뺄셈 기능(준비 중)\n");
} else {
printf("[오류] 존재하지 않는 메뉴입니다.\n");
}
}
return 0;
}
설명:
- 메뉴 프로그램의 코어는
while루프입니다. 한 번 실행하고 끝나는 게 아니라, 종료 조건을 만날 때까지 반복됩니다. scanf반환값 검사를 통해 입력 실패를 잡습니다. 실패 시 버퍼를 비우지 않으면 같은 실패가 무한 반복됩니다.- 종료는
menu == 0처럼 명시적인 조건 하나로 관리하는 편이 안전합니다.
예제 2) 기능 분리 + 계산 로직 연결
#include <stdio.h>
int read_two_ints(int *a, int *b) {
printf("정수 2개 입력 (예: 3 5): ");
if (scanf("%d %d", a, b) != 2) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {}
return 0;
}
return 1;
}
void run_add(void) {
int x, y;
if (!read_two_ints(&x, &y)) {
printf("[오류] 입력 형식이 올바르지 않습니다.\n");
return;
}
printf("결과: %d\n", x + y);
}
void run_div(void) {
int x, y;
if (!read_two_ints(&x, &y)) {
printf("[오류] 입력 형식이 올바르지 않습니다.\n");
return;
}
if (y == 0) {
printf("[오류] 0으로 나눌 수 없습니다.\n");
return;
}
printf("결과: %.2f\n", (double)x / (double)y);
}
설명:
read_two_ints를 공통 루틴으로 분리하면 코드 중복이 크게 줄어듭니다.- 기능 함수(
run_add,run_div)는 UI와 계산을 한곳에서 처리하되, 실패 시 조용히 복귀하도록 설계했습니다. - 나눗셈은 분모 0 체크가 필수입니다. 계산식 자체보다 입력 검증이 먼저입니다.
예제 3) 디버깅 가능한 통합 버전
#include <stdio.h>
enum Menu {
MENU_EXIT = 0,
MENU_ADD = 1,
MENU_SUB = 2,
MENU_MUL = 3,
MENU_DIV = 4
};
int read_menu(int *menu) {
printf("\n0:종료 1:+ 2:- 3:* 4:/\n선택: ");
if (scanf("%d", menu) != 1) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {}
return 0;
}
return 1;
}
int read_pair(double *a, double *b) {
printf("실수 2개 입력: ");
if (scanf("%lf %lf", a, b) != 2) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {}
return 0;
}
return 1;
}
void execute_menu(int menu) {
double a, b;
if (menu == MENU_EXIT) return;
if (!read_pair(&a, &b)) {
printf("[오류] 숫자 두 개를 올바르게 입력하세요.\n");
return;
}
switch (menu) {
case MENU_ADD: printf("= %.3f\n", a + b); break;
case MENU_SUB: printf("= %.3f\n", a - b); break;
case MENU_MUL: printf("= %.3f\n", a * b); break;
case MENU_DIV:
if (b == 0.0) {
printf("[오류] 0으로 나눌 수 없습니다.\n");
} else {
printf("= %.3f\n", a / b);
}
break;
default:
printf("[오류] 지원하지 않는 메뉴 번호입니다: %d\n", menu);
}
}
int main(void) {
int menu;
while (1) {
if (!read_menu(&menu)) {
printf("[오류] 메뉴는 정수로 입력하세요.\n");
continue;
}
if (menu == MENU_EXIT) {
printf("정상 종료\n");
break;
}
execute_menu(menu);
}
return 0;
}
설명:
enum으로 메뉴 상수를 선언하면 매직 넘버를 줄이고 가독성을 높일 수 있습니다.execute_menu는 메뉴 처리의 중심 함수입니다. 이후 기능 추가(예: 제곱, 평균)도 이 함수에서 관리하면 됩니다.- 디버깅 시에는 입력 함수 실패 경로,
default분기, 0 나눗셈 분기를 먼저 점검하세요.
자주 하는 실수
실수 1) scanf 성공/실패를 확인하지 않는다
- 원인: 입력이 늘 정상이라고 가정함.
- 해결:
scanf(...) != 기대개수체크를 습관화하고, 실패 시 버퍼 정리 루틴을 반드시 넣는다.
실수 2) 입력 버퍼를 비우지 않아 무한 오류 루프에 빠진다
- 원인: 문자 입력 후 줄바꿈/잔여 문자가 다음
scanf를 망가뜨림. - 해결: 실패 경로에서
getchar()반복으로\n또는EOF까지 소비한다.
실수 3) 나눗셈에서 분모 0 검사를 누락한다
- 원인: 정상 케이스 중심으로만 테스트함.
- 해결: 계산 직전에 예외 조건을 먼저 검사하고, 실패 메시지 규약을 통일한다.
실수 4) 기능 함수가 프로그램 전체 종료를 직접 호출한다
- 원인: 빠르게 끝내려는 습관으로
exit()남용. - 해결: 종료 결정은
main루프에서만 하도록 역할을 분리한다.
실무 패턴
- 입력 함수 공통화:
read_int,read_double,read_line같은 유틸을 만들면 유지보수성이 크게 오른다. - 오류 메시지 일관성:
[오류]접두어, 사용자 행동 지침(다시 입력하세요)을 통일하면 UX가 좋아진다. - 분기 정책 명시: 실패 시 즉시 재입력인지, 메뉴 복귀인지 팀 규칙으로 정한다.
- 확장 포인트 설계: 메뉴를
switch에 하드코딩하더라도, 기능 함수 이름과 책임을 명확히 유지하면 추후 파일 분리가 쉽다. - 테스트 시나리오 작성: 정상 입력만이 아니라 비정상 입력(문자, 빈 입력, 큰 수, 0 나눗셈)을 체크리스트로 관리한다.
오늘의 결론
한 줄 요약: 메뉴형 콘솔 프로그램의 완성도는 계산 공식이 아니라 입력 검증·흐름 제어·함수 분리에서 결정된다.
연습문제
- 현재 메뉴 계산기에
5) 나머지(%)기능을 추가하세요. 단, 실수 입력이 들어오면 에러를 출력하고 메뉴로 복귀하세요. read_menu,read_pair를 별도 파일(input.c,input.h)로 분리해 보세요. 헤더 include guard를 적용하세요.- 잘못된 입력이 3회 연속 발생하면 “입력 오류가 반복되어 메인 메뉴로 복귀합니다.”를 출력하도록 정책을 추가하세요.
이전 강의 정답
47강 연습문제(EOF 처리, 버퍼링, argc/argv) 핵심 정답 요약:
while (fgets(buf, sizeof buf, stdin) != NULL)형태가 EOF 안전 패턴이다.- 표준입력에서 EOF는 macOS/Linux 터미널 기준
Ctrl + D로 전달된다. argc는 인자 개수(프로그램 이름 포함),argv[0]는 실행 경로/이름 문자열이다.- 인자 기반 모드 전환 시, 인자가 부족하면 사용법을 먼저 출력하고
return 1로 종료하는 습관이 좋다.
실습 환경/재현 정보
- 컴파일러: clang 17.x 또는 gcc 13.x 이상
- 컴파일 옵션:
-Wall -Wextra -Wpedantic -O0 -g - 실행 환경: macOS(arm64), Linux(x86_64) 터미널
- 재현 체크:
- 정상 입력(정수/실수)으로 각 메뉴 동작 확인
- 문자 입력 시 에러 메시지 및 복귀 확인
- 0으로 나누기 시 예외 처리 확인
- 존재하지 않는 메뉴 번호 입력 시
default분기 확인