하나의 iOS 프로젝트에서 개발, 스테이징, 프로덕션 서비스에 모두 연결할 때 가장 위험한 실수는 대개 컴파일 실패가 아니라 잘못된 환경으로 아카이브를 성공적으로 생성하는 것입니다. 개발용 주소, 로그 스위치 또는 내부 기능 플래그가 릴리스 패키지에 포함되어도 파이프라인은 여전히 성공으로 표시될 수 있습니다. 더 안전한 방법은 공개 가능한 설정을 계층별로 관리하고, 빌드 진입점을 고정하며, Xcode에서 누군가 올바른 메뉴를 선택하기를 기대하는 대신 아카이브 검증을 릴리스 절차에 포함하는 것입니다.
설정과 비밀 정보의 경계부터 정하기
xcconfig는 API 기본 주소, Bundle ID 접미사, 로그 수준, 기능 플래그의 기본값처럼 클라이언트에 공개되어도 되는 빌드 매개변수를 저장하는 데 적합합니다. 하지만 비밀 정보를 보관하는 금고는 아닙니다. 클라이언트 빌드 과정에 입력되는 값은 모두 Info.plist, 리소스 파일, 컴파일 인수 또는 실행 파일에 포함될 수 있습니다.
사용자가 설치 패키지를 받은 뒤 알아서는 안 되는 값이라면 Xcode가 App에 포함하도록 전달하지 않는다는 간단한 기준을 적용하면 됩니다.
서버 관리 키, 서명 서비스 자격 증명, 쓰기 권한이 있는 토큰은 서버 측이나 접근이 통제된 CI 자격 증명 저장소에 보관해야 합니다. 클라이언트에는 수명이 짧고 권한이 제한적이며 폐기할 수 있는 인증 결과만 전달합니다. CI가 환경 변수로 비밀 정보를 주입하더라도 “최종 산출물을 분석할 수 있다”는 사실은 달라지지 않습니다.
먼저 설정 목록을 만들고 각 항목의 공개 여부, 출처, 아카이브 생성 후 예상 위치를 표시합니다. 팀이 검토해야 할 대상은 Build Settings 곳곳에 흩어진 수십 개의 값이 아니라 이 목록입니다.
검토 가능한 xcconfig 계층 만들기
공통 설정, 환경별 차이, 로컬 재정의를 분리하는 것이 좋습니다.
Config/
Base.xcconfig
Development.xcconfig
Staging.xcconfig
Production.xcconfig
LocalOverrides.xcconfig.example
Base.xcconfig에는 모든 환경에서 공유하는 값만 둡니다. 각 환경 파일에서는 먼저 이 파일을 포함한 뒤 소수의 차이만 재정의합니다.
#include "Base.xcconfig"
APP_ENVIRONMENT = staging
API_BASE_URL = https:/$()/staging-api.invalid
ENABLE_VERBOSE_LOGGING = YES
PRODUCT_BUNDLE_IDENTIFIER = com.example.product.staging
여기서 $()는 URL의 이중 슬래시가 주석으로 해석되지 않게 합니다. 예시 도메인은 해석할 수 없는 문서용 값일 뿐이므로 실제 프로젝트에서는 자체 서비스 주소로 교체해야 합니다.
로컬 재정의 파일은 저장소에 커밋하지 말고, 선택적 포함 구문을 사용해 최초 체크아웃 시의 진입 장벽을 낮출 수 있습니다.
#include? "LocalOverrides.xcconfig"
다만 로컬 재정의는 개발 편의를 위해서만 사용해야 하며 공식 아카이브의 암묵적 입력이 되어서는 안 됩니다. 프로덕션 설정은 이 파일이 없어도 완전히 해석되어야 합니다. Development, Staging, Release 세 Configuration을 각각 해당 파일에 연결하고, CI에서 사용하는 Scheme이 Shared로 설정되어 있는지 확인합니다.
명령줄 빌드 진입점 고정하기
클라우드 Mac의 아카이브 작업이 이전 GUI 조작에서 남은 상태를 상속해서는 안 됩니다. workspace, scheme, configuration, 출력 경로를 모두 명시적으로 전달해야 합니다.
set -euo pipefail
WORKSPACE="App.xcworkspace"
SCHEME="App"
CONFIGURATION="Release"
ARCHIVE_PATH="$PWD/build/App.xcarchive"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-showBuildSettings > build-settings.txt
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$ARCHIVE_PATH" \
clean archive
아카이브 전에 해석된 결과 검증하기
소스 파일만 확인하지 말고 Xcode가 최종적으로 해석한 설정을 검사해야 합니다. build-settings.txt에서 핵심 항목을 추출하고 프로덕션 환경에 대한 강제 검증을 추가할 수 있습니다.
grep -E "APP_ENVIRONMENT|API_BASE_URL|PRODUCT_BUNDLE_IDENTIFIER|ENABLE_VERBOSE_LOGGING" \
build-settings.txt
grep -q "APP_ENVIRONMENT = production" build-settings.txt
grep -q "ENABLE_VERBOSE_LOGGING = NO" build-settings.txt
프로젝트에서 여러 Target을 사용한다면 Target마다 별도로 확인해야 합니다. 확장 기능, 테스트 번들, 기본 App이 서로 다른 Base Configuration을 상속할 수 있기 때문입니다. 차이가 발생했을 때 당시 상태를 재현할 수 있도록 스크립트에서 현재 Git 커밋, Xcode 버전, 사용한 Scheme도 출력해야 합니다.
실제로 배포되는 아카이브 검사하기
Build Settings가 올바르다고 해서 산출물도 반드시 올바른 것은 아닙니다. Run Script, 생성된 코드 또는 리소스 복사 단계에서 테스트 설정이 아카이브에 포함될 수 있습니다. 아카이브가 완료되면 최소한 기본 App의 속성 목록, 포함된 리소스, 실행 파일 문자열을 검사해야 합니다.
APP_PATH="$(find build/App.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"
plutil -p "$APP_PATH/Info.plist"
if grep -RInaE "staging-api|debug-token|INTERNAL_ONLY" "$APP_PATH"; then
echo "forbidden marker found in archive" >&2
exit 1
fi
EXECUTABLE_NAME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$APP_PATH/Info.plist")
if strings "$APP_PATH/$EXECUTABLE_NAME" | grep -E "staging-api|debug-token|INTERNAL_ONLY"; then
echo "forbidden marker found in executable" >&2
exit 1
fi
검사 문자열에는 팀에서 정의한 마커를 사용하고 실제 비밀 정보를 스크립트나 로그에 직접 넣어서는 안 됩니다. 일반 단어로 인한 오탐을 줄이려면 “허용되는 항목”과 “금지되는 항목”을 담은 짧은 목록을 각각 관리하는 것이 좋습니다. 검사 결과에는 파일 경로와 규칙 이름만 남기고, 일치한 민감한 내용을 전체 출력하지 않습니다.
| 검사 대상 | 확인할 내용 | 실패 시 처리 |
|---|---|---|
| 최종 Build Settings | 환경 이름, Bundle ID, 로그 스위치 | 즉시 아카이브 중단 |
| Info.plist | 서비스 주소, URL Scheme, 환경 태그 | 내보내기 차단 |
| App 리소스 디렉터리 | 디버그 설정, 테스트 데이터, 임시 파일 | 원인을 제거하고 다시 빌드 |
| 실행 파일 | 내부 주소 패턴, 토큰 마커 | 생성 단계를 찾아 관련 자격 증명 교체 |
흔한 드리프트와 오해 방지하기
가장 흔한 드리프트 원인은 “로컬에는 Scheme이 있지만 공유되지 않은” 상태입니다. 저장소에 해당 xcshareddata가 없으면 CI가 Scheme을 찾지 못하거나 엔지니어가 임시로 다른 진입점을 사용할 수 있습니다. 두 번째 문제는 환경 이름을 스크립트의 분기 조건에 넣으면서 Configuration을 유일한 기준으로 삼지 않는 것입니다. 시간이 지나면 서로 다른 두 가지 판단 로직이 생깁니다.
또 다른 오해는 변수를 난독화하거나 분할하거나 바이너리에 기록하면 안전하다고 생각하는 것입니다. 클라이언트의 장기 비밀 정보를 확실하게 숨길 방법은 없습니다. 비밀 정보가 과거 아카이브에 포함된 사실을 발견했다면 먼저 자격 증명을 폐기하거나 교체한 다음 빌드 설정을 수정해야 합니다. 현재 브랜치에서 문자열만 삭제해도 이미 배포된 위험은 사라지지 않습니다.
MangoVM의 고정 물리 노드에서 이 절차를 실행할 때는 Runner가 깨끗한 작업 디렉터리를 사용하도록 하고, 이전 작업에서 생성된 DerivedData, 임시 설정, 내보내기 디렉터리를 재사용하지 않아야 합니다. 정리 대상은 현재 작업 공간으로 한정해 동시 실행 작업이 서로의 파일을 삭제하지 않도록 합니다.
검사를 병합 및 릴리스 게이트로 만들기
최종적으로 절차를 두 단계로 나눌 수 있습니다. 병합 요청 단계에서는 설정을 해석하고 설정 파일 구조를 검사하며, 릴리스 단계에서는 전체 아카이브와 산출물 검사를 수행합니다. 전자는 빠르게 피드백하고 후자는 실제 배포 산출물을 기준으로 판단합니다. 환경 검증이 하나라도 실패하면 로그에 경고만 남기지 말고 반드시 0이 아닌 상태를 반환해야 합니다.
커밋 전에 다음 체크리스트로 마무리할 수 있습니다.
- Scheme이 공유되어 있고 Configuration과 xcconfig의 매핑이 버전 관리에 포함되어 있습니다.
- 프로덕션 아카이브에서 workspace, scheme, configuration을 명시적으로 지정합니다.
showBuildSettings의 환경 이름, Bundle ID, 로그 스위치가 예상과 일치합니다.- 기본 App과 모든 확장 기능에서 속성 목록 및 문자열 검사를 완료했습니다.
- CI 로그에 변수의 전체 값이 출력되지 않습니다.
- 작업이 끝나면 임시 재정의 파일을 삭제합니다.
- 유출을 발견하면 먼저 자격 증명을 교체한 뒤 다시 아카이브하고 재검사합니다.
설정 출처, 해석 결과, 최종 산출물이 3단계 증거를 이루면 환경 선택은 더 이상 작업 습관에 의존하지 않습니다. 릴리스 오류는 사용자가 팀보다 먼저 발견하는 대신 아카이브가 파이프라인을 벗어나기 전에 차단됩니다.
자주 묻는 질문
API 비밀 키를 xcconfig에 저장해도 되나요?
안 됩니다. 설정값은 Info.plist, 컴파일 인자, 리소스나 실행 파일에 포함될 수 있습니다. 앱 아카이브는 공개 정보로 간주하고 권한이 있는 비밀 키는 서버에 보관해야 합니다.
로컬과 CI가 서로 다른 환경으로 아카이브하는 이유는 무엇인가요?
공유되지 않은 Scheme, 잘못된 Configuration 연결, 대화형 Shell에만 있는 변수가 흔한 원인입니다. workspace, scheme, configuration을 명시하고 showBuildSettings 출력을 비교해야 합니다.
개발 파이프라인에 고정 클라우드 Mac 구성하기
MangoVM은 M4와 M4 Pro 두 가지 Apple Silicon 물리 노드를 제공합니다. 대여 기간, 리전, 스토리지 추가 옵션을 선택하면 전체 주문 내역을 확인할 수 있습니다.