Tuist_How? - Scheme 분리편 feat.Xcconfig

김재형_LittleTale·2025년 6월 23일
0

Tuist

목록 보기
3/5

들어가기에 앞서

Tusit 편은 Why 편부터 보고 오셔야 도움이 됩니다.
이번편에서는 Scheme 분리와 Xcconfig 분리를 배워보록 하겠습니다.

기본 개념

Scheme

Xccode 개발 환경을 분리하기 위해서 실무에서 많이 사용하는 방법이죠
저번 편에서는 하나의 Project.swift를 통해 Target을 구성했습니다.
scheme은 빌드할 Target Collection, Configuration, Test Collection을
정의합니다.

Xcconfig

Xcode 빌드 설정을 외부화 관리하는 파일입니다.
버전관리나 infoPlist 값이나 조건부등 을 정의할수 있죠

시작

Scheme 구성

Scheme을 분리하는 법을 이제 검색을 해보시면
타겟을 여러개 만들어서 각각이 Dev, QA, Prod 등을 만드시는 분이 있구요
하나의 타겟에 여러 스킴을 등록하는 방법이 있습니다.
저는 후자를 선택하겠습니다.
만약 infoPlist 가 여러개인 방법으로 하겠다 라면 전자를
Xcconfig를 분리해서 하겠다면 후자를 선택하시면 됩니다.

import ProjectDescription
import Foundation

public enum SchemeMode: CaseIterable {
    case dev
    case stage
    case live
    
    public var schemeName: String {
        switch self {
        case .dev:
            return "Dev"
        case .live:
            return "Live"
        case .stage:
            return "Stage"
        }
    }
    
    public static func getSelf(schemeString: String) -> SchemeMode? {
        return SchemeMode.allCases.first { $0.schemeName == schemeString }
    }

    public var infoPlist: String {
        return self.schemeName + ".Plist"
    }
}

위와 같이 코드를 작성한후
해당 코드는 Plugin에서 작성하였습니다.
Plugin 관련해서는 Docs를 참고 바랍니다.

extension Settings {
    
    public static var appSettings: Self {
        return .settings(
            configurations: [
                .debug,
                .release,
                .dev,
                .stage
            ]
        )
    }   
 }
 
 extension ConfigurationName {
    
    public static let dev: Self = .configuration(SchemeMode.dev.schemeName)
    
    public static let stage: Self = .configuration(SchemeMode.stage.schemeName)
    
    public static let live: Self = .configuration(SchemeMode.live.schemeName)
}


extension Configuration {
    public static let debug: Self = .debug(name: "Debug", xcconfig: .xcconfigPath("Debug"))
    public static let release: Self = .debug(name: "Release", xcconfig: .xcconfigPath("Release"))
    public static let dev: Self = .debug(name: .dev, xcconfig: .xcconfigPath(SchemeMode.dev.schemeName))
    public static let stage: Self = .debug(name: .stage, xcconfig: .xcconfigPath(SchemeMode.stage.schemeName))
    public static let live: Self = .release(name: .live, xcconfig: .xcconfigPath(SchemeMode.live.schemeName))
}

위와같이 좀더 편하게 쓰기위해
Extension 하여 정의했습니다.

extension Scheme {
    
    public static func schemes(name: String, root: Bool = true) -> [Self] {
        var targets: [TargetReference] = []
        if root {
            targets.append("App")
            
            return [
                .scheme( // Dev
                    name: name + "_DEV",
                    shared: true,
                    hidden: false,
                    buildAction: .buildAction(targets: targets),
                    runAction: .runAction(configuration: .dev),
                    archiveAction: .archiveAction(configuration: .dev),
                    profileAction: .profileAction(configuration: .dev),
                    analyzeAction: .analyzeAction(configuration: .dev)
                ),
                .scheme(
                    name: name + "_Stage",
                    shared: true,
                    hidden: false,
                    buildAction: .buildAction(targets: targets),
                    runAction: .runAction(configuration: .stage),
                    archiveAction: .archiveAction(configuration: .stage),
                    profileAction: .profileAction(configuration: .stage),
                    analyzeAction: .analyzeAction(configuration: .stage)
                )
            ]
        }
        else {
            return []
        }
    }
}

이부분이 중요할 것 같아요
각 각이 우엇을 의미하는지 설명하겠습니다.

buildAction

어떤 타겟을 빌드할지 정의합니다.

// targets: 빌드할 대상 타겟 이름들 (App, Frameworks 등)
.buildAction(targets: ["App", "Core", "Feature"])

runAction

앱을 실행할 때 사용할 설정을 정의합니다.

.runAction(configuration: .dev) 

archiveAction

Archive (배포용 빌드) 를 만들 때 사용할 설정

.archiveAction(configuration: .dev)wift

profileAction

Instruments 같은 성능 분석 도구 실행 시 사용되는 설정

.profileAction(configuration: .dev)

analyzeAction

정적 분석 (Static Analysis) 실행할 때 사용할 설정

.analyzeAction(configuration: .dev)
 return Project(
                name: config.name,
                packages: config.packages,
                settings: .appSettings, <--- 이부분 주목
                targets: targets + config.customTargets,
                schemes: config.schemes,
                resourceSynthesizers: [
                    .custom(name: "Assets", parser: .assets, extensions: ["xcassets"]),
                    .custom(name: "Fonts", parser: .fonts, extensions: ["otf"]),
                ]
            )

Xcconfig

위에서 코드를 따라 오셨다면 아마 Xcconfig 관련 코드가 있었어요

extension Configuration {
    public static let debug: Self = .debug(name: "Debug", xcconfig: .xcconfigPath("Debug"))
    public static let release: Self = .debug(name: "Release", xcconfig: .xcconfigPath("Release"))
    public static let dev: Self = .debug(name: "Dev", xcconfig: .xcconfigPath("Dev"))
    public static let stage: Self = .debug(name: "Stage", xcconfig: .xcconfigPath("Stage"))
    public static let live: Self = .release(name: "Live", xcconfig: .xcconfigPath("Live"))
}

해당 코드인데 Configuration 을 정의했죠
이친구가 무엇있냐 Xcode의 Build Configuration 을 의미합니다.
다시말해 빌드할 때 어떤 설정으로, 어떤 조건으로 앱을 만들지를 정하는 것 이라고
쉽게 정의해보죠
저희가 위에서 Scheme을 구성하였는데
이의 목적은 개발용, 배포전 서버 테스트용, 배포용으로 분리하고자 한거죠

이때 개발용 SDK나, 개발용 번들아이디, 개발용 서버 URL 등
정의할 공간이 필요한거죠
이때 Xcconfig에 정의를해서 좀더 편리하게(?) 작업이 가능합니다.

Base Xcconfig

공통적인 정보를 정의해 볼겁니다.

// App Setting
ALWAYS_SEARCH_USER_PATHS = NO
// User paths를 검색하지 않음. 보통 NO로 설정함 (deprecated된 설정)

ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES
// image, color, symbol 등 asset 카탈로그 자동 접근자 생성

CLANG_ENABLE_MODULES = YES
// 모듈(Modules) 시스템 사용. framework/module 사용 시 필수

ENABLE_STRICT_OBJC_MSGSEND = YES
// Objective-C 메시지 전송에 대해 stricter checking 활성화

//ENABLE_USER_SCRIPT_SANDBOXING = YES
// 사용자 정의 스크립트를 샌드박싱된 환경에서 실행 (보안 목적)
CLANG_WARN_CONSTANT_CONVERSION              = YES
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS  = YES
CLANG_WARN_DIRECT_OBJC_ISA_USAGE            = YES_ERROR
COPY_PHASE_STRIP                            = NO
GCC_WARN_64_TO_32_BIT_CONVERSION            = YES
MTL_FAST_MATH                               = YES
SDKROOT                                     = iphoneos

//SDKROOT = iphoneos
// SDK 경로 설정 (보통은 자동으로 설정됨)

IPHONEOS_DEPLOYMENT_TARGET = 16
// 최소 지원 iOS 버전


// ARC With Swift
CLANG_ENABLE_OBJC_ARC                               = YES
// Automatic Reference Counting 사용

CLANG_ENABLE_OBJC_WEAK                              = YES
// weak 참조 허용 (ARC 기반의 약한 참조)

CLANG_CXX_LANGUAGE_STANDARD                         = gnu++20
// C++ 표준 설정 (GNU 확장 포함 C++20 사용)

GCC_C_LANGUAGE_STANDARD                             = gnu17
// C 언어 표준 설정 (GNU C17)

CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING              = YES // Bool값 변경 감지 경고


// Debugging
DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf
DEBUG_INFORMATION_FORMAT[config=Dev] = dwarf
DEBUG_INFORMATION_FORMAT[config=Stage] = dwarf
DEBUG_INFORMATION_FORMAT[config=Release] = dwarf-with-dsym
// Debug: 기본 디버깅 정보 (DWARF)
// Release: dSYM 포함


ENABLE_TESTABILITY[config=Debug] = YES
ENABLE_TESTABILITY[config=Dev] = YES
// 테스트 가능한 코드로 빌드 (internal → accessible)

MTL_ENABLE_DEBUG_INFO[config=Debug] = INCLUDE_SOURCE
MTL_ENABLE_DEBUG_INFO[config=Dev] = INCLUDE_SOURCE
MTL_ENABLE_DEBUG_INFO[config=Release] = NO
// Metal 디버깅 정보 포함 여부

//ONLY_ACTIVE_ARCH[config=Debug] = YES
//ONLY_ACTIVE_ARCH[config=Dev] = YES
// 활성화된 아키텍처만 빌드 (디버깅 시 빌드 속도 향상)
ONLY_ACTIVE_ARCH = YES

GCC_DYNAMIC_NO_PIC[config=Debug] = NO
GCC_DYNAMIC_NO_PIC[config=Dev] = NO
GCC_DYNAMIC_NO_PIC[config=Stage] = NO
// PIC (Position Independent Code) 비활성화 금지 (보통 NO)

GCC_OPTIMIZATION_LEVEL[config=Debug] = 0
GCC_OPTIMIZATION_LEVEL[config=Dev] = 0
GCC_OPTIMIZATION_LEVEL[config=Stage] = 0

// Swift 컴파일 조건부 매크로 (`#if DEBUG`) 활성화
SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone
SWIFT_OPTIMIZATION_LEVEL[config=DEV] = -Onone
SWIFT_OPTIMIZATION_LEVEL[config=STAGE] = -Onone
//SWIFT_OPTIMIZATION_LEVEL[config=LIVE] = -Onone

ENABLE_NS_ASSERTIONS[config=Release] = NO

SWIFT_COMPILATION_MODE[config=Release] = wholemodule
// Release 빌드는 whole module 최적화 방식 사용

// Firebase
OTHER_LDFLAGS = $(inherited) -ObjC

... 민감한 정보들 예를들어

// CommonSettings
APP_VERSION                     = 1.0.4
BUILD_NUMBER                    = 0.2.5
PRODUCT_NAME                    = 굴비잇기
DISPLAY_NAME                    = 굴비잇기
CODE_SIGN_STYLE                 = Automatic
DEVELOPMENT_TEAM                = 비이이이밀
MARKETING_VERSION               = $(APP_VERSION)
CURRENT_PROJECT_VERSION         = $(BUILD_NUMBER)
PRODUCT_BUNDLE_IDENTIFIER       = com.Goolbitg-iOS


// KAKAO URL
KAKAO_URL_SCHEME                = 비이이이밀
KAKAO_URL_DEV                   = 비이이이밀

위에서 정의한것처럼 해당 파일은 Base.xcconfig 로 정의하고
깃 이그노어 하게 구성했습니다.

Dev, Stage 등의 Xcconfig

Dev xcconfig 를 구성해볼께요
Base를 상속받아 재구성 가능합니다.

#include "Base.xcconfig"

OTHER_SWIFT_FLAGS[config=STAGE][sdk=*]   = $(inherited) -DDEV

PRODUCT_BUNDLE_IDENTIFIER               = com.Goolbitg-iOS.dev

// 전처리기 정의를 설정합니다. 여기서는 DEV 매크로를 1로 정의하고,
// 이전 설정을 상속합니다. 이는 DEV 빌드에서 사용됩니다.
GCC_PREPROCESSOR_DEFINITIONS            = DEV=1 $(inherited)
KAKAO_URL_SCHEME                        = $(KAKAO_URL_DEV)

DISPLAY_NAME                            = 굴비잇기_DEV

다른 파일에서도 이런식으로 구성해서 환경 값을 수정할 수가 있습니다.

그럼 infoPlist 값은요?

예를 들어 카카오 로그인 으로 예를 들어볼께용
카카오 로그인을 구성하기 위해선
URL Types 를 구성해야하며 kakao비밀키 를 작성해야
redirect 가 가능해져서 정상 동작을 하죠?
이를 대응하려면 두개의 방법으로 여러 인포피리스트 혹은 여러 Xcconfig인데
현재 후자 방법이기에 다음과 같이 구성을 했다 라고 생각하시면 됩니다.

위처럼 Xcconfig값을 사용하다록 할 수 있기때문에
좀더 유연하게 작업이 가능합니다.

사용 예시

    public var baseURL: String {
        switch self {
        case .rootLogin:
            return SecretKeys.baseURL + version
        default:
#if DEV
return SecretKeys.devBaseURL + version
#else
return SecretKeys.baseURL + version
#endif
        }
    }

마무리 하며

이번 시간에는 Scheme + Xcconfig 분리하는 방법에 대해서 작성했습니다.

다른 블로그글은 이제 프로젝트를 구성하는 방법은 꽤 많아서
저번편은 간단하게 작성을 했는데
해당편 같은 경우 자료가 많이 없어서 좀 자세하게 작성했습니다.
막히는 부분이 있다면 해당 글에 댓글을 달아주시면
업데이트 해놓겠습니다.

모두 고생하셨습니다.
감사합니다.

profile
IOS 개발자 새싹이, 작은 이야기로부터

0개의 댓글