MiniRent 엔지니어링 가이드

클라우드 Mac에서 Swift 코드 스타일 증분 게이트 구축하기

클라우드 Mac에서 Swift 코드 스타일 증분 게이트 구축하기

중간 규모의 Swift 저장소에서 클라우드 Mac을 사용해 전체 포맷 검사를 실행할 때, 흔한 병목은 도구 자체의 속도가 아니라 병합 요청마다 변경되지 않은 소스 코드와 생성 디렉터리, 외부 의존성까지 반복해서 검사하는 데서 발생합니다. 더 큰 문제는 개발자 로컬 환경과 CI가 서로 다른 버전을 사용해 같은 코드 한 줄에 대해 상반된 결과를 내는 경우입니다. 해결책은 규칙을 끄는 것이 아니라 도구 버전, 설정 파일, 변경 사항 계산 로직을 모두 저장소에 포함하는 것입니다.

두 도구의 역할부터 명확히 구분하기

SwiftFormat은 들여쓰기, 줄바꿈, 불필요한 공백처럼 결과가 명확하고 자동 수정할 수 있는 서식을 처리하는 데 적합합니다. SwiftLint는 강제 타입 변환, 지나치게 긴 함수, 명명 규칙 같은 위험 패턴을 검사하는 데 더 적합합니다. 두 도구가 같은 규칙을 동시에 관리하면 포맷을 적용한 직후 정적 검사에서 다시 거부되는 상황이 발생할 수 있습니다.

먼저 간단한 규칙 담당표를 만드는 것이 좋습니다.

검사 유형 담당 도구 병합 요청에서의 동작
들여쓰기, 공백, 매개변수 줄바꿈 SwiftFormat 검사만 수행하고 자동으로 수정하지 않음
강제 타입 변환 및 강제 언래핑 SwiftLint 실패 처리하고 파일 위치 출력
생성 코드 및 외부 의존성 두 도구 모두 제외 변경 파일 목록에 포함하지 않음
기존 경고 SwiftLint 기준선 새로 추가된 문제만 차단

CI에서는 소스 코드를 직접 수정하는 명령을 실행하지 마세요. 게이트는 차이만 보고해야 합니다. 개발자가 로컬에서 수정한 뒤 다시 제출해야 저장소 상태를 쉽게 재현할 수 있습니다.

버전과 규칙을 저장소에 포함하기

클라우드 Mac에 “현재 설치된” 도구에 의존하지 마세요. 먼저 팀에서 검증한 버전을 선택한 다음, 예상 버전을 스크립트에 명시해야 합니다. 아래에서는 SwiftFormat 0.54.3과 SwiftLint 0.55.1을 예로 들지만, 실제 프로젝트에서는 자체 검증 결과로 교체해야 합니다.

.swiftformat은 간결하게 유지할 수 있습니다.

--swiftversion 5.10
--indent 4
--wraparguments before-first
--exclude .build,DerivedData,Vendor,Generated

.swiftlint.yml에는 소스 범위와 제외할 디렉터리를 명확히 지정합니다.

included:
  - Sources
  - Tests
excluded:
  - .build
  - DerivedData
  - Vendor
  - Generated
only_rules:
  - force_cast
  - force_try
  - trailing_whitespace
  - unused_import

규칙 파일도 반드시 코드 리뷰 대상에 포함해야 합니다. 새 규칙을 추가할 때는 먼저 별도 브랜치에서 저장소 전체를 검사해 경고 규모를 확인한 뒤, 한 번에 수정할지 기준선을 만들지 결정하세요. 수백 개의 기존 경고를 그대로 병합 요청 게이트에 포함하면 결국 팀 전체가 실패 표시를 무시하는 데 익숙해질 뿐입니다.

병합 요청의 Swift 변경 파일을 정확히 추출하기

git diff HEAD^를 직접 사용하는 방식은 커밋이 하나뿐인 브랜치에만 적합합니다. 병합 요청에 여러 커밋이 포함되어 있거나 리베이스가 수행된 경우에는 파일을 누락하기 쉽습니다. 더 안정적인 방법은 현재 커밋과 대상 브랜치의 공통 조상을 먼저 계산한 다음, 추가·복사·수정·이름 변경된 파일을 추출하는 것입니다.

set -euo pipefail

expected_format="0.54.3"
expected_lint="0.55.1"

[[ "$(swiftformat --version)" == "$expected_format" ]]
[[ "$(swiftlint version)" == "$expected_lint" ]]

base_ref="${LINT_BASE_REF:-origin/main}"
merge_base="$(git merge-base HEAD "$base_ref")"
changed=("${(@0)$(git diff --name-only -z --diff-filter=ACMR "$merge_base" HEAD)}")

swift_files=()
for file in "${changed[@]}"; do
  [[ "$file" == *.swift ]] || continue
  [[ "$file" == .build/* ]] && continue
  [[ "$file" == DerivedData/* ]] && continue
  [[ "$file" == Vendor/* ]] && continue
  [[ "$file" == Generated/* ]] && continue
  swift_files+=("$file")
done

(( ${#swift_files[@]} > 0 )) || exit 0

swiftformat --lint "${swift_files[@]}"
swiftlint lint --strict --force-exclude "${swift_files[@]}"

여기서는 null 문자로 구분된 파일 목록을 사용하므로 경로에 공백이 있어도 잘못 분리되지 않습니다. --diff-filter=ACMR은 삭제된 파일을 제외해 검사 도구에 존재하지 않는 경로가 전달되는 것을 방지합니다. 대상 브랜치 이름은 환경 변수로 전달하므로 동일한 스크립트를 기본 브랜치와 릴리스 브랜치 모두에서 사용할 수 있습니다.

얕은 클론 처리하기

git merge-base가 공통 조상을 찾지 못한다면 대개 스크립트 오류가 아니라 CI가 매우 얕은 커밋 기록만 가져왔기 때문입니다. 먼저 체크아웃 깊이를 늘려 대상 브랜치 참조가 존재하도록 한 뒤 스크립트를 실행해야 합니다. HEAD^ 방식으로 되돌아가면 브랜치 구조에 따라 게이트 결과가 달라질 수 있습니다.

실패 결과를 추적하고 재현할 수 있게 만들기

게이트가 실패했을 때 로그는 최소한 세 가지 질문에 답할 수 있어야 합니다. 어떤 도구 버전을 사용했는지, 어떤 기준 커밋과 비교했는지, 어떤 파일이 검사 대상이었는지입니다. 버전이 일치하지 않으면 즉시 중단해야 하며, 계속 실행해 대량의 포맷 차이를 만들어서는 안 됩니다.

로컬에서 재현할 때 개발자는 대상 브랜치 참조를 가져온 뒤 동일한 스크립트만 실행하면 됩니다.

git fetch origin main
LINT_BASE_REF=origin/main zsh Scripts/lint-changed-swift.zsh

흔한 실수로는 검사 명령을 파이프라인 뒤에 연결한 다음 잘못된 종료 코드를 읽거나, 보기 좋은 로그를 만들기 위해 || true를 사용해 실제 실패를 무시하는 경우가 있습니다. 스크립트에서 set -euo pipefail을 활성화하면 어떤 도구든 0이 아닌 상태를 반환할 때 작업이 종료됩니다. CI에서 로그를 정리해야 한다면 먼저 원래 종료 코드를 저장하고, 요약을 출력한 뒤 해당 값으로 종료해야 합니다.

2단계 전략으로 장기 비용 관리하기

증분 게이트는 “이번 변경으로 새 문제가 추가되지 않았다”는 점만 보장하며, 기존 코드 전체가 현재 규칙을 준수한다는 사실까지 증명하지는 못합니다. 따라서 검사를 다음과 같이 두 단계로 나눌 수 있습니다.

  1. 모든 병합 요청에서 변경 사항 검사를 실행합니다. 빠르고 안정적이어야 하며 로컬에서 완전히 재현할 수 있어야 합니다.
  2. 예약 작업에서 저장소 전체 검사를 실행해 기준선 변동, 더 이상 유효하지 않은 제외 항목, 장기간 처리되지 않은 기존 경고를 찾습니다.
  3. 규칙 업그레이드는 별도 커밋으로 제출하고 기능 변경과 섞지 않아야 포맷 변경으로 발생한 대규모 차이를 쉽게 검토할 수 있습니다.
  4. 생성 디렉터리는 출처가 명확해야 하며, 도구 설정에서 제외하는 동시에 변경 사항 스크립트에서도 필터링해 생성 작업 순서가 결과에 영향을 주지 않도록 해야 합니다.

적용하기 전에 다시 한번 확인하세요. 대상 브랜치 참조가 존재하는지, 도구 버전이 정확히 일치하는지, 이름이 변경된 파일이 포함되는지, 삭제된 파일이 제외되는지, 공백이 포함된 경로가 안전하게 처리되는지, Swift 변경 사항이 없을 때 정상적으로 종료되는지 점검해야 합니다. 이러한 확인을 마치면 코드 스타일 게이트는 “가끔 오류를 내는 스크립트”가 아니라 신뢰할 수 있는 제출 경계로 자리 잡을 수 있습니다.

자주 묻는 질문

증분 검사를 사용하면 전체 저장소 검사가 필요 없나요?

아닙니다. 증분 검사는 병합 요청의 빠른 피드백에 적합하고, 수정되지 않은 기존 파일의 규칙 위반은 예약된 전체 검사로 찾아야 합니다.

SwiftFormat과 SwiftLint에 같은 규칙을 설정해야 하나요?

역할을 분리하는 편이 좋습니다. 자동 수정 가능한 서식은 SwiftFormat이, 위험 패턴과 팀 규칙은 SwiftLint가 담당하도록 중복 규칙의 소유자를 하나로 정합니다.

독점 물리 장비

클라우드 Mac mini로 다음 개발 또는 빌드 작업 실행하기

두 가지 M4 구성, 네 가지 대여 기간, 현재 판매 중인 다섯 개 노드 중에서 선택하세요. 실제 사용 가능 여부는 콘솔의 실시간 응답을 기준으로 합니다.

구성 선택 후 대여하기