• module은 무엇인가?
    • 라이브러리나 프레임워크의 인터페이스를 표현하는 코드 배포 단위

    • Swift 타겟은 여러 개의 Swift 파일을 가지고 있고 이 파일들이 모여서 모듈을 표현한다.

      • 보통은 단일 타겟이나 프레임워크 안에 있는 모든 Swift 파일은 같은 모듈이 된다.
    • Swift 파일의 인터페이스는 접근 제어자를 통해서 표현된다.

      • public으로 마킹되면 importer에 의해서 보이게 된다.
    • 모듈은 다른 모듈을 import할 수 있고, 프로젝트 전체적으로 비순환 모듈 그래프가 만들어지게 된다.

    • Swift 컴파일러는 외부 인터페이스들을 모아서 .swiftinterface 파일에 모아놓게 된다.

      • 여기는 딱 인터페이스만 들어간다.
    • Objective-C나 C계열 언어에서는 Swift와 다르게 모듈의 인터페이스는 수동으로 작성된다.

      • 헤더와 모듈맵으로 구성된다.

      • 모듈맵이 알려주는 것

        • 어떤 모듈인지(ex. framework)
        • 모듈 이름(ex. UIKit)
          • @import 구문에서 사용하기 위한 것
          • 소스코드 자체는 이를 정의하지 않는다.
        • 모듈이 포함하는 헤더
          • 그 중에서 UIKit.h는 다른 헤더들을 모두 포함하는 Umbrella 헤더
        • export: 해당 모듈이 import하는 모듈들은 UIKit을 import하면 다 사용할 수 있다.
        framework module UIKit {
        	umbrella header "UIKit.h"
        	export *
        }
        
    • 모듈 이름으로 import하거나 모듈에 포함된 헤더를 include하면 모듈을 사용하는 것이다.

      import SwiftUI // Swift 모듈
      import ResearchKit // Objective-C 모듈
      
      #import "ORKActiveStepTimer.h" // 프로젝트 자체의 헤더
      #import "ORKHelpers_Internal.h"
      
      @import UIKit; // UIKit 모듈 import
      #include <mach/mach.h> // SDK의 헤더 include. Clang이 module import로 바꿔줌
      #include <mach/mach_time.h>
      
    • 모듈은 인터페이스 파싱을 여러 소스파일에서 공유할 수 있게 해준다.

      • 프로젝트 소스를 컴파일 할 때 각 모듈을 별도로 컴파일러가 읽을 수 있는 형태로 컴파일하고, 해당 모듈이 참조될 때마다 이 public 인터페이스를 import한다.
      • Swift에서는 .swiftmodule, Clang에서는 .pcm(precompiled module) 파일로 표현됨
        • .pch(precompiled header) 도 같은 역할
    • 그러려면 컴파일러가 모듈을 찾고 컴파일 해야 한다.

  • module은 어떻게 사용되는가?
    • 컴파일러가 import 구문을 만나면 import가 어떤 모듈을 가리키는지 발견하고 해당 모듈의 컴파일 된 형태를 알야아 한다.

      • 컴파일러가 @import UIKit을 만나면, UIKit의 모듈맵을 SDK에서 먼저 찾는다.
      • UIKit 모듈이 무엇인지 알았으면, 해당 모듈의 .pcm 파일을 찾는다.
      • pcm이 없는 경우 빌드를 해야한다.
    • 이 모듈을 빌드하는 부분에서 Xcode 16에서 달라지는 부분이 있다.

      • 그동안은 컴파일러가 Xcode 모르게 암시적으로 모듈을 빌드하는 과정을 중간에 끼워넣었다. Swift와 Clang은 처음부터 이런식으로 빌드를 해왔다.
    • 이렇게 암시적으로 빌드하는 모듈이 포함된 Swift, C, Objective-C 코드가 포함된 경우 종종 오래 걸리는 작업들이 생기게 된다.

      • 빌드 시스템이 컴파일을 시작하면 여러 컴파일러 프로세스가 암시적으로 모듈을 찾아서 빌드가 안된경우는 빌드를 하거나 이미 존재하면 있는 걸 쓰게 된다.

        스크린샷 2024-06-14 오전 12.18.18.png

    • 이 때 한 컴파일러가 암시적으로 모듈을 찾아서 빌드하고 있으면 해당 모듈이 필요한 다른 컴파일러는 작업이 블록되게 된다.

      • 이 작업은 빌드 과정에서 수없이 일어날 수 있다.

        스크린샷 2024-06-14 오전 12.19.41.png

    • Xcode 16에서는 이를 명시적으로 하게 바뀌었다.

      • Xcode가 직접 컴파일러를 조율하여 모듈을 찾고 빌드한다.
      • 이제 모듈을 빌드하는 작업도 명시적인 시스템 작업이 된다.
    • 이를 위해서 Xcode는 컴파일 작업을 3단계로 나눈다.

      • Scan
        • 각 소스파일을 스캔해서 전체 프로젝트의 모듈 그래프를 만든다.
        • 이떄 여러 타겟에서 공유하는 모듈도 파악한다.
      • Build Module
        • 그래프를 기반으로 모듈 빌드 작업을 수행한다.
        • 이 모듈을들 의존하는 모듈을 빌드 할 때의 빌드 로그에 명시적으로 남음
      • Build Source
        • 의존하는 모듈들을 포함하도록 변경이 된 채로 원래 소스코드를 빌드하게 됨
    • 이 모든 작업은 타임라인에 명시적으로 남는다.

      • 모듈을 알게되면 아직 돌아갈 준비가 안된 작업이 대기상태로 레인을 채우지 않고 필요한 모듈이 준비되어야만 실제 작업을 시작하게 된다.

      • 빌드시스템이 이를 알게 되면 가능한 실행 레인을 효율적으로 쓸 수 있게 된다.

        스크린샷 2024-06-14 오전 12.27.38.png

    • 명시적으로 모듈을 빌드했을 때의 장점

      • 정확하고 결정적인 빌드 그래프를 통해서 컴파일러가 매번 같은 순서로 돌고 빌드 실패도 각 태스크를 따로 돌려서 재현할 수 있게 되면서 신뢰성이 높아진다.
        • 암시적인 상태가 남지 않고, 클린빌드 할때 모듈도 다시 빌드한다.
      • 빌드 시스템이 더 많은 정보를 알게 되었으므로 스케쥴링을 더 효율적으로 할 수 있다.
      • 빌드 시스템이 모듈을 디버거에 넘겨서 디버거 시작 시간을 줄일 수 있다.
        • 기존에는 빌드시스템과 디버거가 서로 다른 모듈 그래프를 가지고 있었으나 이제는 빌드시스템의 모듈 그래프를 디버거가 쓸 수 있다.
        • 이는 디버거가 Swift 타입을 알아야 할때(ex. p, po) 모듈을 재빌드 하지 않아도 되게 한다.
  • module 빌드 로그 비교
    • C와 Objective-C에서는 항상, Swift에서는 프리뷰
      • Explicitly built Module 옵션을 활성화해서 사용. 이러면 Clang과 Swift에서 모두 사용
    • 이러면 빌드 로그에 수많은 scan 태스크가 찍히게 된다.
      • 각 소스코드마다 하나씩 돌아가고, 빌드 시스템을 위한 모듈 import 그래프를 만들게 된다.
      • 이는 내장 작업이라서 새로운 프로세스를 만들지는 않는다. 덕분에 빌드시스템에서 소스코드 간에 스캔 정보를 캐시할 수 있다.
    • 이후에는 모듈 컴파일 태스크들이 보인다.
      • 이 작업은 타겟별로 공유가 되기 때문에 안보일 수도 있다.
      • 빌드 시스템은 각 태스크 별로 새로운 프로세스를 만들어서 할당한다.
      • 이 태스크는 명시된 모듈을 컴파일해서 컴파일된 모듈 파일로 만드는데, 원래는 암시적으로 하던 일을 따로 떼놓은 것이다.
      • 혹시 문제가 생겨도 이 모듈을 빌드하게 한 소스파일이 아니라 여기서 바로 알려준다.
      • 한 모듈이 여러 번 빌드될 수도 있는데 예제에서는 UIKit 모듈이 여러번 빌드 되었다.
        • 이는 서로 다른 타겟의 특정 빌드세팅에 의해서 모듈을 다르게 빌드해야 하기 때문이다.
        • 예제에서는 2개의 Swift 모듈, 4개의 Clang 모듈 변형이 필요했다.
      • 로그에서 보면 이 모듈들이 서로 다른 해시를 가지는 것을 볼 수 있는데, 이는 모듈을 빌드할 때 필요한 커맨드라인 인자들을 나타낸다.
        • 언어 표준, 피쳐 매크로, include path 등을 포함한다.
        • 이는 암시적으로 빌드하던 때에도 자주 만나던 문제지만 빌드시스템에 노출되어 있지 않아서 알아채기 어려웠다.
        • 컴파일러는 이 중에서 모듈을 빌드하는데 영향을 주지 않는 인자를 스캐닝 단계에서 없애는 방식으로 최적화를 한다.
  • 빌드 최적화
    • Product → Perform Action → Build with Timing Summary를 수행해서 빌드 성능에 대한 추가 정보를 얻을 수 있다.
    • 여러번 모듈을 빌드하는 것은 서로 다른 소스 파일이 호환되지 않는 빌드 설정을 가지기 때문이다.
      • 예를 들어, C파일와 Objective-C 소스 파일은 모듈을 매우 다르게 파싱한다.
      • 불필요한 모듈 변형은 빌드 과정에서 불필요한 작업을 수행하게 한다.
    • 모듈을 다르게 빌드하게 만드는 일반적인 요소들
    • Timing Summary를 킨 상태로 빌드를 끝내면 ‘modules report’ 를 검색해서 정보를 찾을 수 있다.
    • 예제에서는 특정 타겟에 ENABLE_FEATURE 매크로가 있어서 그랬다.