[SwiftUI] 침하하 앱을 만들어보자 / Step 0 - SwiftUI 알아보기

팔랑이·2026년 2월 24일

iOS/Swift

목록 보기
85/89

SwiftUI 침하하 앱 개발기


2026-02-24 · 프로젝트 시작!

SwiftUI를 공부하자

UIKit로 개발해온 지 어느 정도 됐고, Flutter도 해 봤겠다 SwiftUI도 한번 사용해보고 싶었다.
튜토리얼 따라 만드는 건 재미없으니까 실제로 쓰고 싶은 앱을 만들어보기로 했다.

뭘 만들까

침착맨을 최~고로 좋아하는 팬 중 한 명인데, 공식 팬 커뮤니티 사이트인 chimhaha.net이 웹으로밖에 없다.
모바일로 볼 때 UI가 썩 만족스럽지 않아서, 공부할 겸 iOS 앱으로 만들어보면 어떨까 싶었다.

어차피 공개 API가 없으니 실제 서비스가 아니라 학습용 클론이고,
데이터는 JSONPlaceholder로 대체해서 사용한다.

Claude와 함께 설계하기

CLAUDE.mdPLAN.md를 만들고 Claude Code와 함께 작업한다.

  • CLAUDE.md — 앱의 기술 스택, 아키텍처, 디자인 시스템, 개발 규칙을 정의해두는 파일.
    Claude가 새 세션을 시작할 때 이 파일을 읽고 컨텍스트를 파악한다.
  • PLAN.md — 전체 개발 단계를 Step으로 나눠서 체크리스트처럼 관리.
    지금 어디까지 했는지, 다음에 뭘 해야 하는지 한눈에 보인다.

레이아웃은 실제 chimhaha.net 스크린샷을 Claude한테 넘겨줬더니
게시판 드로어 구조, 탭바 구성, 각 화면 레이아웃을 CLAUDE.md에 정리해줬다.
색상, 폰트도 다 따준다.
jsx로 프로토타입도 만들어주는데 진짜 신통방통하지 않을 수 없다.


클로드가 plan에 따라 코드를 보여주면, 직접 쳐가면서 공부하기로 한다.

기술 스택 선택

레이어선택이유
UISwiftUI학습 목적
아키텍처TCASwiftUI와 궁합이 좋고, 단방향 데이터 흐름 학습
비동기Swift ConcurrencyTCA가 Swift Concurrency로 전환했다고 함
DITCA @DependencySwinject 대신 TCA 내장 DI 시스템 사용

UIKit에서 Clean Architecture + MVVM-C + Swinject를 써왔는데,
SwiftUI + TCA에서는 Reducer가 ViewModel 역할을 겸하고,
@Dependency가 Swinject 역할을 한다고 한다.
추후 차차 공부해나가는걸로...


Step 0 - SwiftUI 대략적으로 알아보기

SwiftUI는 UIKit처럼 ViewController 중심 구조가 아니라,
상태(State)를 기반으로 화면을 선언적으로 그리는 프레임워크

UIKit이 “어떻게 그릴지”를 명령형으로 작성하는 방식이라면,
SwiftUI는 “이 상태라면 이런 화면이다”를 선언하는 방식에 가깝다.

1. View는 struct

SwiftUI의 View는 class가 아니라 struct

UIViewController (Class)

  • 인스턴스가 힙에 올라가고
  • 여러 곳에서 같은 객체를 참조할 수 있으며
  • 명시적으로 해제하지 않는 한 메모리에 남는다.

View-SwiftUI (Struct)

  • 힙이 아닌 스택에 올라가고
  • 복사로 전달되며
  • 스코프를 벗어나면 즉시 사라진다.
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
    }
}
  • View는 값 타입 (세상에)
  • 불변 구조를 기본 전제로 함
  • 화면 변경은 View를 수정하는 게 아니라 상태가 바뀌면서 다시 그려지는 것

UIKit처럼 직접 label.text를 바꾸는 개념이 아니다!

2. SwiftUI Lifecycle vs UIKit (오늘의 핵심)

UIKit은 Lifecycle이 굉장히 복잡했는데 SwiftUI는 상대적으로 간단한 편이다.

1번에서 말했던 것처럼, UIKit의 UIViewController는 Class인 반면 SwiftUI의 View는 Struct이므로 근본적으로 다르다. 이걸 이해하고 생각하면 쉬운 것 같음

SwiftUI에서 뷰가 "재생성"된다는 의미

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의 존재 의미

그래서 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뷰가 소유하는 ObservableObjectSwiftUI 프레임워크
@ObservedObject외부에서 주입받은 ObservableObject외부
@EnvironmentObject뷰 계층 전체에서 공유외부

TCA를 쓰면 위 래퍼들을 직접 쓸 일이 많이 줄어든다. Store가 상태 관리를 전담하기 때문이다.

생명주기

UIKitSwiftUI비고
viewDidLoad.task {}뷰 해제 시 Task 자동 취소
viewDidAppear.onAppear
viewDidDisappear.onDisappear

.task {} 권장. .onAppear 안에서 직접 Task { }를 만들면 뷰가 사라져도 Task가 남을 수 있음. TCA Effect.run은 내부적으로 취소 자동 처리.

TabView는 탭 뷰를 해제하지 않아서 탭이 많아지면 Store가 계속 메모리에 남는 점 주의.

생명주기가 "고정적이지 않다"는 의미

UIKit은 viewDidLoadviewWillAppearviewDidAppearviewDidDisappear 등의 순서가 명확하고, 각 시점에 무슨 일을 해야 하는지 잘 정의되어 있다.

반면, SwiftUI는 뷰가 언제 생성되고 사라지는지 프레임워크가 결정한다.
최적화를 위해 예상보다 일찍 만들거나, 예상보다 늦게 해제할 수 있다.
그래서 기존과 같이 생명주기에 의존한 코드를 짜는 게 까다롭다.

// UIKit
override func viewDidLoad() {
    super.viewDidLoad()
    setupUI()
    fetchData()
}

// SwiftUI — .task는 뷰가 화면에 나타날 때, 사라지면 자동 취소
.task {
    await store.send(.fetchPosts).finish()
}

.task가 viewDidLoad의 대응처럼 쓰이지만, 정확히 같지는 않다. 뷰가 화면에서 사라졌다 다시 나타나면 .task도 다시 실행된다.

3. View는 Composition

SwiftUI는 Composition을 사용해서 View를 그리는데,

VStack {
    Text("Title")
    Button("Tap") { }
}

이렇게 VStack, HStack, ZStack, Modifier 체인 등을 사용해서 View Tree를 쌓는다.
이건 Flutter랑 비슷하기도 하고, 전에 해보기도 해서 넘어감

4. Navigation도 선언적

이 부분은 나중에 TCA의 상태 기반 네비게이션과 연결된다고 하는데,
화면 전환도 “명령”이 아니라 “상태 기반 표현”에 가깝다고 함

UIKit:

navigationController?.pushViewController(...)

SwiftUI:

NavigationStack {
    NavigationLink("Detail", value: ...)
}

5. TCA?

SwiftUI 관련 공고를 볼 때, TCA 아키텍처를 많이 언급하는 것 같아 사용해보기로 한 것인데, 그럼 도대체 TCA가 뭘까?

TCA는 pointfree에서 만든 오픈소스 상태 관리 아키텍처 라이브러리인데, 상태를 기반으로 렌더링되는 SwiftUI와 잘 맞는 아키텍처로 자주 사용된다.
https://github.com/pointfreeco/swift-composable-architecture
여기에 아주 잘 설명되어있다.

TCA의 핵심

  • 모든 화면은 State로 표현되고
  • 사용자의 입력은 Action으로 들어오며
  • 상태 변경은 Reducer에서만 일어난다.

구조를 아주 단순화한다면:

StateView
ActionReducerState 변경

❗️ 참고: Reducer는 현재 상태와 Action을 받아 새로운 상태를 만들어내는 함수로, 상태를 직접 바꾸는 코드는 Reducer에만 존재한다.

func reduce(into state: inout State, action: Action) -> Effect<Action>

차차 공부하면서 더 깊게 알아보도록 하겠당.


이후 단계는 새로 알아가는 것들 위주로 포스팅을 할 예정!

앱 아이콘도 등록 ㅎ

profile
정체되지 않는 성장

0개의 댓글