Swift 6 Concurrency Q&A 정리: isolation부터 Sendable까지

2026. 4. 25. 18:12·iOS

오랜만에 글 쓰네요 날씨가 따듯해지면서 오늘은 좋은 카페에 와서 글을 써요 오늘의 카페 추천은

합정에 있는 콤파일입니다.

어두컴컴하고 아주 제 취향이더라고요. (주말 2시간 컷은 너무하긴하네요)


암튼 오늘은 오랜만에 swift에 대해 재밌는 Q&A 영상 올라와서 정리해보려고요:)

Apple 엔지니어들이 직접 나와서 Swift Concurrency 관련 질문들에 답해주는 영상인데, 실제로 헷갈렸던 것들이 꽤 많이 해소됐어요.

 

Swift 6로 넘어오면서 data race 검사가 빡세지면서 경고 폭탄을 맞은 분들도 많을 것 같아서, 제가 정리한 내용 같이 공유해봐요 :)


isolation 이란?

Swift Concurrency에서 가장 먼저 이해해야 할 개념이 있어요. 바로 isolation입니다.

isolation(격리 도메인)은 한 마디로 "이 코드가 어느 스레드에서 실행되어야 하는가" 를 나타내는 개념이에요.

 

@MainActor는 "이 타입/함수는 반드시 메인 스레드에서만 써라"라는 선언입니다. 

nonisolated는 정반대예요. "나는 특정 isolation에 묶이지 않는다. 어디서든 써도 된다."라는 선언입니다.

@MainActor
class ViewModel {
    var title = "Hello"          // 메인 스레드에서만 접근 가능

    nonisolated func greet() -> String {
        return "안녕하세요"        // isolation 없음 — 어디서든 호출 가능
        // return self.title      // ❌ 컴파일 에러! MainActor 상태에 접근 불가
    }
}

nonisolated로 마킹된 함수는 어느 isolation domain에서든 호출할 수 있습니다.

대신 MainActor 상태 같은 격리된 데이터엔 직접 접근 못합니다. 컴파일러가 막아줘요.

Foundation의 대부분 API가 nonisolated인 이유가 이거예요 — 앱에서 메인 스레드든 백그라운드든 어디서나 쓸 수 있도록요.

async 함수에서 달라진 동작

여기서 한 가지 중요한 변화가 있어요. 아래 코드, heavyWork()는 메인 스레드에서 실행될까요 백그라운드에서 실행될까요?

@MainActor
class ViewModel {
    func onButtonTap() {
        Task {
            await loadData()
        }
    }

    nonisolated func loadData() async {
        let result = heavyWork()  // 어느 스레드?
        print(result)
    }
}

정답은 Swift 버전에 따라 다릅니다 🫢

 

Swift 5(approachable concurrency 꺼져있을 때)에서는 nonisolated async 함수를 호출하면 무조건 concurrent thread pool(백그라운드)로 hop 했습니다

 

아무 이유 없이 context switching이 발생하고 예상치 못한 스레드에서 실행되는 문제가 생겼어요.

Apple도 올바른 trade-off가 아니라는 걸 인정했습니다.

 

그래서!!

새 Xcode 기본값(approachable concurrency 켜져있을 때)에서는 nonisolated async 함수는 호출한 곳의 isolation을 유지합니다.

 

"isolation 없음 = 호출한 곳을 따라간다."

그럼 백그라운드에서 돌리고 싶으면 @concurrent를 명시적으로 붙이면 됩니다.

@MainActor
class ViewModel {
    // ❌ 새 동작에서는 MainActor에서 실행됨
    nonisolated func loadData() async {
        let result = heavyWork()
    }

    // ✅ 무조건 백그라운드(concurrent thread pool)에서 실행 보장
    @concurrent func loadData() async {
        let result = heavyWork()
    }
}

@concurrent는 옛날 nonisolated async의 동작을 명시적으로 표현한 것입니다.

"나는 항상 백그라운드에서 실행되어야 한다"는 의도를 코드에 드러내는 거예요.

새 Xcode 프로젝트는 approachable concurrency가 기본으로 켜져 있어요.

백그라운드 실행을 원한다면 @concurrent를 명시적으로 써줘야 해요.


Task 만들기 — 뭘 어떻게 쓰면 될까?

isolation 개념을 알고 나면, 이제 Task를 만드는 세 가지 방법이 왜 다른지 바로 이해돼요.

Task { }, Task.detached, Task { @concurrent in }

아래 세 코드, 실행 스레드가 각각 어디일까요?

// A
@MainActor
func onButtonTap() {
    Task {
        await doWork()
    }
}

// B
@MainActor
func onButtonTap() {
    Task.detached {
        await doWork()
    }
}

// C
@MainActor
func onButtonTap() {
    Task { @concurrent in
        await doWork()
    }
}

정답은 A: 메인 스레드, B: 백그라운드, C: 백그라운드입니다.

핵심 차이는 주변 context를 얼마나 상속받느냐예요.

priority도 여기서 등장하는데, Task가 얼마나 빨리 실행되어야 하는지를 나타내는 값으로 .userInitiated, .background 같은 게 있어요. 명시하지 않으면 Task를 만든 곳의 priority를 이어받습니다.

Task { } 는 현재 isolation과 priority를 그대로 가져갑니다. 대부분의 상황에서 이걸 쓰면 됩니다.

Task.detached { } 는 isolation도 priority도 아무것도 상속받지 않습니다. 로그 저장, 분석 이벤트 전송처럼 현재 작업과 완전히 무관하게 독립적으로 실행되어야 할 때만 씁니다.

Task { @concurrent in } 는 isolation만 백그라운드로 고정하고, isolation을 제외한 나머지 context(priority 등)는 부모에서 그대로 가져옵니다. "백그라운드에서 실행하되, 완전히 독립적이진 않아도 될 때" 씁니다.


Task { @MainActor in } vs await MainActor.run { } — 취향 차이인가?

저는 둘 다 사실 똑같은 건 줄 알고 "취향 차이?" 라고 생각했어요. 근데 완전히 다릅니다.

아래 두 코드에서 heavyProcessing(data)는 어느 스레드에서 실행될까요?

// 1번
Task { @MainActor in
    let data = await fetchData()
    let processed = heavyProcessing(data)  // 어느 스레드?
    self.items = processed
}

// 2번
Task {
    let data = await fetchData()
    let processed = heavyProcessing(data)  // 어느 스레드?
    await MainActor.run {
        self.items = processed
    }
}

1번은 메인 스레드, 2번은 백그라운드입니다.

1번은 @MainActor in이 Task 전체에 붙어있기 때문에 fetchData() await 이후 resume될 때도, heavyProcessing(data)도 전부 메인 스레드에서 실행됩니다. 무거운 작업이 메인 스레드를 점령해버려요. UI가 버벅일 수 있습니다.

2번은 heavyProcessing(data)는 백그라운드에서 돌아갑니다. 마지막 UI 업데이트만 MainActor.run으로 메인 스레드에서 처리합니다. 메인 스레드 부담이 훨씬 적습니다.


@MainActor 뷰모델에서 heavy 작업 빼내기

이제 isolation과 Task 종류를 알았으니 실전 케이스를 봐볼게요.

뷰모델 전체를 @MainActor로 마킹하는 건 흔한 패턴인데, 그 안에 무거운 작업이 있다면 문제가 생깁니다.

@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []

    func loadItems() async {
        let data = await fetchFromNetwork()
        let processed = heavyParsing(data)  // 😱 메인 스레드에서 실행됨
        self.items = processed
    }
}

@MainActor 클래스 안의 모든 메서드는 기본적으로 메인 스레드에서 실행됩니다. 

heavyParsing이 오래 걸리면 UI가 그대로 멈춰요.

클래스 전체를 바꿀 필요 없습니다.

그 메서드 하나에만 @concurrent를 붙이면 됩니다.

@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []

    func loadItems() async {
        let processed = await parseItems()  // 백그라운드에서 실행
        self.items = processed              // ✅ 메인 스레드로 돌아와서 업데이트
    }

    @concurrent
    func parseItems() async -> [Item] {
        let data = await fetchFromNetwork()
        return heavyParsing(data)           // ✅ 백그라운드에서 실행
        // self.items = ...                 // ❌ 컴파일 에러! MainActor 상태 접근 불가
    }
}

@concurrent를 붙이면 컴파일러가 자동으로 해당 메서드에서 @MainActor 상태에 직접 접근하는 걸 막아줍니다.

데이터 경쟁을 컴파일 타임에 잡아주는 거예요.

그럼 무조건 @concurrent로 빼야 할까? 아닙니다. Apple 엔지니어들이 강조한 말이 있어요.

"먼저 profile 하세요. 문제가 있을 때 최적화하세요."

선제적으로 모든 걸 백그라운드로 빼려 하면 오히려 코드가 복잡해지고 불필요한 context switching만 늘어납니다.

 

권장 흐름은 이렇습니다.

  1. 일단 @MainActor에서 실행
  2. UI 버벅임 느껴지면 Instruments로 프로파일링
  3. 메인 스레드 점령하는 코드 확인
  4. 그때 @concurrent로 빼기

새 앱 타겟은?

Xcode 26부터 새 앱 타겟은 기본으로 main actor isolation이 켜집니다.

SwiftUI 뷰가 이미 @MainActor이고 뷰모델도 대부분 UI 상태를 다루기 때문에 앱 타겟에서는 매우 자연스러운 선택입니다.

단, 라이브러리는 다릅니다. Foundation처럼 누가 어디서 쓸지 모르는 코드는 nonisolated가 기본이어야 해요.


Task 생명주기 — 취소, 에러, 메모리

Task를 어떻게 만드는지 알았다면, 이번엔 만들고 나서 자주 빠지는 함정들을 정리해볼게요.

취소는 자동으로 되는 게 아닙니다

@MainActor
class ViewModel: ObservableObject {
    private var task: Task<Void, Never>?

    func load() {
        task = Task {
            await loadData()
        }
    }

    func cancel() {
        task?.cancel()
        task = nil
    }

    deinit {
        task?.cancel() // 안 하면 ViewModel 사라져도 Task는 계속 실행됨
    }
}

취소하려면 handle을 프로퍼티에 저장해야 합니다. 저장 안 하면 취소할 방법이 없어요.

cancel() 호출하면 즉시 멈출까요?

task = Task {
    await doSomething()     // (1)
    heavySyncWork()         // (2) — cancel 호출됨
    await doSomethingElse() // (3)
}

task?.cancel()

아닙니다. Swift의 Task 취소는 cooperative(협력적)입니다. 

.cancel()을 호출하면 "취소 요청" 플래그만 세울 뿐, 실행 중인 코드를 강제로 중단하지 않아요.

  • (1) doSomething() — 내부에서 취소 감지 → CancellationError throw → 중단
  • (2) heavySyncWork() — 동기 코드라 취소 체크 없음 → 끝까지 실행됨
  • (3) doSomethingElse() — (2)가 끝난 후에야 취소 감지

동기 코드가 강제 중단되지 않는 건 의도된 동작입니다. 오래 걸리는 동기 코드 안에서 직접 취소를 체크하고 싶으면:

@concurrent
func heavySyncWork() async throws {
    for item in largeArray {
        try Task.checkCancellation() // 취소됐으면 CancellationError throw
        process(item)
    }
}

weak self — 써야 할까요?

// 방식 A — weak capture
task = Task { [weak self] in
    let data = await fetchData()
    self?.items = data
}

// 방식 B — strong capture
task = Task {
    let data = await fetchData()
    self.items = data
}

정답은 상황에 따라 다릅니다.

방식 B(strong capture) 는 Task가 완료될 때까지 self를 살려두고 싶을 때. 예: 네트워크 요청 결과를 반드시 저장해야 할 때.

방식 A(weak capture) 는 View처럼 Task보다 먼저 사라져도 되는 경우. 단, self가 nil이 되면 이후 작업이 조용히 스킵됩니다.

특히 무한 루프 Task + self에 handle 저장 조합은 retain cycle 위험이 있습니다.

무한 루프 Task라면 weak self 또는 명시적 cancel 로직 필수입니다.

 

Task가 에러를 조용히 삼켜버린다?

Task {
    try await riskyOperation() // 에러 발생해도 아무 일 없음
}

Task는 내부에서 throw된 에러를 처리하지 않으면 조용히 무시합니다. 앱이 이상하게 동작하는데 원인을 못 찾는 케이스 중 하나예요. Swift 6.4부터는 이걸 경고로 잡아줍니다.

// 방법 1: task body 안에서 직접 처리
Task {
    do {
        try await riskyOperation()
    } catch {
        self.errorMessage = error.localizedDescription
    }
}

// 방법 2: handle로 나중에 받기
let task = Task {
    try await riskyOperation()
}

do {
    try await task.value
} catch {
    print(error)
}

 

버튼 탭 → async 요청, 어디서 Task 만드는게 좋을까?

그랬던적 있죠? 버튼 누르면 로그인 API 쏘거나 해야할때 궁금했던게 있어요. button에서 비동기로 전달하는 것과 뷰모델에서 비동기로 전달하는것과 동작은 같지 않을까?

// ❌ View에서 직접
Button("로드") {
    Task {
        let data = try await network.fetch()
        self.items = data
    }
}

// ✅ Model에서 Task 관리
Button("로드") {
    viewModel.load()
}

@MainActor
class ViewModel {
    func load() {
        Task {
            let data = try await network.fetch()
            self.items = data
        }
    }
}

 

하지만 async 코드는 Model로 이동시키는 게 좋습니다.

View는 동기 메서드만 호출. 왜 그렇게 해야할까요? 이것도 테스트를 생각하면 이해가 쉽습니다.

=> View 반응성 유지 + Model만 독립적으로 테스트 하기 위해!!!

 

수천 개 파일 처리 — Task 하나씩 만들면?

파일이 수백 개 수준이면 Task 하나씩 만들어도 충분할 수 있습니다.

이론으로 최적화하지 말고, 실제로 느리다고 느껴질 때 profile 먼저 해보는게 맞다구 하네요:)

profile 결과 Task 생성 오버헤드 + 스케줄링 부담이 문제라면 그때 withTaskGroup으로 묶어야 겠어요

// 일단 이렇게 해보고 — 생각보다 괜찮을 수 있어요
for file in files {
    Task {
        await process(file)
    }
}

// 느리다는 게 확인됐을 때
await withTaskGroup(of: Void.self) { group in
    for file in files {
        group.addTask {
            await process(file)
        }
    }
}

Actor — 언제, 얼마나 써야 할까?

Actor는 자신의 상태를 한 번에 하나의 작업만 접근할 수 있도록 보호하는 타입입니다.
쉽게 말하면 자기만의 isolation domain을 가진 타입이에요.

actor NetworkCache {
    private var cache: [String: Data] = [:]

    func store(_ data: Data, for key: String) {
        cache[key] = data
    }

    func get(_ key: String) -> Data? {
        cache[key]
    }
}

// 외부에서 접근할 때는 await 필요
let cache = NetworkCache()
await cache.store(data, for: "key")

여러 Task가 동시에 cache에 접근해도 Actor가 직렬화해줍니다. data race 없음.!!!!

Actor 많이 만들수록 좋을까?

아닙니다. Actor 하나 = isolation domain 하나 = 복잡도 하나 추가입니다.

Actor끼리 데이터를 주고받으려면 전부 await이 필요해요. Actor가 많아질수록 코드 전체가 async 투성이가 됩니다.

// Actor 많으면 이런 코드가 됨
let user = await userActor.getUser()
let cached = await cacheActor.get(user.id)
await analyticsActor.log(user.id)
await networkActor.fetch(cached)

Actor가 적합한 경우는 공유 가변 상태가 명확히 존재하고, 여러 곳에서 동시에 접근할 때입니다.

예: 네트워크 캐시, DB 접근.

 

반대로 대부분의 앱 코드는 @MainActor 하나로 충분합니다.

모든 UI 상태가 메인 스레드에서 관리되면 Actor 없이도 data race가 없으니까요:) 

"이게 정말 여러 isolation domain에서 동시에 접근해야 하나?" => ㅇㅇ 액터 사용 아니면 X


Task 좀 더 파헤치기.

.task modifier는 언제 취소될까?

struct MyView: View {
    var body: some View {
        List(items) { item in
            Text(item.title)
        }
        .task {
            await viewModel.load()
        }
    }
}

.task modifier가 만든 Task는 view가 사라질 때 자동으로 취소됩니다. onDisappear와 같은 타이밍이에요.

 

state 변경 시에는 취소되지 않아요.

state 변경은 view를 파괴하지 않고 body만 재평가합니다. Task는 그대로 살아있어요.

 

단, 이런 경우는 취소됩니다.

struct MyView: View {
    @State private var showDetail = false

    var body: some View {
        VStack {
            if showDetail {
                DetailView()
            } else {
                Text("메인")
                    .task {
                        await load() // showDetail = true 되면 취소됨
                    }
            }
        }
    }
}

if 분기가 바뀌면서 해당 branch의 view가 파괴될 때 취소됩니다.

 

더 세밀하게 제어하려면?

.task modifier는 handle을 노출하지 않습니다. 직접 소유하면 됩니다.

@MainActor
class ViewModel: ObservableObject {
    private var task: Task<Void, Never>?

    func startLoad() {
        task = Task {
            await load()
        }
    }

    func cancelLoad() {
        task?.cancel()
        task = nil
    }
}

struct MyView: View {
    @ObservedObject var vm: ViewModel

    var body: some View {
        ContentView()
            .onAppear { vm.startLoad() }
            .onDisappear { vm.cancelLoad() }
    }
}

Actor 데이터를 SwiftUI에 노출하려면?

actor DataStore {
    var items: [Item] = []
}

@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    private let store = DataStore()

    func load() async {
        let data = await store.items
        self.items = data // ✅ MainActor에서 UI 업데이트
    }
}

Actor → SwiftUI 사이에는 동기화 레이어가 불가피합니다.
Actor 상태를 SwiftUI가 직접 observe 할 수 없으니까요. ViewModel이 그 다리 역할을 합니다.

 

이때 동기화가 복잡하다고 느낄때 하.. actor를 진짜 써야해? 라고 의문점을 가져야해요!!

 

UI 상태를 다루는 모델이라면 custom actor 대신 @MainActor 클래스가 훨씬 자연스럽습니다.


Sendable 경고 어떻게 해결하나?

여기까지 왔으면 Sendable도 어렵지 않아요.

isolation domain 사이를 넘나들 때 안전한지를 컴파일러가 체크하는 건데, 그게 바로 Sendable이에요.

Sendable은 다른 isolation domain으로 안전하게 전달할 수 있는 타입입니다.

// ✅ Sendable — value type은 대부분 자동
struct User: Sendable {
    let name: String
    let age: Int
}

// ❌ non-Sendable — 가변 상태 가진 class
class UserSession {
    var token: String = "" // 여러 스레드에서 동시 접근 위험
}

NSObject 상속 클래스에서 Sendable 경고

// 1. 직접 thread-safe하게 보호하고 있을 때 — @unchecked Sendable
final class UserSession: NSObject, @unchecked Sendable {
    private let lock = NSLock()
    private var _token: String = ""

    var token: String {
        lock.withLock { _token }
    }
}

// 2. 한 번만 전달하면 될 때 — sending
func process(_ session: sending UserSession) async {
    // session을 이 isolation domain으로 넘겨받음
    // 원래 isolation에서는 더 이상 접근 불가
}

// 3. 전체 객체 말고 필요한 값만 추출
let token = session.token // String은 Sendable
Task {
    await process(token) // ✅ String만 전달
}

Generic Codable 함수에서 Swift 6 에러

// ⚠️ Swift 6에서 에러 발생
func fetch<T: Codable>(_ type: T.Type) async -> [T] {
    let data = await network.request()
    return try! JSONDecoder().decode([T].self, from: data)
}

// ✅ Sendable 추가
func fetch<T: Codable & Sendable>(_ type: T.Type) async -> [T] {
    let data = await network.request()
    return try! JSONDecoder().decode([T].self, from: data)
}

// ✅ 또는 반환값에 sending
func fetch<T: Codable>(_ type: T.Type) async -> sending [T] {
    let data = await network.request()
    return try! JSONDecoder().decode([T].self, from: data)
}

Swift 6 마이그레이션 — 어디서부터 시작하지?

개념을 다 잡았으니 마지막으로 실제로 마이그레이션할 때 어떻게 접근하면 좋은지 정리해볼게요.

가장 큰 실수: 한 번에 다 바꾸려는 것

Swift 6 마이그레이션에서 많은 분들이 "한 번에 완전한 Swift concurrency 모델로 리팩터링"을 시도합니다.

그러다 경고 폭탄에 지쳐서 포기하죠.

두 단계를 분리하세요.

  1. 현대화: API를 async/await으로 바꾸기
  2. 안전화: Swift 6 언어 모드 활성화, data race 경고 해결

순서는 상관없습니다. 동시에 하려 하면 힘들어요.

추천 순서

1단계: approachable concurrency 먼저 활성화

Xcode Build Settings → Swift Compiler → Upcoming Features에서 활성화. 새 async 함수 기본 동작(hopping 없음)을 먼저 적용합니다.

2단계: 경고 카테고리별로 나눠서 처리

한 번에 전체 활성화 대신 SwiftStrictConcurrency = minimal → targeted → complete 순으로. static variables → actor isolation → sendable 순으로 카테고리별 처리. 하나 해결하면 연쇄적으로 많은 경고가 사라집니다.

3단계: 새로 작성하는 코드에 우선 적용

오래된 안정적인 코드는 나중에. 새로 쓰는 코드부터 Swift concurrency로 작성하는 게 가장 효과적입니다.

GCD/OperationQueue → Swift Concurrency

// ❌ 기존 GCD
DispatchQueue.global().async {
    let data = self.heavyWork()
    DispatchQueue.main.async {
        self.items = data
    }
}

// ✅ Swift Concurrency
Task {
    let data = await heavyWork()
    await MainActor.run {
        self.items = data
    }
}

// ❌ 기존 OperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4
for item in items {
    queue.addOperation { process(item) }
}

// ✅ Swift Concurrency
await withTaskGroup(of: Void.self) { group in
    for item in items {
        group.addTask { await process(item) }
    }
}

App termination — async shutdown 처리

// ❌ 절대 하면 안 됨 — deadlock 위험
func applicationWillTerminate(_ application: UIApplication) {
    let semaphore = DispatchSemaphore(value: 0)
    Task {
        await cleanup()
        semaphore.signal()
    }
    semaphore.wait() // 💀 concurrent thread pool이 꽉 차면 아무것도 진행 못함
}

// ✅ 가능한 한 동기로 처리
func applicationWillTerminate(_ application: UIApplication) {
    saveStateSynchronously()
    flushLogsSynchronously()
}

termination callback은 동기입니다. 그 안에서 처리해야 할 코드를 최대한 동기로 만드는 게 근본 해결책이에요.


이 내용은 Apple Developer의 Q&A: Swift concurrency | Meet with Apple 세션을 기반으로 정리했습니다.

틀린 내용이나 추가했으면 하는 내용 있으면 댓글로 알려주세요 :)

https://www.youtube.com/watch?v=E95agtPgaa0&list=LL&index=1&t=2415s

'iOS' 카테고리의 다른 글

푸시 알람을 어떻게 설계할까?  (2) 2025.12.21
iOS 이미지 포맷과 압축 방식 - 사진은 HEIC인데 스크린샷은 왜 PNG일까?  (0) 2025.12.17
UIKit / SwiftUI에서는 무슨 디자인 패턴을 사용해야할까?  (0) 2025.10.17
URLSession 한 줄 뒤에 숨겨진 것들: DNS부터 전자기파까지  (2) 2025.10.11
HTTP 캐싱(Etag & max-age) 그리고 iOS에서는?  (0) 2025.10.01
'iOS' 카테고리의 다른 글
  • 푸시 알람을 어떻게 설계할까?
  • iOS 이미지 포맷과 압축 방식 - 사진은 HEIC인데 스크린샷은 왜 PNG일까?
  • UIKit / SwiftUI에서는 무슨 디자인 패턴을 사용해야할까?
  • URLSession 한 줄 뒤에 숨겨진 것들: DNS부터 전자기파까지
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (146)
      • SWIFT개발일지 (34)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (45)
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (3)
      • 인생회고 (1)
      • AI 라이브러리 (1)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
Swift 6 Concurrency Q&A 정리: isolation부터 Sendable까지
상단으로

티스토리툴바