약 3개월의 iOS CI/CD 파이프라인 구축 회고록: Tuist, Fastlane, GitHub Actions

꾸Jun·2025년 7월 1일
0

🍎 iOS

목록 보기
19/25
post-thumbnail

1. 도입 - 자동화의 필요성

프로젝트 초기, Tuist를 통해 모듈화된 아키텍처는 잘 갖춰져 있었으나, 배포 과정은 전적으로 수작업에 의존했다. 개발자는 기능 개발 후 develop 브랜치에 코드를 병합하고, 수동으로 버전과 빌드 넘버를 올린 뒤 main 브랜치로 다시 병합했다. 그 후 Xcode에서 직접 아카이브(Archive)를 하고, App Store Connect에 업로드하여 빌드를 선택하고, TestFlight과 App Store에 배포하는 전 과정을 사람이 직접 수행했다.

이러한 수동 배포 방식은 다음과 같은 명확한 한계를 지니고 있었다.

  • 인적 오류(Human Error)의 위험: 반복적인 과정에서 발생하는 사소한 실수가 배포 실패나 지연으로 이어졌다.
  • 개발 생산성 저하: 배포에 소요되는 시간과 노력으로 인해 개발자가 핵심 로직 개발에 집중하지 못했다.
  • 일관성 부재: 배포 환경이나 절차가 사람마다 미묘하게 달라 일관성 있는 배포를 보장하기 어려웠다.

이러한 문제를 근본적으로 해결하고, 개발자가 코드 작성과 리뷰에만 집중할 수 있는 환경을 만들기 위해 Fastlane과 GitHub Actions를 도입하여 CI/CD 파이프라인 구축을 시작했다.

목표는 명확했다.

  • develop 브랜치에 코드가 병합되면, 빌드 넘버를 자동으로 증가시켜 TestFlight으로 자동 배포한다.
  • main 브랜치에 코드가 병합되면, App Store로 자동 배포한다.


2. 문제 해결

CI/CD 파이프라인 구축은 단순히 툴을 연결하는 작업이 아니었다. 로컬 환경과 CI 환경의 차이, 인증, 보안 정책, 그리고 각 도구의 특성이 복합적으로 얽힌 문제들을 해결해나가는 과정의 연속이었다.

1) 초기 설정의 어려움 - 버전 충돌과 Target Membership

가장 먼저 마주한 문제는 환경의 비일관성이었다. 로컬과 GitHub Actions Runner의 Ruby, Bundler 버전이 달라 Gemfile.lock과의 충돌로 fastlane이 정상적으로 설치되지 않았다. 또한, Tuist로 생성된 프로젝트의 테스트 타겟이 메인 앱 타겟에 잘못 포함되어 있어 XCTest 관련 링크 에러가 발생했다.

  • 해결: Gemfile의 버전 제한을 유연하게 조정하고 bundle update를 통해 의존성을 최신화했다. 테스트 타겟의 Target Membership을 명확히 분리하여 빌드 오류를 해결했다. 이 과정은 CI/CD의 기본은 "어디서 실행되든 동일한 결과를 보장하는 것"임을 상기시켜 주었다.

2) 안정적인 Tuist 설치

초기 워크플로우는 curl 스크립트로 Tuist를 설치했다. 하지만 어느 날, 해당 스크립트의 URL(https://install.tuist.io)이 404 에러를 반환하며 파이프라인 전체가 멈췄다.

  • 해결: 외부 스크립트 의존성의 위험성을 깨닫고, GitHub Actions의 macOS Runner에 기본적으로 설치된 Homebrew(brew install tuist)를 사용하는 방식으로 전환했다. 이는 특정 URL의 유효성에 의존하지 않는, 훨씬 더 안정적이고 신뢰할 수 있는 기반을 마련하는 계기가 되었다.
// ios.yml

...
      - name: Install Tuist CLI
        run: brew install tuist
...

3) Git 인증과 fastlane match

fastlane match가 private 인증서 레포지토리에 접근하지 못하는 Error cloning certificates git repo 오류에 직면했다. GitHub Secrets에 MATCH_GIT_BASIC_AUTHORIZATION 토큰을 설정했음에도 문제가 지속됐다.

  • 해결: 단계적인 디버깅을 통해 두 가지 핵심 원인을 찾아냈다.
    1. 잘못된 사용자 이름: Base64 인코딩 시 사용한 사용자 이름이 이메일 주소로 되어 있었다. 이를 정확한 GitHub 사용자 이름(JunnKyuu)으로 수정했다.
    2. 토큰 권한 부족: Personal Access Token(PAT)에 repo 스코프가 누락되어 있었다. repo 권한을 포함한 새 토큰을 발급하고, 올바른 사용자 이름과 조합하여 Base64 값을 재생성한 뒤 Secret을 업데이트하여 문제를 해결했다.
// MatchFile

...
username("JunnKyuu")
...

4) pull_request와 Secrets 보안 정책

Git 인증 문제를 해결하자, 이번에는 app_store_connect_api_key 단계에서 invalid curve name 오류가 발생했다. 로그를 면밀히 분석한 결과, GitHub Actions가 Secrets 값을 전혀 불러오지 못하고 비어있는 상태로 전달하고 있음을 발견했다.

  • 해결: 원인은 워크플로우의 실행 트리거인 on: pull_request 의 보안 정책 때문이었다. GitHub는 보안상 pull_request 이벤트로 실행되는 워크플로우의 Secrets 접근을 제한한다. 이를 해결하기 위해 트리거를 on: push 로 변경했다. PR이 develop이나 main 브랜치에 머지될 때 발생하는 push 이벤트를 기반으로 워크플로우를 실행시키자, 모든 Secrets가 정상적으로 주입되어 문제가 해결됐다. 이 과정에서 push 이벤트와 맞지 않던 if 조건(github.event.pull_request.merged == true)을 제거하는 추가 작업도 병행했다.
// ios.yml
...
on:
  push:
    branches:
      - main
      - develop

jobs:
  # develop 브랜치 전용 TestFlight 배포 작업
  beta:
    name: TestFlight Deploy
    runs-on: macos-14
    # develop 브랜치에 push될 때만 실행되도록 조건을 설정
    if: github.ref == 'refs/heads/develop' && !startsWith(github.event.head_commit.message, '[fastlane]')
...

5) Tuist 프로젝트와 코드 서명

모든 인증 문제를 해결하고 마주한 마지막 관문은 build_ios_app 단계의 코드 서명 오류였다.

  1. xcargs에 DEVELOPMENT_TEAM을 추가하자, 서명이 필요 없는 라이브러리 모듈에서 Core does not support provisioning profiles 오류가 발생했다.
  2. xcargs에서 프로파일 관련 설정을 제거하자, 이번에는 메인 앱 타겟이 프로파일을 찾지 못하는 No profiles for 'com.tomyongji.ios' were found 오류가 발생했다.

이 딜레마를 통해 문제의 근본 원인이 Fastfile이 아닌, tuist generate로 생성된 Xcode 프로젝트 설계도 자체에 서명 정보가 전무하다는 것임을 깨달았다.

  • 해결: 가장 올바르고 근본적인 해결책으로, Tuist의 Project.swift 파일을 직접 수정했다.
    1. 앱 타겟에만 서명 정보 명시: 실제 앱을 구성하는 ToMyongJi-iOS 타겟에만 settings 파라미터를 추가하고, 그 안에 DEVELOPMENT_TEAM, PROVISIONING_PROFILE_SPECIFIER 등 모든 서명 정보를 정의했다.
    2. 프로젝트 파일 재생성: 수정된 Project.swift를 기반으로 tuist generate를 실행하여, 서명 정보가 완벽하게 내장된 .xcodeproj 파일을 새로 생성하고 Git에 커밋했다.
    3. Fastfile 단순화: 프로젝트 자체가 완전해졌으므로, Fastfile의 build_ios_app에서 불필요해진 xcargs와 export_options를 모두 제거하고 가장 단순한 형태로 되돌렸다.
// Project.swift
...
// 서명 설정을 위한 Settings 객체
let settings: Settings = .settings(
    base: [:],
    configurations: [
        .release(name: "Release", settings: [
            "DEVELOPMENT_TEAM": "FN67GXC5GH", // 개발팀 ID
            "CODE_SIGN_STYLE": "Manual",
            "PROVISIONING_PROFILE_SPECIFIER": "match AppStore com.tomyongji.ios",
            "CODE_SIGN_IDENTITY": "Apple Distribution"
        ]),
        .debug(name: "Debug", settings: [:])
    ],
    defaultSettings: .recommended
)

// ToMyongJi-iOS 타겟에만 settings 파라미터를 추가
let project = Project(
    name: "ToMyongJi",
    organizationName: "ToMyongJi",
    packages: [
        .remote(url: "https://github.com/Alamofire/Alamofire.git", requirement: .upToNextMajor(from: "5.0.0"))
    ],
    targets: [
        // MARK: - App Target
        .target(
            name: "ToMyongJi-iOS",
            destinations: [.iPhone],
            product: .app,
            bundleId: "com.tomyongji.ios",
            infoPlist: .file(path: "App/Resources/Info.plist"),
            sources: ["App/Sources/**"],
            resources: [
                "UI/Resources/Assets.xcassets",
                "UI/Resources/Fonts/**"
            ],
            dependencies: [
                .target(name: "Core"),
                .target(name: "Feature"),
                .target(name: "UI")
            ],
            settings: settings
        ),        
...


3. 결과 - 견고한 파이프라인의 완성

3개월간의 집요한 노력 끝에, 마침내 안정적이고 효율적인 CI/CD 파이프라인을 완성하는 데 성공했다. 지금은 혼자지만 나중에 합류할 iOS 개발 동료들은 이제 기능 개발 후 Pull Request를 머지하는 행동만으로 TestFlight 배포까지의 모든 과정을 시스템에 온전히 위임할 수 있게 되었다.

이 과정은 순탄치 않았다. 하나의 문제를 해결하면 연쇄적으로 다른 문제가 발생하는 상황 속에서 수없이 좌절을 마주했다. 하지만 포기하는 대신, 문제의 표면이 아닌 근본 원인을 찾기 위해 집요하게 파고들었다.

Apple, Tuist, Fastlane의 공식 문서를 깊이 파고들었고, 각 도구의 철학과 메커니즘을 이해했고, iOS 분야를 넘어 다른 직무의 선후배 동료들의 조언을 통해 새로운 관점을 얻었다. 또한, GPT와 Gemini와 같은 최신 AI 어시스턴트를 적극적으로 활용하여 디버깅 시간을 단축하고 해결의 실마리를 찾는 등, 동원할 수 있는 모든 자원을 전략적으로 활용했다.

이번 경험을 통해 얻은 가장 큰 수확은 단순히 자동화 시스템을 구축했다는 사실을 넘어선다.

  • 근본 원인 분석의 중요성: 임시방편이 아닌, 문제의 근본 원인을 찾아 해결하는 과정의 중요성을 체감했다.
  • 역할과 책임의 분리: 프로젝트 설정(Project.swift)과 자동화 스크립트(Fastfile)의 역할을 명확히 분리함으로써, 유지보수성이 높고 이해하기 쉬운 시스템을 구축했다.
  • 시스템에 대한 깊은 이해: GitHub Actions의 보안 정책, Tuist의 프로젝트 생성 원리, Fastlane의 코드 서명 메커니즘 등 각 도구의 핵심 원리를 깊이 있게 이해하는 계기가 되었다.

이 경험은 앞으로 더 복잡한 자동화 요구사항이나 예상치 못한 문제에 직면했을 때, 흔들리지 않고 해결해 나갈 수 있는 단단한 기술적 자산이 될 것이라 확신한다.

이번 글이 나와 같은 문제를 겪고 있는 분들께 조금이라도 도움이 되었으면 한다.

profile
꾸준🐢

0개의 댓글