CDNMac 엔지니어링

iOS 공유 확장의 입력 형식과 임시 파일 검증

iOS 공유 확장의 입력 형식과 임시 파일 검증

사용자가 사진 앱에서 iOS 앱으로 이미지를 공유합니다. 공유 확장은 메뉴에 표시되지만, 열면 “읽을 수 없음”이라는 오류가 나타납니다. 이런 문제는 확장이 정상적으로 빌드됐는지만 확인해서는 해결하기 어렵습니다. 원본 앱이 제공하는 것은 NSItemProvider이며, 여러 유형을 선언할 수 있습니다. 파일 형태로 제공된 입력의 경로는 로드 콜백이 실행되는 동안에만 유효할 수도 있습니다. “표시되는가”, “읽을 수 있는가”, “호스트 앱에 전달할 수 있는가”를 나눠 검증해야 문제가 발생한 지점을 찾기 쉽습니다.

먼저 입력 계약 정의하기

실제로 지원할 원본 앱과 데이터를 정하고, “모든 이미지”를 하나의 모호한 테스트 항목으로 묶지 마세요. 최소한 원본 앱, 공유할 콘텐츠, registeredTypeIdentifiers, 예상 읽기 방식, 호스트 앱이 최종적으로 받아야 할 결과를 기록합니다. 같은 사진이라도 어느 앱에서 공유하느냐에 따라 파일, 데이터 또는 둘 다 제공될 수 있습니다. 확장이 배열의 첫 번째 유형에만 의존해서는 안 됩니다.

입력 확장에서 확인할 사항 합격 기준
이미지 한 장 이미지 유형에 부합하는 표현이 있는지 콘텐츠를 읽고 전달 기록을 생성할 수 있음
이미지 여러 장 각 첨부 항목이 선언한 유형 전체 개수가 성공·실패 개수의 합과 일치함
일반 텍스트 텍스트 표현을 제공하는지 제품 요구사항에 따라 수락하거나 명확히 거부함
지원하지 않는 콘텐츠 일치하는 유형이 없음 이해할 수 있는 오류를 표시하고 빈 기록을 생성하지 않음

공유 확장의 활성화 규칙은 메뉴에 언제 표시될지를 결정하고, 런타임의 유형 검사는 현재 첨부 항목을 처리할 수 있는지를 결정합니다. 둘 다 테스트해야 합니다. “메뉴에 보인다”는 사실만으로 읽기 검증을 대신할 수는 없습니다.

선언된 유형을 확인한 뒤 표현 선택하기

개발 중에는 첨부 항목의 유형 식별자를 로컬 진단 기록에 남기되, 사용자 콘텐츠까지 함께 업로드하지는 마세요. 유형을 선택할 때는 대상 유형에 부합하는지 확인하고, 특정 원본 앱이 제공한 식별자 전체를 하드코딩하지 마세요. 제품이 이미지만 받는다면 텍스트 입력은 명확한 거부 경로로 처리해야 합니다. 알 수 없는 콘텐츠를 이미지로 억지로 디코딩하지 마세요.

콜백 안에서 파일 복사하기

파일 표현을 처리할 때 가장 위험한 방식은 loadFileRepresentation이 반환한 URL을 저장해 두었다가 확장이 종료된 후 호스트 앱에서 열도록 하는 것입니다. 이 URL은 로드 과정에서 제공되는 임시 파일을 가리키므로, 장기적인 파일 전달 주소로 사용할 수 없습니다.

합격 기준은 “콜백 안에서 경로를 출력할 수 있다”가 아니라 “콜백이 끝난 뒤에도 호스트 앱이 접근 권한을 가진 복사본을 읽을 수 있다”입니다.

다음 Swift 코드는 이미지 파일을 처리할 때의 핵심 경계를 보여줍니다. 확장과 호스트 앱이 파일을 공유해야 한다면 동일한 App Group을 설정하고, 그룹 식별자를 배포 설정을 통해 전달하세요. 예시 식별자를 프로젝트에 그대로 넣지 마세요. 다른 공유 작업의 파일을 덮어쓰지 않도록 복사 대상에는 새 파일 이름을 사용합니다.

import Foundation
import UniformTypeIdentifiers

enum ShareCopyError: Error {
    case unsupportedType
    case containerUnavailable
    case missingFile
}

func copySharedImage(
    from provider: NSItemProvider,
    groupID: String,
    completion: @escaping (Result<URL, Error>) -> Void
) {
    guard let typeID = provider.registeredTypeIdentifiers.first(where: {
        UTType($0)?.conforms(to: .image) == true
    }) else {
        completion(.failure(ShareCopyError.unsupportedType))
        return
    }

    provider.loadFileRepresentation(forTypeIdentifier: typeID) { source, error in
        if let error {
            completion(.failure(error))
            return
        }
        guard let source else {
            completion(.failure(ShareCopyError.missingFile))
            return
        }
        guard let container = FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: groupID) else {
            completion(.failure(ShareCopyError.containerUnavailable))
            return
        }

        do {
            let directory = container.appendingPathComponent(
                "Incoming", isDirectory: true
            )
            try FileManager.default.createDirectory(
                at: directory, withIntermediateDirectories: true
            )
            let ext = UTType(typeID)?.preferredFilenameExtension ?? "bin"
            let target = directory.appendingPathComponent(
                UUID().uuidString + "." + ext
            )
            try FileManager.default.copyItem(at: source, to: target)
            completion(.success(target))
        } catch {
            completion(.failure(error))
        }
    }
}

이 코드는 이미지 첨부 항목 하나만 처리합니다. 실제 구현에서는 여러 첨부 항목의 결과를 취합하고, 허용할 파일 크기를 제한하며, 일부만 성공했을 때 복사된 파일을 유지할지 전체 작업을 취소할지도 결정해야 합니다. 호스트 앱에는 관리되는 디렉터리 안의 파일 식별자를 전달하세요. 호스트 앱은 파일을 읽은 뒤 비즈니스 규칙에 따라 정리해야 하며, 복사본을 무한정 쌓아 두어서는 안 됩니다.

읽기 경계를 자동화 테스트에 포함하기

먼저 유형 선택 함수와 복사 함수의 테스트를 작성한 다음, 전체 공유 화면을 테스트하세요. 최소한 이미지 유형, 지원하지 않는 유형, 로드 오류, 공유 컨테이너를 사용할 수 없는 경우, 여러 첨부 항목 중 일부가 실패하는 경우, 이름이 같은 입력이 서로를 덮어쓰지 않는 경우를 확인해야 합니다. 테스트 대역에서는 콜백 시점을 제어할 수 있어야 합니다. 콜백 안에서 파일을 제공하고, 콜백이 반환된 뒤 원본 파일을 치운 다음, 복사 대상이 여전히 읽히는지 단언하세요. 이렇게 해야 URL이 비어 있지 않은지만 확인하는 대신 파일의 수명주기를 검증할 수 있습니다.

CDNMac 클라우드 Mac에서는 먼저 프로젝트에 공개된 scheme과 사용 가능한 시뮬레이터를 확인한 뒤 해당 테스트를 실행하세요. 아래 명령을 실행하려면 세 환경 변수를 현재 프로젝트의 실제 값으로 설정해야 합니다. 결과 번들은 새로 만든 디렉터리에 저장해 재실행할 때 기존 경로와 충돌하지 않도록 합니다.

: "${PROJECT:?set PROJECT to an existing .xcodeproj path}"
: "${SCHEME:?set SCHEME to a shared test scheme}"
: "${SIMULATOR_ID:?set SIMULATOR_ID to an available device ID}"

xcodebuild -list -project "$PROJECT"
xcrun simctl list devices available

RESULT_ROOT="$(mktemp -d)"
xcodebuild -project "$PROJECT" \
  -scheme "$SCHEME" \
  -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
  -resultBundlePath "$RESULT_ROOT/share-input.xcresult" \
  test

CI는 테스트 결과 번들을 보관하고, 테스트 프로세스의 종료 상태로 작업의 성공 여부를 판단해야 합니다. 로그에서 “passed”만 검색하지 마세요. 확장이 시작되지 않았거나 대상 테스트가 scheme에 포함되지 않은 경우도 감지해야 합니다. 처음 CI에 연결할 때는 단언 하나를 일부러 실패시켜 파이프라인이 실제로 작업을 차단하는지 확인하세요.

실제 공유로 테스트의 사각지대 확인하기

단위 테스트로 자체 읽기 로직은 검증할 수 있지만, 원본 앱이 어떤 표현을 제공할지 보장할 수는 없습니다. 테스트 기기 한 대를 정해 지원할 원본 앱에서 항목별로 공유해 보세요. 먼저 확장이 표시되는지 확인하고, 실제로 선언된 유형과 읽기 결과, 호스트 앱에 저장된 복사본을 확인합니다. 큰 이미지나 여러 이미지를 공유할 때는 특히 메모리 사용량, 대기 중 피드백, 취소 동작을 살펴봐야 합니다. 작은 이미지 한 장이 통과했다고 검증을 끝내지 마세요.

문제를 조사할 때는 순서대로 확인하세요. 확장이 표시되지 않으면 활성화 규칙을 확인합니다. 표시되지만 유형이 일치하지 않으면 원본 앱이 제공한 식별자를 기록합니다. 로드에 실패하면 오류 유형을 남깁니다. 확장에서는 읽기에 성공했지만 호스트 앱에서는 실패한다면 App Group 권한, 콜백 안에서 복사가 완료됐는지, 호스트 앱이 받은 경로가 복사본의 경로인지 확인하세요. 테스트 기록에는 필요한 유형, 단계, 오류 정보만 남기고 공유 콘텐츠가 빌드 로그에 들어가지 않도록 합니다.

배포 전 점검 항목

배포 전에 네 가지를 다시 확인하세요. 확장이 선언한 유형과 실제 지원 유형이 일치하는지, 각 파일의 복사가 로드 콜백 안에서 완료되는지, 호스트 앱이 권한이 있는 디렉터리의 복사본만 읽는지, 거부·취소·일부 실패에 모두 사용자가 확인할 수 있는 결과가 있는지 확인합니다. App Group 설정을 변경했다면 새로 설치한 테스트 기기에서 전달 흐름을 다시 실행해야 합니다. 개발 기기에 이미 존재하는 컨테이너만으로 검증해서는 안 됩니다.

유형 매트릭스, 자동화 테스트 결과, 기기에서의 공유 기록을 같은 변경 사항에 포함하세요. 그러면 나중에 입력 형식을 추가할 때 명확한 출발점이 생깁니다. 먼저 입력 계약과 실패 테스트를 보강하고 읽기 구현을 추가해야 합니다. 사용자가 “공유했는데 파일이 없다”고 신고한 뒤에야 원본 앱의 동작을 추측해서는 안 됩니다.

자주 묻는 질문

공유 확장에서 받은 파일 URL을 본체 앱에 그대로 넘겨도 되나요?

그대로 넘기지 마세요. loadFileRepresentation 콜백 안에서 본체 앱이 읽을 수 있는 위치로 복사한 뒤 새 파일의 식별자를 전달해야 합니다.

시뮬레이터 테스트가 통과하면 무엇을 더 확인해야 하나요?

실제 기기에서 대상 앱의 공유 메뉴를 열어 확장 표시 여부, 제공 형식, 큰 파일 처리 결과와 본체 앱의 복사본 읽기를 확인하세요.

CDNMac M4

다음 iOS 빌드는 전용 클라우드 Mac에서 실행하세요

M4, 16GB RAM, 256GB SSD를 갖춘 고정 물리 환경을 제공합니다. 6개 노드와 4가지 대여 기간 중 현재 작업에 맞는 구성을 선택하세요.

대여 시작