비트 연산으로 저수준 감각을 익혔다면, 이제는 실제 프로젝트처럼 여러 파일을 나누고 자동화된 빌드 과정을 준비해야 합니다. 손으로 clang main.c utils.c -o app을 매번 입력하는 대신, Makefile을 활용하면 변경된 파일만 다시 컴파일하고 빌드 명령을 짧게 유지할 수 있습니다.
이번 글에서 새로 나오는 용어
- Makefile:
make도구가 읽는 빌드 규칙 파일 - 타깃 (Target): 만들어야 하는 결과물 이름 (예: 실행 파일, 객체 파일)
- 의존성 (Dependency): 타깃을 만들 때 필요한 입력 파일 목록
- 규칙 (Rule): 타깃을 만들기 위한 명령어 세트
핵심 개념
학습 메모
- 소요 시간: 70분 내외
- 준비물: 헤더/소스 분리 경험, 터미널에서
clang또는gcc사용 경험- 학습 목표: 간단한 프로젝트 트리 구성, Makefile 작성, 증분 빌드 이해
프로젝트가 커질수록 파일 역할을 나누고 반복 작업을 줄이는 것이 중요합니다. src, include, build 폴더 등으로 구조를 나누면 협업과 유지보수가 쉬워지고, Makefile은 "어떤 파일이 바뀌었을 때 어떻게 다시 빌드할지"를 자동으로 판단합니다.
처음에는 아래 흐름으로 이해하면 쉽습니다.
- 손으로
clang main.c stats.c -o app를 직접 입력한다. - 같은 명령을 반복하기 귀찮아진다.
- Makefile에 빌드 순서를 적어 두고
make한 번으로 실행한다.
이번 글에서는 아래 흐름을 따라갑니다.
- 기본 폴더 구조 설계 (
src,include,build) - 개별
.c파일을 객체 파일로 컴파일하고, 마지막에 링크하기 - Makefile: 의 타깃/의존성/명령 문법 이해
phony타깃과clean명령으로 정리하기
코드로 따라하기
샘플 프로젝트 구조
project/
├── include/
│ └── stats.h
├── src/
│ ├── main.c
│ └── stats.c
└── Makefile
stats.h는 함수 선언과 구조체를 담고, stats.c는 실제 구현을 갖습니다. main.c는 헤더만 포함해 함수를 사용합니다.
include/stats.h
#ifndef STATS_H
#define STATS_H
#include <stddef.h>
typedef struct {
double min;
double max;
double average;
} Stats;
Stats calculate_stats(const int *values, size_t length);
#endif
src/stats.c
#include "stats.h"
Stats calculate_stats(const int *values, size_t length) {
Stats result = {0};
size_t i;
int sum = 0;
if (length <= 0) {
return result;
}
result.min = values[0];
result.max = values[0];
for (i = 0; i < length; i++) {
if (values[i] < result.min) {
result.min = values[i];
}
if (values[i] > result.max) {
result.max = values[i];
}
sum += values[i];
}
result.average = (double)sum / length;
return result;
}
src/main.c
#include <stdio.h>
#include "stats.h"
int main(void) {
int data[5] = {10, 40, 30, 25, 60};
Stats stats = calculate_stats(data, 5);
printf("min=%.0f max=%.0f avg=%.2f\n",
stats.min, stats.max, stats.average);
return 0;
}
Makefile 작성
Makefile
CC = clang
CFLAGS = -Wall -Wextra -std=c11 -Iinclude
BUILD_DIR = build
SRCS = src/main.c src/stats.c
OBJS = $(SRCS:src/%.c=$(BUILD_DIR)/%.o)
TARGET = $(BUILD_DIR)/stats-app
.PHONY: all clean run
all: $(TARGET)
$(TARGET): $(OBJS)
@mkdir -p $(BUILD_DIR)
$(CC) $(OBJS) -o $(TARGET)
$(BUILD_DIR)/%.o: src/%.c include/stats.h | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR):
@mkdir -p $(BUILD_DIR)
run: $(TARGET)
./$(TARGET)
clean:
rm -rf $(BUILD_DIR)
여기서 SRCS:src/%.c=$(BUILD_DIR)/%.o 구문은 패턴 대체를 이용해 src/foo.c를 build/foo.o로 바꿉니다. | $(BUILD_DIR)는 해당 규칙을 실행하기 전에 build 폴더가 있어야 함을 나타내는 순서 전용 의존성입니다.
처음 읽을 때는 아래 한 줄로 기억해도 충분합니다.
target: dependencies는 "이 결과물을 만들려면 이 파일들이 필요하다"는 뜻입니다.
증분 빌드 확인
make를 실행하면src/main.c,src/stats.c가 각각 컴파일되어build/main.o,build/stats.o가 생성되고, 마지막에 링크하여build/stats-app이 만들어집니다.src/main.c만 수정한 뒤 다시make를 실행하면,make는main.o만 다시 컴파일하고stats.o는 그대로 둡니다. 즉, 바뀐 파일만 다시 빌드하므로 작업 속도가 빨라집니다.make run으로 실행 파일을 바로 실행할 수 있습니다.make clean은build폴더 전체를 삭제해 정리합니다.
왜 중요한가
- 파일을 역할별로 나누면 재사용성과 테스트가 쉬워집니다.
- Makefile은 의존성을 추적해 필요한 부분만 다시 빌드하므로 시간을 절약합니다.
CC,CFLAGS같은 변수를 사용하면 환경에 맞게 쉽게 조정할 수 있습니다.- 프로젝트 구조가 잡혀 있어야 캡스톤 같은 큰 예제를 안정적으로 관리할 수 있습니다.
실제 프로젝트에서는 여기에 -MMD -MP 옵션을 더해 헤더 의존성을 자동으로 추적하기도 합니다. 지금 단계에서는 "Makefile이 빌드 순서를 기억한다"는 감각부터 확실히 잡으면 충분합니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요