Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
5594c1f
chore: UI 렌더링 성능 측정 인프라 준비 - #308
clxxrlove May 17, 2026
f8e1ddc
test: 콜드 런치와 스크롤 성능 측정 블록 추가 - #308
clxxrlove May 17, 2026
56b5b63
test: 콜드 런치 지표를 XCTClockMetric으로 교체 - #308
clxxrlove May 17, 2026
78215b4
docs: Phase 3 기준 성능 지표 정리 - #308
clxxrlove May 17, 2026
883b9af
refactor: HomeCoordinatorView IfLetStore 제거 - #308
clxxrlove May 17, 2026
f85c65b
refactor: HomeView IfLetStore 제거 - #308
clxxrlove May 17, 2026
1c5e397
refactor: GoalDetailView IfLetStore 제거 - #308
clxxrlove May 17, 2026
e786fc9
refactor: StatsCoordinatorView IfLetStore 제거 - #308
clxxrlove May 17, 2026
f8456e8
test: Home 상태 변경 지연 측정 시나리오 추가 - #308
clxxrlove May 17, 2026
97ffee7
test: Home 토스트 프로브 시나리오 추가 - #308
clxxrlove May 17, 2026
ba8b790
docs: Home 프로브 측정 기준 정리 - #308
clxxrlove May 17, 2026
24e93f6
test: Home 프로브 하네스 실행 조건 분리 - #308
clxxrlove May 18, 2026
5fabeb2
test: Home 피드 스크롤 렌더링 드라이버 추가 - #308
clxxrlove May 18, 2026
0fbd8f8
docs: Instruments 드라이런 결과 정리 - #308
clxxrlove May 18, 2026
d1aa316
test: Home 피드 스크롤 렌더링 드라이버 개선 - #308
clxxrlove May 18, 2026
e7c4423
docs: Pass 3 렌더링 기준선 정리 - #308
clxxrlove May 18, 2026
382bde1
test: Home 캘린더 주차 스와이프 렌더링 드라이버 추가 - #308
clxxrlove May 18, 2026
cbf1ca1
fix: Home 피드 스크롤 드라이버 좌표 보정 - #308
clxxrlove May 18, 2026
556132c
test: GoalDetail 리액션 렌더링 시나리오 추가 - #308
clxxrlove May 18, 2026
b0d43ed
test: ProofPhoto 업로드 전 렌더링 시나리오 추가 - #308
clxxrlove May 18, 2026
36766b2
test: Stats 렌더링 시나리오 추가 - #308
clxxrlove May 18, 2026
af07cc4
docs: Pass 3 기준선 수집 계획 고정 - #308
clxxrlove May 18, 2026
d6482c9
refactor: HomeView 읽기 범위 분리 - #308
clxxrlove May 18, 2026
328a288
chore: GoalDetail 예제 사용자 상태 분기 - #308
clxxrlove May 18, 2026
261fe7d
fix: GoalDetail 유휴 TimelineView 실행 방지 - #308
clxxrlove May 18, 2026
aae16d3
docs: GoalDetail TimelineView 개선 측정 기록 - #308
clxxrlove May 18, 2026
766a6c3
docs: GoalCardView 조사 및 스킵 결정 기록 - #308
clxxrlove May 18, 2026
e9b6e45
docs: Pass 3 최종 리포트 정리 - #308
clxxrlove May 18, 2026
5d507fa
fix: 측정용 의존성에 대해 Configuration 분리 - #308
clxxrlove May 19, 2026
b8449de
chore: Firebase 의존성을 SPM 미러 저장소로 변경 - #302
clxxrlove May 15, 2026
cd989de
test: ProofPhoto 대용량 이미지 렌더링 시나리오 추가 - #310
clxxrlove May 19, 2026
4cfabd0
refactor: ProofPhoto 미리보기 디코딩 위치 개선 - #310
clxxrlove May 19, 2026
a9eea07
docs: ProofPhoto 이미지 파이프라인 Pass 4 결과 정리 - #310
clxxrlove May 20, 2026
fb83216
docs: SwiftUI Template 앱 전역 감사 기록 - #310
clxxrlove May 20, 2026
8f17d45
docs: TXCalendarDateCell 측정 결과 정리 - #310
clxxrlove May 20, 2026
b7a791f
docs: GoalDetailView 측정 결과 정리 - #310
clxxrlove May 20, 2026
79b6393
test: SwiftUI Template 셀프런 입력 하네스 검증 - #310
clxxrlove May 20, 2026
0b19112
docs: Home 셀프런 스크롤 측정 계획 작성 - #310
clxxrlove May 20, 2026
fde7d41
test: Home 셀프런 스크롤 하네스 추가 - #310
clxxrlove May 20, 2026
0c0da63
refactor: GoalCardView 외곽선 렌더링 중복 제거 - #310
clxxrlove May 20, 2026
bf15856
docs: Home 외곽선 렌더링 개선 결과 정리 - #310
clxxrlove May 20, 2026
b6297f0
docs: Home 셀프런 스크롤 최적화 종료 - #310
clxxrlove May 20, 2026
16330c4
docs: Stats 셀프런 스크롤 측정 계획 작성 - #310
clxxrlove May 20, 2026
687901c
docs: Stats 스탬프 그리드 원인 분석 정리 - #310
clxxrlove May 20, 2026
3026b03
docs: Stats 스탬프 그리드 개선 계획 작성 - #310
clxxrlove May 20, 2026
caa26be
test: Stats 셀프런 스크롤 하네스 추가 - #310
clxxrlove May 20, 2026
da2278e
refactor: Stats 스탬프 그리드 행 레이아웃 적용 - #310
clxxrlove May 20, 2026
ac1f73c
docs: Stats 스탬프 그리드 개선 되돌림 결정 기록 - #310
clxxrlove May 20, 2026
aa4a160
refactor: Stats 스탬프 그리드 행 레이아웃 되돌림 - #310
clxxrlove May 20, 2026
21c734d
docs: Stats 스탬프 그리드 되돌림 검증 기록 - #310
clxxrlove May 20, 2026
7873646
docs: Stats 셀프런 스크롤 조사 종료 - #310
clxxrlove May 20, 2026
78b592c
docs: Pass 4 렌더링 최종 리포트 정리 - #310
clxxrlove May 20, 2026
e2523f8
docs: Pass 5 렌더링 후보 인계 문서 작성 - #310
clxxrlove May 20, 2026
349e1d3
docs: Pass 5 실행 계획 및 H-C5-b 가설 작성 - #312
clxxrlove May 21, 2026
3f83193
refactor: StatsCardView 외곽선 렌더링 중복 제거 - #312
clxxrlove May 21, 2026
6eb6048
docs: Stats outsideBorder 개선 KEEP 검증 기록 - #312
clxxrlove May 21, 2026
fff6b75
docs: Pass 5 렌더링 최종 리포트 정리 - #312
clxxrlove May 21, 2026
4712343
docs: 최종 렌더링 누적 리포트 발행 - #312
clxxrlove May 21, 2026
173f29a
docs: 한국어 렌더링 최종 성과 요약 추가 - #312
clxxrlove May 21, 2026
f664f2a
refactor: 리뷰 피드백 반영 - #312
clxxrlove May 21, 2026
562fecc
refactor: 리뷰 피드백 반영 및 SwiftLint 오류 수정 - #312
clxxrlove May 21, 2026
138d6b6
chore: TestFlight 배포 설정 및 버전 갱신 - #312
clxxrlove May 21, 2026
0f423ea
fix: 리뷰 반영 - #312
clxxrlove May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ Network Trash Folder
Temporary Items
.apdisk
src/SupportingFiles/Booket/GoogleService-Info.plist
# Performance traces and local probe workspace (large, generated)
.perf/

# Temporary files AI agent uses
.agent/handoff
# Claude Code scheduled-task runtime lock (per-machine, not shared)
.claude/scheduled_tasks.lock
3 changes: 1 addition & 2 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ private let commonInfoPlist: [String: Plist.Value] = Project.Environment.InfoPli
"DEEPLINK_HOST": "$(DEEPLINK_HOST)",
"API_BASE_URL": "$(API_BASE_URL)",
"NSCameraUsageDescription": "UseCamera",
"CFBundleShortVersionString": "1.1.2"
"CFBundleShortVersionString": "1.1.3"
], uniquingKeysWith: { current, _ in current })

private let commonDependencies: [TargetDependency] = [
Expand All @@ -43,7 +43,6 @@ private let commonDependencies: [TargetDependency] = [
.external(dependency: .KakaoSDKAuth),
.external(dependency: .KakaoSDKCommon),
.external(dependency: .GoogleSignIn),
.external(dependency: .FirebaseCore),
.external(dependency: .FirebaseMessaging),
.external(dependency: .FirebaseRemoteConfig),
.core(implements: .crashlytics)
Expand Down
3 changes: 1 addition & 2 deletions Projects/Domain/Auth/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ let project = Project.makeModule(
.external(dependency: .KakaoSDKCommon),
.external(dependency: .KakaoSDKAuth),
.external(dependency: .KakaoSDKUser),
.external(dependency: .GoogleSignIn),
.external(dependency: .GoogleSignInSwift)
.external(dependency: .GoogleSignIn)
]
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ import Foundation
public struct PhotoLogUpdateReactionResponseDTO: Decodable {
public let photologId: Int64
public let reaction: String

public init(photologId: Int64, reaction: String) {
self.photologId = photologId
self.reaction = reaction
}
}
7 changes: 7 additions & 0 deletions Projects/Feature/Auth/Example/Sources/AuthApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@

import ComposableArchitecture
import FeatureAuth
import SharedPerfTestingSupport
import SwiftUI

@main
struct AuthApp: App {
init() {
UITestMode.configureApplication()
}

var body: some Scene {
WindowGroup {
AuthView(
Expand All @@ -19,6 +24,8 @@ struct AuthApp: App {
reducer: { AuthReducer() }
)
)
.perfRoot("auth")
.perfReadyMarker("auth")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import SharedPerfTestingSupportUITests
import XCTest

final class AuthExampleSmokeTests: XCTestCase {
func testExampleRendersReadyState() {
_ = XCUIApplication.launchForPerf(seed: "default")
waitForFeatureReady("auth")
}
}
3 changes: 2 additions & 1 deletion Projects/Feature/Auth/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ let project = Project.makeModule(
.external(dependency: .ComposableArchitecture)
]
)
)
),
.feature(exampleUITests: .auth)
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ import SwiftUI
import ComposableArchitecture
import CoreCaptureSession
import CoreCaptureSessionInterface
import SharedPerfTestingSupport

@main
struct GoalDetailApp: App {
init() {
UITestMode.configureApplication()
}

var body: some Scene {
WindowGroup {
GoalDetailExampleView()
.perfRoot("goal-detail")
.perfReadyMarker("goal-detail")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,38 @@
// Created by 정지훈 on 1/23/26.
//

import AVFoundation
import SwiftUI

import ComposableArchitecture
import CoreCaptureSession
import CoreCaptureSessionInterface
import DomainGoalInterface
import DomainPhotoLogInterface
import FeatureGoalDetail
import FeatureGoalDetailInterface
import FeatureProofPhoto
import FeatureProofPhotoInterface
import SharedPerfTestingSupport
import SharedDesignSystem

struct GoalDetailExampleView: View {
var body: some View {
GoalDetailView(
store: Store(
initialState: GoalDetailReducer.State(
currentUser: .mySelf,
// Branch by launch scenario.
//
// - Probe / rendering scenarios target `ReactionBarView`,
// which is gated by `isShowReactionBar = !isFrontMyCard
// && isCompleted`. They require `.you`.
// - Default-mode tests (Smoke / Navigation / ColdLaunch)
// target the primary-cta button (`feature.goal-detail
// .primary-cta`), which is only present when
// `isFrontMyCard` (i.e. `.mySelf`). Forcing `.you` for
// them would hide the button and break the navigation
// test.
currentUser: Self.initialCurrentUser,
id: 1,
verificationDate: "2026-02-07"
),
Expand All @@ -29,15 +45,74 @@ struct GoalDetailExampleView: View {
proofPhotoReducer: ProofPhotoReducer()
)
}, withDependencies: {
$0.captureSessionClient = .liveValue
$0.captureSessionClient = UITestMode.isEnabled ? .perfMock : .liveValue
$0.proofPhotoFactory = .liveValue
$0.goalClient = .previewValue
// Local no-op mock for the reaction update path. Without
// it, `reactionEmojiTapped` would hit a real network
// client and either crash (testValue) or fan out to the
// server. Rendering scenarios must stay local.
$0.photoLogClient = .perfMock
}
)
)
}
}

private extension GoalDetailExampleView {
/// Pick `.you` only when a Pass 3 PERF scenario is active so the
/// reaction bar is reachable. Otherwise default to `.mySelf` so the
/// primary-cta button is visible — required by the existing
/// `GoalDetailExampleNavigationTests`.
static var initialCurrentUser: GoalDetail.Owner {
if UITestMode.isProbeScenario || UITestMode.isRenderingScenario {
return .you
}
return .mySelf
}
}

#Preview {
GoalDetailExampleView()
}

private extension CaptureSessionClient {
static let perfMock = Self(
fetchIsAuthorized: { true },
setUpCaptureSession: { _ in AVCaptureSession() },
stopRunning: {},
capturePhoto: { Data() },
switchCamera: { _ in },
switchFlash: { _ in }
)
}

private extension PhotoLogClient {
/// Local no-op mock for Pass 3 rendering scenarios. Each closure returns
/// an empty success response without touching the network. Only
/// `updateReaction` is exercised by the reaction rapid-fire scenario;
/// the others are stubs to satisfy the struct's required initializer.
static let perfMock = Self(
fetchUploadURL: { _ in
PhotoLogUploadURLResponseDTO(uploadUrl: "", fileName: "")
},
uploadImageData: { _, _ in },
createPhotoLog: { _ in
PhotoLogCreateResponseDTO(
photologId: 0,
goalId: 0,
imageUrl: "",
comment: "",
verificationDate: ""
)
},
updateReaction: { photologId, request in
PhotoLogUpdateReactionResponseDTO(
photologId: photologId,
reaction: request.reaction
)
},
updatePhotoLog: { _, _ in },
deletePhotoLog: { _ in }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import SharedPerfTestingSupportUITests
import XCTest

final class GoalDetailExampleColdLaunchTests: XCTestCase {
func testColdLaunch() {
measure(metrics: [
XCTClockMetric(),
XCTMemoryMetric(),
XCTCPUMetric()
]) {
_ = XCUIApplication.launchForPerf(seed: "default")
waitForFeatureReady("goal-detail", timeout: 30)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import SharedPerfTestingSupportUITests
import XCTest

final class GoalDetailExampleNavigationTests: XCTestCase {
func testPrimaryCtaPresentsProofPhoto() {
let app = XCUIApplication.launchForPerf(seed: "default")
waitForFeatureReady("goal-detail")

let primaryCta = app.descendants(matching: .any)["feature.goal-detail.primary-cta"]
XCTAssertTrue(primaryCta.waitForExistence(timeout: 5), "primary-cta not found")
primaryCta.tap()

let destinationReady = app.descendants(matching: .any)["feature.goal-detail-to-proof-photo.ready"]
XCTAssertTrue(
destinationReady.waitForExistence(timeout: 10),
"goal-detail-to-proof-photo ready marker did not appear"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import SharedPerfTestingSupportUITests
import XCTest

/// Pass 3 **rendering driver** UITests for FeatureGoalDetailExample.
///
/// These tests are NOT benchmarks. They drive deterministic UI activity so
/// that a real-device xctrace recording (Time Profiler + Animation Hitches)
/// captures the GoalDetail rendering path. XCTest pass/fail and any timing
/// the harness prints are not the metric.
///
/// ## Intended use
///
/// 1. Launch on a real device. The test launches with
/// `-UITEST` + `-UITEST_RENDERING_SCENARIO` + `-UITEST_SEED default`
/// and `disableAnimations: false`. The GoalDetail Example app does not
/// have a PERF probe harness today, but rendering scenarios still
/// require the rendering launch flag so any future probe additions
/// stay gated off.
/// 2. Attach `xcrun xctrace record --attach FeatureGoalDetailExample`
/// once the driver has the GoalDetail view ready (UITest log shows
/// `feature.goal-detail.ready` exists).
/// 3. Stop the trace when the test reports completion.
///
/// ## Scenarios
///
/// - `testRendering_goalDetailInitialRender` — launch + idle window so the
/// trace covers initial render + `FlyingReactionOverlay.TimelineView`
/// continuously ticking at 60 Hz on an empty `reactions` array.
/// - `testRendering_goalDetailReactionRapidFire` — cycles through all five
/// `ReactionEmoji` cases, dispatching `.reactionEmojiTapped` for each.
/// Each tap mutates `state.selectedReactionEmoji`, fans out 20 flying
/// particles via the overlay, and posts to a local no-op
/// `photoLogClient.updateReaction` mock injected by
/// `GoalDetailExampleView`.
///
/// ## Determinism
///
/// - `goalClient.previewValue` returns a deterministic GoalDetail item; the
/// Example launches with `currentUser: .you` so the reaction bar is
/// guaranteed visible.
/// - PhotoLogClient is a local in-process mock (`PhotoLogClient.perfMock`)
/// so no network call is issued.
final class GoalDetailExampleRenderingTests: XCTestCase {

/// Drives initial render + 7s idle window. Captures FlyingReactionOverlay
/// TimelineView's 60 Hz idle cost — relevant to the GoalDetail "무겁게
/// 느껴진다" VoC.
func testRendering_goalDetailInitialRender() {
let app = XCUIApplication.launchForPerf(
seed: "default",
scenario: .rendering,
disableAnimations: false
)
waitForFeatureReady("goal-detail", timeout: 30)

// 7s idle window. xctrace recording should cover this entirely.
Thread.sleep(forTimeInterval: 7.0)
}

/// Cycles through all five reaction emojis and taps each repeatedly.
/// Different emoji per tap so `state.selectedReactionEmoji != reactionEmoji`
/// guard always passes and the reducer actually mutates state. Each tap
/// emits 20 flying particles for ~0.85–1.35s, so consecutive taps keep
/// FlyingReactionOverlay continuously busy.
func testRendering_goalDetailReactionRapidFire() {
let app = XCUIApplication.launchForPerf(
seed: "default",
scenario: .rendering,
disableAnimations: false
)
waitForFeatureReady("goal-detail", timeout: 30)

// 5 ReactionEmoji rawValues. Cycling these guarantees each tap
// passes the `selectedReactionEmoji != reactionEmoji` guard so the
// reducer mutates state on every tap (not just on the first).
let reactionIdentifiers = [
"feature.goal-detail.reaction-ICON_HAPPY",
"feature.goal-detail.reaction-ICON_TROUBLE",
"feature.goal-detail.reaction-ICON_LOVE",
"feature.goal-detail.reaction-ICON_DOUBT",
"feature.goal-detail.reaction-ICON_FUCK"
]
var firstReactionExists = false
for identifier in reactionIdentifiers {
let element = app.descendants(matching: .any).matching(identifier: identifier).firstMatch
if !firstReactionExists {
firstReactionExists = element.waitForExistence(timeout: 10)
XCTAssertTrue(firstReactionExists, "Reaction bar not visible: \(identifier) missing")
} else {
XCTAssertTrue(element.exists, "Missing reaction identifier: \(identifier)")
}
}

// 8 cycles × 5 emojis = 40 taps total. Each tap triggers state
// mutation + 20 particle emit + async no-op photoLogClient call.
for _ in 0..<8 {
for identifier in reactionIdentifiers {
app.descendants(matching: .any)
.matching(identifier: identifier)
.firstMatch
.tap()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import SharedPerfTestingSupportUITests
import XCTest

final class GoalDetailExampleSmokeTests: XCTestCase {
func testExampleRendersReadyState() {
_ = XCUIApplication.launchForPerf(seed: "default")
waitForFeatureReady("goal-detail")
}
}
Loading
Loading