[C 시리즈 19편] 빌드 자동화와 프로젝트 구성 익히기

English version

비트 연산으로 저수준 감각을 익혔다면, 이제는 실제 프로젝트처럼 여러 파일을 나누고 자동화된 빌드 과정을 준비해야 합니다. 손으로 clang main.c utils.c -o app을 매번 입력하는 대신, Makefile을 활용하면 변경된 파일만 다시 컴파일하고 빌드 명령을 짧게 유지할 수 있습니다.

이번 글에서 새로 나오는 용어

  1. Makefile: make 도구가 읽는 빌드 규칙 파일
  2. 타깃 (Target): 만들어야 하는 결과물 이름 (예: 실행 파일, 객체 파일)
  3. 의존성 (Dependency): 타깃을 만들 때 필요한 입력 파일 목록
  4. 규칙 (Rule): 타깃을 만들기 위한 명령어 세트

핵심 개념

학습 메모

  • 소요 시간: 70분 내외
  • 준비물: 헤더/소스 분리 경험, 터미널에서 clang 또는 gcc 사용 경험
  • 학습 목표: 간단한 프로젝트 트리 구성, Makefile 작성, 증분 빌드 이해

프로젝트가 커질수록 파일 역할을 나누고 반복 작업을 줄이는 것이 중요합니다. src, include, build 폴더 등으로 구조를 나누면 협업과 유지보수가 쉬워지고, Makefile은 "어떤 파일이 바뀌었을 때 어떻게 다시 빌드할지"를 자동으로 판단합니다.

처음에는 아래 흐름으로 이해하면 쉽습니다.

  1. 손으로 clang main.c stats.c -o app를 직접 입력한다.
  2. 같은 명령을 반복하기 귀찮아진다.
  3. 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.cbuild/foo.o로 바꿉니다. | $(BUILD_DIR)는 해당 규칙을 실행하기 전에 build 폴더가 있어야 함을 나타내는 순서 전용 의존성입니다.

처음 읽을 때는 아래 한 줄로 기억해도 충분합니다.

target: dependencies는 "이 결과물을 만들려면 이 파일들이 필요하다"는 뜻입니다.

증분 빌드 확인

  1. make를 실행하면 src/main.c, src/stats.c가 각각 컴파일되어 build/main.o, build/stats.o가 생성되고, 마지막에 링크하여 build/stats-app이 만들어집니다.
  2. src/main.c만 수정한 뒤 다시 make를 실행하면, makemain.o만 다시 컴파일하고 stats.o는 그대로 둡니다. 즉, 바뀐 파일만 다시 빌드하므로 작업 속도가 빨라집니다.
  3. make run으로 실행 파일을 바로 실행할 수 있습니다.
  4. make cleanbuild 폴더 전체를 삭제해 정리합니다.

왜 중요한가

  • 파일을 역할별로 나누면 재사용성과 테스트가 쉬워집니다.
  • Makefile은 의존성을 추적해 필요한 부분만 다시 빌드하므로 시간을 절약합니다.
  • CC, CFLAGS 같은 변수를 사용하면 환경에 맞게 쉽게 조정할 수 있습니다.
  • 프로젝트 구조가 잡혀 있어야 캡스톤 같은 큰 예제를 안정적으로 관리할 수 있습니다.

실제 프로젝트에서는 여기에 -MMD -MP 옵션을 더해 헤더 의존성을 자동으로 추적하기도 합니다. 지금 단계에서는 "Makefile이 빌드 순서를 기억한다"는 감각부터 확실히 잡으면 충분합니다.

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. starter를 fork한 뒤 hello.c 같은 실습 파일을 만든다
  2. 본문 코드를 붙여 넣고 clang 또는 gcc가 있으면 직접 컴파일한다
  3. 코드를 고친 뒤 다시 빌드하고 실행 결과를 비교한다

C는 브라우저 미리보기보다 터미널 빌드 흐름이 핵심입니다. Universal starter의 컴파일러 구성이 환경에 따라 다를 수 있으니, 먼저 clang이나 gcc 사용 가능 여부부터 확인하세요.

실습

  • 따라 하기: 위 폴더 구조를 그대로 만들고 make, make run을 실행해 결과를 확인합니다.
  • 확장하기: stats.c에 표준편차 계산 함수를 추가하고, Makefile에서 SRCS 목록을 늘린 뒤 재빌드합니다.
  • 디버깅: 헤더 의존성을 규칙에서 빼면 어떤 문제가 생기는지 실험해 보고, make가 왜 다시 빌드하지 않았는지 분석합니다.
  • 완료 기준: 최소한 두 개 이상의 .c 파일을 가진 프로젝트에서 Makefile로 빌드/실행/정리를 할 수 있으면 됩니다.

마무리

빌드 자동화는 단순히 편의를 넘어, 코드 변경과 결과 확인 사이의 시간을 줄여 줍니다. 다음 20편에서는 지금까지 배운 포인터, 구조체, 파일, 빌드 기술을 모두 합쳐 텍스트 기반 시스템 유틸리티를 직접 만들어 보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요