예전 회사에 있을때 부터 앱이 커짐에 따라 모듈화의 필요성을 느끼기는 했었다.
내부 서비스인 "Inpose" IOS의 앱의 규모는 cocoapod 제외하고 총 888개의 파일과 147,566의 line수를 가지고 있다.
작성한 프로젝트의 총 라인수는 다음과 같이 구할 수 있다.
터미널로 원하는 프로젝트로 이동하여 다음 명령어를 실행하면 알 수 있다.
find . \( -iname \*.swift \) -exec wc -l '{}' \+
프로젝트가 이것 하나만 있는 것이 아닌 외주로 받고 지속적으로 관리해준 앱 중에서도 이와 같은 규모를 가진것이 2 ~ 3 개 정도 있었다.
이런 프로젝트들은 특성상 개발하는 와중에 요구사항과 디자인이 많이 바뀌기도 하고 출시한 후에도 전면 리뉴얼을 할 만큼 수정사항이 많았다.
이럴때마다 모듈화를 해놓지 않아서 작은 수정사항에도 전체 프로젝트를 다시 빌드하고 확인하고를 수십번 반복했었다.
따라서 모듈화를 해서 조금 생산성을 높여 보려 했지만 이미 너무 타이트하게 커플링이 되어 있기도 했고, 결정적으로 재정적인 문제로 다른 외주 작업들을 해야했기에 도저히 모듈화까지 할 엄두가 나지 않았다.
그나마 다행인 것은 디자인패턴에 맞춰 개발을 했기 때문에 유지보수 및 확장은 큰 문제가 없었다.
여담이 길었지만 결론 그동안 모듈화에 대한 생각이 있었고 마침 시간도 많아지고 해서 TUIST를 이용한 모듈화 작업을 시도해보았다.
1. 들어가기 전에 목표 구성
우선 TUIST를 들어가기 전에 어떤 방식으로 프로젝트가 구성되길 원하는 가를 생각해 보았다.
지금 놀이터의 개념으로 각종 애니메이션을 만들고 있는 프로젝트가 하나 있고 구성은 다음과 같다.
위 앱은 보면 알 수 있듯이 각 화면은 굉장히 독립적이다. 하지만 각 화면을 테스트하거나 추가할려고 하면 SceneDelegate에서
시작 화면을 바꿔서 실행하거나 아니면 굳이 MainViewController에서 해당 화면으로 이동하는 버튼을 눌러야 했다.
따라서 필자는 각 화면을 마치 독립된 앱처럼 사용할 수 있고 최종 결과물에만 프레임워크 형태로 합치는 것을 목표로 삼았다.
최종적인 구성은 위와 같다 이제 그러면 본격적으로 Tuist를 적용을 시작해보자.
필자도 이틀동안 해서 겨우 감만 잡은 상태이기에 우선 친숙해지기 위하여 아주아주 기본적이고 최대한 단계별로 상세하게 기술해볼 예정이다.
2. Tuist 설치, 시작 방법 및 기본 구조 설명
2-1. 설치 및 시작
Tuist를 사용하기 위해서는 우선 당연하게 다운로드를 받아야 한다.
다운로드는 터미널에서 다음 명령어를 실행하면 된다.
curl -Ls https://install.tuist.io | bash
새로운 프로젝트를 시작하던 기존 프로젝트에 도입하려고 하던 일단 새로운 폴더에 Tuist환경을 만들어놓고 시작을 해야 한다.
실행 방법은 다음 명령어를 통해서 가능하다.
mkdir TuistTemp
cd TuistTemp
tuist init --platform ios
여기까지 했다면 다음과 같은 결과가 나온다.
tree . 이라는 명령어를 사용하면 아래와 같이 하위 디렉토리들을 한눈에 확인 가능하다.
만약 tree가 설치 안되어 있다면 다음 명령어를 통해 설치 가능하다
brew install tree
2-2. 기본 구조 설명
여기서 제일 중요한 파일은 Project.swift라는 파일이다.
이 파일이 바로 프로젝트를 어떤 조건으로 생성할 것이냐는 지정하는 파일이다.
그리고 열어보면 다음과 같이 되어 있을 것이다.
/*
+-------------+
| |
| App | Contains TuistTemp App target and TuistTemp unit-test target
| |
+------+-------------+-------+
| depends on |
| |
+----v-----+ +-----v-----+
| | | |
| Kit | | UI | Two independent frameworks to share code and start modularising your app
| | | |
+----------+ +-----------+
*/
// MARK: - Project
// Local plugin loaded
let localHelper = LocalHelper(name: "MyPlugin")
// Creates our project using a helper function defined in ProjectDescriptionHelpers
let project = Project.app(name: "TuistTemp",
platform: .iOS,
additionalTargets: ["TuistTempKit", "TuistTempUI"])
보면 뭔지 모르지만 Plugin을 참조하는 localHelper와 Project.app통해 프로젝트를 만든 다는 것을 알 수 있다.
좀더 디테일 하게 들어가기 전에 우선 명시적으로 알아야 할것이 있다.
혹시나 모르는 사람들을 위해 Workspace, Project, Target에 대해서 우선 아주 간단하게 짚고 넘어가자.
- Workspace: Project가 2개 이상일시 이를 묶어서 관리하기 위한 개념이라 생각하면 편하다.
Cocoapod을 생각하면 편하다. 원래 기본 프로젝트는 .xcodeproj라고 되어 있지만,
pod install을 하면 .xcworkspace가 하나 더 생긴다.
Cocoapod용 프로젝트 하나 우리가 작업하는 프로젝트 하나 총 2개가 되었기때문이다. - Project: 각종 파일이나 리소스를 보관하기 위한 저장소라고 생각하면 편하겠다.
이때 Project는 Target하나 이상을 무조건 포함하고 있다. - Target: 최종 형태의 결과물이다. Project의 결과물 일 수도 있고 Workspace의 결과물일 수도 있다.
또한 형태가 app, framework, library일 수도 있다.
더 자세한 설명은 다음 링크를 보면 된다.
https://ios-development.tistory.com/1008
이제 다시 Project.swift의 코드를 보자.
let project = Project.app(name: "TuistTemp",
platform: .iOS,
additionalTargets: ["TuistTempKit", "TuistTempUI"])
대충 감이 올것이다.
Tuist에서 자동으로 생성해준 프로젝트는
일단 Project.app() 인걸로 보아 최종 산출물이 app의 형태이며
이름이 TuistTemp이고
동작 플랫폼은 .iOS이며
Target은 TuistTempKit과 TuistTempUI를 포함하고 있다.
좀더 자세하게 들어가보자. Tuist-> ProjectDescriptions->Project+Templates.swift를 열어보면 어떠한 코드들이 쫙 등장한다.
(project+Templates는 Project.swift를 위한 Util을 모아놓은 파일이라고 생각하면 편하다. 물론 Project+Something이런식으로 직접 파일을 만들어도 된다.)
처음보면 이게 뭐야??.... 라고 생각되기 마련이다. 당연하다 필자도 보고 처음에 어지러웠다.
침착하게 천천히 함수 하나하나 살펴 보자.
우선 app()이라는 함수를 파헤쳐 보자.
public static func app(name: String, platform: Platform, additionalTargets: [String]) -> Project {
var targets = makeAppTargets(name: name,
platform: platform,
dependencies: additionalTargets.map { TargetDependency.target(name: $0) })
targets += additionalTargets.flatMap({ makeFrameworkTargets(name: $0, platform: platform) })
return Project(name: name,
organizationName: "tuist.io",
targets: targets)
}
생각보다 별거 없다.
인자로 받아온 additionalTargets를 가지고 AppTarget을 하나 만들고 makeFrameWorkTarget이라는 것으로 보아 FrameWork를 만드는 것을 알 수 있다.
그리고 Project에 만들어준 타겟들을 넣어주어 생성한다.
한층 더 깊게 makeAppTargets로 들어가 보자
/// Helper function to create the application target and the unit test target.
private static func makeAppTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] {
let platform: Platform = platform
let infoPlist: [String: InfoPlist.Value] = [
"CFBundleShortVersionString": "1.0",
"CFBundleVersion": "1",
"UIMainStoryboardFile": "",
"UILaunchStoryboardName": "LaunchScreen"
]
let mainTarget = Target(
name: name,
platform: platform,
product: .app,
bundleId: "io.tuist.\(name)",
infoPlist: .extendingDefault(with: infoPlist),
sources: ["Targets/\(name)/Sources/**"],
resources: ["Targets/\(name)/Resources/**"],
dependencies: dependencies
)
let testTarget = Target(
name: "\(name)Tests",
platform: platform,
product: .unitTests,
bundleId: "io.tuist.\(name)Tests",
infoPlist: .default,
sources: ["Targets/\(name)/Tests/**"],
dependencies: [
.target(name: "\(name)")
])
return [mainTarget, testTarget]
}
살펴 보면 우리는 makeAppTarget함수가
- infoPlist를 만들어주고
- app 형태인 Target과 unitTest 형태의 Target을 만든다.
여기서 Target생성자를 살펴보자. (* 중요. 전반적으로 계속 쓰인다)
- name : Target의 이름
- platform : .iOS, macOs, watchOs등등을 설정
- product : 산출 형태, app, framework, unittest 등등
- bundleId : 번들 아이디
- infoPlist : infoPlist
- sources : 어떤 경로에 있는 코드들을 포함할건지 지정한다.
위의 코드에서는 "Targets/\(name)/Sources 아래의 모든 소스파일을 포함한다" 이렇게 설정되어 있다. - resources: 보통 에셋,font등과 같은 것들을 모아놓은 폴더를 지정한다.
즉 에셋,font같은 것이 없으면 nil로 처리해 두어도 상관 없다.
위의 코드에서는 "Targets/\(name)/Resources 아래의 모든 파일을 포함한다" 이렇게 설정되어 있다. - dependencies: 다른 프로젝트 혹은 framework, target을 의존하고 있는지 설정하는 부분이다.
이외에도 많은 인자들이 있다. 더 알아보기 위해서는 다음 링크를 보면 된다.
https://github.com/tuist/tuist/blob/main/Sources/ProjectDescription/Target.swift
현재 까지 Project의 산출물을 한눈에 보기위해 그리면 다음과 같다.
Project
|---Target(name : TuistTemp, product: .app)
|--- dependencies
|--- TuistTempKit(형태 아직 모름)
|--- TuistTempUI(형태 아직 모름)
|---Target(name : TuistTempTest, product : . unitTest)
|--- dependencies
|--- TuistTemp(형태 .app)
다음으로 makeFrameWorksTarget을 들여다 보자
/// Helper function to create a framework target and an associated unit test target//
/// name = ["TuistTempKit","TuistTempUI"]
private static func makeFrameworkTargets(name: String, platform: Platform) -> [Target] {
let sources = Target(name: name,
platform: platform,
product: .framework,
bundleId: "io.tuist.\(name)",
infoPlist: .default,
sources: ["Targets/\(name)/Sources/**"],
resources: [],
dependencies: [])
let tests = Target(name: "\(name)Tests",
platform: platform,
product: .unitTests,
bundleId: "io.tuist.\(name)Tests",
infoPlist: .default,
sources: ["Targets/\(name)/Tests/**"],
resources: [],
dependencies: [.target(name: name)])
return [sources, tests]
}
makeAppTarget과 별반 다를게 없다.
해당 함수의 결과는
[TuistTempKit-frameWork, TuistTempKitTests -unitTest(dependencies :TuistTempKit-frameWork) ],
[TuistTempUI -frameWork, TuistTempUITests-unitTest(dependencies :TuistTempUI -frameWork)] 이다.
이제 위에서 그렸던 산출물에 추가를 하여 최종 산출물을 그려보자
Project
|---Target(name : TuistTemp, product: .app)
|--- dependencies
|--- TuistTempKit(frameWork)
|--- TuistTempUI(frameWork)
|---Target(name : TuistTempTest, product : . unitTest)
|--- dependencies
|--- TuistTemp(.app)
|---Target(name : TuistTempKit, product : . frameWork)
|---Target(name : TuistTempKitTest, product : . unitTest)
|--- dependencies
|--- TuistTempKit(.frameWork)
|---Target(name : TuistTempUI, product : . frameWork)
|---Target(name : TuistTempUITest, product : . unitTest)
|--- dependencies
|--- TuistTempUI(.frameWork)
코드 분석은 끝났으니 실제로 한번 실행을 해보자
tuist generate
터미널에서 해당 명령어를 실행하면 Project.swift와 Project+Templates.swift를 기반으로 xcode프로젝트가 생성된다.
보면 예상한대로 하나의 프로젝트가 있고 그안에 타겟 6개가 있는 것을 볼 수 있다.
그리고 해당 타겟의 Frameworks, Libraries를 보면 dependency 또한 설정되어 있음을 알 수 있다.
그리고 프로젝트 네비게이터를 펼쳐보면 하나의 규칙성이 있다는 것을 알 수 있다.
|---Feature
|--- Sources(코드 파일들의 디렉토리)
|--- Resources(에셋, 폰트,storyBoard 등을 위한 디렉토리)
|--- 필요에 따라서 더 만들 수 있다.(ex DevSources)
어떤 프로젝트,모듈,프레임워크던지 위의 형태를 가지고 만들어지게 된다.
이것이 Tuist에서 가장 기본이 되는 단위라고 생각하면 된다.
여기까지 차근차근 읽었으면 이해했으리라 생각된다.
다음글 부터 본격적으로 어떻게 우리 입맛에 맞게 바꾸는지 시도해 볼 것이다.
'iOS' 카테고리의 다른 글
Tuist(3) - 환경변수 (0) | 2023.02.13 |
---|---|
Tuist(2) - 기존 프로젝트에 도입하기 (2) | 2023.02.13 |
MVVM(6) - ViewModel (1) | 2023.01.13 |
MVVM(5) - Coordinator (2) | 2023.01.05 |
MVVM(4) - Coordinator (1) | 2022.12.27 |