Skip to content

InnoSquadCorp/InnoSample

Repository files navigation

InnoSample

InnoSamplebanksalad-iosFeatures / Layers / Cores / ThirdParties / Util 분리를 참고해 만든 Tuist 기반 baseline sample 입니다.
목적은 기능 구현보다 먼저, Inno 계열 앱에서 공통으로 쓸 모듈 경계, composition 방식, DI wiring, navigation ownership, Tuist helper 정책을 고정하는 데 있습니다.
즉 이 저장소는 “샘플 앱”이면서 동시에 개발 시작 전 기본 뼈대(scaffold) 역할을 합니다.

구조 원칙은 다음 의존 방향으로 정리합니다.

  • Feature -> Domain
  • Data -> Domain
  • Remote -> Data + CoreNetwork
  • Layers -> Domain + Data + Remote
  • Features -> Domain + Feature
  • App -> CoreNetwork + Layers + Features + ThirdParty

InnoDI, InnoFlow, InnoNetwork, InnoRouter를 쓰는 각 모듈은 독립 Project.swift로 관리하고, 루트는 Workspace.swift로 조립합니다.
Inno 라이브러리 의존성은 로컬 path package가 아니라 Tuist/Package.swift에서 관리하는 remote package + exact version 고정 방식으로 소비합니다.

기본 개발 환경은 Xcode 26.4+Swift 6.3입니다.

상세 구조 평가와 개선 우선순위는 Docs/ArchitectureReview.md 에 정리합니다.

Architecture Intent

이 샘플은 banksalad-ios/Layers의 루트 composition 아이디어를 InnoDI 방식으로 축소 적용한 baseline scaffold 입니다.

핵심 의도는 세 가지입니다.

  • 개발을 시작하기 전에 레이어 경계와 책임을 먼저 고정한다.

  • root composition과 feature composition을 분리해서 상위 wiring 비용을 낮춘다.

  • 샘플 코드가 아니라 실제 앱 시작점으로 이어질 수 있는 구조를 제공한다.

  • InnoNetwork

    • 가장 안쪽 transport 구현입니다.
    • NetworkClient, APIDefinition, retry, interceptor, logger 같은 실행 메커니즘을 가집니다.
  • CoreNetwork

    • InnoNetwork를 감싸는 infrastructure boundary 입니다.
    • 앱 공통 환경값, 기본 header, request/response interceptor, retry policy, logger를 소유합니다.
    • NetworkTransport를 통해 상위 레이어에 "요청을 실행하는 방법"만 노출합니다.
  • Remote

    • 외부 API의 endpoint 의미와 remote model 변환만 가집니다.
    • Data가 소유한 remote data source contract를 구현합니다.
    • CoreNetworkRequestDefinition, NetworkTransport만 사용합니다.
  • Data

    • domain repository 구현과 remote data source contract를 함께 소유합니다.
    • 비어 있는 응답 처리나 리스트 curate 같은 데이터 정책이 이 레이어에 있습니다.
  • Domain

    • entity, repository contract, use case implementation만 가집니다.
    • 네트워크 구현이나 remote model을 모릅니다.
    • repository는 레이어 경계 계약이라 protocol을 유지합니다.
    • use case는 현재 stateless 단일 구현체라 protocol을 유지할 실익이 작아서 concrete type으로 제공합니다.
    • 즉 use case는 feature에 주입되는 의존성이지만, domain에서는 shared scope로 보관하지 않고 shared repository 위에 매번 가볍게 조합되는 concrete stateless 값으로 다룹니다.
    • concrete use case의 public surface는 callAsFunction() 하나로 제한하고, 생성 책임은 Domain 내부에만 둡니다.
  • Layers

    • Remote -> Data -> Domain을 연결하는 composition-only 모듈입니다.
    • RemoteContainer, DataContainer, DomainContainer를 순서대로 생성하고, feature가 바로 받는 use case만 외부에 노출합니다.
    • business logic, remote model, use case 구현을 두지 않습니다.
  • Features

    • leaf feature router들을 묶는 root feature composition 모듈입니다.
    • App이 전달한 use case로 각 leaf feature input을 만들고, root FeatureContainer가 leaf Router 타깃만 조립합니다.
    • App은 개별 feature wiring 대신 root FeatureContainer만 연결합니다.
    • leaf feature는 Interface / Logic / UI / Router / Testing / Tests 타깃으로 나눠서 컴파일 단계에서 구조를 강제합니다.
    • cross-feature navigation도 leaf끼리 직접 연결하지 않고, EntireTab 같은 상위 coordinator가 child coordinator를 중재하는 방식으로만 처리합니다.

이 선택은 DI를 제거한 것이 아닙니다.

  • repository는 Data -> Domain 경계를 넘는 진짜 계약이라 protocol을 유지합니다.
  • use case는 지금 기준으로 구현이 하나뿐인 stateless wrapper라, protocol 추상화보다 concrete type이 더 읽기 쉽습니다.
  • 따라서 Feature에는 use case를 의존성으로 주입하되, Domain에서는 concrete type을 computed value로 제공합니다.
  • 그리고 feature가 보는 public interface는 callAsFunction() 하나뿐이라, use case를 단일 action object처럼 다룰 수 있습니다.
  • 추후 use case 구현이 둘 이상 생기거나 decorator/cache/logging wrapper가 붙으면 protocol 재도입을 다시 검토할 수 있습니다.

컨테이너 경계는 아래처럼 나뉩니다.

  • AppContainer
    • 전역 인프라와 상위 composition 연결만 담당
  • LayerContainer
    • RemoteContainer -> DataContainer -> DomainContainer 조립을 담당하고 feature가 바로 쓸 use case만 외부에 노출
  • FeatureContainer
    • root Features 프로젝트에서 leaf Router 타깃만 조립
    • LayerContainer 자체를 모르고 concrete use case만 입력으로 받음
    • 외부에는 root coordinator만 공개하고, child router/input/use case는 내부 구현으로 유지
  • 각 leaf feature
    • Interface
      • 외부에 공개할 feature entry/input/output 계약만 둠
    • Logic
      • InnoFlow reducer/state/action/effect와 use case 호출만 담당
      • SwiftUI, InnoRouter import 금지
    • UI
      • SwiftUI scene/view와 Util 기반 공용 UI 조합만 담당
      • Domain, InnoRouter 직접 import 금지
    • Router
      • InnoRouter coordinator, push/modal/tab wiring 담당
      • root Features가 조립하는 concrete 진입점
    • Testing
      • fixture와 test helper 제공

InnoRouter는 상위 coordinator가 child coordinator를 조립하는 구조와 잘 맞지만, sibling navigation을 자동으로 해결해 주지는 않습니다.
이 샘플에서는 People -> Settings -> People 왕복 이동 예제를 넣되, leaf feature는 sibling router를 직접 모르고 intent만 올리며 EntireTabCoordinator가 실제 탭 전환과 child detail push를 중재합니다.

  • People UI -> People Logic intent -> People Coordinator consume -> EntireTabCoordinator mediation -> SettingsCoordinator push
  • Settings UI -> Settings Logic intent -> Settings Coordinator consume -> EntireTabCoordinator mediation -> PeopleCoordinator push

즉 런타임 이동은 왕복이 가능하지만, 컴파일 의존은 People -> EntireTab <- Settings 구조만 유지합니다.

이렇게 둔 이유는 두 가지입니다.

  • AppContainer가 remote/data/domain 구현이나 개별 feature wiring을 직접 조립하지 않게 해서 상위 composition 책임을 줄이기 위해
  • banksalad-ios와 비슷하게 루트 Layers 프로젝트가 Remote/Data/Domain 연결을 맡고, 루트 Features 프로젝트는 domain 의존성만 받아 feature 조립을 맡게 하기 위해

즉, 이 샘플에서 CoreNetwork는 단순 factory가 아니라 transport adapter 입니다.
또한 Layers 루트 프로젝트와 Features 루트 프로젝트는 구현 레이어가 아니라 composition 레이어입니다.

Architecture Graph

graph TD
    App["App"]
    AppContainer["AppContainer"]
    Analytics["Analytics"]
    CoreNetwork["CoreNetwork"]
    NetworkTransport["NetworkTransport"]
    Layers["Layers"]
    LayerContainer["LayerContainer"]
    RemoteContainer["RemoteContainer"]
    DataContainer["DataContainer"]
    DomainContainer["DomainContainer"]
    Features["Features"]
    FeatureContainer["FeatureContainer"]
    EntireTab["EntireTabFeature"]
    People["PeopleFeature"]
    Posts["PostsFeature"]
    Settings["SettingsFeature"]

    App --> AppContainer
    AppContainer --> Analytics
    AppContainer --> CoreNetwork
    CoreNetwork --> NetworkTransport
    AppContainer --> NetworkTransport
    AppContainer --> Layers
    AppContainer --> Features

    Layers --> LayerContainer
    LayerContainer --> RemoteContainer
    LayerContainer --> DataContainer
    LayerContainer --> DomainContainer
    RemoteContainer --> CoreNetwork
    DataContainer --> RemoteContainer
    DomainContainer --> DataContainer

    Features --> FeatureContainer
    FeatureContainer --> DomainContainer
    FeatureContainer --> EntireTab
    FeatureContainer --> People
    FeatureContainer --> Posts
    FeatureContainer --> Settings
    EntireTab --> People
    EntireTab --> Posts
    EntireTab --> Settings
Loading

이 그래프는 현재 샘플의 실제 composition 의도를 문서용으로 수동 관리합니다.
InnoDI-DependencyGraph는 시각화보다 DAG 검증용으로 사용합니다.

Manifest Structure

  • Workspace.swift
    • 전체 모듈 project를 묶는 루트 workspace
  • Tuist/ProjectDescriptionHelpers
    • 공용 destinations / deployment targets / dependency helper
  • App/Project.swift
  • Features/Project.swift
  • Cores/CoreNetwork/Project.swift
  • Layers/*/Project.swift
  • Features/*/Project.swift
  • ThirdParties/*/Project.swift (SDK wrapper가 생길 때 사용)
  • Utils/*/Project.swift

Tuist helper는 현재 두 단계로 destinations / deployment targets를 나눕니다.

  • 일반 모듈: defaultDestinations / defaultDeploymentTargets (iPhone / iPad / macOS)
  • shared multi-platform 모듈: sharedModuleDestinations / sharedModuleDeploymentTargets (iPhone / iPad / macOS / tvOS / visionOS / watchOS) 실제 iOS 앱과 함께 설치되는 watchOS companion은 별도 watch app/watch extension 타깃이 있어야 하며, 현재 샘플은 App 프로젝트에 그 타깃을 포함합니다.

Module Map

App

  • App/Sources/InnoSampleApp.swift
    • Composition root
    • AppContainer에서 InnoDI graph를 만들고 root Features scene에 전달
    • 샘플 analytics wrapper를 통해 앱 시작 이벤트를 기록
  • App/Sources/AppContainer.swift
    • InnoDI root container
    • base URL 입력으로 NetworkTransport -> LayerContainer -> FeatureContainer 순서로 조립
    • Layers 내부의 data/domain container를 모르고, 개별 feature wiring 대신 use case를 root Features에 전달
    • AnalyticsClient concrete 구현을 앱 composition root에서 생성
    • Project.app(watchCompanion: ...)로 iOS companion watch app / watch extension 타깃을 함께 생성
    • App/Project.swift에서 앱 타깃 정의

Cores

  • Cores/CoreNetwork
    • InnoNetwork를 앱 공통 규칙으로 감싸는 네트워크 코어
    • NetworkEnvironment로 base URL / app metadata / header policy source of truth 관리
    • NetworkFactoryNetworkConfiguration, retry policy, transport 생성
    • RequestDefinition, HeaderPolicy, NetworkFailure를 통해 request/error 정책을 명시
    • HeaderPolicy.custom은 환경 기본 헤더를 opt-out하지만 X-Request-ID는 추적을 위해 계속 자동 추가
    • request/response interceptor와 logger를 공통 적용
    • InnoNetwork 3.1.0LowLevelNetworkClient.perform(executable:)를 소비해 concrete client 타입 의존을 줄임
    • NetworkTransportRemote 대신 InnoNetwork를 직접 다룸
    • Cores/CoreNetwork/Project.swift에서 독립 project로 관리

Layers

  • Layers
    • composition-only framework
    • LayerContainer는 plain composition wrapper로 남기고, RemoteContainer -> DataContainer -> DomainContainer를 연결한 뒤 feature 입력용 use case만 외부에 노출
    • App과 root Features는 내부 조립 단계를 모르고 LayerContainer.featureUseCases 같은 feature-facing surface만 사용
    • SwiftUI, InnoFlow, InnoRouter, InnoNetwork를 import 하지 않음
    • 현재 제약:
      • FeatureContainer, AppContainer 같은 UI/root composition만 @MainActor
      • LayerContainer는 non-UI composition이므로 actor-agnostic 유지
      • Domain, Data, Remote의 shared contract는 actor-agnostic 유지
      • FeaturesDomainContainer concrete를 직접 알지 않고 FeatureUseCaseContaining만 소비
      • AppRemoteContainer, DataContainer, DomainContainer를 직접 조립하지 않음
  • Layers/Domain
    • summary 모델은 도메인별 Models에 두고, DomainContainer와 도메인별 composition extension은 Container 아래로 분리
    • repository protocol과 concrete use case 정의
    • shared contract는 actor-agnostic로 유지하고, main actor는 Layers / Features / App composition 경계에서만 적용
    • DomainContainer가 모든 repository 입력을 받고, use case는 도메인별 extension에서 computed로 제공
    • 현재 제약:
      • DomainData contract만 알고 RemoteCoreNetwork를 모름
      • summary 모델, repository protocol, use case는 도메인별 폴더에 두고, composition helper는 Container 아래로 분리
  • Layers/Data
    • repository 구현과 remote data source contract 정의
    • DataContainer가 모든 repository를 shared로 제공
    • User/Post/Todo 도메인 아래 Models/DataSources/Repositories/Factories로 폴더를 나눠 책임을 드러냄
    • 현재 제약:
      • Data가 DTO -> Domain 매핑과 curate / empty response 정책을 담당
      • DataRemoteDataSourceContaining contract만 의존하고 RemoteContainer concrete는 직접 퍼뜨리지 않음
  • Layers/Remote
    • JSONPlaceholder request definition과 remote model 매핑
    • User/Post/Todo 도메인 아래 Requests/DataSources/Factories로 폴더를 나눠 역할을 드러냄
    • raw 응답을 그대로 퍼뜨리지 않고, 필요한 필드만 평탄화/정규화한 DTO를 반환
    • 외부 도메인 호출은 HeaderPolicy.external을 사용
    • Data가 정의한 remote data source contract 구현
    • RemoteContainerCoreNetwork transport 기반 data source를 shared로 제공
    • InnoNetwork를 직접 import 하지 않음
    • 각 layer는 별도 Project.swift로 분리
    • 현재 제약:
      • Remote는 DTO decode와 remote data source 구현만 담당하고 Domain 모델을 직접 만들지 않음
      • request 실행은 NetworkTransport가 맡고, request 타입은 순수 명세값으로 유지

Features

  • Features
    • root feature composition framework
    • FeatureContainerFeatureRootScene를 제공
    • App이 전달한 use case로 leaf feature Router 타깃과 tab container를 조립
    • 외부 공개 표면은 root coordinator 중심으로 유지
    • generated 파일과 test support를 제외한 production 코드는 가급적 한 파일에 한 top-level 객체만 두고, 같은 씬 안에서만 쓰는 보조 뷰는 Screen+*.swift extension 파일로 분리
    • UI 규칙:
      • 메인 화면과 push destination은 *Screen, modal은 *Sheet suffix 사용
      • 한 씬 안에서만 쓰는 보조 뷰와 section builder는 Screen+*.swift extension 파일로 분리
      • 재사용 가능한 공용 컴포넌트만 SampleDesignSupport로 승격
    • Router 규칙:
      • coordinator와 route 정의는 Router에 두고, navigation/modal host view는 *RouteHost suffix 사용
      • *RouteHostNavigationHost, ModalHost, TabCoordinatorView를 통해 route를 실제 screen/sheet에 매핑
  • Features/EntireTabFeature
    • 3탭 셸 (People, Posts, Settings)
    • InnoRouter.TabCoordinatorView 사용
    • EntireTabFeatureInterface / Logic / UI / Router / Testing / Tests로 분리
    • EntireTabContainerRouter 타깃에서 탭 coordinator를 조립
  • Features/PeopleFeature
    • /users 호출
    • row 선택 시 push detail
    • Overview 버튼으로 modal sheet
    • PeopleFeatureInterface / Logic / UI / Router / Testing / Tests로 분리
    • PeopleFeatureContainerRouter 타깃에서 coordinator를 조립
  • Features/PostsFeature
    • /posts 호출
    • row 선택 시 push detail
    • Highlights 버튼으로 modal sheet
    • PostsFeatureInterface / Logic / UI / Router / Testing / Tests로 분리
    • PostsFeatureContainerRouter 타깃에서 coordinator를 조립
  • Features/SettingsFeature
    • /todos 호출
    • row 선택 시 push detail
    • Digest 버튼으로 modal sheet
    • SettingsFeatureInterface / Logic / UI / Router / Testing / Tests로 분리
    • SettingsFeatureContainerRouter 타깃에서 coordinator를 조립
    • 각 feature는 별도 Project.swift에서 다중 타깃으로 분리

ThirdParties

  • ThirdParties/Analytics
    • 외부 SDK wrapper 예시 모듈
    • AnalyticsInterface framework에 상위 모듈이 의존할 계약을 둡니다.
    • Analytics static library에 가상의 SDK adapter 구현을 둡니다.
    • App이 composition root로서 concrete 구현을 생성하고 앱 시작 이벤트를 기록합니다.
  • Project.thirdParty(...)
    • Interface framework + implementation static library + tests 조합을 기본으로 제공
    • ThirdParty는 SDK wrapper/provider adapter 전용이라는 전제를 반영합니다.

Utils

  • Utils/SampleDesignSupport
    • 로딩/에러/metric card/pill 같은 공용 UI 컴포넌트
    • 샘플 내부 helper/UI support 모듈이므로 ThirdParty가 아니라 Util로 분리합니다.
    • 독립 framework project

Run

cd /Users/changwoo.son/Developer/InnoSquad/InnoSample
tuist generate --no-open
open InnoSample.xcworkspace

or

cd /Users/changwoo.son/Developer/InnoSquad/InnoSample
make generate
make open

Verify

cd /Users/changwoo.son/Developer/InnoSquad/InnoSample
./Scripts/check-layer-boundaries.sh
xcodebuild -workspace InnoSample.xcworkspace -scheme Domain -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme Data -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme PeopleFeature -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme PostsFeature -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme SettingsFeature -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme EntireTabFeature -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme Layers -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme Features -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme InnoSampleApp -destination 'platform=macOS' test
xcodebuild -workspace InnoSample.xcworkspace -scheme InnoSampleApp -destination 'platform=macOS' build

InnoDI graph CLI도 같이 쓰려면 아래를 사용할 수 있습니다.

cd /Users/changwoo.son/Developer/InnoSquad/InnoSample
./Scripts/check-di-graph.sh validate
make verify

UI Test Scope

  • App/UITests는 app 레벨 smoke test와 핵심 사용자 흐름 검증만 담당합니다.
  • 현재 baseline 시나리오:
    • 앱 실행
    • 3개 탭 전환
    • People / Posts / Settings 리스트 렌더링 확인
    • People detail push
    • People overview modal 표시/닫기
  • feature별 UI test 타깃은 두지 않고, 루트 시나리오 중심으로 유지합니다.

Docs

Build Notes

  • Layers는 현재 framework로 유지합니다.
  • generated artifact hygiene는 .gitignore 기준으로 유지합니다.
    • Derived/
    • .xcodeproj/
    • xcuserdata/
    • .DS_Store
  • 이유는 App과 root Features가 모두 Layers를 소비하는 구조라, static product로 두면 duplicate link 경고가 발생하기 때문입니다.
  • 추후 AppLayers를 직접 참조하지 않는 구조로 바뀌면 static product 복귀를 다시 검토할 수 있습니다.

Workspace Hygiene

  • Derived/, xcuserdata/, .DS_Store, .xcuserstate는 repo 관리 대상이 아닙니다.
  • .gitignore에서 이 산출물을 무시하고, boundary script와 wiring test로 실제 구조 회귀를 검증합니다.

Dependency Strategy

  • InnoSample은 로컬 monorepo path dependency 샘플이 아니라, 릴리즈된 Inno 라이브러리를 소비하는 샘플입니다.
  • Inno 라이브러리 의존성 source of truth는 Tuist/Package.swift 입니다.
  • 버전은 exact로 고정하고 Tuist/Package.resolved를 함께 관리합니다.
  • 따라서 프레임워크를 로컬 수정해 즉시 반영하는 용도보다는, 외부 사용자 관점의 통합 예제로 보는 편이 맞습니다.

현재 확인된 빌드 상태:

  • InnoDIInnoFlow 모두 consumer package graph에서 swift-docc-plugin을 제거한 뒤에도 tuist generate, xcodebuild는 성공합니다.
  • SwiftPM lockfile을 새로 해석하면 기존 swift-docc-plugin identity 경고도 사라집니다.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors