클라우드 Mac에서 iOS CSV 내보내기를 검증하는 방법

클라우드 Mac에서 iOS CSV 내보내기를 검증하는 방법

클라우드 Mac에서 iOS CSV 내보내기를 검증하는 방법

팀에서 iOS 앱의 기록을 CSV로 내보낸 뒤 원격 빌드 노드에서 테스트를 실행했다. 단위 테스트에는 ‘파일 쓰기 성공’이라고 표시됐지만, 동료가 파일을 열어 보니 메모 하나가 두 줄로 나뉘고 다른 사용자 입력은 스프레드시트 수식으로 바뀌어 있었다. 여기서 검증할 대상은 공유 버튼이 아니라 내보낸 바이트, 필드 경계, 위험한 입력의 처리 결과다. 클라우드 Mac을 사용하면 사람의 클릭이 필요 없는 이 검사들을 매 빌드마다 실행할 수 있다.

먼저 내보내기 규약을 명확히 정하기

이 글에서는 텍스트 규약을 다음과 같이 정한다. 출력은 바이트 순서 표시 없이 UTF-8로 인코딩한다. 모든 필드를 큰따옴표로 감싸고, 필드 안의 큰따옴표는 두 개로 중복한다. 레코드 사이와 파일 끝에는 CRLF를 사용한다. 필드 안의 줄바꿈은 원래대로 유지하므로 CSV 전체를 줄바꿈 문자로 나눈 뒤 레코드 수를 세어서는 안 된다.

보안 규칙도 하나 둔다. 자유 입력 필드의 양끝에서 공백과 제어 문자를 제거해 검사했을 때 =, +, -, @ 중 하나로 시작하면 내보내기를 오류 처리한다. 이 규칙은 일반 텍스트로 취급할 수도 있는 일부 음수까지 거부한다. 제품에 숫자 열이 실제로 필요하다면 모든 텍스트 열의 제한을 완화하지 말고, 열 정의에 따라 숫자를 파싱하고 검증해야 한다. 헤더에도 같은 인코딩 함수를 적용해 이스케이프 로직이 두 벌로 나뉘지 않게 한다.

CSV의 따옴표가 해결하는 문제는 필드 구분이지, 스프레드시트의 수식 해석이 아니다. 텍스트 앞에 작은따옴표를 붙이는 방식은 가져오기 도구마다 결과가 다를 수 있다. 위험한 입력을 거부하는 편이 규칙을 정의하고 테스트하기 쉽지만, 대상 가져오기 도구의 동작은 여전히 확인해야 한다.

한 함수로 검증 가능한 바이트 생성하기

인코딩 로직은 앱과 테스트 타깃이 모두 참조할 수 있는 Swift 파일에 둔다. 아래 함수는 2차원 문자열 배열을 받는다. 실제 앱에서는 먼저 비즈니스 모델을 정해진 순서의 열로 변환하면 된다. trimmingCharacters는 위험 여부를 확인할 때만 사용한다. 출력에는 원래 값을 그대로 남겨 사용자 데이터를 몰래 바꾸지 않는다.

import Foundation

enum CSVExportError: Error {
    case unsafeValue
}

func csvField(_ value: String) throws -> String {
    let ignored = CharacterSet.whitespacesAndNewlines
        .union(.controlCharacters)
    let probe = value.trimmingCharacters(in: ignored)

    if let first = probe.first, "=+-@".contains(first) {
        throw CSVExportError.unsafeValue
    }

    let escaped = value.replacingOccurrences(of: "\"", with: "\"\"")
    return "\"" + escaped + "\""
}

func csvDocument(_ rows: [[String]]) throws -> Data {
    let lines = try rows.map { row in
        try row.map(csvField).joined(separator: ",")
    }
    return Data((lines.joined(separator: "\r\n") + "\r\n").utf8)
}

호출하는 쪽에서는 각 행의 열 수가 헤더와 일치하는지도 확인해야 한다. 데이터가 비었을 때 헤더만 내보낼지 사용자에게 알릴지 결정하고, 오류는 사용자에게 보이는 메시지로 바꾼다. unsafeValue를 잡은 뒤 검사하지 않은 원문을 대신 출력해서는 안 된다. 그러면 테스트는 통과해도 실제 공유 파일은 규칙을 우회하게 된다. 내보내는 내용에 민감한 정보가 있다면 공유 전에 대상 파일과 접근 권한도 확인해야 한다.

정상 경로와 거부 경로를 함께 테스트하기

Data가 비어 있지 않다는 사실만 단언하지 말고 실제 출력을 비교해야 한다. 첫 번째 테스트는 쉼표, 따옴표, 중국어, 필드 안의 줄바꿈을 다룬다. 두 번째 테스트는 앞에 탭 문자를 붙여도 검사를 우회할 수 없음을 확인한다.

import XCTest

final class CSVExportTests: XCTestCase {
    func testQuotingAndLineEndings() throws {
        let data = try csvDocument([
            ["name", "note"],
            ["张三", "甲,\"乙\"\n丙"]
        ])

        XCTAssertEqual(
            String(decoding: data, as: UTF8.self),
            "\"name\",\"note\"\r\n\"张三\",\"甲,\"\"乙\"\"\n丙\"\r\n"
        )
    }

    func testFormulaLikeTextIsRejected() {
        for value in ["=1+1", "\t@SUM(A1:A2)", "+1", " -2"] {
            XCTAssertThrowsError(try csvDocument([[value]]))
        }
    }
}

이 테스트는 정해 둔 출력 형식과 거부 정책을 확인할 뿐, 모든 스프레드시트가 예상대로 파일을 가져온다는 뜻은 아니다. 특히 따옴표 안의 줄바꿈, 날짜처럼 보이는 텍스트, ASCII 범위 밖의 문자는 실제 전달 대상이 사용하는 가져오기 방식으로 검증해야 한다.

빌드 노드에 재검토 가능한 결과 남기기

먼저 클라우드 Mac에서 Xcode 명령줄 도구를 사용할 수 있는지 확인한 뒤, 프로젝트에서 공유한 scheme으로 테스트를 실행한다. 예시 이름은 저장소의 실제 이름으로 바꾼다. 실행 전 xcodebuild -list -project App.xcodeproj로 실행 대상을 확인할 수 있다. 테스트 타깃이 인코딩 파일에 직접 접근할 수 있어야 하며, 테스트용으로 구현을 따로 복사해서는 안 된다.

xcodebuild test \
  -project App.xcodeproj \
  -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:AppTests/CSVExportTests \
  -resultBundlePath "$PWD/CSVExport.xcresult"

시뮬레이터 이름은 해당 노드에 설치된 런타임과 맞아야 한다. 먼저 xcrun simctl list devices available을 실행해 확인할 수 있다. 실행 전에 CSVExport.xcresult 경로가 이미 존재해서는 안 된다. 다시 실행할 때는 작업마다 별도 디렉터리를 지정해 이전 결과가 섞이지 않게 한다. 이 테스트는 순수 Swift 인코딩만 검증하며 그래픽 인터페이스에 의존하지 않는다. 공유 흐름까지 검증하려면 UI 테스트를 위한 세션과 권한 조건을 별도로 준비해야 한다.

테스트 데이터에서 실제 파일까지 확인하기

단위 테스트가 통과하면 앱의 실제 내보내기 경로를 통해 고정된 샘플 파일을 생성한다. 샘플에는 최소한 빈 필드, 쉼표, 큰따옴표, 필드 안의 줄바꿈, 중국어를 포함한다. 공유 계층에서 문자열을 다시 이어 붙이는 대신 동일한 csvDocument로 파일을 생성했는지 확인한다. 앱이 쓸 수 있는 위치에 저장할 때는 Data.write(to:options: .atomic)을 사용하면 쓰기가 끝나지 않은 파일을 읽는 일을 피할 수 있다. 공유 실패 후 임시 파일을 어떻게 정리하는지도 앱 흐름에 따라 테스트해야 한다.

터미널에서 ‘줄 수’만 보고 성공을 판단해서는 안 된다. 따옴표 안에도 줄바꿈이 있을 수 있다. 샘플과 대조하며 필드별로 가져와 열 순서, 마지막 행, 오류 메시지를 확인하는 편이 더 안전하다. 가져오기 도구가 식별 번호를 멋대로 숫자로 바꾸거나 날짜처럼 보이는 텍스트를 날짜로 바꾼다면, 이는 별도의 형식 규약으로 다뤄야 한다. 명시적인 열 타입이나 전용 교환 형식으로 해결해야지, CSV 따옴표가 셀 타입까지 고정해 준다고 생각해서는 안 된다.

병합 전 체크리스트

CSV 내보내기에서 가장 찾기 어려운 문제는 정상적인 예시가 아니라 실제 사용자가 남긴 메모 한 건에 숨어 있는 경우가 많다. 경계 입력을 먼저 고정된 테스트로 만들고 최종 파일까지 확인하는 편이, 파일 생성 후 눈으로만 오류를 찾는 것보다 훨씬 믿을 만하다.

자주 묻는 질문

CSV 필드를 큰따옴표로 감싸면 내부 큰따옴표는 그대로 둘 수 있나요?

아니요. 내부 큰따옴표를 각각 두 개로 바꾼 뒤 필드 전체를 큰따옴표로 감싸야 합니다.

앞에 작은따옴표를 붙이면 모든 스프레드시트에서 수식 실행을 막을 수 있나요?

보장할 수 없습니다. 가져오기 동작이 다르므로 예제에서는 의심스러운 텍스트를 거부하고, 실제 사용하는 가져오기 도구에서도 확인합니다.

UDPMac 클라우드 Mac

다음 Xcode 빌드는 전용 물리 서버에서 실행하세요.

Apple Silicon 모델, 노드, 이용 기간을 선택하고 원격으로 연결해 원하는 빌드 워크플로를 실행하세요. 실제 이용 가능 여부는 주문 시 콘솔에 표시되는 정보를 기준으로 합니다.

클라우드 Mac 이용 플랜 보기