SwiftUI로 아이폰 앱 만들기 완벽 가이드 — Xcode 프로젝트 생성부터 MVVM, 데이터 저장과 시뮬레이터 실행까지

Android 앱을 개발해 본 경험이 있다면 iPhone 앱 개발에도 관심을 갖게 됩니다.

Android에서는 Kotlin과 Android Studio를 사용하지만, iPhone 앱에서는 주로 Swift와 Xcode를 사용합니다.

UI를 만드는 방식에도 차이가 있습니다.

Android에서는 XML 레이아웃과 Jetpack Compose를 사용할 수 있고, iOS에서는 UIKit과 SwiftUI를 사용할 수 있습니다.

최근에 새로운 iOS 앱을 개발한다면 SwiftUI를 우선 검토하는 것이 좋습니다.

SwiftUI는 선언형 UI 프레임워크로, 화면의 상태가 변경되면 해당 상태에 맞춰 UI를 갱신할 수 있습니다.

이번 글에서는 Xcode 프로젝트를 생성하고, SwiftUI로 화면을 만들고, MVVM 구조를 적용한 간단한 메모 앱을 구현하겠습니다.

특히 Android 개발 경험이 있는 개발자가 iOS 개발을 시작할 때 알아두면 좋은 차이점도 함께 설명하겠습니다.

개발 환경

  • macOS

  • Xcode

  • Swift

  • SwiftUI

  • iOS 17 이상

  • MVVM

  • UserDefaults

  • iPhone Simulator

이번 예제에서는 별도 서버나 데이터베이스 라이브러리 없이 메모를 추가하고 삭제하며 앱을 다시 실행해도 데이터를 유지하도록 구현합니다.


1. Android와 iOS 개발 환경 비교

먼저 Android와 iOS의 개발 환경을 비교해 보겠습니다.

구분AndroidiOS
개발 언어KotlinSwift
개발 도구Android StudioXcode
UI 프레임워크XML, Jetpack ComposeUIKit, SwiftUI
앱 진입점Activity 등App 구조체 등
상태 관리LiveData, StateFlow@State, @Observable 등
로컬 데이터베이스SQLite, RoomSwiftData, Core Data, SQLite
설정 저장DataStore, SharedPreferencesUserDefaults
앱 배포Google PlayApp Store
빌드 산출물APK, AABIPA 등

Kotlin과 Swift는 문법적으로 비슷한 부분이 많습니다.

또한 Jetpack Compose를 사용해 본 개발자라면 SwiftUI의 선언형 UI 방식도 비교적 쉽게 이해할 수 있습니다.


Xcode 프로젝트 생성

2. Xcode 설치

iOS 앱을 개발하려면 Xcode가 필요합니다.

Xcode는 Mac App Store 또는 Apple Developer 사이트에서 설치할 수 있습니다.

설치 후 Xcode를 실행합니다.

Xcode에서는 다음 작업을 수행할 수 있습니다.

  • iOS 프로젝트 생성

  • Swift 코드 작성

  • SwiftUI Preview

  • iPhone Simulator 실행

  • 실제 iPhone 디버깅

  • 앱 서명

  • App Store 배포

Xcode와 iOS SDK의 지원 범위는 버전에 따라 달라지므로 프로젝트 생성 전에 사용하는 Xcode 버전을 확인하는 것이 좋습니다.


3. 새로운 iOS 프로젝트 만들기

Xcode에서 다음 순서로 프로젝트를 생성합니다.

  1. Xcode 실행

  2. Create New Project 선택

  3. iOS 플랫폼 선택

  4. App 템플릿 선택

  5. Product Name 입력

  6. Interface는 SwiftUI 선택

  7. Language는 Swift 선택

  8. 프로젝트 저장 위치 선택

이번 예제의 프로젝트 이름은 다음과 같이 설정하겠습니다.

SimpleMemo

Organization Identifier는 본인이 사용하는 역방향 도메인 형식으로 입력합니다.

예를 들어 다음과 같습니다.

com.example

이 경우 Bundle Identifier는 다음과 같이 생성될 수 있습니다.

com.example.SimpleMemo

실제 앱을 배포할 때는 본인이 관리하는 고유한 식별자를 사용해야 합니다.


SwiftUI 프로젝트 구조

4. 기본 파일 살펴보기

새 프로젝트를 생성하면 일반적으로 다음과 같은 파일을 확인할 수 있습니다.

SimpleMemo/
 ├── SimpleMemoApp.swift
 ├── ContentView.swift
 └── Assets.xcassets

각 파일의 역할은 다음과 같습니다.

파일역할
SimpleMemoApp.swift앱 시작 지점
ContentView.swift기본 화면
Assets.xcassets이미지와 색상 등 리소스

Android 프로젝트와 비교하면 SimpleMemoApp.swift는 앱 진입점 역할을 하며, ContentView.swift는 UI 화면을 정의하는 파일입니다.


SwiftUI 앱 진입점

5. @main과 App

SwiftUI 앱의 기본 구조는 다음과 같습니다.

import SwiftUI

@main
struct SimpleMemoApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

@main은 앱의 진입점을 나타냅니다.

WindowGroup은 앱의 화면을 관리하는 Scene입니다.

ContentView()는 처음 표시할 화면입니다.

Android의 Activity와 완전히 동일한 개념은 아니지만, 앱 실행과 화면 구성의 시작 지점을 이해하는 데 도움이 됩니다.


SwiftUI 기본 문법

6. Text와 VStack

SwiftUI에서는 View를 조합해 화면을 구성합니다.

import SwiftUI

struct ContentView: View {

    var body: some View {

        VStack {
            Text("안녕하세요")
            Text("첫 번째 iPhone 앱입니다.")
        }
    }
}

VStack은 View를 세로 방향으로 배치합니다.

Android XML의 세로 방향 LinearLayout과 비슷하게 이해할 수 있습니다.

SwiftUI에는 다음과 같은 레이아웃 컨테이너가 있습니다.

SwiftUI설명
VStack세로 배치
HStack가로 배치
ZStack겹쳐서 배치
ScrollView스크롤 영역
List목록
Form설정 및 입력 화면

7. SwiftUI Modifier

SwiftUI에서는 Modifier를 사용해 View의 모양과 동작을 설정합니다.

Text("Hello SwiftUI")
    .font(.title)
    .fontWeight(.bold)
    .foregroundStyle(.blue)
    .padding()

Modifier는 위에서 아래로 연결해 작성합니다.

Android XML의 속성이나 Compose의 Modifier와 비슷한 역할을 하지만 적용 순서에 따라 결과가 달라질 수 있습니다.


Swift와 Kotlin 문법 비교

8. 변수 선언

Kotlin에서는 다음과 같이 변수를 선언합니다.

val name = "SwiftUI"
var count = 10

Swift에서는 다음과 같이 작성합니다.

let name = "SwiftUI"
var count = 10

let은 값을 다시 할당할 수 없는 상수를 선언합니다.

var는 값을 변경할 수 있는 변수를 선언합니다.


9. 데이터 클래스와 구조체

Kotlin의 data class는 다음과 같습니다.

data class Memo(
    val id: Long,
    val title: String
)

Swift에서는 struct를 사용할 수 있습니다.

struct Memo {
    let id: UUID
    var title: String
}

Swift의 구조체는 값 타입입니다.

따라서 참조 타입인 class와 동작 방식이 다릅니다.

이 차이는 상태 관리와 데이터 전달에서 중요합니다.


메모 앱 만들기

10. 이번에 구현할 기능

이번 예제에서는 다음 기능을 구현하겠습니다.

  • 메모 목록 표시

  • 새 메모 추가

  • 메모 삭제

  • 메모 작성 날짜 표시

  • 앱 재실행 후 메모 유지

  • MVVM 구조 적용

프로젝트 구조는 다음과 같이 구성합니다.

SimpleMemo/
 ├── SimpleMemoApp.swift
 ├── Model/
 │    └── Memo.swift
 ├── ViewModel/
 │    └── MemoViewModel.swift
 ├── Repository/
 │    └── MemoRepository.swift
 └── View/
      └── ContentView.swift

Memo 모델

11. Identifiable과 Codable

메모 데이터 모델을 생성합니다.

Memo.swift

import Foundation

struct Memo: Identifiable, Codable {

    var id: UUID
    var title: String
    var createdAt: Date

    init(
        id: UUID = UUID(),
        title: String,
        createdAt: Date = Date()
    ) {
        self.id = id
        self.title = title
        self.createdAt = createdAt
    }
}

여기서 두 가지 프로토콜을 사용했습니다.

Identifiable은 목록에서 각 항목을 구분할 수 있도록 합니다.

Codable은 데이터를 인코딩하거나 디코딩할 때 사용합니다.

이번 예제에서는 메모 데이터를 JSON으로 변환해 UserDefaults에 저장하겠습니다.


UserDefaults 이해하기

12. Android SharedPreferences와 비교

Android에서는 간단한 설정값을 SharedPreferences 또는 DataStore에 저장할 수 있습니다.

iOS에서는 UserDefaults를 사용할 수 있습니다.

예를 들어 문자열을 저장하려면 다음과 같이 작성합니다.

UserDefaults.standard.set(
    "Dark",
    forKey: "theme"
)

저장된 값을 가져오는 코드는 다음과 같습니다.

let theme = UserDefaults.standard.string(
    forKey: "theme"
)

UserDefaults는 간단한 설정이나 소량의 데이터 저장에 적합합니다.

대량의 메모, 이미지, 복잡한 관계형 데이터까지 저장하는 용도로는 적합하지 않습니다.

그러한 경우에는 SwiftData, Core Data 또는 SQLite 등을 검토하는 것이 좋습니다.


Repository 구현

13. MemoRepository 만들기

메모 저장과 조회를 담당하는 Repository를 구현하겠습니다.

MemoRepository.swift

import Foundation

final class MemoRepository {

    private let storageKey = "saved_memos"

    func loadMemos() -> [Memo] {

        guard let data = UserDefaults.standard.data(
            forKey: storageKey
        ) else {
            return []
        }

        do {
            return try JSONDecoder().decode(
                [Memo].self,
                from: data
            )
        } catch {
            print("메모 불러오기 실패: \(error)")
            return []
        }
    }

    func saveMemos(_ memos: [Memo]) throws {

        let data = try JSONEncoder().encode(memos)

        UserDefaults.standard.set(
            data,
            forKey: storageKey
        )
    }
}

이렇게 하면 데이터 저장 로직을 화면 코드와 분리할 수 있습니다.

실제 앱에서는 저장 실패나 데이터 손상 상황을 사용자에게 알리는 정책도 추가하는 것이 좋습니다.


MVVM 적용

14. iOS에서 MVVM 구조

Android에서 MVVM을 사용해 본 개발자라면 SwiftUI에서도 비슷한 구조를 적용할 수 있습니다.

ContentView
     │
     ▼
MemoViewModel
     │
     ▼
MemoRepository
     │
     ▼
UserDefaults

View는 화면을 표시합니다.

ViewModel은 메모 목록과 사용자 이벤트를 관리합니다.

Repository는 데이터를 저장하고 불러옵니다.


MemoViewModel 구현

15. @Observable 사용

iOS 17 이상에서는 Observation 프레임워크의 @Observable을 사용할 수 있습니다.

MemoViewModel.swift

import Foundation
import Observation

@MainActor
@Observable
final class MemoViewModel {

    var memos: [Memo] = []
    var errorMessage: String?

    private let repository: MemoRepository

    init(
        repository: MemoRepository = MemoRepository()
    ) {
        self.repository = repository
        loadMemos()
    }

    func loadMemos() {
        memos = repository.loadMemos()
    }

    func addMemo(title: String) {

        let trimmedTitle = title.trimmingCharacters(
            in: .whitespacesAndNewlines
        )

        guard !trimmedTitle.isEmpty else {
            return
        }

        let memo = Memo(title: trimmedTitle)

        memos.insert(memo, at: 0)

        save()
    }

    func deleteMemo(at offsets: IndexSet) {

        memos.remove(atOffsets: offsets)

        save()
    }

    private func save() {

        do {
            try repository.saveMemos(memos)
            errorMessage = nil
        } catch {
            errorMessage = "메모를 저장하지 못했습니다."
        }
    }
}

@Observable을 사용하면 관찰 가능한 속성의 변경을 SwiftUI 화면에 반영할 수 있습니다.

@MainActor는 ViewModel의 상태 접근이 메인 액터에서 이루어지도록 합니다.

이번 예제는 소량의 데이터를 저장하므로 단순하게 구현했습니다. 대량 데이터를 저장할 경우 파일 I/O와 데이터베이스 작업을 별도 실행 컨텍스트로 분리해야 합니다.


ContentView 구현

16. 메모 목록 화면

이제 SwiftUI 화면을 구현하겠습니다.

ContentView.swift

import SwiftUI

struct ContentView: View {

    @State private var viewModel = MemoViewModel()
    @State private var memoTitle = ""

    var body: some View {

        NavigationStack {

            VStack(spacing: 16) {

                HStack {

                    TextField(
                        "새 메모를 입력하세요",
                        text: $memoTitle
                    )
                    .textFieldStyle(.roundedBorder)
                    .submitLabel(.done)
                    .onSubmit(addMemo)

                    Button("추가") {
                        addMemo()
                    }
                    .buttonStyle(.borderedProminent)
                }
                .padding(.horizontal)

                List {

                    ForEach(viewModel.memos) { memo in

                        VStack(
                            alignment: .leading,
                            spacing: 6
                        ) {

                            Text(memo.title)
                                .font(.body)

                            Text(
                                memo.createdAt,
                                format: .dateTime
                                    .year()
                                    .month()
                                    .day()
                                    .hour()
                                    .minute()
                            )
                            .font(.caption)
                            .foregroundStyle(.secondary)
                        }
                        .padding(.vertical, 4)
                    }
                    .onDelete(
                        perform: viewModel.deleteMemo
                    )
                }
            }
            .navigationTitle("나의 메모")
            .alert(
                "저장 오류",
                isPresented: Binding(
                    get: {
                        viewModel.errorMessage != nil
                    },
                    set: { isPresented in
                        if !isPresented {
                            viewModel.errorMessage = nil
                        }
                    }
                )
            ) {
                Button("확인", role: .cancel) {
                    viewModel.errorMessage = nil
                }
            } message: {
                Text(
                    viewModel.errorMessage ?? ""
                )
            }
        }
    }

    private func addMemo() {

        viewModel.addMemo(title: memoTitle)

        if !memoTitle.trimmingCharacters(
            in: .whitespacesAndNewlines
        ).isEmpty {
            memoTitle = ""
        }
    }
}

이 코드에서는 SwiftUI의 List, ForEach, TextField, Button을 사용했습니다.

메모를 추가하면 목록이 변경되고, @Observable에 의해 화면이 갱신됩니다.

목록 항목을 왼쪽으로 밀면 삭제할 수 있습니다.


@State와 @Binding 이해하기

17. @State

SwiftUI에서 @State는 View가 소유하는 상태를 관리할 때 사용합니다.

@State private var count = 0

버튼을 눌러 상태를 변경할 수 있습니다.

Button("증가") {
    count += 1
}

count가 변경되면 해당 상태를 사용하는 UI가 갱신됩니다.


18. @Binding

@Binding은 다른 View가 소유한 상태를 참조하고 수정할 수 있도록 연결합니다.

예를 들어 다음과 같은 컴포넌트를 만들 수 있습니다.

struct MemoInputView: View {

    @Binding var text: String

    var body: some View {

        TextField(
            "메모 입력",
            text: $text
        )
        .textFieldStyle(.roundedBorder)
    }
}

부모 View에서는 다음과 같이 사용합니다.

MemoInputView(text: $memoTitle)

Android Jetpack Compose에서 상태를 상위 컴포넌트로 끌어올리고 이벤트를 전달하는 방식과 비슷한 개념으로 이해할 수 있습니다.


NavigationStack 사용

19. 화면 이동 구현

SwiftUI에서는 NavigationStack을 이용해 화면 이동을 구현할 수 있습니다.

NavigationStack {

    NavigationLink(
        "상세 화면 열기"
    ) {
        MemoDetailView()
    }
}

상세 화면은 다음과 같이 만들 수 있습니다.

struct MemoDetailView: View {

    var body: some View {

        Text("메모 상세 화면")
            .navigationTitle("상세 정보")
    }
}

Android의 Navigation Component와 역할이 비슷하지만 API와 상태 관리 방식은 다릅니다.


iPhone Simulator 실행

20. 시뮬레이터 선택

Xcode 상단의 실행 대상에서 iPhone Simulator를 선택할 수 있습니다.

예를 들어 설치된 시뮬레이터 중 하나를 선택합니다.

iPhone Simulator

이후 Run 버튼을 누르면 앱이 빌드되고 시뮬레이터에서 실행됩니다.

시뮬레이터 목록은 Xcode 버전과 설치된 iOS Simulator Runtime에 따라 달라집니다.


21. 시뮬레이터가 보이지 않을 때

원하는 iPhone 시뮬레이터가 목록에 없다면 다음 항목을 확인합니다.

  1. Xcode Settings 열기

  2. Components에서 iOS Simulator Runtime 확인

  3. 필요한 Runtime 설치

  4. Window → Devices and Simulators 열기

  5. Simulators 탭에서 기기 추가

특정 iPhone 모델이 목록에 없다면 설치된 Xcode가 해당 기기 유형을 지원하는지도 확인해야 합니다.


실제 iPhone에서 실행

22. iPhone 연결

실제 iPhone에서 앱을 실행하려면 Mac과 iPhone을 연결하고 개발자 서명 설정을 구성해야 합니다.

일반적인 순서는 다음과 같습니다.

  1. iPhone을 Mac에 연결

  2. 기기 신뢰 설정

  3. Xcode에서 프로젝트 선택

  4. Signing & Capabilities 열기

  5. Team 선택

  6. Bundle Identifier 확인

  7. 실행 대상에서 iPhone 선택

  8. 앱 빌드 및 실행

필요한 경우 iPhone에서 개발자 모드를 활성화해야 합니다.

개인 Apple Account로도 제한적인 개발 테스트가 가능하지만, App Store 배포에는 Apple Developer Program 등록이 필요합니다.


SwiftUI Preview

23. 미리보기 사용

SwiftUI에서는 Preview를 이용해 화면을 미리 확인할 수 있습니다.

#Preview {
    ContentView()
}

Preview를 사용하면 전체 앱을 매번 시뮬레이터에서 실행하지 않고도 UI를 확인할 수 있습니다.

다만 실제 데이터베이스, 네트워크, 권한, 카메라처럼 시스템과 연동되는 기능은 시뮬레이터나 실제 기기에서 별도로 테스트해야 합니다.


Android 개발자가 주의할 점

24. Activity와 View를 동일하게 생각하지 않기

Android XML 기반 앱에서는 Activity나 Fragment가 화면 생명주기와 이벤트를 관리하는 경우가 많습니다.

SwiftUI에서는 View가 상태를 기반으로 선언적으로 구성됩니다.

따라서 SwiftUI View 안에 모든 비즈니스 로직과 저장 로직을 넣는 것은 피하는 것이 좋습니다.


25. ViewModel 생명주기 이해하기

SwiftUI의 View는 상태 변경에 따라 다시 평가될 수 있습니다.

따라서 ViewModel을 어디에서 생성하고 누가 소유하는지 명확하게 설계해야 합니다.

이번 예제에서는 다음과 같이 작성했습니다.

@State private var viewModel = MemoViewModel()

iOS 17 이상의 Observation 기반 구조에서 View가 소유하는 관찰 가능한 모델을 관리하는 방식입니다.

기존 ObservableObject 기반 코드에서는 @StateObject를 사용하는 경우도 있습니다.

두 방식은 사용하는 관찰 시스템이 다르므로 무조건 혼용하지 않는 것이 좋습니다.


데이터 저장 방식 확장

26. UserDefaults에서 SwiftData로

이번 예제에서는 구현을 단순하게 유지하기 위해 UserDefaults를 사용했습니다.

하지만 메모가 많아지거나 다음과 같은 기능이 필요하다면 SwiftData를 검토할 수 있습니다.

  • 메모 검색

  • 정렬 및 필터

  • 메모 수정

  • 여러 데이터 모델 간 관계

  • 대량 데이터 저장

  • 데이터 변경 추적

SwiftData는 Apple이 제공하는 데이터 영속성 프레임워크이며 iOS 17 이상에서 사용할 수 있습니다.


iOS 앱 테스트 체크리스트

27. 출시 전 확인 사항

  • Xcode 프로젝트 생성

  • SwiftUI 화면 표시

  • Bundle Identifier 설정

  • Deployment Target 확인

  • Memo 모델 구현

  • Repository 구현

  • ViewModel 구현

  • @Observable 상태 관리

  • 메모 추가

  • 메모 삭제

  • UserDefaults 저장

  • 앱 재실행 후 데이터 유지

  • 빈 문자열 입력 처리

  • 저장 오류 처리

  • 다크 모드 확인

  • 글자 크기 변경 테스트

  • iPhone Simulator 실행

  • 실제 iPhone 테스트

  • 앱 서명 설정

  • App Store 배포 요구사항 확인


28. 마무리

SwiftUI는 iPhone 앱 개발을 시작할 때 유용한 선택지입니다.

특히 Kotlin과 Jetpack Compose에 익숙한 개발자라면 선언형 UI와 상태 관리 개념을 이해하는 데 도움이 됩니다.

이번 글에서는 다음 내용을 살펴봤습니다.

  1. Xcode에서 iOS 프로젝트를 생성하는 방법

  2. SwiftUI의 기본 화면 구성

  3. Swift와 Kotlin의 문법 차이

  4. Identifiable과 Codable을 활용한 데이터 모델

  5. UserDefaults를 이용한 간단한 데이터 저장

  6. MVVM 구조 적용

  7. @Observable 기반 상태 관리

  8. List와 TextField를 이용한 메모 앱 구현

  9. NavigationStack 화면 이동

  10. iPhone Simulator와 실제 기기 실행

Android 앱 개발 경험이 있다면 iOS 개발을 완전히 새로운 분야로 생각할 필요는 없습니다.

MVVM, Repository, 비동기 처리, 데이터 저장, 화면 상태 관리처럼 공통으로 활용할 수 있는 개념이 많기 때문입니다.

다만 iOS의 앱 생명주기와 SwiftUI 상태 관리 방식은 Android와 다르므로 각각의 특징을 이해하는 것이 중요합니다.

첫 번째 iPhone 앱은 기능을 많이 만드는 것보다 프로젝트 생성부터 실행, 상태 관리, 데이터 저장까지 전체 흐름을 직접 경험하는 것이 좋습니다.


참고 자료

태그: iOS, iPhone 앱 개발, Swift, SwiftUI, Xcode, MVVM, Observable, UserDefaults, iPhone Simulator, Kotlin, Android 개발자, SwiftUI 입문, iOS 앱 만들기