본문으로 건너뛰기

PaperConfetti: 렌더러 실험 뒤에도 Swift 패키지 API를 작게 유지하기

Keeping a Swift package’s API small after experimenting with several renderers (PaperConfetti)

www.theswift.dev

핵심 요약

  1. PaperConfetti 0.2.0은 Core Animation의 CAEmitterLayer로 재생을 처리하며, flat과 3D를 별도 렌더러가 아닌 하나의 엔진에서 제공한다.
  2. 레이아웃 변화로 이미 발사된 조각이 움직이지 않도록 위치를 한 번 캡처해 안정적인 overlay에 배치하고, 취소·Reduce Motion·화면 이탈 동작도 관리한다.
  3. CPU 사용량만으로 부드러운 애니메이션을 판단하지 않는다. 실제 기기 측정과 누락 입자를 감지하는 테스트를 함께 사용해 렌더링 결과와 인터페이스 동작을 확인했다.

도입

Wordie의 승리 연출은 다섯 개의 cannon에서 총 180개 조각을 발사했지만, 레이아웃이 바뀌는 순간 이미 발사된 종이가 튀는 문제와 간헐적인 끊김이 있었다. 이를 개선하며 만든 PaperConfetti는 iOS 16 이상을 대상으로 하는 Swift 패키지다. 저자는 여러 렌더링 방식을 실험한 뒤 Core Animation 기반 구현을 선택하고, 호출 API는 pop()을 중심으로 작게 유지했다.

상세 요약

패키지 사용 방식과 적용 범위

PaperConfetti는 iOS 16 이상용 Swift 패키지다. Xcode의 Add Package Dependencies에서 패키지를 추가하고 0.2.0을 선택한 뒤, 앱 target에 PaperConfetti product를 포함한다. SwiftUI에서는 overlay마다 @StateObject로 ConfettiController를 하나 만들고 .confettiOverlay(confetti)를 적용한다. 기본 pop()은 컨테이너 아래 중앙에서 80개 조각을 요청한다. configuration으로 flat 외형을 고르고, cancel()로 중단할 수 있다. 새 pop()은 진행 중인 burst를 대체한다.

SwiftUI wrapper는 터치 입력을 통과시키고 Reduce Motion을 존중한다. scene이 비활성화되거나 overlay가 화면에서 사라지면 재생을 취소하며, 돌아왔을 때 취소된 연출을 다시 재생하지 않는다. UIKit에서는 ConfettiLayerView를 직접 쓸 수 있지만 Reduce Motion과 scene 처리 정책은 앱이 맡아야 한다. 렌더링은 iOS와 iPadOS 대상이며, macOS target은 테스트용 physics model만 제공한다.

좌표와 렌더러를 단순화하기

Wordie의 마지막 두 턴에는 행이 추가되고, 승리 시 키보드와 결과 레이아웃이 바뀐다. 움직이는 글자 셀에 cannon을 붙이면 이미 발사된 조각까지 함께 점프할 수 있다. 해결책은 당첨 글자의 좌표를 한 번 캡처하고, 발사 위치와 overlay에 같은 좌표계를 사용하며, overlay를 레이아웃 애니메이션 바깥에 두는 것이다. 좌표를 캡처해도 해당 좌표계 자체가 움직이면 문제가 남는다.

구현 전 per-piece SwiftUI animation, Canvas, Core Animation layers, emitter particles, SpriteKit 변형을 실험했다. 최종 0.2.0은 발사 한 번마다 궤적, 공유 texture, 제한된 emitter layer와 animation 트리를 준비하고 Core Animation이 재생한다. 조각별 SwiftUI view나 매 프레임 모든 입자를 갱신하는 app display-link callback, 입자별 timer는 두지 않는다. flat과 3D는 이 엔진의 외형 설정이다. 실험한 렌더러를 모두 공개 API 선택지로 남기지 않았다.

조각 움직임과 발사 신뢰성

패키지는 별, 휘어진 streamer, 속이 빈 ring 등 13개 실루엣을 제공한다. Core Graphics path에서 만든 작은 CGImage texture를 공유하며, 조각마다 초기 방향과 움직임을 달리해 같은 모양이 한꺼번에 같은 면을 보이거나 같은 속도로 떨어지는 현상을 줄인다. 상승은 정규화한 진행도 u에 대해 2u − u², 하강은 v²를 따른다. 꼭대기에서 명시적으로 멈추는 구간은 없다. 조각별 낙하 속도 경향과 하강 시간 ±20% 변화, 도달 거리와 방향의 seeded 변화를 적용한다. 이는 공기역학 시뮬레이션이 아니라 연출을 위한 모델이며, 반경은 엄격한 경계가 아니다.

초기 emitter 구현은 5,760개를 요청했을 때 첫 cannon이 보이지 않는 경우가 있었다. 짧은 birth window가 큰 layer 트리를 render server가 방문하기 전에 끝날 수 있었고, 객체 생성만으로 입자 표시를 보장할 수 없었다. birthRate는 일정 시간당 발생률이지, 정확한 개수를 즉시 그리는 명령이 아니다. 수정 버전은 모든 cannon을 공유된 일시 정지 clock 아래 준비한 뒤 여유 시간을 두고 함께 시작한다. cannon delay는 별도의 짧은 발사 창이 아니라 화면에 보이는 비행 시점을 조절한다.

테스트에는 일부러 오래된 발사 창을 만료시키는 negative control을 넣었다. 테스트가 누락 영역을 감지하고, 동시에 정상 기준 영역은 표시되는지 확인해야 빈 화면이 통과 결과로 오인되지 않는다. 최종 구현은 simulator pixel 검사와 실제 iPhone의 세 가지 발사 회귀 검사를 통과했다. 이는 시험한 설정에 대한 결과이지 정확한 실시간 입자 수의 보장은 아니다.

성능 결과와 인터페이스 검증

10월 29일 측정은 iPhone 18 Pro Max, iOS 27.0.1에서 Release 빌드로 진행했다. 180개 burst에서 flat/3D CPU는 각각 106.2/110.2ms, 720개에서는 162.4/153.0ms, 5,760개에서는 369.5/394.2ms였다. 이는 약 6초 시험 동안의 앱 전체 누적 CPU 시간이며 frame time이나 GPU 비용이 아니다. 한 기기의 작은 차이만으로 외형별 속도 우위를 정하지 않았다.

Animation Hitches 측정에서는 준비 후 burst 두 번씩을 합산해 1,440개까지 hitch가 기록되지 않았다. 5,760개에서는 flat 225.0ms, 3D 145.8ms의 누적 hitch가 기록됐고, 가장 긴 개별 hitch는 각각 50.0ms와 16.7ms였다. 측정은 FPS나 Apple의 animation-active hitch-time ratio가 아니다. 메모리는 자연 완료와 취소를 번갈아 시험한 짧은 실행에서, warm-up 후 보관한 다섯 샘플 동안 앱 footprint의 증가 추세가 없었다. render server/GPU 메모리 전체나 모든 상황의 누수 부재를 입증하는 결과는 아니다.

화면 녹화에는 추가 부하가 생기므로 영상은 시각적 비교로, 계측 결과는 성능 근거로 구분했다. 앱에서도 overlay가 활성화된 상태의 탭·전환·스크롤, 부모 레이아웃 변경, 화면 이탈과 취소, Reduce Motion, 반복 실행 후 정리를 확인해야 한다. 실제 UI 검사 14건에는 터치 통과와 취소, 발사 회귀 테스트가 포함됐다. SwiftUI 업데이트 원인을 Instruments로 추적하는 접근은 Xcode Instruments에서 SwiftUI 뷰 다시 그리기와 원인 추적하기에서도 다룬다.

결론

PaperConfetti의 핵심은 렌더러 선택지를 계속 늘리는 대신, 하나의 재생 엔진과 간결한 API를 유지하면서 좌표 안정성·입력 통과·취소·누락 입자 검증을 챙긴 데 있다. 측정에서도 CPU 시간, hitch, 메모리를 서로 다른 지표로 다루고, 화면 녹화의 영향을 성능 근거와 분리했다. 다만 제시된 결과는 특정 기기와 시험 설정의 관찰이며, 모든 환경의 성능이나 정확한 실시간 입자 수를 보장하지 않는다.