작은 프로젝트로 완성하는 C 기초

문법을 아는 것과 프로그램을 만드는 것은 다르다
지금까지 우리는 변수, 조건문, 반복문, 함수, 배열, 문자열, 포인터, 구조체, 동적 메모리, 파일 입출력까지 차근차근 배웠다. 그런데 막상 "이걸로 뭘 만들지?"라는 질문 앞에 서면 손이 잠깐 멈추곤 한다. 아주 자연스러운 일이다. 문법을 이해하는 것과 프로그램을 스스로 조립하는 것은 비슷해 보이지만, 사실은 한 단계 차이가 있다.
그 차이를 메워 주는 것이 바로 작은 프로젝트다. 작은 프로젝트는 함수, 반복문, 조건문, 구조체, 파일 처리처럼 따로 배운 요소를 한 프로그램 안에서 연결하게 해 준다. 이렇게 연결하는 경험이 쌓일수록 코드는 외운 문장이 아니라 손에 익은 도구가 된다.1
초보자를 위한 C 프로젝트 자료를 보면 계산기, 학생 기록 시스템, 간단한 텍스트 편집기나 메모장 같은 주제가 자주 등장한다. 이유는 분명하다. 이 주제들은 어렵지 않으면서도 실제 프로그램의 핵심 구조를 연습하기에 아주 좋기 때문이다.24
이번 장에서는 세 가지 미니 프로젝트를 통해 지금까지 배운 내용을 하나의 흐름으로 묶어 보겠다. 첫째는 계산기, 둘째는 성적 관리 프로그램, 셋째는 파일 기반 메모장이다. 이름은 작아 보여도, 이 세 가지를 직접 만들어 보면 "아, 프로그램은 이런 식으로 짜는구나" 하는 감각이 생긴다.
프로젝트를 시작하기 전에 먼저 그려야 할 것
초보자가 프로젝트를 만들 때 가장 흔히 하는 실수는 곧바로 코드를 치기 시작하는 것이다. 그러면 십중팔구 중간에 길을 잃는다. 코드보다 먼저 정리해야 할 것은 프로그램의 뼈대다.
작은 프로젝트를 시작할 때는 먼저 다섯 가지를 적어 보자.
프로그램이 해결할 문제는 무엇인가?
사용자에게 어떤 입력을 받을 것인가?
결과를 어떤 형태로 보여 줄 것인가?
어떤 데이터를 저장해야 하는가?
기능을 어떤 함수들로 나눌 것인가?
예를 들어 계산기는 "숫자 두 개와 연산자를 입력받아 결과를 출력한다"라는 한 줄로 정의할 수 있다. 성적 관리 프로그램은 "학생 정보를 입력받아 평균과 학점을 계산하고, 필요하면 파일에 저장한다"라고 정리할 수 있다. 파일 기반 메모장은 "메모를 입력하고 파일에 저장하며, 나중에 다시 읽어 온다"라고 정리하면 된다.
프로젝트는 거창한 아이디어보다 명확한 기능 목록이 중요하다. 기능이 분명하면 함수가 보이고, 함수가 보이면 코드가 훨씬 쉬워진다.
프로젝트 1: 메뉴형 계산기 만들기
가장 먼저 만들 프로젝트는 계산기다. 계산기는 단순해 보이지만, C의 기본기를 놀랄 만큼 많이 담고 있다. 입력, 출력, 조건문, 반복문, 함수 분리, 예외 처리까지 한 번에 연습할 수 있다. 초보자용 프로젝트 목록에서 계산기가 거의 빠지지 않는 이유가 바로 여기에 있다.2
계산기의 기능 설계
아주 기본적인 계산기라면 다음 기능이면 충분하다.
덧셈
뺄셈
곱셈
나눗셈
종료
여기에 메뉴를 붙이면 사용자는 프로그램을 여러 번 다시 실행하지 않고 한 번 실행한 뒤 계속 계산할 수 있다. 이때 while 반복문과 switch 문이 아주 잘 어울린다.
계산기에서 배우는 핵심
계산기를 만들며 익힐 수 있는 것은 다음과 같다.
함수로 기능 나누기
switch문으로 메뉴 처리하기0으로 나누는 예외 검사하기
double자료형으로 실수 계산하기반복문으로 프로그램 계속 실행하기
계산기 예제 코드
#include <stdio.h>
double add(double a, double b) {
return a + b;
}
double sub(double a, double b) {
return a - b;
}
double mul(double a, double b) {
return a * b;
}
double divide(double a, double b) {
return a / b;
}
int main(void) {
int menu;
double a, b, result;
while (1) {
printf("n=== 계산기 ===n");
printf("1. 덧셈n");
printf("2. 뺄셈n");
printf("3. 곱셈n");
printf("4. 나눗셈n");
printf("5. 종료n");
printf("선택: ");
scanf("%d", &menu);
if (menu == 5) {
printf("프로그램을 종료합니다.n");
break;
}
printf("두 수 입력: ");
scanf("%lf %lf", &a, &b);
switch (menu) {
case 1:
result = add(a, b);
printf("결과: %.2fn", result);
break;
case 2:
result = sub(a, b);
printf("결과: %.2fn", result);
break;
case 3:
result = mul(a, b);
printf("결과: %.2fn", result);
break;
case 4:
if (b == 0) {
printf("0으로 나눌 수 없습니다.n");
} else {
result = divide(a, b);
printf("결과: %.2fn", result);
}
break;
default:
printf("잘못된 메뉴입니다.n");
}
}
return 0;
}이 코드는 길지 않지만 꽤 많은 내용을 담고 있다. main()은 전체 흐름을 관리하고, 실제 계산은 별도 함수가 맡는다. 이렇게 나누면 코드가 읽기 쉬워지고, 나중에 기능을 추가하기도 편해진다. 예를 들어 제곱, 나머지, 절댓값 같은 연산을 추가하고 싶다면 함수를 하나 더 만들고 메뉴에 연결하면 된다.
계산기를 더 좋게 만드는 방법
처음에는 위 코드 정도면 충분하다. 하지만 여기서 한 걸음 더 나가면 훨씬 좋은 연습이 된다.
예를 들면 이런 개선이 가능하다.
정수 계산기와 실수 계산기를 분리해 보기
이전 계산 결과를 저장해 다음 계산에 재사용하기
잘못된 입력이 들어왔을 때 다시 입력받기
계산 기록을 파일에 저장하기
계산기는 작은 프로젝트 중에서도 특히 "기능 추가의 재미"를 느끼기 좋다. 처음엔 사칙연산만 하다가, 어느새 기록 저장 기능까지 붙이면 제법 그럴듯한 프로그램이 된다.
프로젝트 2: 성적 관리 프로그램 만들기
이제 한 단계 올라가 보자. 계산기가 수를 다루는 프로그램이었다면, 성적 관리 프로그램은 여러 개의 데이터를 묶어서 다루는 프로그램이다. 여기서부터 구조체의 진가가 본격적으로 드러난다.
학생 성적 관리 프로그램은 초보자 프로젝트에서 매우 자주 추천되는 주제다. 학생 정보와 점수, 평균, 학점을 하나의 레코드처럼 묶어서 저장하고, 검색·출력·파일 저장 같은 기능을 차례대로 붙여 나갈 수 있기 때문이다. 실제 C 기반 학생 성적 관리 예제들 역시 학생 목록, 점수 기록, 평균 계산, 파일 읽기/쓰기를 주요 기능으로 삼는다.3
어떤 데이터를 저장할까?
학생 한 명에 대해 이런 정보가 필요하다고 해 보자.
학번
이름
국어 점수
영어 점수
수학 점수
평균
학점
이 정보는 서로 따로 노는 데이터가 아니라 하나의 학생을 설명하는 묶음이다. 따라서 구조체로 표현하는 것이 자연스럽다.
구조체 설계 예제
#include <stdio.h>
#define MAX_STUDENTS 100
typedef struct {
int id;
char name[50];
int kor;
int eng;
int math;
double avg;
char grade;
} Student;이제 Student students[MAX_STUDENTS];처럼 선언하면 학생 정보를 여러 명 저장할 수 있다. 배열이 "여러 칸"이라면, 구조체는 "한 칸 안의 묶음"이라고 생각하면 쉽다.
평균과 학점 계산 함수 만들기
성적 관리 프로그램에서는 계산 로직을 함수로 빼 두는 것이 좋다.
double calc_average(Student s) {
return (s.kor + s.eng + s.math) / 3.0;
}
char calc_grade(double avg) {
if (avg >= 90) return 'A';
else if (avg >= 80) return 'B';
else if (avg >= 70) return 'C';
else if (avg >= 60) return 'D';
else return 'F';
}이런 함수는 보기에도 좋고, 나중에 규칙이 바뀌었을 때 수정하기도 쉽다. 예를 들어 학점 기준이 달라지면 calc_grade()만 고치면 된다.
학생 추가 기능의 흐름
학생 정보를 입력받을 때는 다음 순서로 생각하면 된다.
배열의 현재 학생 수를 확인한다.
새 학생의 학번과 이름, 점수를 입력받는다.
평균을 계산한다.
학점을 계산한다.
학생 수를 하나 늘린다.
이 흐름은 아주 단순하지만, 실제 프로그램 구조를 이해하는 데 큰 도움이 된다. 입력과 계산과 저장이 서로 어떻게 연결되는지를 눈으로 보게 되기 때문이다.
출력과 검색 기능 붙이기
학생이 한 명뿐이라면 프로그램은 금방 끝난다. 하지만 여러 명이 생기면 반복문이 필요하다. 전체 목록을 출력할 때는 for 문을 사용하고, 특정 학번을 찾을 때는 배열을 처음부터 끝까지 훑는 선형 탐색을 사용하면 된다.
int find_student_by_id(Student students[], int count, int id) {
int i;
for (i = 0; i < count; i++) {
if (students[i].id == id) {
return i;
}
}
return -1;
}반환값이 -1이면 찾지 못했다는 뜻이다. 이런 규칙을 정해 두면 프로그램 전체가 깔끔해진다.
파일 저장 기능 추가하기
여기서부터 프로그램이 훨씬 진짜 같아진다. 프로그램을 종료해도 데이터가 남으려면 파일에 저장해야 한다. fprintf()로 저장하고, fscanf()로 다시 읽어 오면 된다.
void save_students(Student students[], int count) {
FILE *fp = fopen("students.txt", "w");
int i;
if (fp == NULL) {
printf("파일을 열 수 없습니다.n");
return;
}
for (i = 0; i < count; i++) {
fprintf(fp, "%d %s %d %d %d %.2f %cn",
students[i].id,
students[i].name,
students[i].kor,
students[i].eng,
students[i].math,
students[i].avg,
students[i].grade);
}
fclose(fp);
}이 코드에서 꼭 기억할 점은 두 가지다. 첫째, fopen()의 결과가 NULL인지 검사해야 한다. 둘째, 파일을 다 쓴 뒤에는 반드시 fclose()를 호출해야 한다. 이것은 단순한 예절이 아니라, 데이터를 안전하게 다루기 위한 기본 습관이다.
성적 관리 프로그램에서 실력이 자라는 지점
이 프로젝트가 좋은 이유는 기능을 조금씩 자연스럽게 늘릴 수 있기 때문이다. 처음에는 학생 추가와 전체 출력만 만들고, 다음에는 검색을 붙이고, 그다음에는 저장과 불러오기를 붙이면 된다. 한 번에 모든 걸 만들 필요가 없다.
이 프로젝트를 완성하면 다음 감각이 생긴다.
구조체가 왜 필요한지 몸으로 이해하게 된다.
배열이 실제 데이터를 보관하는 창고처럼 느껴진다.
함수 분리가 왜 중요한지 분명해진다.
파일 저장이 프로그램의 지속성을 만든다는 사실을 배우게 된다.
프로젝트 3: 파일 기반 메모장 만들기
세 번째 프로젝트는 파일 기반 메모장이다. 이것은 아주 작은 텍스트 도구지만, 문자열 처리와 파일 입출력을 함께 연습하기에 정말 좋다. 초보자용 미니 프로젝트 자료에서도 간단한 텍스트 에디터나 메모장, 할 일 목록 프로그램이 자주 추천되는데, 그 이유는 파일 처리와 문자열 입력을 실제 용도에 맞게 써 볼 수 있기 때문이다.4
여기서 말하는 메모장은 그래픽 프로그램이 아니다. 콘솔에서 동작하는 아주 소박한 메모장이다. 하지만 오히려 그래서 C 기초를 연습하기 좋다.
메모장의 최소 기능
처음에는 기능을 욕심내지 말고 이 세 가지만 만들자.
메모 추가
메모 전체 보기
프로그램 종료
이 정도만 만들어도 이미 파일 열기, 문자열 입력, 파일 쓰기, 파일 읽기, 반복문 사용을 모두 경험하게 된다.
메모 추가 함수 예제
문자열 입력에서는 이전 scanf()가 남긴 개행 문자를 어떻게 처리하느냐가 중요하다. 무조건 getchar()를 한 번 호출하는 방식은 입력 상황에 따라 메모의 첫 글자를 먹어 버릴 수 있다. 그래서 남은 문자를 확실히 비우는 쪽이 더 안전하다.
#include <stdio.h>
#include <string.h>
void clear_input_buffer(void) {
int ch;
while ((ch = getchar()) != 'n' && ch != EOF) {
}
}
void add_memo(void) {
FILE *fp;
char memo[200];
fp = fopen("memo.txt", "a");
if (fp == NULL) {
printf("파일을 열 수 없습니다.n");
return;
}
clear_input_buffer();
printf("메모를 입력하세요: ");
if (fgets(memo, sizeof(memo), stdin) == NULL) {
printf("입력을 읽지 못했습니다.n");
fclose(fp);
return;
}
fputs(memo, fp);
fclose(fp);
printf("저장되었습니다.n");
}여기서 "a" 모드는 append, 즉 파일 끝에 이어 쓰기 모드다. 기존 메모를 지우지 않고 뒤에 계속 덧붙인다. 메모장에는 딱 어울리는 방식이다.
fgets()는 문자열 입력에서 꽤 자주 쓰이는 함수다. scanf("%s", ...)는 공백을 만나면 입력이 끊기지만, fgets()는 한 줄 전체를 읽을 수 있다. 메모장처럼 문장을 다루는 프로그램에서는 fgets()가 훨씬 낫다.
메모 보기 함수 예제
void show_memos(void) {
FILE *fp;
char line[200];
fp = fopen("memo.txt", "r");
if (fp == NULL) {
printf("저장된 메모가 없습니다.n");
return;
}
printf("n=== 저장된 메모 ===n");
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
}이 함수는 파일을 한 줄씩 읽어 화면에 출력한다. while (fgets(...) != NULL) 형태는 텍스트 파일을 읽을 때 매우 자주 등장하는 기본 패턴이다. 지금 이 한 줄이 나중에 로그 파일 읽기, 설정 파일 읽기, CSV 파일 읽기 같은 더 큰 작업의 출발점이 된다.
메모장에서 확장할 수 있는 기능
메모장이 재미있는 이유는 조금만 손보면 금방 더 똑똑해지기 때문이다.
특정 단어 검색 기능 추가하기
메모 번호를 붙여 출력하기
메모 삭제 기능 만들기
새 파일로 저장하기와 이어 쓰기 구분하기
제목과 내용을 구분해서 저장하기
예를 들어 검색 기능을 붙이려면 strstr() 함수를 쓸 수 있다. strstr()는 문자열 안에 특정 문자열이 들어 있는지 찾아 주는 함수다. 이런 기능을 붙이는 순간 문자열 처리가 갑자기 살아 움직이는 느낌을 주기 시작한다.
세 프로젝트를 통해 배운 문법이 어떻게 연결되는가
지금까지 만든 세 프로젝트는 각각 달라 보이지만, 사실은 같은 기초를 다른 방식으로 쓰고 있다. 이 연결이 보이기 시작하면 C 언어가 훨씬 입체적으로 느껴진다.
| 배운 내용 | 계산기 | 성적 관리 | 파일 기반 메모장 |
|---|---|---|---|
| 변수와 자료형 | 숫자와 결과 저장 | 점수, 평균, 학점 저장 | 문자열 버퍼와 메뉴 저장 |
| 조건문 | 연산 선택, 0 나누기 검사 | 학점 계산, 검색 결과 처리 | 파일 존재 여부 검사 |
| 반복문 | 계속 계산하기 | 학생 목록 순회 | 파일의 모든 줄 읽기 |
| 함수 | 사칙연산 분리 | 계산, 검색, 저장 기능 분리 | 추가, 보기, 검색 기능 분리 |
| 배열 | 필요 시 기록 저장 | 학생 배열 | 문자 배열, 줄 버퍼 |
| 문자열 | 거의 사용 안 함 | 이름 처리 | 메모 입력과 검색 |
| 구조체 | 보통 생략 가능 | 학생 정보를 하나로 묶음 | 제목형 메모로 확장 가능 |
| 포인터 | 함수 인자에서 간접 사용 | 배열 전달, 파일 포인터 | FILE * 사용 |
| 파일 입출력 | 계산 기록 저장으로 확장 가능 | 학생 데이터 저장 | 메모 저장과 읽기 |
이 표를 보면 중요한 사실 하나가 보인다. 문법은 따로따로 존재하지 않는다. 반복문은 반복문만의 세계에 있고, 구조체는 구조체만의 세계에 있는 것이 아니다. 실제 프로그램에서는 기능 하나를 만들기 위해 여러 문법이 동시에 움직인다.
예를 들어 성적 관리 프로그램에서 학생을 추가하는 기능 하나만 봐도 구조체, 배열, 입력, 함수, 조건문이 모두 연결된다. 메모장에서는 문자열, 파일 포인터, 반복문이 함께 움직인다. 이 연결을 이해하는 순간부터 코드는 퍼즐 조각이 아니라 도구 상자가 된다.
작은 프로젝트를 완성도 있게 만드는 습관
프로젝트의 크기보다 더 중요한 것은 만드는 습관이다. 같은 계산기를 만들어도 어떤 사람은 금방 엉키고, 어떤 사람은 차근차근 완성한다. 차이는 재능보다 습관에서 나온다.
좋은 습관은 다음과 같다.
한 번에 전부 만들지 말고 기능을 하나씩 완성한다.
입력값이 잘못되었을 때를 항상 생각한다.
파일을 열 때는 실패 가능성을 반드시 검사한다.
길어지는 코드는 함수로 나눈다.
기능 하나를 만들 때마다 바로 실행해 본다.
예를 들어 성적 관리 프로그램을 만든다고 해서 처음부터 저장, 검색, 수정, 삭제, 정렬을 한꺼번에 구현할 필요는 없다. 먼저 학생 추가, 다음 전체 출력, 그 다음 검색, 마지막에 파일 저장처럼 순서를 정하면 훨씬 수월하다.
디버깅 습관도 중요하다. 프로그램이 이상하게 동작하면 막연히 코드를 다시 쓰지 말고, 어디서부터 값이 이상해졌는지 printf()로 확인해 보자. 메뉴 입력은 제대로 들어왔는지, 평균 계산 전에 점수가 올바르게 저장되었는지, 파일 포인터가 NULL은 아닌지 확인하면 문제는 생각보다 빨리 잡힌다.
추천 확장 과제
이 장의 프로젝트를 다 만들었다면, 이제는 스스로 문제를 조금 키워 볼 차례다.
첫째, 계산기에는 계산 기록 저장 기능을 넣어 보자. 사용자가 연산할 때마다 history.txt 파일에 식과 결과를 저장하면 파일 입출력이 자연스럽게 복습된다.
둘째, 성적 관리 프로그램에는 정렬 기능을 넣어 보자. 평균이 높은 순으로 출력하거나, 학번 순으로 정렬해 보는 것이다. 이때는 배열 안의 구조체를 서로 교환하는 연습까지 할 수 있다.
셋째, 메모장에는 검색과 삭제 기능을 붙여 보자. 삭제는 조금 더 어렵지만, 임시 파일을 만들어 남길 줄만 다시 저장하는 방식으로 구현할 수 있다. 이 과정을 통해 파일 처리의 현실적인 감각이 생긴다.
이 확장 과제를 해 보면 "프로그램은 완성된 뒤 끝나는 것이 아니라, 기능을 계속 다듬어 가는 과정"이라는 사실도 자연스럽게 배우게 된다.
마무리: 작은 프로그램 하나가 실력을 크게 바꾼다
처음 C 언어를 배울 때는 문법 하나하나가 따로 떠다니는 것처럼 보인다. if 문은 그냥 분기문 같고, 배열은 그냥 여러 값을 담는 문법처럼 보인다. 하지만 작은 프로젝트를 만들어 보면 생각이 달라진다. 조건문은 메뉴가 되고, 배열은 데이터 저장소가 되며, 구조체는 현실의 정보를 담는 그릇이 된다. 파일 입출력은 프로그램이 기억을 갖게 만든다.
작은 프로젝트는 작아서 좋은 것이 아니다. 작기 때문에 끝까지 만들 수 있고, 끝까지 만들어 보았기 때문에 실력이 된다.
계산기를 만들었다면 숫자를 다루는 힘이 생긴다. 성적 관리 프로그램을 만들었다면 데이터를 구조화하는 힘이 생긴다. 파일 기반 메모장을 만들었다면 프로그램이 입력을 받아 기록을 남기는 감각이 생긴다. 이 세 가지를 직접 손으로 만들어 본 사람은 이제 단순히 C 문법을 아는 사람이 아니라, C로 프로그램을 만들 수 있는 사람에 훨씬 가까워진다.
다음 장에서는 여기서 한 걸음 더 나아가, C 기초를 바탕으로 어떤 방향으로 학습을 확장할 수 있는지 살펴보게 된다. 자료구조, 시스템 프로그래밍, 임베디드처럼 C가 진짜 힘을 발휘하는 세계가 그다음에 기다리고 있다.
참고
1C Projects and Practical Applications
2Top 25 C Projects with Source Codes for 2025 - GeeksforGeeks
420+ C Programming Mini Projects with Source Code for Beginners
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.