SwiftUI 침하하 앱 개발기
UIKit로 개발해온 지 어느 정도 됐고, Flutter도 해 봤겠다 SwiftUI도 한번 사용해보고 싶었다.
튜토리얼 따라 만드는 건 재미없으니까 실제로 쓰고 싶은 앱을 만들어보기로 했다.
침착맨을 최~고로 좋아하는 팬 중 한 명인데, 공식 팬 커뮤니티 사이트인 chimhaha.net이 웹으로밖에 없다.
모바일로 볼 때 UI가 썩 만족스럽지 않아서, 공부할 겸 iOS 앱으로 만들어보면 어떨까 싶었다.

어차피 공개 API가 없으니 실제 서비스가 아니라 학습용 클론이고,
데이터는 JSONPlaceholder로 대체해서 사용한다.
CLAUDE.md와 PLAN.md를 만들고 Claude Code와 함께 작업한다.
레이아웃은 실제 chimhaha.net 스크린샷을 Claude한테 넘겨줬더니
게시판 드로어 구조, 탭바 구성, 각 화면 레이아웃을 CLAUDE.md에 정리해줬다.
색상, 폰트도 다 따준다.
jsx로 프로토타입도 만들어주는데 진짜 신통방통하지 않을 수 없다.

클로드가 plan에 따라 코드를 보여주면, 직접 쳐가면서 공부하기로 한다.
| 레이어 | 선택 | 이유 |
|---|---|---|
| UI | SwiftUI | 학습 목적 |
| 아키텍처 | TCA | SwiftUI와 궁합이 좋고, 단방향 데이터 흐름 학습 |
| 비동기 | Swift Concurrency | TCA가 Swift Concurrency로 전환했다고 함 |
| DI | TCA @Dependency | Swinject 대신 TCA 내장 DI 시스템 사용 |
UIKit에서 Clean Architecture + MVVM-C + Swinject를 써왔는데,
SwiftUI + TCA에서는 Reducer가 ViewModel 역할을 겸하고,
@Dependency가 Swinject 역할을 한다고 한다.
추후 차차 공부해나가는걸로...
SwiftUI는 UIKit처럼 ViewController 중심 구조가 아니라,
상태(State)를 기반으로 화면을 선언적으로 그리는 프레임워크
UIKit이 “어떻게 그릴지”를 명령형으로 작성하는 방식이라면,
SwiftUI는 “이 상태라면 이런 화면이다”를 선언하는 방식에 가깝다.
SwiftUI의 View는 class가 아니라 struct
struct ContentView: View {
var body: some View {
Text("Hello, world!")
}
}
UIKit처럼 직접 label.text를 바꾸는 개념이 아니다!
UIKit은 Lifecycle이 굉장히 복잡했는데 SwiftUI는 상대적으로 간단한 편이다.
1번에서 말했던 것처럼, UIKit의 UIViewController는 Class인 반면 SwiftUI의 View는 Struct이므로 근본적으로 다르다. 이걸 이해하고 생각하면 쉬운 것 같음
SwiftUI는 상태가 바뀔 때마다 body를 다시 호출해서 새로운 View 구조체를 만든다.
UIKit처럼 기존 뷰 객체를 업데이트하는 게 아니라, 새 구조체를 만들어서 이전 것과 비교(diff)한 뒤 실제 화면 변경 최솟값만 반영한다.
struct CounterView: View {
@State var count = 0
var body: some View { // count가 바뀔 때마다 이 블록 전체가 다시 실행됨
VStack {
Text("\(count)")
Button("+") { count += 1 }
}
}
}
계속 재생성되면 비효율적인거 아니야? 했었는데, 가벼운 Struct 타입이라 자주 재생성돼도 성능 문제가 없다고 한다.
무튼 이 View 안에 뭔가를 두게 되면, 뷰가 재생성될 때 함께 새로 만들어진다.
struct MyView: View {
var data: [Post] = [] // 뷰가 재생성되면 data도 새로 만들어짐
var body: some View { ... }
}
그래서 State를 View 안에 직접 두면 안된다.
Struct인 구조체는 var로 선언된 프로퍼티를 mutating 없이 바꿀 수 없다. 그리고 바꾼다 해도 뷰가 재생성되면 값이 사라진다.
SwiftUI가 @State를 제공하는 이유가 여기에 있는데, @State는 실제 값을 View 구조체 안이 아니라 SwiftUI 프레임워크가 관리하는 별도 저장소에 보관한다. View는 그 저장소를 가리키는 포인터만 들고 있다.
struct CounterView: View {
// 이렇게 하면 뷰가 재생성될 때 count가 0으로 초기화됨
var count = 0
// SwiftUI가 별도 저장소에 보관, 뷰가 재생성돼도 값 유지
@State var count = 0
}
| 래퍼 | 용도 | 소유 위치 |
|---|---|---|
@State | 뷰 내부 단순 상태 | SwiftUI 프레임워크 |
@Binding | 부모로부터 받은 상태 | 부모 뷰 |
@StateObject | 뷰가 소유하는 ObservableObject | SwiftUI 프레임워크 |
@ObservedObject | 외부에서 주입받은 ObservableObject | 외부 |
@EnvironmentObject | 뷰 계층 전체에서 공유 | 외부 |
TCA를 쓰면 위 래퍼들을 직접 쓸 일이 많이 줄어든다. Store가 상태 관리를 전담하기 때문이다.
| UIKit | SwiftUI | 비고 |
|---|---|---|
viewDidLoad | .task {} | 뷰 해제 시 Task 자동 취소 |
viewDidAppear | .onAppear | |
viewDidDisappear | .onDisappear |
.task {} 권장. .onAppear 안에서 직접 Task { }를 만들면 뷰가 사라져도 Task가 남을 수 있음. TCA Effect.run은 내부적으로 취소 자동 처리.
TabView는 탭 뷰를 해제하지 않아서 탭이 많아지면 Store가 계속 메모리에 남는 점 주의.
UIKit은 viewDidLoad → viewWillAppear → viewDidAppear → viewDidDisappear 등의 순서가 명확하고, 각 시점에 무슨 일을 해야 하는지 잘 정의되어 있다.
반면, SwiftUI는 뷰가 언제 생성되고 사라지는지 프레임워크가 결정한다.
최적화를 위해 예상보다 일찍 만들거나, 예상보다 늦게 해제할 수 있다.
그래서 기존과 같이 생명주기에 의존한 코드를 짜는 게 까다롭다.
// UIKit
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
fetchData()
}
// SwiftUI — .task는 뷰가 화면에 나타날 때, 사라지면 자동 취소
.task {
await store.send(.fetchPosts).finish()
}
.task가 viewDidLoad의 대응처럼 쓰이지만, 정확히 같지는 않다. 뷰가 화면에서 사라졌다 다시 나타나면 .task도 다시 실행된다.
SwiftUI는 Composition을 사용해서 View를 그리는데,
VStack {
Text("Title")
Button("Tap") { }
}
이렇게 VStack, HStack, ZStack, Modifier 체인 등을 사용해서 View Tree를 쌓는다.
이건 Flutter랑 비슷하기도 하고, 전에 해보기도 해서 넘어감
이 부분은 나중에 TCA의 상태 기반 네비게이션과 연결된다고 하는데,
화면 전환도 “명령”이 아니라 “상태 기반 표현”에 가깝다고 함
UIKit:
navigationController?.pushViewController(...)
SwiftUI:
NavigationStack {
NavigationLink("Detail", value: ...)
}
SwiftUI 관련 공고를 볼 때, TCA 아키텍처를 많이 언급하는 것 같아 사용해보기로 한 것인데, 그럼 도대체 TCA가 뭘까?
TCA는 pointfree에서 만든 오픈소스 상태 관리 아키텍처 라이브러리인데, 상태를 기반으로 렌더링되는 SwiftUI와 잘 맞는 아키텍처로 자주 사용된다.
https://github.com/pointfreeco/swift-composable-architecture
여기에 아주 잘 설명되어있다.
TCA의 핵심
- 모든 화면은 State로 표현되고
- 사용자의 입력은 Action으로 들어오며
- 상태 변경은 Reducer에서만 일어난다.
구조를 아주 단순화한다면:
State → View
Action → Reducer → State 변경
❗️ 참고: Reducer는 현재 상태와 Action을 받아 새로운 상태를 만들어내는 함수로, 상태를 직접 바꾸는 코드는 Reducer에만 존재한다.
func reduce(into state: inout State, action: Action) -> Effect<Action>
차차 공부하면서 더 깊게 알아보도록 하겠당.
이후 단계는 새로 알아가는 것들 위주로 포스팅을 할 예정!

앱 아이콘도 등록 ㅎ