· Workflow · 9분 읽기

Fastlane 없이 SwiftUI App Store 스크린샷 자동화하기 (2026)

Fastlane 없이 SwiftUI App Store 스크린샷 자동화하기 (2026)
TL;DR. Fastlane은 iOS 스토어 스크린샷을 자동화하는 유일한 방법이 아니에요 — 가장 유명할 뿐이에요. Apple은 필요한 모든 걸 네이티브로 제공해요. 실행 중인 앱을 캡처하는 XCUITest + XCUIScreenshot, 같은 일을 더 적은 보일러플레이트로 해내는 새로운 Swift Testing 첨부 파일 API, 시뮬레이터 실행 없이 SwiftUI 뷰를 곧바로 PNG로 렌더링하는 ImageRenderer가 그거예요. swift-snapshot-testingAppScreenshotKit 같은 오픈소스는 이들을 감싸는 도구예요. 이 중 어느 것도 마케팅 캐러셀 — 기기 프레임, 캡션, 배경, 50개 로케일 카피 — 은 합성하지 않아요. 그건 별개의 단계이고, 바로 그 지점에 Mokbi가 맞아요. 캡처한 스크린샷을 합성하고 현지화해요. 캡처는 하지 않아요.

Fastlane의 snapshot은 「내 App Store 스크린샷을 자동화하고 싶다」는 질문에 대한 기본 답이고, 그럴 만한 이유가 있어요 — 성숙하고 CI 친화적이에요. 하지만 동시에 Ruby 툴체인이고, Snapfile이고, 테스트 타겟에 복사해 넣는 SnapshotHelper.swift이며, 근본적으로는 이미 갖고 있는 동일한 XCUITest API를 얇게 감싼 것치고는 만만치 않은 설정이 필요해요. SwiftUI로 만들고 있고 iOS 프로젝트에 Ruby 의존성을 추가하고 싶지 않다면, Xcode와 Swift만으로 스크린샷을 자동화할 수 있어요. 이 글은 2026년 기준 네이티브 선택지를 그대로 붙여넣을 수 있는 코드와 함께, 어느 도구가 어떤 일을 하는지 명확하게 정리한 솔직한 지도예요. 정면 비교는 Mokbi vs Fastlane을 참고하세요.

혼란을 많이 줄여주는 구분이 하나 있어요. 프레임 캡처(앱을 특정 상태로 조작하고 화면에 보이는 걸 PNG로 저장하는 것)와 스토어 스크린샷 합성(그 프레임을 기기 목업 안에 넣고, 헤드라인과 배경을 더하고, 번역된 카피를 얹어 필요한 모든 크기로 내보내는 것)은 서로 다른 문제예요. 이 글에 나오는 모든 방법은 첫 번째 문제를 해결해요. 두 번째 문제는 그중 어느 것도 해결하지 않아요.

Fastlane을 굳이 건너뛰는 이유

Fastlane이 잘못됐다는 건 아니에요. 개발자가 네이티브 방식을 택하는 이유는 이념적이 아니라 실용적이에요.

  • 루프에 Ruby가 없어요. Fastlane은 자체 버전 변동(Bundler, gem 충돌, Ruby 버전업 후 가끔 발생하는 CI 장애)이 있는 Ruby gem이에요. 나머지 프로젝트가 순수 Swift라면 오직 이 하나의 작업을 위해 런타임 전체를 유지해야 해요.
  • 테스트 타겟은 이미 갖고 있어요. snapshot은 내부적으로 UI 테스트를 생성해요. 어차피 UI 테스트를 작성하고 있다면 스크린샷 캡처는 두 줄만 더하면 돼요 — 새 도구가 필요 없어요.
  • SwiftUI가 계산법을 바꿨어요. ImageRenderer를 쓰면 앱을 아예 실행하지 않고도 화면을 PNG로 렌더링할 수 있는데, 이건 Fastlane이 할 수 없는 일이에요 — Fastlane은 항상 시뮬레이터를 조작해요.
  • CI에서 움직이는 부품이 더 적어요. 스크린샷을 뽑아내는 평범한 xcodebuild test는, xcodebuild를 셸아웃으로 호출한 뒤 결과 번들을 후처리하는 Fastlane 레인보다 이해하기 쉬워요.

반대급부도 있어요. Fastlane은 성가신 부분(.xcresult 번들에서 첨부 파일을 모으고, 로케일 × 기기별로 정리하고, HTML 미리보기를 생성하고, deliver로 업로드하는 것)을 명령어 하나로 묶어줘요. 네이티브로 가면 그 수집 배관 작업 일부가 여러분 몫이 돼요. 이 트레이드오프가 결정의 전부이고, 의식적으로 판단할 가치가 있어요.

옵션 1 — XCUITest + XCUIScreenshot (실행 중인 앱 캡처)

이건 Fastlane 자체가 구축된 토대예요. UI 테스트 타겟이 시뮬레이터에서 실제 앱을 실행하고, 여느 UI 테스트와 같은 finder/tap API로 조작한 뒤, XCUIScreen.main이 네이티브 해상도로 전체 기기 화면을 캡처해요. 그 스크린샷을 XCTAttachment로 감싸고 lifetime = .keepAlways로 설정해 테스트가 통과해도 Xcode가 계속 보관하게 해요(기본값은 성공 시 첨부 파일을 버려요).

// ScreenshotUITests.swift  — runs in a UI test target
import XCTest

final class ScreenshotUITests: XCTestCase {
    func testCaptureCarouselScreens() {
        let app = XCUIApplication()
        // A launch argument your app reads to seed demo data + hide
        // anything that would make a noisy screenshot (debug banners, etc).
        app.launchArguments += ["-FASTLANE_SNAPSHOT", "NO", "-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Drive the app to the state you want, then capture the WHOLE screen.
        snapshot("01_Home")

        app.buttons["openLibrary"].tap()
        snapshot("02_Library")
    }

    private func snapshot(_ name: String) {
        // XCUIScreen.main captures the full device screen at native
        // resolution — exactly what App Store Connect wants.
        let shot = XCUIScreen.main.screenshot()
        let attachment = XCTAttachment(screenshot: shot)
        attachment.name = name
        attachment.lifetime = .keepAlways // or Xcode discards it when the test passes
        add(attachment)
    }
}

스크린샷은 .xcresult 번들 안에 들어가요. 실행 후 결과 번들을 Xcode에서 열어 드래그로 꺼내거나, CI에서는 번들을 훑어 이름 붙인 PNG를 폴더에 써주는 xcparse / xc-screenshot 같은 도구로 추출해요. 이 추출 단계가 정확히 Fastlane이 대신 자동화해주는 부분이고, 네이티브로 갈 때 떠안게 되는 주된 몫이에요.

코드 변경 없는 현지화. 여기서 깔끔한 요령은 Xcode 테스트 플랜이에요. 테스트 플랜 하나를 만들고 언어/지역별로 구성(각각 앱 언어 + 지역을 설정)을 추가하면, 같은 테스트가 구성마다 한 번씩 실행돼요. 테스트 코드를 건드리지 않고도 현지화된 스크린샷 — 각 로케일로 렌더링된 앱 UI — 을 얻어요. App Store Connect가 요구하는 해상도를 맞추려면 6.9인치 iPhone급 시뮬레이터와 13인치 iPad급 시뮬레이터에서 실행하세요.

장단점:

  • 실제 앱 픽셀. 폰트, 테마, 동적 데이터까지 모두 네이티브인 실제 실행 중인 뷰가 시뮬레이터의 정확한 해상도로 나와요.
  • Fastlane도, Ruby도 없어요. 순수 Xcode + Swift예요. 어떤 CI에서도 xcodebuild test로 실행돼요.
  • 로케일은 코드가 아니라 테스트 플랜 구성이에요. 언어별 구성을 추가하고 재실행하면 돼요. Swift에 로케일별 분기가 필요 없어요.
  • 추출은 여러분 몫이에요. .xcresult 번들에서 PNG를 꺼내는 건 직접 해야 해요(한 번 만들면 되는 스크립트지만, 실제 작업이에요).
  • 유지 비용. finder(buttons["openLibrary"], 탭)는 여느 UI 테스트처럼 UI가 바뀌면 깨져요.

옵션 2 — Swift Testing 첨부 파일 (더 현대적이고 가벼운 버전)

Xcode 26과 함께 나온 Apple의 매크로 기반 프레임워크인 Swift Testing은 XCTAttachment가 하던 일을 더 적은 격식과 더 산뜻한 호출 방식으로 해내는 Attachment API를 추가했어요. 여전히 XCUIApplication으로 앱을 조작하고 XCUIScreen.main.screenshot()으로 캡처하지만(이건 XCUITest에 속해요), 이미지는 @Test 함수 안에서 Attachment.record(...)로 기록해요. 첨부 파일 API는 이미지가 어떻게 만들어졌는지 신경 쓰지 않아요. 조건을 만족하는 이미지 타입이면 무엇이든 받아 테스트의 결과 번들에 기록해줘요.

// ScreenshotTests.swift  — Swift Testing (Xcode 26+)
import Testing
import XCTest // still needed for XCUIApplication / XCUIScreen

@MainActor
struct ScreenshotTests {
    @Test func homeAndLibrary() async throws {
        let app = XCUIApplication()
        app.launchArguments += ["-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Attachment.record attaches to THIS test's result bundle.
        let home = XCUIScreen.main.screenshot()
        Attachment.record(home.image, named: "01_Home.png")

        app.buttons["openLibrary"].tap()
        let library = XCUIScreen.main.screenshot()
        Attachment.record(library.image, named: "02_Library.png")
    }
}

기능적으로는 옵션 1과 동일한 실제 앱 픽셀을 캡처해요 — 내부적으로 같은 화면 캡처예요. 얻는 건 프로젝트가 이미 Swift Testing으로 옮겨갔을 때의 더 깔끔한 사용성(매개변수화된 테스트, 기본으로 async, XCTestCase 서브클래싱 불필요)이에요. 피할 수 없는 건 동일한 추출 단계예요. PNG는 여전히 결과 번들 안에 있고 업로드하려면 여전히 꺼내야 해요. 이미 Swift Testing을 쓰고 있다면 이 옵션을 쓰세요. 그렇지 않다면 결과는 옵션 1과 같아요.

옵션 3 — ImageRenderer (시뮬레이터 실행 자체가 없음)

이건 Fastlane이 구조적으로 제공할 수 없는 옵션이에요. ImageRenderer(SwiftUI, iOS 16+ / macOS 13+)는 SwiftUI 뷰를 받아 그 계층 구조를 곧바로 UIImage / CGImage로 렌더링해요 — 앱 실행도, 시뮬레이터 UI 자동화도, 깨질 finder도 없어요. App Store 픽셀 크기 그대로에 맞춘 뷰를 넘기면 PNG 바이트를 그대로 돌려받아요.

// Render a SwiftUI view straight to a PNG — no simulator UI run.
import SwiftUI

@MainActor
func renderScreen() -> Data? {
    let view = HomeScreen()                 // your real SwiftUI screen
        .frame(width: 1320, height: 2868)   // 6.9-inch iPhone, exact pixels at 1x

    let renderer = ImageRenderer(content: view)
    renderer.scale = 1                       // size is already in target pixels
    renderer.isOpaque = true                 // App Store rejects alpha channels

#if canImport(UIKit)
    return renderer.uiImage?.pngData()
#else
    guard let cg = renderer.cgImage else { return nil }
    let rep = NSBitmapImageRep(cgImage: cg)
    return rep.representation(using: .png, properties: [:])
#endif
}

스토어용 출력에서는 두 가지가 중요해요. isOpaque = true로 설정하세요 — App Store Connect는 알파 채널이 있는 스크린샷을 거절하는데, 불투명 렌더링이 이를 막아줘요. 그리고 scale에 신경 쓰세요. 대상 픽셀이 아니라 포인트 단위로 뷰 크기를 잡았다면 출력이 흐려지지 않도록 renderer.scale을 기기 스케일에 맞춰 설정하고, 위처럼 프레임을 정확한 대상 픽셀로 잡았다면 스케일을 1로 유지하세요.

가장 큰 주의점은 ImageRenderer렌더링할 수 없는 것들이에요. SwiftUI는 렌더링하지만 UIKit/AppKit 기반 뷰는 렌더링하지 않아요. 웹뷰, 지도 뷰, 미디어 플레이어, 카메라 프리뷰, 일부 시스템 컨트롤은 플레이스홀더로 나와요. 깔끔하고 결정론적인 SwiftUI 화면이라면 훌륭해요 — 즉시, 헤드리스로, 완벽한 크기로, 온디바이스를 포함해 어디서나 실행돼요. 플랫폼 뷰가 포함된 화면이라면 실제 컴포지터 출력을 캡처하기 위해 다시 옵션 1~2로 돌아가야 해요.

  • 단연 가장 빨라요. 이미지당 밀리초 단위. 시뮬레이터 부팅도, UI 조작도 없어요. CI에 잘 맞아요.
  • 정확한 크기를, 손쉽게. 프레임을 1320 × 2868(또는 필요한 아무 크기)로 잡으면 그게 곧 출력이에요 — "App Store 크기로 리사이즈" 같은 추측이 필요 없어요.
  • 상태를 직접 구성해요. 실행 중인 앱이 없으니 모의 데이터를 뷰에 직접 전달해요 — 결정론적이고, 데모용 실행 인자가 필요 없어요.
  • SwiftUI 전용이에요. 플랫폼 기반 뷰는 플레이스홀더로 렌더링돼요. 화면을 신뢰하기 전에 확인하세요.

옵션 4 — 위 방법들을 감싼 오픈소스

인접한 문제를 해결하는 오픈소스 프로젝트 두 개가 끊임없이 언급돼요.

pointfree/swift-snapshot-testing은 가장 인기 있는 SwiftUI 스냅샷 라이브러리인데, 그 목적을 정확히 짚어둘 필요가 있어요. 이건 스토어 에셋 제작이 아니라 리그레션 테스트를 위해 만들어졌어요. 첫 실행은 기준 PNG를 기록하고 일부러 실패하며, 이후 실행은 그 기준과 비교(diff)해 여백이나 폰트 변경으로 UI가 어긋난 날을 잡아내요.

// pointfree/swift-snapshot-testing — built for regression, not store assets.
import SnapshotTesting
import SwiftUI
import Testing

@MainActor
struct HomeSnapshotTests {
    @Test func home_iPhone16ProMax() {
        let view = HomeScreen()
        // First run records the reference PNG and FAILS on purpose;
        // later runs diff against it. To re-record: record: .all
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
    }
}

기록된 기준 이미지를 원본 스크린샷으로 재활용할 수는 있고, .image(layout: .device(...)) 전략은 실제 기기 크기로 렌더링해요 — 하지만 그건 이 도구의 엄격함과 부딪혀요(픽셀 완벽 일치를 요구하기 때문에 기기마다 다른 안티앨리어싱 차이가 불안정한 실패를 일으켜요). UI 리그레션을 막는 용도로 쓰고, 주력 스크린샷 파이프라인으로는 쓰지 마세요.

AppScreenshotKit(shitamori1272 제작)은 이 작업을 위해 목적에 맞게 만들어졌어요. 기기 클래스와 로케일을 지정하는 @AppScreenshot 매크로로 SwiftUI 뷰를 선언하고, 실제 화면을 DeviceView로 감싸 Apple 기기 프레임 안에 렌더링되게 한 뒤, Swift Testing 함수에서 내보내요. 결과물은 깔끔한 Screenshots/<locale>/<device>/ 트리로 정리돼요.

// AppScreenshotKit — declarative SwiftUI screenshots, runs under Swift Testing.
import AppScreenshotKit
import SwiftUI

@AppScreenshot(.iPhone69Inch(), options: .locale([Locale(identifier: "en_US")]))
struct HomeShot: View {
    var body: some View {
        DeviceView { HomeScreen() } // your real screen, wrapped in a device frame
    }
}

// In a test target:
import AppScreenshotKitTestTools
import Testing

@Test @MainActor
func exportScreens() throws {
    let out = URL(fileURLWithPath: "Screenshots")
    let exporter = AppScreenshotExporter(option: .file(outputURL: out))
    try exporter.export(HomeShot.self) // → Screenshots/en_US/iPhone_6_9_inch/...
}

이 영역에서 알아둘 만한 관련 도구도 있어요. storescreens 같은 CLI는 설정 파일 하나로 App Store Connect 파이프라인 전체(여러 시뮬레이터를 병렬로 돌리는 XCUITest 캡처, 캡션이 있는 프레임 렌더링, Apple API를 통한 메타데이터 업로드)를 다뤄요. 단일하고 멱등적이며 CI 친화적인 체인을 원한다면 정말 유용해요. 다만 캡션 레이아웃에 대한 나름의 견해도 함께 딸려오는데, 이 부분이 바로 마케팅 표면에서는 전용 비주얼 에디터가 이기는 경향이 있는 이음매예요.

위 방법 중 어느 것도 하지 않는 부분: 합성 + 50개 언어 현지화

여기서부터는 경계를 명확히 한 솔직한 대목이에요. 위의 모든 방법은 날것의 프레임을 만들어요 — 기기 해상도의 앱 UI(또는 AppScreenshotKit류라면 평범한 기기 목업 안에 담긴 프레임)예요. 전환을 일으키는 App Store 캐러셀은 날것의 프레임이 아니에요. 스타일이 입혀진 기기 목업 안에, 배경 위에, 짧은 헤드라인 아래에, 흔히 여러 패널로 이어지는 시퀀스로, 타겟으로 삼는 모든 로케일에 걸쳐 반복되는 게 캐러셀이에요. 그 합성 단계가 바로 Mokbi의 역할이에요. XCUITest, ImageRenderer, 시뮬레이터 등 어디서 캡처했든 그 프레임을 가져와 브라우저 에디터에 드롭하고, 기기 프레임과 캡션과 배경을 더한 뒤, 최대 50개 App Store 언어로 캡션을 대략 클릭 한 번에 번역하고, 필요한 모든 크기로 일괄 내보내기해요. 워터마크가 있는 미리보기로 디자인은 무료이고, 워터마크 없는 무제한 내보내기와 스토어 게시는 구독으로 제공돼요 — Solo €29.99/mo(앱 1개) 또는 Studio €49.99/mo(앱 최대 5개), 일회성 구매는 없어요. 경계를 분명히 하자면 Mokbi는 원본 스크린샷을 캡처하지 않아요 — 앱을 실행하지도, 뷰를 렌더링하지도 않아요. 캡처는 위의 네이티브 Swift 툴링 몫이고, Mokbi는 그 툴링이 만들어낸 결과물을 합성하고 현지화해요.

현실적인 SwiftUI 통합 워크플로

  1. 소스 프레임을 네이티브로 캡처하세요. 결정론적인 SwiftUI 화면이라면 ImageRenderer로 정확한 픽셀 크기로 렌더링하세요(옵션 3) — 가장 빠르고 시뮬레이터가 필요 없어요. 플랫폼 뷰가 있는 화면이거나 실제 실행 상태를 원한다면 XCUITest(옵션 1)나 Swift Testing(옵션 2) 캡처를 작성하고 6.9인치 iPhone급과 13인치 iPad급 시뮬레이터에서 실행하세요.
  2. 필요하다면 캡처를 현지화하세요. 언어별로 테스트 플랜 구성을 추가해 각 로케일로 앱 UI가 렌더링되게 하거나, ImageRenderer에 넘기는 뷰에 로케일을 전달하세요. 이건 앱 픽셀을 현지화하는 거예요 — 마케팅 헤드라인이 아니에요.
  3. 마케팅 캐러셀을 합성하세요. 프레임을 Mokbi에 드롭하고, 스타일이 입혀진 프레임/캡션/배경을 더해 멀티 패널 시퀀스를 만드세요. 이건 네이티브 툴링이 할 수 없는 단계예요.
  4. 캡션을 번역하고 내보내세요. 헤드라인 카피를 타겟 App Store 로케일 전체에 걸쳐 원클릭으로 번역한 뒤, 필요한 모든 크기를 일괄 내보내기하세요.
  5. 다음 UI 변경 시 다시 캡처하고 다시 열어보세요. 렌더러나 UI 테스트를 다시 실행하고, 저장된 Mokbi 프로젝트를 다시 열어 프레임을 교체한 뒤 다시 내보내세요. 합성과 번역은 그대로 유지돼요.

자동화 자체를 건너뛸 수 있는 경우

연 1~3회 출시한다면 어떤 스크린샷 하니스든 — 네이티브든 Fastlane이든 — 구축 비용이 절약되는 시간보다 항상 더 커요. iOS 시뮬레이터를 알맞은 기기 클래스로 실행하고, 각 화면으로 이동해 시뮬레이터의 스크린샷 명령(정확한 기기 해상도로 캡처해요)을 실행하면 10분 만에 App Store용 PNG가 완성돼요. 그런 다음 한 번 합성하고 현지화한 뒤 넘어가세요. 위의 자동화는 「직접 다시 찍기」가 더 이상 10분짜리 작업이 아니게 될 때 — 잦은 출시, 많은 로케일, 여러 앱으로 이루어진 포트폴리오일 때 — 정확히 그 값어치를 해요. 장비를 출시 주기에 맞추세요, 그럴듯해 보이는지에 맞추지 말고요. 다른 플랫폼에서도 같은 논리가 적용돼요. React Native 버전의 같은 문제와 더 넓은 Fastlane vs 노코드 트레이드오프를 참고하세요.

무엇으로 캡처하든 내보내기 전에 대상을 확인하세요. 정확한 픽셀 크기와 포맷 규칙(PNG/JPEG, RGB, 알파 채널 없음)은 App Store 스크린샷 크기 가이드에서 확인할 수 있어요. Apple은 픽셀 하나만 틀려도 예외 없이 거절하니 30초를 들일 가치가 있어요.

다음으로 읽을 내용

에디터 열기 →