최근에 Fastlane을 Github Actions에 연동하는 작업을 진행했고 실제로 프로젝트에도 적용해봤습니다.
Github Actions는 어떤 기능을 제공해줄까요?
트리거가 발생했을 때, 자동으로 어떤 작업을 대신 수행해주는 자동화 시스템
실제로 저희가 원했던 작업은 다음과 같습니다.
- 특정 브랜치에 PR & Push 작업이 진행되었을 때 Build & Test 가 성공하는지 확인
- 특정 브랜치에 Push 되었을 때 자동으로 TestFlight 배포
- 버전을 Tag 했을 때 자동으로 AppStore 제출
이런 작업 외에도 할 수 있는 작업은 매우 많습니다. 반복적인 작업에 CRON 을 사용하여 스케쥴링이 가능하고, SwiftLint 를 적용하여 코드 규칙을 엄격하게 적용할 수도 있습니다.
이번 포스트에서는 특정 브랜치에 PR & Push 작업이 진행되었을 때 Build & Test 가 제대로 성공하는지 확인하는 작업을 다룹니다.
Github Actions를 적용하는 방법은 적용하려는 레파지토리에서 Actions 를 클릭합니다.

직접 작업을 모두 구성하고 싶다면, Set up a workflow yourself를 선택합니다.
Suggested for this repository를 선택하면 현재 레파지토리와 연관이 있는 작업들의 기본 템플릿을 제공해주기도 합니다.
아무거나 선택해보면 .yml 형식의 파일을 하나 제공해줍니다. 여기서 Github Actions가 어떤 방식을 통해서 작업하는지 알아 볼 필요가 있습니다.
Github Actions는 워크플로우를 통해 동작합니다. 워크플로우의 의미와 특징은 다음과 같습니다.
.yml, yaml 형식이여야 한다. .github/workflows/ 에 위치해야 한다.워크플로우 파일들은 YAML 문법으로 언제(on), 어떤 환경에서(runs-on), 무엇을(run) 할지 적어두는 스크립트입니다.
name: CI # 워크플로우 이름
on: # 이 워크플로우를 언제 실행할지 정의하는 트리거 (on)
push:
branches: [ "dev" ] # dev에 push 될 때
pull_request:
branches: [ "dev" ] # dev에 PR 할 때
jobs: # 워크플로우의 작업 단위. 여러 job이 존재할 수 있다.
build:
name: Build and Test default scheme using any available iPhone simulator
runs-on: macos-latest # 해당 job을 어떤 환경에서 실행할지? (runs-on)
steps: # 이 job 안에서 실행할 단계들 (순차적으로 실행)
- name: Build
run: xcodebuild build-for-testing # 터미널에서 실행할 명령어 (run)
- name: Test
run: xcodebuild test-without-building # 터미널에서 실행할 명령어 (run)

🚨 만약 트리거를
dev브랜치로 설정했다면, 해당 워크플로우 파일은 반드시dev브랜치에 존재해야 합니다.
아래의 워크플로우 코드는 실제로 팀 프로젝트에 적용한 코드입니다. 매우 복잡해 보이지만, 사실 크게 어려운 작업은 없습니다. 각 단계별로 어떤 작업을 하는지 알아볼까요?
name: iOS CI
on:
push:
branches: [ "dev" ]
pull_request:
branches: [ "dev" ]
jobs:
build:
name: Build and Test default scheme using any available iPhone simulator
runs-on: macos-latest
# Xcode 빌드 환경 변수
env:
project: ${{ 'AIProject/iCo.xcodeproj' }} # 환경 변수로 설정하여 "$project" 로 접근이 가능하다.
scheme: ${{ 'iCoTests' }}
platform: ${{ 'iOS Simulator' }}
device: ${{ 'iPhone 16 Pro' }}
os: ${{ '18.6' }}
steps:
# 레파지토리 체크인
- name: Checkout
uses: actions/checkout@v4
# XCConfig / GoogleService 등의 파일을 클론
- name: Clone Secret file
uses: actions/checkout@v4
with:
repository: ESTiOSAI/Secrets
path: temp/
token: ${{ secrets.SECRET_TOKEN }}
# 가져온 파일 이동
- name: Move config
run: |
mv temp/XCConfig/Secrets.xcconfig AIProject/iCo/App/Resource/
mv temp/GoogleServices/GoogleService-Info.plist AIProject/iCo/App/Resource/
# SPM Dependency 캐싱
- name: Cache Swift Packages
uses: actions/cache@v4
with:
path: |
~/Library/Developer/Xcode/DerivedData/*/SourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
# 빌드 확인
- name: Build
run: |
xcodebuild build-for-testing \
-project "$project" \
-scheme "$scheme" \
-destination "platform=$platform,name=$device,OS=18.6"
# 테스트 확인
- name: Test
run: |
xcodebuild test-without-building \
-project "$project" \
-scheme "$scheme" \
-destination "platform=$platform,name=$device,OS=18.6"
해당 워크플로우가 언제 실행될지 정의합니다.
기본적으로 OR 연산 기반이기 때문에, dev 브랜치에 push, pr 할 때 모두 실행됩니다.
on:
push:
branches: [ "dev" ]
pull_request:
branches: [ "dev" ]
워크플로우 파일에서 필요한 환경변수를 정의합니다.
# Xcode 빌드 환경 변수
env:
project: ${{ 'AIProject/iCo.xcodeproj' }} # 환경 변수로 설정하여 "$project" 로 접근이 가능하다.
scheme: ${{ 'iCoTests' }}
platform: ${{ 'iOS Simulator' }}
device: ${{ 'iPhone 16 Pro' }}
os: ${{ '18.6' }}
환경변수를 사용하면 다음과 같이 접근이 가능합니다.
"$project" -> 'AIProject/iCo.xcodeproj'
보안을 위해 로컬에서만 사용하는 파일들이 있다면, 해당 파일들을 직접 주입해야 합니다.
저희 팀 프로젝트에서는 로컬에서만 사용하는 파일 2개가 존재합니다.
- Secrets.xcconfig
- GoogleService.Info.plist
다음과 같은 파일들은 원격에 올리면 보안 문제가 발생하기 때문에, 로컬에서만 사용하고 공유합니다. 해당 파일들을 Github Actions 환경에도 주입해줘야 문제 없이 빌드 & 테스트 동작을 할 수 있습니다.
저희가 선택한 방식은 Private Repo에 해당 파일들을 저장해두고 불러오는 방식을 사용했습니다.
1️⃣ Private Repo를 생성합니다.

2️⃣ 해당 레파지토리에 빌드 & 테스트에 필요한 파일들을 넣습니다.

3️⃣ 해당 Private Repo에 접근할 수 있도록 PAT 토큰을 발급받습니다.
Fine-grained personal access tokens에서 해당 레파지토리에 대한 접근 권한 토큰을 발급받습니다.

4️⃣ Github Secrets에 PAT 토큰을 등록합니다.
레포지토리 안에서 민감한 값을 안전하게 저장하고, Github Actions에서만 사용할 수 있는 암호화된 환경 변수
해당 토큰도 외부에 노출해서는 안되기 때문에, Github Secrets를 통해서 안전하게 보관해서 사용해야 합니다.
아래 화면에서 환경 변수를 만들어서 Github Actions에서 사용이 가능합니다.

5️⃣ Github Actions에서 해당 파일들을 주입하고 원하는 디렉토리에 이동합니다.
# XCConfig / GoogleService 등의 파일을 주입
- name: Clone Secrets File
uses: actions/checkout@v4
with:
repository: ESTiOSAI/Secrets # Repository 위치
path: temp/ # 'temp/' 디렉토리 위치에 가져옴.
token: ${{ secrets.SECRET_TOKEN }} # Github Secrets에 있는 환경 변수 사용
# 주입된 파일 temp/ 디렉토리에서 AIProject/iCo/App/Resource/ 디렉토리로 이동
- name: Move config
run: |
mv temp/XCConfig/Secrets.xcconfig AIProject/iCo/App/Resource/
mv temp/GoogleServices/GoogleService-Info.plist AIProject/iCo/App/Resource/
Github Actions에서의 실행 환경은 매 빌드마다 완전히 새로 만들어지는 작업입니다.
즉, 실행할 때마다 아래와 같은 작업이 발생합니다.
SPM 다운로드 -> 빌드 -> 의존성 체크
여기서 SPM 작업은 빌드 과정에서 굉장히 많은 시간을 할애하게 됩니다.
SPM Dependency 캐싱 작업을 진행하면 이전 Action 실행에서 다운받았던 패키지를 캐싱하여 재사용하여 빌드 시간을 단축할 수 있습니다.
# SPM Dependency 캐싱
- name: Cache Swift Packages
uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData/*/SourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: ${{ runner.os }}-spm-
마지막으로 빌드 & 테스트 작업입니다.
이 작업을 위해서 위에 있는 모든 작업들을 진행했다고 봐도 될 거 같습니다.
먼저 빌드를 먼저 진행하고, 테스트를 진행하는 형식으로 구성했습니다.
xcodebuild build-for-testing # 테스트는 진행하지 않고, 테스트 타깃을 컴파일합니다.
xcodebuild test-without-building # 빌드 없이 테스트만 진행합니다.
프로젝트와 빌드할 스키마, 시뮬레이터 환경까지 환경 변수를 통해서 지정하면 완료입니다!
# 빌드 확인
- name: Build
run: |
xcodebuild build-for-testing \
-project "$project" \
-scheme "$scheme" \
-destination "platform=$platform,name=$device,OS=18.6"
# 테스트 확인
- name: Test
run: |
xcodebuild test-without-building \
-project "$project" \
-scheme "$scheme" \
-destination "platform=$platform,name=$device,OS=18.6"
이번 기회에 CI를 적용해 봤는데, 테스트를 자동으로 실행하고, 이 과정을 통과한 코드만이 통합되도록 함으로써 코드 품질의 안정성을 보장할 수 있다고 느꼈습니다.
가끔씩 실수로 빌드나 테스트가 제대로 수행되지 않는 상태로 PR & Merge를 하는 경우도 있었는데, 이런 상황을 자동화를 통해서 해결할 수 있다는 건 매우 큰 장점인 거 같습니다.
https://hackjsp.tistory.com/72