Skip to content

[Feat] 사용자 이벤트 수집을 위한 도메인 및 인프라 구축#209

Open
Remaked-Swain wants to merge 10 commits intodevelopfrom
feat/#207-ga4
Open

[Feat] 사용자 이벤트 수집을 위한 도메인 및 인프라 구축#209
Remaked-Swain wants to merge 10 commits intodevelopfrom
feat/#207-ga4

Conversation

@Remaked-Swain
Copy link
Copy Markdown
Member

@Remaked-Swain Remaked-Swain commented Apr 14, 2026

🌴 작업한 브랜치

✅ 작업한 내용

개요

'사용자 행동 기반 기능 고도화 우선순위 결정'이라는 목표를 달성하기 위해 GA4 기반의 사용자 이벤트 수집 기능을 구현하는 첫 번째 단계입니다.
클린 아키텍처와 TCA 생태계에 부합하는 방향으로 Domain, Data, Infrastructure 계층의 기반 타입들을 설계하고 구현했습니다.

참고
Presentation Layer에 직접 이벤트를 붙이는 것은 별도의 Issue로 진행하려고 합니다.
이 PR내용이 머지된 이후, 각자의 담당 파트에서 병렬적으로 이벤트 수집 로직 붙이기 하면 될 것 같습니다.

Domain Layer

  • AnalyticsEvent: 모든 이벤트의 규격을 표현
  • AnalyticsEventName, AnalyticsParameterKey: GA4 계획서 기반의 이벤트명과 파라미터 키를 네임스페이스화 하여 정리

Data & Infra-Layer

  • AnalyticsService: Firebase 또는 그 외의 이벤트 수집 도구가 제공해야 할 인터페이스 선언
  • AnalyticsRepository: 도메인 엔티티를 인프라 모델로 매핑하여 Service에게 데이터를 전달할 인터페이스

❗️PR Point

주요 기술적 결정사항과 근거는 다음과 같습니다.

  1. 이벤트 및 파라미터의 열거형 관리

    • 결정사항: 모든 이벤트명과 파라미터 키를 네임스페이스로 두어 하드코딩을 방지합니다.
    • 근거: 휴먼 에러 등으로 인한 문제를 컴파일 수준에서 방지하고, 기획 변경 시 해당 SSOT만을 수정하는 것으로 유지보수성을 챙길 수 있습니다.
  2. POP-DIP

  • 결정사항: Firebase SDK의 타입을 직접 쓰지 않고, Service와 Repository 인터페이스를 각각두었습니다.
  • 근거: 프로젝트 내 기존 아키텍처 컨벤션을 유지함과 동시에, 추후 다른 분석 도구(Amplitude 등)가 추가되거나 교체되더라도 기존 로직에 영향을 주지 않도록 OCP를 준수했습니다.
  1. 유저 식별과 이벤트 로깅 인터페이스 분리

    • 결정사항: logEvent의 파라미터로 userID를 매번 넘기지 않고, 최초 초기화 용도의 configure 메서드를 별도로 유지했습니다.
    • 근거: Firebase는 한 번 설정된 식별값을 유지한다고 합니다. 무엇보다 각 하위 기능인 지도, 아카이빙, 포즈추천 등이 유저의 로그인 상태를 억지로 알아야 하는, 강결합을 방지하고자 했습니다.
  2. 그 외 주요 피드백 요청 사항

  • FirebaseAnalytics 라이브러리로 할 수 있는 것들은 많겠지만, 현재 목표하고 있는 수준에서는 식별자 설정(setUserId), 이벤트 로깅(logEvent) 밖에 없긴 합니다. 따라서 'FirebaseAnalytics 라이브러리를 어떻게 사용하는가' 보다는, 이제 각 Reducer에 어떻게 붙이고 이벤트 수집을 해야 좋을지 생각하는 것이 좋을 것 같습니다.
  • Firebase Analytics 라이브러리의 Analytics라는 타입은 thread-safety한지 공식문서 설명을 못찾았어서(내부 Queue로 직렬화하도록 설계되어 있다는 스택오버플로우 글이 있긴하다만) 혹시 몰라서 actor로 선언했습니다.

📸 스크린샷

구현 내용 상, 별도의 스크린샷은 없습니다.

📟 관련 이슈

Summary by CodeRabbit

  • 새로운 기능
    • 앱의 사용자 행동 추적 및 이벤트 로깅 기능 추가
    • 사용자 세션 관리 및 식별자 설정 기능 구현
    • Firebase 기반 분석 서비스 연동
    • 앱 실행, 사진 업로드, 앨범 생성, 지도 보기 등 다양한 이벤트 정의 및 추적 가능
    • 이벤트에 대한 키/값 파라미터 지원 및 백그라운드 비동기 전송 처리

@Remaked-Swain Remaked-Swain added this to the 4차 스프린트 milestone Apr 14, 2026
@Remaked-Swain Remaked-Swain requested a review from OneTen19 April 14, 2026 14:39
@Remaked-Swain Remaked-Swain self-assigned this Apr 14, 2026
@Remaked-Swain Remaked-Swain added Add ✚ 코드, 파일, 에셋 추가 Feat 💻 기능 구현 금용 🐲 금용 작업 labels Apr 14, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b01d1140-68c4-44d3-bfc2-f71c7e65605f

📥 Commits

Reviewing files that changed from the base of the PR and between fffaac4 and 8098d9a.

📒 Files selected for processing (4)
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/DefaultAnalyticsRepository.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/FirebaseAnalyticsService.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsEvent.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsParameterKey.swift
✅ Files skipped from review due to trivial changes (3)
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsEvent.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/FirebaseAnalyticsService.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsParameterKey.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/DefaultAnalyticsRepository.swift

Walkthrough

Firebase Analytics 연동을 위한 추상화와 구현체를 추가합니다. AnalyticsService/AnalyticsRepository 프로토콜, Firebase 기반 서비스(actor), 저장소(actor), 의존성 주입용 클라이언트 및 이벤트/파라미터 타입들이 새로 도입됩니다. (50 words 이내)

Changes

Cohort / File(s) Summary
Protocols & Domain Entities
Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Interfaces/AnalyticsService.swift, Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Interfaces/AnalyticsRepository.swift, Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsEvent.swift, Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsEventName.swift, Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsParameterKey.swift
애널리틱스 관련 프로토콜(서비스/레포지토리)과 이벤트, 이벤트명, 파라미터 키 열거형을 추가하여 강타입 모델을 정의.
Service Implementation
Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/FirebaseAnalyticsService.swift
Firebase Analytics SDK 호출을 래핑한 FirebaseAnalyticsService actor 추가 (sendEvent, setUserID).
Repository & Dependency Wiring
Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/DefaultAnalyticsRepository.swift
도메인 모델을 인프라 타입으로 변환하고 AnalyticsRepository 구현체로 제공, DependencyKey(liveValue) 및 DependencyValues 접근자 추가.
Client
Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Clients/AnalyticsClient.swift
의존성 주입 클라이언트 추가 (configure, logEvent)—백그라운드 Task로 레포지토리 호출을 비동기 디스패치.

Sequence Diagram(s)

sequenceDiagram
    participant Feature as Feature Module
    participant Client as AnalyticsClient
    participant Repository as DefaultAnalyticsRepository
    participant Service as FirebaseAnalyticsService
    participant Firebase as Firebase Analytics SDK

    Feature->>Client: configure(userID: Int?)
    activate Client
    Client->>Client: Task.detached(.background)
    Client->>Repository: setUserSession(with: Int?)
    activate Repository
    Repository->>Repository: Int? -> String?
    Repository->>Service: setUserID(_ String?)
    activate Service
    Service->>Firebase: Analytics.setUserID(userID)
    Firebase-->>Service: ✓
    Service-->>Repository: ✓
    deactivate Service
    Repository-->>Client: ✓
    deactivate Repository
    Client-->>Feature: ✓
    deactivate Client

    Feature->>Client: logEvent(_ event)
    activate Client
    Client->>Client: Task.detached(.background)
    Client->>Repository: logEvent(_ event)
    activate Repository
    Repository->>Repository: extract name & parameters
    Repository->>Service: sendEvent(name: String, parameters: [String: Any]?)
    activate Service
    Service->>Firebase: Analytics.logEvent(name, parameters)
    Firebase-->>Service: ✓
    Service-->>Repository: ✓
    deactivate Service
    Repository-->>Client: ✓
    deactivate Repository
    Client-->>Feature: ✓
    deactivate Client
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 Domain 및 Infrastructure 계층의 기반 타입 구축이라는 주요 변경사항을 명확하게 설명하고 있습니다.
Description check ✅ Passed PR 설명이 작업 브랜치, 작업 내용, 주요 설계 결정사항과 근거를 포함하여 템플릿의 대부분 요소를 충족하고 있습니다.
Linked Issues check ✅ Passed PR의 변경사항이 연결 이슈 #207의 '추상화 및 인프라 구축' 목표를 완전히 충족하며, Domain Layer와 Data/Infra Layer의 필수 인터페이스와 구현체를 모두 제공합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 연결 이슈 #207의 '추상화 및 인프라 구축' 범위 내에 있으며, Presentation Layer 연동은 별도 이슈로 분리하여 진행 예정입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#207-ga4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/DefaultAnalyticsRepository.swift (1)

26-37: 파라미터 매핑 로직 간소화 가능

현재 for 루프를 사용한 파라미터 변환 로직을 reduce(into:)를 사용하여 더 간결하게 작성할 수 있습니다.

♻️ 간소화 제안
     public func logEvent(_ event: any AnalyticsEvent) async {
         let eventName = event.name.value
-        var parsedParameters: [String: Any]?
-        
-        if let parameters = event.parameters {
-            parsedParameters = [:]
-            for (key, value) in parameters {
-                parsedParameters?[key.value] = value
-            }
-        }
+        let parsedParameters = event.parameters?.reduce(into: [String: Any]()) { result, pair in
+            result[pair.key.value] = pair.value
+        }
         await service.sendEvent(name: eventName, parameters: parsedParameters)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/DefaultAnalyticsRepository.swift`
around lines 26 - 37, In logEvent, simplify the manual for-loop that maps
event.parameters into parsedParameters by using reduce(into:) to build the
[String: Any] dictionary; locate the logic in the logEvent(_ event: any
AnalyticsEvent) method where parsedParameters is created from event.parameters
and replace the for (key, value) loop with a reduce(into:) that assigns
parsedParameters?[key.value] = value, then continue to call
service.sendEvent(name: eventName, parameters: parsedParameters) as before.
Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Clients/AnalyticsClient.swift (1)

22-26: Fire-and-forget 패턴의 이벤트 손실 가능성 고려

Task.detached로 생성된 태스크가 추적되지 않아, 앱이 갑자기 종료되거나 백그라운드로 전환될 때 이벤트가 손실될 수 있습니다. 또한 실패한 이벤트에 대한 에러 핸들링이 없습니다.

분석 이벤트의 특성상 일부 손실은 허용 가능할 수 있으나, 중요한 이벤트의 경우 재시도 로직이나 로컬 큐잉을 고려해볼 수 있습니다. 현재 구현이 의도된 것이라면 이 부분은 무시하셔도 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Clients/AnalyticsClient.swift`
around lines 22 - 26, The current fire-and-forget usage of Task.detached in the
AnalyticsClient initializer (the userID handler calling
repository.setUserSession and the logEvent handler calling repository.logEvent)
risks losing events and provides no error handling; replace Task.detached with
structured Task (so the tasks are attached to the current context) or otherwise
enqueue the work to your existing analytics queue mechanism, and wrap the await
calls to repository.setUserSession(...) and repository.logEvent(...) with proper
error handling (catch errors and log them via your logger and/or implement a
simple retry/local-queue fallback for failed events) so failures are recorded
and important events can be retried.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsParamterKey.swift`:
- Line 10: Rename the misspelled public enum AnalyticsParamterKey to
AnalyticsParameterKey and rename the file AnalyticsParamterKey.swift →
AnalyticsParameterKey.swift; update all references (e.g., usages in
AnalyticsEvent.swift) to the new name. To avoid breaking downstream consumers,
add a deprecated typealias AnalyticsParamterKey = AnalyticsParameterKey (marked
`@available`(*, deprecated, message: "...") ) in the old-named file or a
compatibility shim and update any exports/imports accordingly. Ensure tests and
any public API docs are updated to the corrected name.

---

Nitpick comments:
In
`@Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/DefaultAnalyticsRepository.swift`:
- Around line 26-37: In logEvent, simplify the manual for-loop that maps
event.parameters into parsedParameters by using reduce(into:) to build the
[String: Any] dictionary; locate the logic in the logEvent(_ event: any
AnalyticsEvent) method where parsedParameters is created from event.parameters
and replace the for (key, value) loop with a reduce(into:) that assigns
parsedParameters?[key.value] = value, then continue to call
service.sendEvent(name: eventName, parameters: parsedParameters) as before.

In
`@Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Clients/AnalyticsClient.swift`:
- Around line 22-26: The current fire-and-forget usage of Task.detached in the
AnalyticsClient initializer (the userID handler calling
repository.setUserSession and the logEvent handler calling repository.logEvent)
risks losing events and provides no error handling; replace Task.detached with
structured Task (so the tasks are attached to the current context) or otherwise
enqueue the work to your existing analytics queue mechanism, and wrap the await
calls to repository.setUserSession(...) and repository.logEvent(...) with proper
error handling (catch errors and log them via your logger and/or implement a
simple retry/local-queue fallback for failed events) so failures are recorded
and important events can be retried.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2124919b-9df2-4a91-b578-eee1e94f664f

📥 Commits

Reviewing files that changed from the base of the PR and between bdcf41d and 8e0b413.

📒 Files selected for processing (8)
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/DefaultAnalyticsRepository.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Implementations/FirebaseAnalyticsService.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Interfaces/AnalyticsRepository.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Data/Sources/Interfaces/AnalyticsService.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Clients/AnalyticsClient.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsEvent.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsEventName.swift
  • Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsParamterKey.swift


import Foundation

public enum AnalyticsParamterKey: String {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

오타 수정 필요: "Paramter" → "Parameter"

파일명과 enum 이름에 "Parameter"가 "Paramter"로 잘못 작성되어 있습니다. public API이므로 다른 모듈에서 사용되기 전에 수정하는 것이 좋습니다.

✏️ 수정 제안

파일명: AnalyticsParamterKey.swiftAnalyticsParameterKey.swift

-public enum AnalyticsParamterKey: String {
+public enum AnalyticsParameterKey: String {

관련 파일 (AnalyticsEvent.swift)도 함께 수정 필요:

-    var parameters: [AnalyticsParamterKey: Any]? { get }
+    var parameters: [AnalyticsParameterKey: Any]? { get }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public enum AnalyticsParamterKey: String {
public enum AnalyticsParameterKey: String {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Neki-iOS/Core/Sources/Analytics/Sources/Domain/Sources/Entities/AnalyticsParamterKey.swift`
at line 10, Rename the misspelled public enum AnalyticsParamterKey to
AnalyticsParameterKey and rename the file AnalyticsParamterKey.swift →
AnalyticsParameterKey.swift; update all references (e.g., usages in
AnalyticsEvent.swift) to the new name. To avoid breaking downstream consumers,
add a deprecated typealias AnalyticsParamterKey = AnalyticsParameterKey (marked
`@available`(*, deprecated, message: "...") ) in the old-named file or a
compatibility shim and update any exports/imports accordingly. Ensure tests and
any public API docs are updated to the corrected name.

Copy link
Copy Markdown
Member

@OneTen19 OneTen19 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GA는 저도 처음 붙여봐서 궁금한 것들이 조금 있네요!!

POP-DIP

너무 좋습니다. 역시 고능하시군여

logEvent의 파라미터로 userID를 매번 넘기지 않고, 최초 초기화 용도의 configure 메서드를 별도로 유지했습니다. 각 하위 기능인 지도, 아카이빙, 포즈추천 등이 유저의 로그인 상태를 억지로 알아야 하는, 강결합을 방지하고자 했습니다.

최초 초기화로 userID를 지니고 캐싱을 해둔다는 뜻인 것 같은데 userID를 어딘가에 저장해두는 부분은 따로 보이지 않네요. 그렇다면 Firebase는 한 번 설정된 식별값을 유지한다고 합니다. 이 내용이 Firebase가 한 번 연결이 되면 알아서 userID를 계속 지니고 그 값을 사용한다는 의미로 해석이 되는데, 이로 인해서 이벤트를 수집할 때 userID가 따로 필요 없나보군요!!

그런데 POP-DIP에서 고려하신 것처럼 추후 다른 수집도구를 붙이게 되고, 해당 도구는 이벤트마다 userID가 요구된다면 지금의 구조에서 조금 달라지게 되겠네요.

이제 각 Reducer에 어떻게 붙이고 이벤트 수집을 해야 좋을지 생각하는 것이 좋을 것 같습니다.

진짜 어떻게 붙이지.

Firebase Analytics 라이브러리의 Analytics라는 타입은 thread-safety한지 공식문서 설명을 못찾았어서(내부 Queue로 직렬화하도록 설계되어 있다는 스택오버플로우 글이 있긴하다만) 혹시 몰라서 actor로 선언했습니다.

굳. 느릴지언정 혹시 모를 문제를 예방한다. actor로 사용할 때 우려되는 단점이 약간 궁금하긴 하네요.

Comment on lines +11 to +12
var name: AnalyticsEventName { get }
var parameters: [AnalyticsParamterKey: Any]? { get }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

연산 프로퍼티로 선언하신 이유가 궁금하네요!!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로토콜 선언부에 저장 프로퍼티 식 선언도 가능한가요?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아 {get} 을 해 둔 이유가 궁금했슨


import Foundation

public enum AnalyticsEventName: String {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

이 파일을 보고 문득 든 궁금증인데, 이런 식으로 이벤트마다 엔티티를 만들어서 분석하는 게 일반적인 방식일까요??
우리처럼 규모가 크지 않은 프로젝트는 이 파일처럼 모든 이벤트를 만들어두고, 추가될 때마다 추가하는 식으로 진행한다 하더라도, 규모가 큰 프로젝트나 실무같은 곳에서도 이런 식으로 할 지 궁금하네요
일일이 이벤트마다 엔티티를 만들고, 필요한 곳에 전부 붙인다?? 이벤트가 수백개가 되어도 그게 가능하려나??? 문뜩 든 의문..

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 수집하고자 하는 이벤트 종류가 굉장히 많아져서 하나의 열거형으로 표현하기 어려울 경우에는 Nested Type을 추가로 만들 것 같긴합니다. (네임스페이스가 주는 장점을 유지하고, 구분하기 쉽도록 분리)

enum AnalyticsEvent {
   enum Pose {
       case randomPose
       // ....
   }
}

이런 식이면 이벤트 전송하는 코드에서는 AnalyticsEvent.Pose.randomPose 같은 식으로 작성할 수 있어서 가독성 떨어지는 문제를 해결할 수 있을듯해요.

Comment on lines +27 to +29
case brandName = "brand_name"
case entryPoint = "entry_point"
case mapType = "map_type"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

노션을 확인해보니 파라미터의 구체적인 value 값이 없는 것 같더라구요?
value값을 클라단에서 정해서 붙이게 되면 안드로이드와 iOS의 value값이 다르게 수집될 가능성이 있을 것 같아요.
그래서 기획단에게 가능하다면 구체적인 Value Case도 같이 제공해달라고 요구하는 게 좋을 것 같아요.


public protocol AnalyticsEvent {
var name: AnalyticsEventName { get }
var parameters: [AnalyticsParamterKey: Any]? { get }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any로 해둔 건 어떤 자료형으로 수집할 지 아직 안 정해져서 그런건가

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수집도구에는 Int, String 등등 다양한 자료형이 전달될 수 있습니다. 그걸 염두하여 Any 타입으로 표현했습니다.
실제로 Firebase Analytics의 이벤트 수집 메서드도 동일한 파라미터 타입을 받습니다!

Comment on lines +23 to +25
Task.detached(priority: .background) { await repository.setUserSession(with: userID) }
} logEvent: { event in
Task.detached(priority: .background) { await repository.logEvent(event) }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

메인스레드에서 할 필요가 없는 작업이니 Task.detached 사용 좋네요

Comment on lines +11 to +12
func sendEvent(name: String, parameters: [String: Any]?) async
func setUserID(_ userID: String?) async
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

그냥 궁금해졌는데 AnalyticsRepository처럼 -> Void로 함수가 Void를 리턴한다고 명시해두는 것과
지금 AnalyticsService처럼 그냥 별 표시 안 하는 거랑 무슨 차이가 있을까요??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아무 차이 없습니다.
Swift 문법일 뿐이지 않을까요.

이를테면, 클로저 블럭 내에 코드가 단 한줄이라면 return 키워드를 생략할 수 있는데 작성해도되고, 안해도되는 것과 같아요.
만약 (너무 극단적이지만) 우리 팀은 모든 코드에 return 키워드를 꼭 써! 라고 강제하는 미친 팀이 있다면 return을 써야겠지만요.

import Foundation

public protocol AnalyticsRepository: Sendable {
func setUserSession(with userID: Int?) async -> Void
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

혹시 여기는 _ 로 안하고 with로 해 둔 이유가???

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수이름이 setUserSession, '사용자 로그인 정보를 설정한다'는 의미로 네이밍했습니다.
함수 호출부에 로그인 정보 설정은 사용자 식별자로 한다는 뜻을 전달하면서 with랑 by랑 여러 매개변수 레이블 네이밍을 고르다가 그냥 with로 했습니다.
언더바로 매개변수 레이블을 감추면 의미 전달이 어려울 것 같았어요

Comment on lines +18 to +20
#if DEBUG
Logger.data.log("[Firebase Service] Event: \(name) | Parameters: \(parameters ?? [:])")
#endif
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

디버깅용으로 사용하는 거면 Logger.data.debug로 #if DEBUG 대신 사용하셔도 될 것 같슴다. level을 debug로 해줘도 되고!!

아니면 콘솔에도 찍히게는 하고 싶은데, 키 값이라던지 내용은 감춰지면 좋겠으면 privacy 설정을 private으로 해 주는 방법도 있겠네요

https://hururuek-chapchap.tistory.com/244

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이러한 방법이 있었군요

}

public func setUserID(_ userID: String?) {
Analytics.setUserID(userID)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

Analytics를 선언한 곳이 안 보이는 걸 보니 FirebaseAnalytics에서 제공하는 것 같은데, 옵셔널인 userID를 사용해도 되는 걸 보면 Analytics.setUserID 이 null을 허용하는 메소드인가보네요?? setUserID인데 옵셔널을 허용한다니 먼가 신기하군

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Analytics에 기본적으로 구현되어 있는 자동수집 로직 떄문에 사용자를 식별할 수 있는 값으로 먼저 기기정보를 쓴다고 합니다. 얘는 앱 런치 시점에 확보가 되는 것 같고요.

그 후에 개발자가 직접 부여한 식별자(우리한테는 지금 UserID Int값)로 사용자를 구별하려거든 setUserID 메서드를 쓰라는데 이러면 그 이후부터는 기기정보가 아닌 개발자가 부여한 값이 식별자로써 로깅이 됩니다.

제 생각에는 아마도 위처럼 식별자가 부여된 상황에서 nil을 전달해 다시 호출하면 그것을 지우는 효과일듯해요

if let parameters = event.parameters {
parsedParameters = [:]
for (key, value) in parameters {
parsedParameters?[key.value] = value
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3

딕셔너리[key.value] = value ???

이벤트가 AnalyticsEvent이고,
AnalyticsEvent는 name(AnalyticsEventName)과 parameters([AnalyticsParamterKey: Any]?)가 있고,

그럼 if let parameters = event.parameters 이거는 parameters가 [AnalyticsParamterKey: Any]? 이거가 되고,
AnalyticsParamterKey의 value값이 parsedParameters 딕셔너리의 Key 값이 되는 거군.
오키 파악완료

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 안그래도 약간 멈칫했는데, 열거형의 rawValue를 열거형의 바깥에서 직접 쓰기 싫어서 value라고 네이밍했는데, 덜 헷갈리는 측면에서 적절한 네이밍 다른거 없을까요..?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니면 저번 피알에서 리뷰해주신 내용처럼
let 어쩌구 = key.value 하는 과정을 추가해서 파악하기 좀 더 쉽게 해주는 것두..?? 불필요한 선언이 추가돼서 성능상 별로이려나

Copy link
Copy Markdown
Member Author

@Remaked-Swain Remaked-Swain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OneTen19

코멘트를 확인했습니다.

다음의 추가수정 예정입니다.

  1. 짜치는 네이밍 어떻게 할지 머리 쥐어짜기
  2. 디버그 모드에서만 로그 찍히도록 Logger 코드 수정하기

Comment on lines +11 to +12
var name: AnalyticsEventName { get }
var parameters: [AnalyticsParamterKey: Any]? { get }
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로토콜 선언부에 저장 프로퍼티 식 선언도 가능한가요?


public protocol AnalyticsEvent {
var name: AnalyticsEventName { get }
var parameters: [AnalyticsParamterKey: Any]? { get }
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수집도구에는 Int, String 등등 다양한 자료형이 전달될 수 있습니다. 그걸 염두하여 Any 타입으로 표현했습니다.
실제로 Firebase Analytics의 이벤트 수집 메서드도 동일한 파라미터 타입을 받습니다!


import Foundation

public enum AnalyticsEventName: String {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 수집하고자 하는 이벤트 종류가 굉장히 많아져서 하나의 열거형으로 표현하기 어려울 경우에는 Nested Type을 추가로 만들 것 같긴합니다. (네임스페이스가 주는 장점을 유지하고, 구분하기 쉽도록 분리)

enum AnalyticsEvent {
   enum Pose {
       case randomPose
       // ....
   }
}

이런 식이면 이벤트 전송하는 코드에서는 AnalyticsEvent.Pose.randomPose 같은 식으로 작성할 수 있어서 가독성 떨어지는 문제를 해결할 수 있을듯해요.

Copy link
Copy Markdown
Member Author

@Remaked-Swain Remaked-Swain Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이걸 발견하셨군요.

Comment on lines +11 to +12
func sendEvent(name: String, parameters: [String: Any]?) async
func setUserID(_ userID: String?) async
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아무 차이 없습니다.
Swift 문법일 뿐이지 않을까요.

이를테면, 클로저 블럭 내에 코드가 단 한줄이라면 return 키워드를 생략할 수 있는데 작성해도되고, 안해도되는 것과 같아요.
만약 (너무 극단적이지만) 우리 팀은 모든 코드에 return 키워드를 꼭 써! 라고 강제하는 미친 팀이 있다면 return을 써야겠지만요.

import Foundation

public protocol AnalyticsRepository: Sendable {
func setUserSession(with userID: Int?) async -> Void
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수이름이 setUserSession, '사용자 로그인 정보를 설정한다'는 의미로 네이밍했습니다.
함수 호출부에 로그인 정보 설정은 사용자 식별자로 한다는 뜻을 전달하면서 with랑 by랑 여러 매개변수 레이블 네이밍을 고르다가 그냥 with로 했습니다.
언더바로 매개변수 레이블을 감추면 의미 전달이 어려울 것 같았어요

Comment on lines +18 to +20
#if DEBUG
Logger.data.log("[Firebase Service] Event: \(name) | Parameters: \(parameters ?? [:])")
#endif
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이러한 방법이 있었군요

}

public func setUserID(_ userID: String?) {
Analytics.setUserID(userID)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Analytics에 기본적으로 구현되어 있는 자동수집 로직 떄문에 사용자를 식별할 수 있는 값으로 먼저 기기정보를 쓴다고 합니다. 얘는 앱 런치 시점에 확보가 되는 것 같고요.

그 후에 개발자가 직접 부여한 식별자(우리한테는 지금 UserID Int값)로 사용자를 구별하려거든 setUserID 메서드를 쓰라는데 이러면 그 이후부터는 기기정보가 아닌 개발자가 부여한 값이 식별자로써 로깅이 됩니다.

제 생각에는 아마도 위처럼 식별자가 부여된 상황에서 nil을 전달해 다시 호출하면 그것을 지우는 효과일듯해요

if let parameters = event.parameters {
parsedParameters = [:]
for (key, value) in parameters {
parsedParameters?[key.value] = value
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 안그래도 약간 멈칫했는데, 열거형의 rawValue를 열거형의 바깥에서 직접 쓰기 싫어서 value라고 네이밍했는데, 덜 헷갈리는 측면에서 적절한 네이밍 다른거 없을까요..?

@Remaked-Swain Remaked-Swain requested a review from OneTen19 April 17, 2026 14:40
@Remaked-Swain
Copy link
Copy Markdown
Member Author

@OneTen19

다음의 수정사항이 발생하여 재검토 요청드립니다.

  1. 애초에 GA4를 붙이는 이유는 프로덕션 상태에서 실제 사용자의 활동을 추적하기 위함입니다. 그런데 디버깅 모드에서 로깅하려는 것은 큰 모순이라는 생각이 들어서 아예 제거해버렸습니다.
  2. 예로부터 '키-밸류' 라는 명칭은 세트로 붙어다녔습니다. 비록 뜻하는 바가 다르더라도 읽기에 낯설은 부분이 있어서, '키.밸류 = 밸류'로 읽히는 것보다 '키.네임 = 밸류'로 하면 그나마 나은 것 같아서(그냥 키가 곧 밸류로 읽히지만 않게하면) 네이밍을 바꿨습니다.

AnalyticsEvent를 프로토콜로 선언한 이유를 설명드리지 못한 것 같아 내용을 이어서 작성합니다.
이제 각 모듈의 도메인 영역에 자신이 발생시킬 수 있는(수집할 수 있는) 이벤트를 정의해야 합니다.
예시는 다음과 같습니다.

struct PhotoUploadEvent: AnalyticsEvent {
   let name: AnalyticsEventName = .photoUpload
   var parameters: [AnalyticsParameterKey: Any]?

   init(method: UploadMethod, count: Int) {
      parameters = [
         .method: method.rawValue, // 이러면 "qr" 또는 "gallery"가 되겠죠?
         .count: count
   }
}

위처럼 이벤트를 정의하고 리듀서에서 결과 액션 처리 시, 이벤트 수집 클라이언트를 부르면서 전달하면 됩니다.
예시코드는 간단히 구조체를 작성한거고 필요하다면 팩토리 객체를 만들어서 구현식을 캡슐화해도 좋을 것 같습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Add ✚ 코드, 파일, 에셋 추가 Feat 💻 기능 구현 금용 🐲 금용 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Firebase Analytics 연동

2 participants