앱 출시를 앞두고 스토어 스크린샷을 만들어야 하는 상황이 됐다. Figma로 하나씩 만드는 건 너무 비효율적이고, 10가지 디자인 시안에 5개 기능 화면을 조합하면 50장인데 수작업으로 한다는 건 말이 안 된다.

그래서 HTML/CSS로 마케팅 프레임을 만들고 Puppeteer로 PNG를 뽑아내는 파이프라인을 짰다. 처음엔 대충 만들었다가 구조적으로 틀린 부분이 있다는 걸 나중에 깨달았는데, 그 과정이 의외로 중요한 교훈을 남겼다.


처음 접근 방식의 문제

처음엔 이런 식으로 기능 배열을 하드코딩했다.

const FEATURES = [
  { id: 'expense', title: '지출을 한번에', sub: 'AI가 영수증을 읽어드립니다', screen: '02_expense_detail' },
  { id: 'camera',  title: '사진 찍으면 끝', sub: 'OCR로 즉시 기록', screen: '03_camera_hub' },
  // ...
];

문제는 이 파일명들(02_expense_detail.png, 03_camera_hub.png)이 실제로 존재하지 않았다는 것이다. 캡처 스크립트가 자동으로 화면을 이동하다가 실패해서 홈 화면만 5번 찍혔는데, 파일명은 다 달랐다. 결과적으로 모든 슬롯이 placeholder(검은 화면)였다.

md5 screenshots/raw/*.png
# 01_home_trip_list.png: 1f477573e7f94a08f86c771c730fa120
# 02_trip_detail.png:    1f477573e7f94a08f86c771c730fa120  ← 동일
# 03_expense_tab.png:    1f477573e7f94a08f86c771c730fa120  ← 동일
# 04_camera_hub.png:     1f477573e7f94a08f86c771c730fa120  ← 동일

MD5가 전부 같다. 같은 홈 화면을 4번 찍어서 파일명만 다르게 저장된 것이다.


진짜 문제: 순서가 거꾸로였다

단순히 파일명이 틀린 게 아니라 워크플로우 자체가 거꾸로였다.

잘못된 순서:

기능/문구 먼저 결정 → 파일명 맞추려 시도 → 없으면 placeholder

올바른 순서:

스크린샷 자유롭게 캡처 → 화면 분석 → 그 화면에 맞는 문구 결정 → 렌더링

스크린샷을 보기 전에 문구를 정하면 나중에 억지로 끼워맞추게 된다. 실제 앱 화면이 어떻게 생겼는지, 어떤 정보를 담고 있는지를 먼저 봐야 “지출을 한번에"가 맞는 표현인지 “카테고리별 자동 분류"가 맞는지 판단할 수 있다.


features.json 분석 주도 방식

이걸 해결하기 위해 features.json을 중간 단계로 추가했다.

캡처 → features.json 작성 → 렌더링

features.json은 단순한 설정 파일이 아니라 화면 분석의 결과물이다. 화면을 직접 보고, 어떤 기능을 가장 잘 보여주는지 판단한 뒤에 작성한다.

[
  {
    "id": "expense",
    "title": "지출을 한번에",
    "sub": "카테고리별 자동 분류",
    "screen": "expense_list"
  },
  {
    "id": "trip_detail",
    "title": "여행의 모든 것",
    "sub": "일정·지출·정산이 한 화면에",
    "screen": "trip_detail"
  }
]

screen 필드는 screenshots/raw/ 안의 실제 파일명(확장자 제외)을 그대로 쓴다. 파일이 없으면 placeholder가 뜨기 때문에 즉시 확인 가능하다.

렌더링 스크립트는 이 파일을 읽어서 처리한다.

const FEATURES_CFG = path.join(ROOT, 'screenshots/features.json');
const FEATURES = JSON.parse(fs.readFileSync(FEATURES_CFG, 'utf8'));

// 매핑 검증
FEATURES.forEach(f => {
  const p = path.join(RAW_DIR, `${f.screen}.png`);
  if (!fs.existsSync(p)) {
    console.warn(`⚠️  [${f.id}] 파일 없음: ${f.screen}.png → placeholder 사용`);
  }
});

전체 파이프라인 구조

screenshots/
├── raw/           # 원본 캡처 PNG (아무 이름이나 가능)
├── features.json  # 화면 분석 결과 (화면파일 + 마케팅 문구)
├── marketing/     # 최종 출력 PNG (1320×2868, 스토어 업로드용)
└── templates/     # 중간 HTML 파일

1단계: 캡처

시뮬레이터에서 화면을 자유롭게 캡처한다. 파일명에 규칙은 없다. 나중에 features.json에서 연결하면 되기 때문이다.

xcrun simctl io D06F2A40-B4E8-4BAE-9151-CF38EE189469 screenshot screenshots/raw/expense_list.png
xcrun simctl io D06F2A40-B4E8-4BAE-9151-CF38EE189469 screenshot screenshots/raw/trip_detail.png

캡처 전에 상태바를 정리해두면 더 깔끔한 스크린샷이 나온다.

xcrun simctl status_bar booted override \
  --time '9:41' \
  --dataNetwork 'wifi' \
  --batteryState 'charged' \
  --wifiBars 3

2단계: 화면 분석 후 features.json 작성

raw/ 폴더를 열어서 각 PNG가 어떤 화면인지 확인한다. 그리고 그 화면이 전달할 수 있는 가장 임팩트 있는 문구를 결정한다.

좋은 마케팅 문구의 기준:

  • 기능 나열이 아닌 혜택 중심: “시간 추적” → “하루 2시간 절약”
  • 2025년 이후 Apple이 스크린샷 텍스트를 검색 메타데이터로 인덱싱하기 때문에 키워드 포함이 중요해졌다
  • 제목은 7단어 이내, 한눈에 읽히는 길이

3단계: 렌더링

node scripts/screenshots/render_templates.js

10가지 디자인 스타일 × features.json의 슬롯 수 = 출력 PNG 수

현재 5개 슬롯이면 50개 PNG가 marketing/ 폴더에 저장된다.


HTML/CSS 마케팅 프레임 구조

각 PNG는 순수 HTML/CSS로 만들어진 1320×2868px 페이지를 Puppeteer가 캡처한 것이다.

<!-- 1320×2868px 캔버스 -->
<body style="width:1320px; height:2868px; background: {style.bg}">

  <!-- 헤더: 배지 + 큰 제목 + 부제목 -->
  <div class="header">
    <div class="badge">AppName</div>          <!-- accent 색 테두리 -->
    <div class="title">{feature.title}</div>   <!-- 116px, 900weight -->
    <div class="subtitle">{feature.sub}</div>  <!-- 52px, 400weight -->
  </div>

  <!-- 디바이스 목업 -->
  <div class="phone-frame">
    <div class="phone-screen">
      <img src="file:///path/to/raw/screenshot.png">
    </div>
  </div>

  <!-- 푸터 -->
  <div class="footer">
    <div class="app-name">AppName</div>
    <div class="store-badge">✈️ AI 여행 기록 앱</div>
  </div>

</body>

폰 프레임 CSS:

.phone-frame {
  width: 640px;
  height: 1385px;
  background: #0a0a0a;
  border-radius: 72px;
  padding: 20px;
  box-shadow: 0 0 60px rgba(32,178,170,0.4), 0 40px 80px rgba(0,0,0,0.5);
}
.phone-screen {
  width: 100%;
  height: 100%;
  border-radius: 56px;
  overflow: hidden;
}
.phone-screen img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
}

Puppeteer 렌더링 핵심 설정

const browser = await puppeteer.launch({
  headless: 'new',
  args: ['--no-sandbox', '--font-render-hinting=none']
});

const page = await browser.newPage();
await page.setViewport({ width: 1320, height: 2868, deviceScaleFactor: 1 });

// networkidle0: 폰트, 이미지 완전 로드 후 캡처
await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle0' });

await page.screenshot({ path: outPath, fullPage: false });

waitUntil: 'networkidle0'이 중요하다. Google Fonts가 로드되기 전에 캡처하면 시스템 폰트로 렌더링돼서 디자인이 깨진다.

deviceScaleFactor: 1로 설정하면 1320px 그대로 출력된다. 2로 올리면 2640px로 나오지만 파일 크기가 4배가 되므로 앱스토어 제출 규격(10MB 이하)에 맞게 조정 필요.


10가지 디자인 스타일

앱 주색에 따라 스타일을 고르면 된다.

스타일 ID이름배경맞는 앱 장르
dark_glass다크 글라스모피즘다크 + 청록 글로우금융, 테크, 피트니스
clean_white클린 화이트밝은 회백색생산성, 헬스
teal_gradient틸 그라디언트네이비 → 청록여행, 소셜
midnight_blue미드나이트 블루딥 네이비 + 퍼플명상, 수면, 금융
warm_sunset워밍 선셋퍼플 → 레드 → 오렌지여행, 음식, 라이프스타일
forest_dark포레스트 다크다크 그린건강, 환경
aurora오로라다크 퍼플 + 그린AI, 명상, 프리미엄
bold_black볼드 블랙순수 블랙패션, 음악, 카메라
soft_lavender소프트 라벤더밝은 라벤더웰니스, 다이어리
ocean_depth오션 딥딥 오션 블루여행, 스포츠

스타일 선택 시 한 가지 함정이 있다. 앱 UI가 밝은 베이지/화이트 배경이면 다크 배경과 극단적으로 대비된다. 프레임이 보이지 않거나 앱 화면이 너무 튀어 보인다.

해결 방법:

/* frameBg를 살짝 밝게 올려서 배경과 분리 */
.phone-frame {
  background: #1a2a2a;  /* #0a0a0a 대신 */
  box-shadow: 0 0 0 1px rgba(255,255,255,0.1),
              0 0 60px rgba(32,178,170,0.4),
              0 40px 80px rgba(0,0,0,0.5);
}

또는 앱 UI가 밝으면 clean_whitesoft_lavender 같은 밝은 배경 스타일을 선택하는 게 낫다.


앱스토어 업로드 규격 (2026 기준)

2024년 9월 iPhone 16 출시 이후 규격이 바뀌었다.

플랫폼슬롯픽셀필수 여부
iOSiPhone 6.9”1320×2868 또는 1290×2796필수
iOSiPad 13”2064×2752필수 (iPad 앱)
iOSiPhone 6.5"1284×2778선택 (6.9" 있으면 자동 스케일)
AndroidPhone1080×1920필수
AndroidFeature Graphic1024×500필수

핵심 변화: 6.9인치 1장만 제출하면 6.5", 6.1", 5.5" 전부 자동 스케일 다운 처리된다. 즉 iPhone용은 1320×2868 하나면 충분하다.

현재 파이프라인이 정확히 1320×2868로 출력하고 있으므로 iOS는 바로 업로드 가능하다. Android용은 별도로 1080×1920 캔버스를 만들어야 한다.


2025-2026 스크린샷 트렌드 반영

조사하면서 알게 된 중요한 변화가 하나 있다.

2025년 6월부터 Apple이 스크린샷의 텍스트를 검색 인덱스에 포함시켰다. 스크린샷 위에 올라가는 문구가 이제 직접 검색 순위에 영향을 미친다. features.json에 문구를 작성할 때 단순한 마케팅 카피가 아니라 검색 키워드 전략과 연동해야 한다는 뜻이다.

그 외 2025-2026 트렌드:

  • 첫 3장이 전체 engagement의 83% → 핵심 가치 제안을 앞쪽 슬롯에 배치
  • 고대비 색상(Teal, Coral, Neon 그라디언트)이 스크롤 중 주목도 높음
  • 디바이스 프레임 없이 raw UI만 올리면 미완성처럼 보임

화면 추가할 때

새 스크린샷이 생기면 세 단계로 처리한다.

# 1. raw/ 에 복사
cp ~/Desktop/new_screen.png screenshots/raw/

# 2. features.json 에 항목 추가
# { "id": "new_id", "title": "임팩트 제목", "sub": "한줄 설명", "screen": "new_screen" }

# 3. 재렌더링
node scripts/screenshots/render_templates.js

features.json 항목 수가 늘어나는 만큼 마케팅 PNG 수가 늘어난다. 6개 슬롯이면 60장.


정리

처음엔 단순히 “스크린샷 파일명이 틀렸다"는 문제인 줄 알았는데, 파고드니 워크플로우 설계 자체가 거꾸로였다. 문구를 먼저 정하고 화면을 끼워맞추려 하면 실제 화면이 문구와 안 맞는 상황이 반드시 생긴다.

분석이 먼저, 문구는 나중. features.json이 그 중간 단계로서 화면을 직접 보고 판단한 결과를 담는 역할을 한다.

파이프라인 전체를 코드로 보고 싶다면 render_templates.jscapture_sim.sh를 참고하면 된다. Puppeteer만 있으면 어떤 앱이든 동일한 구조로 재사용 가능하다.