왜 한번 빌드 이후부터는 빌드가 더 빠를까? - 증분 빌드

2026. 1. 25. 17:10·SWIFT개발일지

iOS 개발자라면 누구나 ⌘ + B를 누르고 Xcode가 프로젝트를 빌드하기를 가만히 기다려본 경험이 있을 겁니다.

 

그렇다면 Xcode에서 ⌘ + ⇧ + K 눌러본 적 있죠?

 

맞아요. Clean Build Folder입니다.
이걸 누르면 빌드 결과물이 싹 지워지고, 다음 빌드는 사실상 처음부터 다시 빌드하게 돼요.

 

그럼 그 이후에 빌드하는 것은 뭐가 다를까요? 왜 처음 클린 빌드 이후에 하는것보다 훨씬 빠르게 진행이 될까요??

 

그게 오늘 이야기할 증분 빌드입니다 😚

증분 빌드란?

증분 빌드는 아주 간단하게 말하면 이거예요.

변경된 부분과, 그 변경으로 인해 영향을 받는 부분만 다시 빌드한다

조금 더 풀어 쓰면:

  • 🔁 바뀐 코드만
  • ➕ 그 코드에 의존하는 코드만
  • 다시 컴파일
  • ❌ 나머지는 이전 빌드 결과 재사용

그래서 대부분의 초기 빌드 이후의 빌드는 훨 빠르게 끝나죠

🤔 Xcode는 무엇이 바뀌었는지 어떻게 알까?

그렇다면 Xcode는 어떻게 “어디까지 다시 빌드해야 할지”를 판단하는 걸까요?

1단계: 변경 감지

Xcode 빌드 시스템은 각 파일의 상태를 기록해 둡니다.

대표적으로 이런 것들이에요.

  • 파일 내용의 hash
  • timestamp
  • 컴파일 옵션
  • 이전 빌드 산출물
    (.o, .swiftmodule 등)

그래서 빌드를 시작할 때, 이전 상태와 현재 상태를 비교해서

  • 변경되지 않은 파일은 재사용
  • 변경된 파일만 다시 컴파일

여기까지만 보면 “아, 그냥 파일 비교구나” 싶죠.

하지만!

🧠 Swift 컴파일러가 한 번 더 하는 일

Swift는 C / Objective-C 처럼 파일 하나 바뀌었다고 그 파일만 딱 컴파일하고 끝이 아닙니다

Swift 컴파일러는 먼저 다음 작업을 수행합니다.

  • 같은 타겟에 속한 모든 .swift 파일을 스캔
  • 타입, 프로토콜, 함수 시그니처를 분석
  • 파일 간의 의존성 관계를 계산

이걸 보통 의존성 그래프라고 불러요.

예를 들어 이런 구조를 생각해봅시다.

ProductResponse.swift
        ↑
APIClient.swift
        ↑
ProductListView.swift

이 상태에서 ProductResponse.swift를 수정하면:

  • 이를 사용하는 APIClient.swift → 영향 있음
  • ProductListView.swift → 연쇄적으로 영향

하지만 전혀 관련 없는 파일은 다시 빌드하지 않습니다.

👉 핵심은 “모든 파일”이 아니라 “영향받는 파일만” 입니다.

근데 누가 이 의존성을 판단할까요?

Swift 컴파일러의 Driver가 이 역할을 담당합니다.

Swift Driver는 컴파일 과정에서 .swiftdeps 파일을 생성하고 이를 기반으로 파일 간 의존성 그래프를 관리합니다.

즉,

  • Xcode는 빌드 전체를 orchestration 하고
  • Swift Driver가 실제로
    “증분 컴파일이 가능한지”를 판단합니다.
“영향을 받았는지 아닌지는 대체 뭘 기준으로 판단할까?”

 

정답은 인터페이스

인터페이스에 포함되는 것

  • stored property
  • 함수 시그니처
  • protocol 요구사항
  • public / open API

중요한 점은 이겁니다.

구현(Body)은 다르더라도 인터페이스가 같으면, 의존성 전파는 일어나지 않는다

Swift 5.x 이후 컴파일러는 함수의 서명(Signature) 과 구현(Body) 를 분리해서 관리합니다. 그래서 가능한 일입니다.

 

 

그래서 함수 body만 바꾸면 빠른 이유!!!

func foo() {
    print("A") → print("B")
}

이건 내부 구현만 바뀐 거죠.

  • 함수 이름 그대로
  • 파라미터 그대로
  • 리턴 타입 그대로

즉,

  • 인터페이스 변화 ❌
  • 의존성 그래프 변화 ❌

그래서 결과는?

  • 이 파일만 재컴파일
  • 나머지는 캐시 재사용
  • ✅ 증분 빌드 성공

문제는 인터페이스가 바뀔 때입니다.

struct User {
    let name: String
    let address: String // 추가
}

이 한 줄이 의미하는 건 꽤 큽니다.

  • 타입 크기 변경
  • 메모리 레이아웃 변경
  • 접근 방식 변경

그래서 User를 사용하는 모든 파일이 전부 영향 대상이 됩니다.



실제 XCode를 보면서 이해하기

이제 실제 예제를 통해 증분 빌드를 확인해봅시다.

struct Product: Identifiable, Decodable {
    let id: Int
    let name: String
    let price: Double
}

@MainActor
final class ProductListViewModel: ObservableObject {
    @Published private(set) var products: [Product] = []
    @Published private(set) var isLoading = false
    @Published private(set) var errorMessage: String?

    private let apiClient: APIClient

    init(apiClient: APIClient = APIClient()) {
        self.apiClient = apiClient
    }

    func load() async {
        isLoading = true
        defer { isLoading = false }

        do {
            products = try await apiClient.fetchProducts()
            errorMessage = nil
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}
struct APIClient {
    func fetchProducts() async throws -> [Product] {
        let json = """
        [
          { "id": 1, "name": "Keyboard", "price": 99.0 },
          { "id": 2, "name": "Mouse", "price": 49.0 }
        ]
        """
        let data = Data(json.utf8)
        return try JSONDecoder().decode([Product].self, from: data)
    }
}

🧱 초기 빌드

 

초기 빌드 후 Build에 관해 로그를 찍은거에요

Compute target dependency graph
fetchedFromCache = false

이는 캐시된 결과가 없고, 의존성 그래프를 처음 계산하고 있다는 의미입니다.

를 발견하실 수 있어요

➡️ 모든 Swift 파일이 컴파일된 것을 확인할 수 있죠

✏️ 함수 body만 수정해보기

struct APIClient {
    func fetchProducts() async throws -> [Product] {
        let json = """
        [
          { "id": 1, "name": "Keyboard", "price": 99.0 },
          { "id": 2, "name": "Mouse", "price": 49.0 }
        ]
        """
        print(json)
        let data = Data(json.utf8)
        return try JSONDecoder().decode([Product].self, from: data)
    }
}

여기에서 그냥 print문 하나만 추가해볼께요

APICLIENT
자체는 이미 fetchedFromCache

 

 

  • 인터페이스 변화 없음
  • Swift Driver는 증분 컴파일 가능하다고 판단
  • 해당 파일만 재컴파일(APIClient만!!)

 

인터페이스 변경해보기

struct Product {
    let id: Int
    let name: String
    let price: Int
    let owner: String
}

이번에는 인터페이스를 변경했습니다.

당연히 product의 fetchedFromCache = false겟죠? 나머지는어떨까요?

 

이를 사용하는 파일들도 → 연쇄적으로 재컴파일하는 것을 확인했습니다.

하지만 중요한 점은, 의존성 그래프 자체를 다시 계산한것이 아니라 재빌드 범위가 넓어진 것입니다

정리

 

  • 증분 빌드는 “안 바뀐 건 다시 빌드하지 않는” 최적화 전략
  • 기준은 파일 변경 여부가 아니라 인터페이스 변경 여부
  • Swift Driver가 의존성 그래프를 기반으로 판단
  • 그래서 body 수정은 빠르고, 인터페이스 수정은 느리다

이걸 알고 나면, 빌드 시간이 왜 갑자기 늘어났는지도 조금은 예측할 수 있게 되는거죠!

 

'SWIFT개발일지' 카테고리의 다른 글

TCA + Clean Architecture에서 의존성 관리, Needle 도입한 이유  (0) 2026.01.11
ProtoBuf - 이게 뭔데 사람들은 환호성을 지를까?  (1) 2025.11.29
아 지겹다 복붙! Xcode 커스텀 템플릿 만들기  (1) 2025.11.25
BLE 완전 기초: CoreBluetooth를 이해하기 위한 필수 개념  (0) 2025.11.16
이미지 URL 저장 시 마주하는 함정 문제들  (0) 2025.09.11
'SWIFT개발일지' 카테고리의 다른 글
  • TCA + Clean Architecture에서 의존성 관리, Needle 도입한 이유
  • ProtoBuf - 이게 뭔데 사람들은 환호성을 지를까?
  • 아 지겹다 복붙! Xcode 커스텀 템플릿 만들기
  • BLE 완전 기초: CoreBluetooth를 이해하기 위한 필수 개념
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (141)
      • SWIFT개발일지 (32)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (44)
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (3)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
왜 한번 빌드 이후부터는 빌드가 더 빠를까? - 증분 빌드
상단으로

티스토리툴바