2025. 4. 27. 22:55ㆍApple/Swift
글을 읽는 예상 독자:
- 1차독자: 현재와 미래의 나 자신
- 2차 독자: ARC를 어느 정도 알고 있는 분들, 그 외 메모리 관리 기법에 관심이 있는 분들을 목표로 작성해 봤어요.
프로그래밍을 하면 기본적으로 데이터를 생성하고, 서버로 전송(요청)하며, 서버로부터 응답을 받아 클라이언트 단에서 이를 표시하는 과정을 반복합니다. 특히, 대규모 서비스에서는 관리해야 할 데이터의 양이 기하급수적으로 많아집니다. 예를 들어, 카카오톡에서는 수많은 채팅 메시지가, 29cm와 쿠팡 같은 쇼핑몰 앱에서는 수천, 수만 개의 상품 정보가 메모리에 적재되고 관리되어야 합니다.
이처럼 방대한 데이터를 효율적으로 다루기 위해서는 메모리 관리가 필수적입니다. 그렇지 않으면 한정된 메모리 자원을 소모하고, 결국 성능 저하나 앱 크래시로 이어질 위험이 있습니다.
메모리란 뭘까요?
컴퓨터 메모리는 컴퓨터라는 하드웨어 장치에 수치, 명령, 자료 등을 기억하는 역할을 해요. 컴퓨터 메모리의 분류의 주기억 장치, 보조 기억 장치 2가지 부류로 볼 수 있어요.
메인 메모리 구성
주기억 장치: CPU가 직접 접근해 실행 중 코드를 읽고 쓰는 메모리 계층입니다.
- RAM(Random Access Memory): 휘발성 기억 장치
- ROM(Read Only Memory): 고정 기억 장치(비휘발성 기억 장치)
보조기억 장치: 대용량 데이터를 영구 보관 가능, CPU 직접 접근이 아닌 I/O 컨트롤러를 통해 사용합니다.
위의 시퀀스 다이어 그램을 참고하면 CPU가 데이터에 접근시 메모리 계층을 어떻게 타고 내려갔다 다시 올라오는지 단계별로 볼 수 있습니다.
1단계. CPU → 캐시 요청
- CPU가 특정 메모리 주소에 있는 데이터를 읽고자 요청을 보냅니다.
- 이 요청은 가장 빠른 계층인 캐시(cache) 로 전달됩니다.
2단계. 캐시 적재 여부 판단
- 캐시에 적재됨
- 캐시에 요청한 데이터가 이미 올라와 있다면,
- 캐시가 즉시 데이터 반환 → CPU로 응답을 돌려보냅니다.
- 캐시에 적재되지 않음
- 원하는 데이터가 캐시에 없으면(미스),
- 캐시는 메인 메모리(RAM) 쪽으로 다시 요청을 전달합니다.
3단계. 메인 메모리 접근
- 메인 메모리에 적재됨
- RAM에 데이터가 존재하면,
- RAM이 먼저 데이터 반환 → 캐시에 로드(적재)
- 캐시가 그 데이터를 CPU로 반환합니다.
- 메인 메모리에 적재되지 않음
- RAM에도 데이터가 없으면(미스),
- 최종 계층인 디스크(Disk) 로 요청이 넘어갑니다.
4단계. 디스크 접근 및 역방향 경로
- 디스크에서 해당 데이터를 읽어와 RAM으로 보냅니다.
- RAM은 받은 데이터를 캐시에 적재한 뒤,
- 캐시가 다시 CPU로 데이터 반환을 합니다.
계층별로 "가장 빠른 곳" 부터 "가장 느린 곳" 까지 순차적으로 탐색하며, 데이터를 찾는 순간 즉시 최상단 cache부터 CPU로 돌아가도록 설계되어 있습니다.
sequenceDiagram participant CPU as CPU participant Cache as 캐시 participant RAM as 메인 메모리(RAM) participant Disk as 디스크 CPU->>Cache: 데이터 요청 alt 캐시에 적재됨 Cache-->>CPU: 데이터 반환 else 캐시에 적재되지 않음 Cache->>RAM: 데이터 요청 alt 메인 메모리에 적재됨 RAM-->>Cache: 데이터 반환 Cache-->>CPU: 데이터 반환 else 메인 메모리에 적재되지 않음 RAM->>Disk: 데이터 요청 Disk-->>RAM: 데이터 반환 RAM-->>Cache: 데이터 반환 Cache-->>CPU: 데이터 반환 end end
하드웨어 메모리 계층에서 캐시 -> RAM -> 디스크 흐름으로 실제 바이트(데이터) 가 어디에 올라가며, 어떻게 올라갔다 내려가는지의 과정입니다. 그래서 Swift 에서는 메모리 관리 기법이 이 흐름과 어떤 식으로 연관이 있는지 살펴보고 싶었습니다.
소프트 엔지니어링 측면에서 메모리를 관리해야 하는 이유?
프로그래밍언어 종류에 따라 메모리를 관리하는 기법에는 Swift에는 ARC, C언어의 malloc, alloc, free, 자바의 Garbage Collection 등이 있어요. 구현 스타일은 달라도 결국 다음과 같은 목적 때문에 사용하는 거죠.
한정된 자원으로서의 메모리
1. 유한성: 물리적, 가상 메모리는 유한해요. 큰 기업이나 공공기관의 서버 규모에도 동시에 수천, 수만개의 요청이 있다면 그걸 처리하기 위해 안정적인 운영이 필요하거든요.
2. 모바일 폰 같은 환경을 생각해보면, PC나 맥북, 맥스튜디오에 비해 메모리 용량이 특히 제한적이잖아요? 그럼 모바일 개발시에 더 중요해질 수 밖에 없을거에요.
3. 과다한 사용을 방지할 수 있어요.
할당한 메모리를 해제하지 않는다면, 메모리 공간이 누수되면서 시스템 전체가 메모리 부족 상태에 빠질 지도 몰라요. 이런 경우엔 Out-Of-Memory 오류나 프로세스 강제 종료와 같은 상황이 벌어질 수 있죠.
성능(Performance) 및 예측 가능성
Swift에서는 Auto Referencing Counting(자동참조카운팅), 즉 용어 그대로 자동으로 더이상 필요하지 않은 인스턴스가 있다면 클래스 인스턴스에 의해 사용된 메모리를 자동으로 해제해줘요. 참조 카운팅이 0이 될때 즉시 해제 시켜줘요. 이렇게 된다면, 또 다른 인스턴스가 메모리 공간을 필요로 할 때 메모리를 확보해 다른 목적으로 쓸 수 있게 돼요.
프로그램 안정성(Safety)
만약, 해제된 메모리에 접근하면 어떻게 될까요? 그런 예측 불가능한 동작은 앱 크래시를 발생시키기도 해요. 이걸 댕글링 포인터(Dangling Pointer)라고 해요. 메모리 관리가 잘못되면 보안이 취약해질 수도 있고, 데이터가 오염될 수도 있대요. 메모리를 관리한다면 이런 것들을 예방할 수 있겠죠?
Swift에서 메모리를 어디에서 관리할까요?
Stack 과 Heap 이라는 2가지 영역에 데이터를 적재합니다.
스택과 힙은 알고리즘에서도 접할 수 있는 개념이죠? Stack은 사전적 의미로 쌓다라는 의미를 가지고 있습니다. 함수를 호출하면 생성되고 지역 변수와 스택에는 매개변수나 복귀 주소 같은 것도 저장할 수 있어요. LIFO(Last-In, First Out, 후입선출) 방식으로 관리해 속도도 상대적으로 빠르고,힙보다 오버헤드가 적은 편이에요. 컴파일 타임 때 생명주기를 결정해 함수를 호출할 때 자동으로 할당(PUSH)되고, 함수가 종료되면 자동으로 해제(POP)돼요. 스택에 저장되는 메모리는 지속되지 않고, 함수의 생명주기에 따라 끝이 나면 사라져요. 운영체제나 스레드별로 크기가 제한되어 있어요.
그럼 Heap은 Stack이랑 다른 성격을 지닌 메모리 관리 공간이겠죠? 런타임(실제 실행될 때)시, 즉 클래스의 인스턴스 등과 같은 동적 할당이 필요한 객체나 값 타입의 대용량 데이터를 저장해줘요. 자유롭게 할당, 해제 가능하고요. Heap은 위의 성능 측면에서 언급한 Swift의 ARC가 가능해요. 그치만, 속도는 스택보다 느리고, 캐시 미스(cache miss)가 발생할 수도 있어요. 여기서 miss는 데이터가 해당 계층에 없다는 의미 입니다.그치만, 스택과는 반대로 용량은 전체 가상 메모리 한도 내에서 훨씬 큰 영역이 사용 가능 하고요. 이런 Heap의 특징들은 런타임(실행) 때 비즈니스 로직에 따라 생성되거나 소멸되어야 하는 데이터 블록을 자연스럽게 관리할 수 있어요. 관리해야할 오버헤드 발생 가능성이 스택에 비해 더 클 수는 있지만 용량은 더 큰 편인거죠.
오버헤드: 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간과 메모리를 의미해요. 어떤 기능을 실행시 10초가 기본적으로 걸린다고 가정을 해본다면, 실제 실행시 20초에서 30초가 넘게 걸리게 돼요. 그럼 시간이 10초에서 20초 넘게 더 걸리게 되는거죠. 그럼 시느템 성능도 저하시키는 요인이기 때문에 이런 오버헤드를 최소화 시키는 게 중요해요.
Dangling Pointer: 이미 해제된 메모리를 참조하거나, 더 이상 유효하지 않은 메모리를 가리키는 포인터를 가리키는 용어에요. 메모리가 해제되었지만 포인터가 여전히 해당 메모리 주소를 가지고 있는 상황에서 발생합니다.
메모리에 관한 개념, 컴퓨터에서 메모리에 접근하고 관리하는 흐름과 메모리를 관리해야 하는 이유에 대해서 언급해보고 싶었어요. 그럼 Swift에서 메모리를 관리하는 방식이 어떻게 되는지 크게 먼저 언급해볼게요.
Swift 메모리 관리 방식
Swift 의 메모리 관리 방식은 소유 정책(Ownership Policy)와 메모리 관리 기법(memory management)이 ARC와 긴밀하게 연관되어 있고, 규칙이 있어요.
(오늘 부트캠프 강의에서 KxCoding 강사님이 다시 짚어주신 내용이라 빼먹고 싶지 않았습니다. ㅎㅎ)
소유자가 하나라도 있으면 유지(retain)되고, 소유자가 없다면 인스턴스가 사라진다는 거(release)에요. release는 ARC가 자동으로 메모리를 관리하고 참조한 것을 해제하고 소유권을 포기합니다.
1. Automatic Reference Counting (ARC), 자동 참조 카운팅
예전에 공식 문서(swift.org)를 참고해서 정리했던 문서 첨부해요. 궁금하신 분들은 여기 링크 눌러주세요.
1.1 강한 참조 순환(Strong Reference Cycle)
클래스는 참조 타입이고 클래스 타입의 인스턴스를 생성하고, 또 다른 클래스가 상위 클래스 타입을 멤버 인스턴스로 가지고 있으면 강한 순환 참조가 발생할 수 있어요.
해제되는 시점을 체크할 때 - Deinit
그런 경우 아래와 같이 약한 참조를 하기도 하지만, 해제되는 순간을 체크하기 위해 deinit(deinitializer, 소멸자)를 써 확인하는 용도로 쓰기도 해요. 관련 코드 살펴볼게요.
import UIKit
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 인스턴스가 초기화되었습니다.")
}
deinit {
print("\(name) 인스턴스가 메모리에서 해제됩니다.")
}
}
var reference1: Person?
var reference2: Person?
reference1 = Person(name: "Nat")
reference2 = reference1
reference1 = nil
print("reference1 해제됨") // 아직 해제되지 않음
reference2 = nil
print("reference2 해제됨") // 이제 해제됨
1.2. 약한 참조(Weak Reference)
1.3. 미소유참조(Unowned Reference)
문자 그대로 해석하자면, 미소유 참조는 소유할 수 없는 참조에요. 따라서, 약한 참조와 비슷한 성질로 인스턴스를 강하게 유지하지 않아요.
A, B라는 두 개의 클래스 타입이 있다고 가정하면, A라는 클래스가 B보다 더 오래 유지(혹은 존재)할 수 있다는 게 명확한 경우 Unowned 를 써요. 그치만 self가 그전에 해제되버리면 댕글링 포인터가 발생하면서 크래시가 날 수 있죠. 성능면에서 느려질 가능성도 있지만, 상황에 따라 unowned reference를 사용해요. 공식문서의 예시 코드를 살펴볼게요.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
CreditCard 라는 클래스는 항상 Customer라는 객체를 가지고 있고 Strong Reference Cycle(강한 순환 참조)을 피하기 위해 클래스타입 Customer 인 customer 라는 멤버 인스턴스 상수에 unowned 키워드를 붙여 미소유 참조로 선언해요. 신용카드는 고객에 의해 언제든지 카드 해지할수도 분실 신고해서 없애버릴수도 있잖아요. 그래서 고객이 더 오래 존재할 가능성이 크죠. 그런 면에서 미소유 참조의 특성을 지닐 수 있다고 볼 수 있어요.
1.4. 클로저의 캡처 리스트
(사실 클로저의 캡처 리스트가 메인 주제였다고 봐도 무방합니다. 그치만 Swift 메모리 관리 특징을 한번 정리해보고 싶었답니다.😇)
Closure 또한 클래스와 같은 참조 타입이에요. 또 다른 참조 타입의 인스턴스를 생성하면 강한 순환 참조가 발생할 수 있어요.
클로저의 캡처란 클로저가 자신이 생성될 때 주변의 값, 외부의 값을 잡아 저장해두는 것을 의미해요. 캡처된 값은 클로저 안에서 지속적으로 접근 가능해요.
클로저는 context(문맥상) 유추가 가능해서 파라미터나 list 반환 타입을 지정하지 않고 생략하면 파라미터와 반환타입은 생략 가능해요. 단 , [list] 써야할 땐 반드시 in을 붙여줘야 해요. 클로저 캡처리스트는 cow 와는 달리 복사본을 다른 공간에 저장해 사용하는 것이 아니라, 원본을 캡처해서 가지고 와서 사용해요.
{ [list] (parameters) -> return type in
}
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body
}
클로저 캡처리스트 유형은 참조타입 캡처리스트와 값 타입 캡처리스트 2가지 유형이 있어요.
값 타입 캡처리스트
값 타입인 struct, enum, String 등을 복사해 클로저에 캡처할 수 있습니다. 기본적으로는 변수 타입의 인스턴스가 주어지면 클로저는 실행 시점의 최신 값을 참조해 값을 보관하게 돼요. 아래 let c1 클로저에서 기존의 변수 값 x가 최신의 값 5를 참조해 값이 변경되어 출력된 걸 볼 수 있어요.
클로저 생성 시점의 값을 복사해서 유지 하고 싶다면, 캡처리스트를 사용할 수 있고 기본 문법 형태인 [ ](Square Bracket)을 사용하면 돼요. 우측의 코드를 보시면, 괄호 안 fixedX라는 클로저의 읽기 전용 상수로서 선언됩니다. weak self, unowned self 말고도 이렇게 선언해서 값을 변경할 수 없게도 해요. 클로저 c2의 x값이 클로저 생성시점에 넣은 x의 값 1이 여전히 유지되고 있는 걸 보실 수 있어요.
참조 타입 캡처리스트
클로저가 클래스의 멤버인스턴스를 참조하거나 다른 참조 타입 객체를 캡처하면, 기본적으로 Strong 참조를 유지해요. 항상 그런건 아니지만, 이럴 경우 클로저와 객체가 서로를 강하게 참조하는 순환 참조(retain cycle)이 발생할 수 있어요. 그럴 때 사용하는 것이 weak self, unowned self 고요. 이 둘의 차이에 대해 특정 예시 상황을 비유로 들어 설명해보겠습니다.
Weak Self 에 대해 간단하게 설명해볼게요. weak self는 기본적인 목적으로 강한 순환 참조를 방지하기 위해 사용해요. self가 해제될 수도 있으니 안전하게 써야할 경우에요. Delegate를 호출하거나 비동기 작업에서도 흔히 쓰여요.
예시 상황은 대략 이러해요. 어떤 이미지나 녹음된 파일 같은 미디어 파일을 다운 받는다고 가정해 봅시다.
[실패 가능성]
1. 언제 다운받아질지 알 수 없습니다.
2. 다운로드가 네트워크 환경에 따라 실패할 수 있습니다
viewController의 현재 인스턴스인 self가 다운로드 완료 시점까지 살아 있다는 보장을 할 수 없어요. 만약 클로저가 그냥 self를 강하게 참조하고 있으면 ,
- ViewController가 해제되지 못해 메모리 누수(memory leak)이 발생할 가능성이 있어요.
- 혹은 이미 해제된 인스턴스에 접근시에는 크래시(앱 크래시)가 발생할 수도 있고요.
class Downloader {
func download(completion: @escaping () -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completion()
}
}
deinit { print("Downloader가 해제되었습니다.") }
}
class ViewController {
var downloader: Downloader? = Downloader()
func startDownload() {
downloader?.download { [weak self] in
guard let self = self else { return }
self.updateUI()
}
}
func updateUI() {
print("다운로드가 완료되어 UI를 업데이트합니다.")
}
deinit { print("ViewController가 해제되었습니다.") }
}
var viewController: ViewController? = ViewController()
viewController?.startDownload()
viewController = nil
// Downloader와 ViewController 모두 정상 해제
그래서 1.4 Closure Capture List 방식을 사용해요. 아래에서 더 자세히 설명해볼게요.
NOTE
탈출 클로저에 대한 설명이나 클로저의 세세한 글을 별도로 다뤄보도록 할게요.
파일을 다운로드하는 함수를 만들 때 다운로드가 실패할 가능성도 고려해야겠죠? 안전하게 self 를 [weak self] 라는 클로저의 캡처 리스트로 지정해줘요. 그럼 Optional로 self? 로 타입을 지정해줍니다. 피연산자가 옵셔널이면 연산을 할 수 없어요. 일반 타입의 self 로 사용하기 위해 그럼 guard문 안에서 안전하게 self를 unwrapping(옵셔널 타입을 non-optional type으로 바꾸는 것) 해줬어요. updateUI()를 호출하고 self가 해제된 경우에는 안전하게 클로저(탈출 클로저 escaping closure)문을 빠져나갈 수 있는거죠.
unowned reference 를 사용할 때는 유의할 점이 있어요. 이미 해제된 인스턴스에 접근하면 crash가 발생해요. 그래서 확실히 그 인스턴스가 다른 것들보다 오래 남아있는 게 확실하다면 unowned 키워드를 붙여요. 또는 해제될 일이 없다는 게 확실하다면 unowned을 사용할 수 있겠죠. unowned는 weak reference 와 달리 non-optional 타입으로 변경되었기 때문에 가독성 측면에서도 Optional이나 nil이 될 걱정 없이 사용할 수 있어요. 그래도 유의점을 잘 고려해서 사용해야합니다.
클로저 캡처리스트 정리
1. 강한 순환 참조 방지하여 [weak self] 또는 [unowned self] 로 참조해 약한 참조를 하여 안전하게 객체가 해제될 수 있도록 합니다.
2. 값타입, 참조타입 캡처리스트는 어느 게 성능이 더 좋고 괜찮다의 문제가 아닙니다. 목적에 따라 값 타입 혹은 참조 타입의 캡처리스트를 선택하는 건데요.
- 값 타입 캡처 리스트의 경우에는 스냅샷 형태로 생성된 시점의 값이 필요할 때, 혹은 외부 변수 변경과 무관하게 불변의 데이터를 참조해야할 때 사용합니다.
- 참조 타입은 메모리의 순환 참조를 방지하기 위해 사용해요. 네트워크 콜백, 델리게이트, DispatchQueue와 같은 동시성을 고려할때 사용해요.
2. 값 타입의 Copy On Write(C.O.W.)
Copy on write는 Swift의 값타입인 Structure, Enum, 등의 자료형에 제공되는 특성이에요. Array, Dictionary, String 등 값 타입은 이 COW 방식으로 메모리를 최적화 할 수 있어요.
--- config: look: handDrawn theme: forest --- classDiagram class BufferA { +데이터 } class BufferB { +복사된 데이터 } class var1 class var2 var1 --> BufferA : 참조 var2 --> BufferA : 참조 BufferA <|-- 복사 : 수정시 var2 --> BufferB : 새 참조
여러 변수가 같은 버퍼(buffer)를 공유하다 원본 변수가 아닌 새로 만든 인스턴스 멤버 변수가 수정이 되면 그 시점에 복사본을 따로 만들어 다른 메모리 저장소에 만들어 수정해요.
관련 코드로 직접 다른 주소를 갖고 있는 상황을 한 번 살펴봐요.
func printBufferAddress(_ arr: inout [Int], label: String) {
let addr = arr.withUnsafeBufferPointer { ptr in
ptr.baseAddress!
}
print("\(label): \(addr)")
}
var a = [1, 2, 3]
var b = a
// 변경 전 주소 (a와 b가 같은 버퍼를 가리킴)
printBufferAddress(&a, label: "a before")
printBufferAddress(&b, label: "b before")
// b를 수정하면 COW 복사가 발생
b.append(4)
// 복사 후 주소
printBufferAddress(&a, label: "a after")
printBufferAddress(&b, label: "b after")
// 최종 값
print("a:", a) // a: [1, 2, 3]
print("b:", b) // b: [1, 2, 3, 4]
a라는 [Int] 배열을 복사하고 있었던 b라는 변수를 하나 더 만들었어요. 그리고 변경되기 전 a, b의 주솟값은 동일 했어요. 4를 추가해 배열의 값에 변경이 생기면 Swift의 COW 정책에 따라 b의 버퍼가 새로 복사되고 주소값도 변경된 걸 디버그창에 출력된 주소값으로 확인할 수 있어요. withUnsafeBufferPointer 클로저는 배열 내부에 실제로 저장된 연속 메모리(버퍼)의 포인터를 안전하게 (UnsafeBufferPointer)를 사용해 꺼내고, baseAddress를 읽을 때 사용해요.
버퍼(Buffer): 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역입니다.
버퍼링(buffering)이란 버퍼를 활용하는 방식 또는 버퍼를 채우는 동작을 말한다. 다른 말로 '큐(Queue)'라고도 표현합니다.
3. UnsafePointer: 특정 유형의 데이터에 액세스(접근)하기 위한 포인터
C API 연동이나 성능 최적화를 할 때 UnsafePointer라는 구조체 타입을 사용해 직접 메모리를 할당하거나 해제할 수도있습니다. 자주 사용하는 Swift 에서의 메모리 관리 기법이니 이 부분은 C언어 연결부분과 연관이 있고, OpenSSL 이라는 네트워크 통신 보안 설정시 사용한다고 해요.
정리하며
메모리 관리 기법은 Swift만의 독자적인 방식은 아니며, 각 언어와 플랫폼마다 다양한 방식이 존재합니다. Swift의 ARC 방식만 보더라도, 기본적인 원리에서부터 실제 개발 현장에서 부딪히는 참조 순환 문제나 클로저 캡처와 같은 깊은 주제까지 폭넓게 다루고 있어, 내용을 어디까지 어떻게 다룰지 고민이 많았습니다. 그치만 참조 순환 문제 해결과 클로저의 캡처링 리스트는 실무적인 코드에서 아주 많이 사용하고 있기에 이론적으로 이해하면서 납득하고 싶었습니다.
이 글을 통해 메모리 관리가 왜 필요한지에 대한 근본적인 질문에서 시작해, 컴퓨터의 메모리 구성이라는 기초적인 배경지식부터 Swift의 ARC 원리와 이를 활용하는 방법까지 단계적으로 설명했습니다.
결국, 메모리 관리 기법은 개발자와 회사 그리고 팀이 함께 정해놓은 개발 환경, 프로젝트의 규모, 그리고 요구사항을 신중히 고려하여 적절하게 선택하고 적용해야 합니다. 그러한 선택과 고민의 과정이 더욱 견고하고 효율적인 모바일 서비스 앱을 만들어 줄 것입니다.
틀린 부분에 대한 피드백과 의견 공유는 환영합니다!
참고문서
Documentation
docs.swift.org
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/
'Apple > Swift' 카테고리의 다른 글
Apple 이 제시하는 콤비네이션 핏자, Combine 살짝 음미해보기 (3) | 2024.12.21 |
---|---|
[ Authentication 개념 ] 사람들의 신분을 증명하는 절차?! (0) | 2024.08.08 |
[ 이메일 유효성 체크 ] - 이메일 가입이 아무거나 다된다고? (1) (0) | 2024.08.07 |
동시성(Concurrency)을 대하는 Swift 의 자세 1 - DispatchQueue (0) | 2024.02.02 |
Swift 기본 가이드라인 - part 1 (0) | 2023.12.14 |