์๋ ํ์ธ์ ์ฌ๋ฌ๋ถ! ์ค๋์ Swift Concurrency์ ํต์ฌ์ธ async/await์ ๋ํด ์์๋ณผ๊ฒ์~ ๐
์ async/await๊ฐ ํ์ํ ๊ฑฐ์? ๐ค
๊ธฐ์กด์ ์ฐ๋ฆฌ๊ฐ ๋น๋๊ธฐ ์์ ์ ํ ๋๋ completion handler๋ฅผ ์ฌ์ฉํ์์์. ๊ทผ๋ฐ ์ด๊ฑฐ ์ง์ง ์ง์ฅ์ด์์ใ ใ ์ฝ๋ฐฑ ์ง์ฅ์ด๋ผ๊ณ ๋ ํ์ฃ . ์ฝ๋๊ฐ ์ด๋ ๊ฒ ๋จ:
func fetchUserData(completion: @escaping (User?) -> Void) {
fetchUserID { id in
fetchUserProfile(id: id) { profile in
fetchUserPosts(profile: profile) { posts in
let user = User(id: id, profile: profile, posts: posts)
completion(user)
}
}
}
}
์ด๋ฐ ์ฝ๋ ๋ณด๋ฉด ์ง์ง ํ๊ธฐ์ฆ ๋์ง ์์? ๋ค์ฌ์ฐ๊ธฐ๋ง ๋ด๋ ํํ ์ค์์ใ ใ
async/await ๋ฑ์ฅ! ๊ตฌ์์! ๐
Swift 5.5์์ ๋๋์ด async/await๊ฐ ๋ฑ์ฅํ์ด์. ์ด์ ์์ ์ฝ๋๋ฅผ ์ด๋ ๊ฒ ์ธ ์ ์์:
func fetchUserData() async throws -> User {
let id = try await fetchUserID()
let profile = try await fetchUserProfile(id: id)
let posts = try await fetchUserPosts(profile: profile)
return User(id: id, profile: profile, posts: posts)
}
์ด๋์? ์ง์ง ๊น๋ํ์ง ์์? ใ ใ ์ด์ ์ฝ๋๊ฐ ์์์ ์๋๋ก ์์ฐจ์ ์ผ๋ก ์ฝํ๊ณ , ์๋ฌ ์ฒ๋ฆฌ๋ try-catch๋ก ํ ์ ์์ด์ ํจ์ฌ ๋ช ํํด์ง!
async ํจ์ ๋ง๋๋ ๋ฒ ๐
async ํจ์๋ ์ด๋ ๊ฒ ๋ง๋ค์ด์:
func downloadImage(url: URL) async throws -> UIImage {
// ๋น๋๊ธฐ ์ฝ๋
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
์ฌ๊ธฐ์ ์ค์ํ ๊ฒ:
- ํจ์ ์ ์ธ์ async ํค์๋ ์ถ๊ฐ
- ๋น๋๊ธฐ ์์ ์์ await ํค์๋ ์ฌ์ฉ
- ์๋ฌ ๋์ง ์ ์์ผ๋ฉด throws ํค์๋ ์ถ๊ฐ
SwiftUI์์ async/await ์ฌ์ฉํ๊ธฐ ๐
์ด์ SwiftUI์์ ์ด๋ป๊ฒ ์ฐ๋์ง ์์๋ณผ๊น์? iOS 17๋ถํฐ๋ @Observable์ ์ธ ์ ์์ด์ ๋ ๊น๋ํด์ก์ด์!
@Observable
class ImageLoader {
var image: UIImage?
var isLoading = false
var errorMessage: String?
func loadImage(from url: URL) {
isLoading = true
errorMessage = nil
Task {
do {
let downloadedImage = try await downloadImage(url: url)
await MainActor.run {
self.image = downloadedImage
self.isLoading = false
}
} catch {
await MainActor.run {
self.errorMessage = "์ด๋ฏธ์ง ๋ก๋ฉ ์คํจใ
ใ
: \(error.localizedDescription)"
self.isLoading = false
}
}
}
}
private func downloadImage(url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw URLError(.badServerResponse)
}
return image
}
}
์ด๊ฑธ SwiftUI ๋ทฐ์์ ์ฌ์ฉํ๋ฉด:
struct ImageViewer: View {
let imageLoader = ImageLoader()
let imageURL = URL(string: "https://example.com/image.jpg")!
var body: some View {
VStack {
if imageLoader.isLoading {
ProgressView()
.progressViewStyle(.circular)
} else if let image = imageLoader.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else if let errorMessage = imageLoader.errorMessage {
Text("์๋ฌ๋ฌ์ใ
ใ
: \(errorMessage)")
.foregroundColor(.red)
}
Button("์ด๋ฏธ์ง ๋ค์ด๋ก๋") {
imageLoader.loadImage(from: imageURL)
}
.buttonStyle(.borderedProminent)
.padding()
}
.padding()
.onAppear {
imageLoader.loadImage(from: imageURL)
}
}
}
์ MainActor๊ฐ ํ์ํ ๊ฑฐ์? ๐ง
UI ์ ๋ฐ์ดํธ๋ ๋ฐ๋์ ๋ฉ์ธ ์ค๋ ๋์์ ํด์ผ ํจ! ๊ทธ๋์ MainActor.run์ ์ฌ์ฉํ์ด์. ์๋๋ฉด ํด๋์ค ์ ์ฒด๋ฅผ MainActor๋ก ์ง์ ํ ์๋ ์์:
@Observable @MainActor
class ImageLoader {
// ์ด์ ๋ชจ๋ ํ๋กํผํฐ ์
๋ฐ์ดํธ๊ฐ ๋ฉ์ธ ์ค๋ ๋์์ ์๋์ผ๋ก ์คํ๋จ!
// ...
}
Swift Concurrency์ ๊ฟํ๋ค ๐ฏ
1. Task ์ฌ์ฉํ๊ธฐ
Task๋ ๋น๋๊ธฐ ์์ ์ ์์ํ๋ ๋ฐฉ๋ฒ์. SwiftUI์์ ์ด๋ฒคํธ์ ๋ฐ์ํด์ ๋น๋๊ธฐ ์์ ์ ์์ํ ๋ ๋ง์ด ์:
Button("๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ") {
Task {
do {
let result = try await fetchData()
// UI ์
๋ฐ์ดํธ
} catch {
// ์๋ฌ ์ฒ๋ฆฌ
}
}
}
2. Task ์ทจ์ํ๊ธฐ
์์ ์ ์ทจ์ํด์ผ ํ ๋:
var loadingTask: Task<Void, Never>?
func startLoading() {
// ๊ธฐ์กด ์์
์ทจ์
loadingTask?.cancel()
// ์ ์์
์์
loadingTask = Task {
do {
// ์ฃผ๊ธฐ์ ์ผ๋ก ์ทจ์๋๋์ง ํ์ธ
try Task.checkCancellation()
let result = try await longRunningTask()
// ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
} catch {
// ์๋ฌ ์ฒ๋ฆฌ
}
}
}
func stopLoading() {
loadingTask?.cancel()
loadingTask = nil
}
3. withTaskGroup์ผ๋ก ์ฌ๋ฌ ์์ ๋์์ ์คํํ๊ธฐ
์ฌ๋ฌ ์ด๋ฏธ์ง๋ฅผ ๋์์ ๋ค์ด๋ก๋ํ๊ณ ์ถ๋ค๋ฉด:
func loadGalleryImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
for (index, url) in urls.enumerated() {
group.addTask {
let image = try await self.downloadImage(url: url)
return (index, image)
}
}
var images = [UIImage?](repeating: nil, count: urls.count)
for try await (index, image) in group {
images[index] = image
}
return images.compactMap { $0 }
}
}
์ค์ ์์ : SwiftUI์์ async ๋ฆฌ์คํธ ๋ก๋ฉ ๐ช
import SwiftUI
// ๋ชจ๋ธ ์ ์
struct Post: Identifiable, Decodable {
let id: Int
let title: String
let body: String
let userId: Int
}
// ๋ฐ์ดํฐ ๋ก๋
@Observable @MainActor
class PostsLoader {
var posts: [Post] = []
var isLoading = false
var errorMessage: String?
func loadPosts() async {
guard !isLoading else { return }
isLoading = true
errorMessage = nil
do {
let downloadedPosts = try await fetchPosts()
self.posts = downloadedPosts
} catch {
self.errorMessage = "๊ฒ์๋ฌผ ๋ก๋ฉ ์คํจ: \(error.localizedDescription)"
}
isLoading = false
}
private func fetchPosts() async throws -> [Post] {
// ๋คํธ์ํฌ ์์ฒญ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ๊ฒ ์๋ฎฌ๋ ์ด์
try await Task.sleep(for: .seconds(1))
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Post].self, from: data)
}
func refreshPosts() async {
posts = []
await loadPosts()
}
}
// ๊ฒ์๋ฌผ ์์ธ ๋ทฐ
struct PostDetailView: View {
let post: Post
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text(post.title)
.font(.title)
.fontWeight(.bold)
Text("์์ฑ์ ID: \(post.userId)")
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text(post.body)
.font(.body)
.lineSpacing(6)
}
.padding()
}
.navigationTitle("๊ฒ์๋ฌผ ์์ธ")
}
}
// ๋ฉ์ธ ๋ฆฌ์คํธ ๋ทฐ
struct PostsListView: View {
@State private var postsLoader = PostsLoader()
var body: some View {
NavigationStack {
ZStack {
// ๋ฆฌ์คํธ ๋ด์ฉ
if postsLoader.isLoading && postsLoader.posts.isEmpty {
ProgressView("๋ถ๋ฌ์ค๋ ์ค...")
.progressViewStyle(.circular)
} else if let errorMessage = postsLoader.errorMessage {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundColor(.red)
Text("์! ์๋ฌ๋ฌ์ด์ ใ
ใ
")
.font(.headline)
Text(errorMessage)
.font(.body)
.multilineTextAlignment(.center)
Button("๋ค์ ์๋") {
postsLoader.loadPosts()
}
.buttonStyle(.borderedProminent)
}
.padding()
} else {
postsList
}
}
.navigationTitle("๊ฒ์๋ฌผ ๋ชฉ๋ก")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
postsLoader.refreshPosts()
} label: {
Label("์๋ก๊ณ ์นจ", systemImage: "arrow.clockwise")
}
.disabled(postsLoader.isLoading)
}
}
.task {
if postsLoader.posts.isEmpty {
postsLoader.loadPosts()
}
}
}
}
private var postsList: some View {
List {
ForEach(postsLoader.posts) { post in
NavigationLink(destination: PostDetailView(post: post)) {
VStack(alignment: .leading, spacing: 8) {
Text(post.title)
.font(.headline)
.lineLimit(1)
Text(post.body)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
.padding(.vertical, 4)
}
}
if postsLoader.isLoading {
HStack {
Spacer()
ProgressView()
Spacer()
}
.padding()
}
}
.listStyle(.inset)
.refreshable {
await postsLoader.refreshPosts()
}
}
}
// ์ฑ ์ง์
์
@main
struct ConcurrencyDemoApp: App {
var body: some Scene {
WindowGroup {
PostsListView()
}
}
}
์ ์ฝ๋๋ฅผ ๋ณด๋ฉด SwiftUI์์ async/await๋ฅผ ์ด๋ป๊ฒ ํ์ฉํ๋์ง ์์ ์ ๋ณผ ์ ์์ด์! ์ค์ํ ํฌ์ธํธ๋ค์ ์ ๋ฆฌํด ๋ณผ๊ฒ์:
- @Observable ์ฌ์ฉ - iOS 17์์ ์๋ก ์ถ๊ฐ๋ ๋งคํฌ๋ก๋ก, ์ํ ๋ณํ๋ฅผ SwiftUI์ ์๋์ผ๋ก ์๋ ค์ค
- Task๋ก ๋น๋๊ธฐ ์์ ์์
- MainActor๋ก UI ์ ๋ฐ์ดํธ
- ์๋ฌ ์ฒ๋ฆฌ
- .refreshable๊ณผ ์ฐ๋
์ฃผ์ํด์ผ ํ ์ ๋ค โ ๏ธ
- ๋ฉ๋ชจ๋ฆฌ ๋์ ์กฐ์ฌ! - Task๊ฐ ์๋ฃ๋๊ธฐ ์ ์ ๋ทฐ๊ฐ ์ฌ๋ผ์ง๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํ ์ ์์. .task ๋ชจ๋ํ์ด์ด๋ onDisappear์์ ์ทจ์ ์ฒ๋ฆฌ๋ฅผ ๊ผญ ํด์ฃผ์ธ์.
.task {
// .task๋ ๋ทฐ๊ฐ ์ฌ๋ผ์ง๋ฉด ์๋์ผ๋ก ์ทจ์๋จ
try? await loadData()
}
- ๋ฐ๋๋ฝ ์ฃผ์! - await ํธ์ถ ์ ํ์ฌ ์คํ ์ปจํ ์คํธ๊ฐ ์ผ์ ์ค๋จ๋๋๋ฐ, ์ด๋ ๋ค๋ฅธ ์์ ์ด ์ด ์ปจํ ์คํธ๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ณ ์์ผ๋ฉด ๋ฐ๋๋ฝ ๋ฐ์ ๊ฐ๋ฅ.
- UI ์ ๋ฐ์ดํธ๋ MainActor์์! - ์ด๊ฑฐ ์ง์ง ์ค์ํจ! UI ๊ด๋ จ ์ ๋ฐ์ดํธ๋ ๋ฐ๋์ ๋ฉ์ธ ์ค๋ ๋์์ ํด์ผ ํจ.
๋ง์ง๋ง ๊ฟํ: async let ๋ฐ์ธ๋ฉ ๐ฏ
์ฌ๋ฌ ๋น๋๊ธฐ ์์ ์ ๋์์ ์คํํ๊ณ ์ถ์ ๋:
async func loadDashboard() throws -> Dashboard {
async let users = fetchUsers()
async let posts = fetchPosts()
async let notifications = fetchNotifications()
return try await Dashboard(
users: users,
posts: posts,
notifications: notifications
)
}
์ด๋ ๊ฒ ํ๋ฉด ์ธ ๊ฐ์ ๋คํธ์ํฌ ์์ฒญ์ด ๋ณ๋ ฌ๋ก ์คํ๋๊ณ , ๋ชจ๋ ์๋ฃ๋๋ฉด Dashboard ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ๋ฐํํจ!
๋ง๋ฌด๋ฆฌ ๐
Swift Concurrency๋ ์ง์ง ์ ๋ง ๋ฉ์ง ๊ธฐ๋ฅ์ด์์! async/await๋ฅผ ์ฌ์ฉํ๋ฉด ๋น๋๊ธฐ ์ฝ๋๊ฐ ๋๊ธฐ ์ฝ๋์ฒ๋ผ ์ฝํ๊ณ ์ดํดํ๊ธฐ ์ฌ์์ง. SwiftUI์ ์กฐํฉํ๋ฉด ๋ ๊ฐ๋ ฅํด์ง๊ณ ์.
์ฌ๋ฌ๋ถ๋ ์ด์ ์ฝ๋ฐฑ ์ง์ฅ์์ ๋ฒ์ด๋์ ๊น๋ํ async/await์ ์ธ๊ณ๋ก ์ค์ธ์~~ ๐
๋ค์์๋ Swift Concurrency์ ๋ค๋ฅธ ๊ธฐ๋ฅ์ธ Actor์ ๋ํด ์์๋ณผ๊ฒ์! ์ฌ๋ฌ๋ถ์ ์ฝ๋ฉ ๋ผ์ดํ๊ฐ ๋ ์ฆ๊ฑฐ์์ง๊ธธ ๋ฐ๋๊ฒ์ ๐
'๐ง๐ปโ๐ป Dev' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๐ SwiftUI์์ @Observable์ ์ฌ์ฉํ์ (0) | 2025.04.03 |
---|---|
SwiftUI Spacer ์ฌ์ฉ ๊ฟํ (0) | 2025.03.27 |
Swift Concurrency์์ ๋ฉ๋ชจ๋ฆฌ ๋์ ์ฃผ์ํ๊ธฐ ๐ง (0) | 2025.03.12 |