클라우드 Mac CI의 UTF-8과 Unicode 파일명 관리

클라우드 Mac CI의 UTF-8과 Unicode 파일명 관리

동일한 iOS 프로젝트라도 개발자의 터미널에서는 정상적으로 빌드되지만, 클라우드 Mac의 비대화형 CI에서는 한국어나 중국어 같은 비ASCII 문자가 포함된 설정을 읽거나, 악센트 문자가 들어간 리소스 이름을 해석하거나, 로그를 잘라 처리하는 과정에서 실패할 수 있습니다. 특히 이런 문제는 종종 의존성 손상으로 오인됩니다. 파일이 분명히 존재하는데도 스크립트는 찾을 수 없다고 보고하고, 로그에서는 똑같아 보이는 경로가 Git에서는 서로 다른 두 경로로 나타납니다. 근본 원인은 대개 UTF-8 자체가 아니라 작업 진입점, 언어 런타임, 저장소 파일명이 인코딩을 해석하는 방식이 서로 일치하지 않는 데 있습니다.

차이가 발생하는 계층부터 확인하기

처음부터 시스템 설정을 변경하지 마십시오. 먼저 문제를 작업 프로세스의 locale, 스크립트 런타임의 기본 인코딩, 파이프라인에 포함된 개별 명령의 동작, 파일명에 적용된 Unicode 정규화 형식이라는 네 계층으로 나눠야 합니다.

대화형 터미널과 CI 작업 시작 지점에서 각각 다음 명령을 실행합니다.

printf 'shell=%s\n' "$SHELL"
locale
printf 'LANG=%s\nLC_ALL=%s\n' "${LANG:-unset}" "${LC_ALL:-unset}"
python3 -c 'import locale,sys; print(locale.getpreferredencoding(False), sys.getfilesystemencoding())'
ruby -e 'p [Encoding.default_external, Encoding.default_internal]'

출력은 일반 빌드 산출물로 저장하고, 스크롤되는 로그에만 남겨 두지 마십시오. 터미널에는 en_US.UTF-8이 표시되지만 작업에서는 변수가 비어 있다면 시작 경계에 문제가 있는 것입니다. locale이 동일한데도 Ruby가 여전히 다른 인코딩으로 파일을 읽는다면 스크립트에서 잘못된 옵션을 명시적으로 전달하고 있는지 확인해야 합니다.

“시스템이 UTF-8을 지원한다”는 말이 “모든 작업이 UTF-8을 사용한다”는 뜻은 아닙니다. CI의 실제 설정은 로그인 터미널에 표시되는 환경이 아니라 프로세스에 전달된 환경입니다.

작업 진입점에서 UTF-8 고정하기

가장 안정적인 방법은 .zshrc에 의존하지 않고 runner 래퍼 스크립트나 파이프라인 작업 진입점에서 환경을 설정하는 것입니다. 비대화형 Shell은 대개 개발자가 예상하는 방식으로 이런 파일을 불러오지 않으며, launchd가 시작한 프로세스에는 별도의 환경이 적용될 수도 있습니다.

#!/bin/zsh
set -euo pipefail

export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

locale
exec "$@"

스크립트를 ci/run-utf8.zsh로 저장하고 실행 권한을 부여한 다음, 모든 빌드 명령이 이 스크립트를 거쳐 실행되도록 합니다.

chmod +x ci/run-utf8.zsh
ci/run-utf8.zsh ./ci/build.zsh

로그를 “수정”한다는 이유로 export LC_ALL=C를 전역 실행하지 마십시오. C locale은 안정적인 바이트 정렬이 필요한 개별 명령에는 적합하지만, 문자 분류, 대소문자 변환, 정규식 동작까지 바꿉니다. 꼭 필요하다면 LC_ALL=C sort input.txt처럼 적용 범위를 해당 명령으로 제한하십시오.

스크립트 자체의 읽기 방식도 확인하기

Python에서는 encoding="utf-8"을 명시하고, Ruby에서는 File.read(path, encoding: "UTF-8")를 사용할 수 있습니다. Shell에서 텍스트를 한 줄씩 읽을 때는 백슬래시와 앞뒤 공백이 변경되지 않도록 IFS= read -r을 사용하십시오. JSON, plist, YAML은 해당 형식의 파서로 처리해야 하며, grepsed로 구조를 추측해서는 안 됩니다.

Unicode 파일명 정규화 검사하기

문자 é는 하나의 코드 포인트일 수도 있고, 문자와 결합 문자로 구성될 수도 있습니다. 두 형식은 화면에서 동일하게 보이지만 바이트는 서로 다릅니다. 저장소가 macOS, Linux, 압축 파일 사이를 오갈 때 이러한 차이로 인해 리소스가 중복되거나, 스크립트를 찾지 못하거나, 대소문자만 변경한 커밋이 올바르게 적용되지 않을 수 있습니다.

CI에 다음 읽기 전용 검사를 추가합니다.

from pathlib import Path
import sys
import unicodedata

bad = []

for path in Path(".").rglob("*"):
    if ".git" in path.parts:
        continue
    raw = path.as_posix()
    normalized = unicodedata.normalize("NFC", raw)
    if raw != normalized:
        bad.append((raw, normalized))

for raw, normalized in bad:
    print(f"non-nfc: {raw!r} -> {normalized!r}")

sys.exit(1 if bad else 0)

파일을 ci/check_unicode_paths.py로 저장하고 의존성을 설치하기 전에 실행합니다. 이 검사기는 문제를 보고하기만 하고 자동으로 이름을 변경하지 않습니다. 정규화된 경로가 기존 파일과 충돌할 수 있기 때문입니다. 수정하기 전에 Git 인덱스에 기록된 실제 이름을 확인하십시오.

git -c core.quotepath=false ls-files
git -c core.quotepath=false status --short

그런 다음 git mv를 사용해 별도의 커밋으로 수정합니다. 대소문자만 바꾸는 경우에는 먼저 임시 이름으로 이동한 뒤 목표 이름으로 다시 이동할 수 있습니다. 작업이 끝나면 반드시 새 디렉터리에 다시 클론해 빌드해야 합니다. 파일 시스템 캐시의 영향을 받은 기존 작업 공간으로 검증해서는 안 됩니다.

로그에 원본 증거 보존하기

인코딩 오류가 tee, 로그 수집기, JSON 이스케이프를 거치면 원본 바이트가 대체되어 최종 로그에는 물음표나 대체 문자만 남을 수 있습니다. 조사 단계에서는 사람이 읽을 수 있는 로그와 핵심 파일의 바이트 표현을 함께 보존해야 합니다.

file -I Config/环境.json
xxd -g 1 -l 96 Config/环境.json
plutil -lint App/Info.plist

file의 판정을 유일한 결론으로 받아들이지 마십시오. 이 명령은 단서만 제공할 수 있습니다. 파일을 실제로 읽는 프로그램에서는 여전히 UTF-8을 명시해야 하며, 디코딩 실패 시 0이 아닌 종료 상태를 반환해야 합니다. 빌드 스크립트에서도 cmd | tee build.log를 실행한 뒤 앞선 명령의 종료 코드를 무시해서는 안 됩니다. zsh에서는 $pipestatus를 확인하거나 핵심 명령과 로그 수집을 분리할 수 있습니다.

로그에는 실패한 경로의 이스케이프 표현도 기록해야 합니다. Python의 repr()과 Ruby의 String#dump는 직접 출력하는 방식보다 결합 문자, 줄바꿈, 보이지 않는 공백을 식별하는 데 적합합니다.

병합 전 게이트와 수정 순서 정립하기

유지 관리 가능한 게이트는 확인된 사실만 검사해야 하며 저장소를 임의로 수정해서는 안 됩니다. 다음 순서로 실행하는 것이 좋습니다.

  1. locale과 각 런타임의 인코딩을 출력합니다.
  2. 저장소 경로가 NFC 형식인지 검사합니다.
  3. 정규화 후 중복 이름이 생기는지 검사합니다.
  4. 명시적인 인코딩으로 핵심 텍스트 설정을 읽습니다.
  5. 완전히 새로운 작업 공간에서 최소 빌드를 실행합니다.
  6. 환경 진단 결과, 경로 보고서, 실패 로그를 저장합니다.

특정 실행기에서만 작업이 실패한다면 모든 캐시를 먼저 비우지 말고 환경 진단 결과를 비교한 다음 Git 인덱스를 비교하십시오. 경로 보고서에 이상이 있다면 먼저 파일명을 수정하고 별도의 커밋을 만드십시오. 경로는 정상인데 파싱이 실패한다면 파일 내용의 인코딩 선언과 읽기 매개변수를 확인합니다. 이렇게 하면 환경 문제, 저장소 문제, 도구 문제를 분리해 처리할 수 있습니다.

RentMini의 작업에서도 이 검사들을 저장소에 포함해 프로젝트 자체에서 버전을 관리해야 하며, 특정 클라우드 Mac의 수동 설정에 의존해서는 안 됩니다. 진입점 스크립트, Unicode 검사기, 로그 증거를 코드와 함께 검토해야 작업 공간을 새로 만들거나 노드를 변경해도 일관된 동작을 얻을 수 있습니다.

자주 묻는 질문

터미널에서는 정상인 스크립트가 CI에서 글자를 깨뜨리는 이유는 무엇인가요?

CI 작업은 대개 비대화형 프로세스로 시작되어 같은 셸 초기화 파일을 읽지 않습니다. 작업 진입점에서 LANG과 LC_ALL을 지정하고 각 런타임이 보고하는 인코딩을 확인해야 합니다.

NFC가 아닌 파일명을 CI에서 자동으로 바꿔도 되나요?

자동 변경보다 먼저 보고서를 만들고 검사를 실패시키는 편이 안전합니다. 대소문자와 정규화 충돌을 검토한 뒤 별도 커밋에서 git mv를 실행하고 깨끗한 체크아웃으로 재검증하세요.

필요할 때 사용하는 클라우드 Mac

다음 빌드를 위한 독점 물리 Mac mini 준비

M4 구성, 대여 기간, 노드를 선택하세요. 주문마다 독점 물리 장치가 할당되며, 실제 사용 가능 상태는 콘솔에서 실시간으로 확인할 수 있습니다.

구성 선택 후 대여