2022. 1. 22. 17:29ㆍSwift, iOS Foundation
이번 글에서는 구조체(Struct)와 클래스(Class)에 대해 아주 자세하게 다뤄보려 한다.
처음 Swift를 배우는 입장도 아닌데, 이제 와서 이 내용을 포스팅하는 이유가 뭐냐고 물어본다면... 음... 몇 번 반복에서 강조해도 아깝지 않을 만큼 정말 중요하기 때문이랄까.....?
본격적으로 들어가기 전에, 객체지향 프로그래밍에서의 중요한 4가지 특성을 먼저 간단하게 살펴보겠다.
왜냐고? 구조체와 클래스를 배우는 내용이 객체지향 프로그래밍에서의 중요한 특징에 많이 해당하기 때문이다.
얼만큼 해당하는지 한번 살펴볼까?
1️⃣ 추상화 : 객체의 공통적인 속성과 기능을 추출하여 정의하는 것 [오늘 배울 클래스, 구조체와 관련]
2️⃣ 캡슐화, 은닉화 : 목적에 따라 데이터 구조 및 처리 방법을 결합시키고, 외부에는 내부 기능 구현 내용을 감추고 이용방법만 알려주는 것 [Swift에서는 접근 제어에 해당하는 내용이었다.]
3️⃣ 상속 : 상위 개념의 특징을 하위 개념이 물려받아 사용하는 것, 재사용을 위함 [오늘 배울 클래스, 구조체와 관련]
4️⃣ 다형성 (Polymorphism) : 한 객체가 다른 여러형태(객체)로 재구성되는 것 [오늘 배울 클래스, 구조체와 관련]
구조체(Struct)와 클래스(Class)는 기본적으로 데이터를 용도에 맞게 묶어 표현하고자 할 때 유용한 기능이다.
조금 풀어서 말하자면, 구조체와 클래스는 모두 ‘프로퍼티'와 ‘메서드'를 사용하여 구조화된 데이터와 기능을 가질 수 있고, 하나의 데이터 타입을 만드는 역할을 하는 애들이라고 할 수 있다.
그러면, 구조체와 클래스를 제대로 이해하기 전에, 항상 헷갈릴 수 있는 용어들을 정리해보고 가보자!
- 인스턴스(instance) : 클래스/구조체에서 생성된 객체 (= 구조체와 구조체라는 틀(frame)안에서 찍어낸 내용물)
- 프로퍼티(property) : 클래스/구조체 객체(= 인스턴스)에 들어있는 정보, 값
- 메서드(method) : 클래스/구조체 객체(= 인스턴스)에 들어있는 함수
1. 구조체(Struct)와 클래스(Class) 기본 형태
그럼 이제 묻지도 따지지도 않고, 구조체와 클래스를 어떻게 사용하는지 알아보자.
위에서 인스턴스, 프로퍼티, 메서드라는 용어를 이미 배웠으므로 이제부터는 이 용어들을 자유롭게 섞어가면서 설명을 하니, 당황하지 말자 ^_^
우선 구조체는 struct라는 키워드를 사용해서 만들어준다.
그리고 아래 코드를 보면, 프로퍼티와 메서드 인스턴스를 각각 어떤 식으로 생성하는지 확인할 수 있을 거다.
프로터티나 메서드의 차이는 아래에서 따로 자세하게 알아보기로 하고, 여기서는 각 인스턴스와 프로퍼티에 따라서 수정 여부가 어떻게 차이가 되는지에 대해서만 집중해보자.
참고로, 여기서 말하는 가변은 변할 수 있는 변수(var), 불변은 변할 수 없는 상수(let)를 의미한다고 보면 된다.
struct SampleStruct {
var mutableProperty: Int = 100 // 가변 프로퍼티
let immutableProperty: Int = 100 // 불변 프로퍼티
static var typeProperty: Int = 100 // 타입 프로퍼티: SampleStruct타입 자체에서만 사용 가능
func instanceMethod() { // 인스턴스 메서드
print("instanceMethod")
}
static func typeMethod() { // 타입 메서드
print("typeMethod")
}
}
// 가변 인스턴스
var mutable: Sample = Sample()
mutable.mutableProperty = 200 // 가변 인스턴스 내부의 가변 프로퍼티: 수정 o
mutable.immutableProperty = 200 // 가변 인스턴스 내부의 불변 프로퍼티: 수정 x
// 불변 인스턴스
let immutable: Sample = Sample()
immutable.mutableProperty = 200 // 불변 인스턴스 내부의 가변 프로퍼티: 수정 x
immutable.immutableProperty = 200 // 불변 인스턴스 내부의 불변 프로퍼티: 수정 x
클래스는 class라는 키워드를 사용해서 만들어줄 수 있다.
클래스도 구조체와 마찬가지로 타입을 정의하는 문법이기 때문에 UpperCamelCase를 사용해서 네이밍을 지정해줘야 한다.
위의 구조체와 비교했을 때, class func 키워드를 사용한 재정의 가능한 타입 메서드 부분이 추가된 것을 확인할 수 있다.
이 부분은 아래에서 다룰 상속 부분과 연관되어 있는 클래스만의 고유한 특징이라고 볼 수 있다.
추가로, 불변 인스턴스 내부에 가변 프로퍼티가 있는 경우에도 구조체와 차이점이 존재하는 것을 확인할 수 있는데,
변하지 않는 클래스나 구조체 안에 있는 var 프로퍼티는 클래스의 경우에만 수정이 가능하다고 한다. 이는 클래스가 참조 타입(Call by Reference)인 것과 연관되어 있는데, 이 역시도 아래에서 집중적으로 다뤄보도록 하겠다.
class SampleClass {
var mutableProperty: Int = 100 // 가변 프로퍼티
let immutableProperty: Int = 100 // 불변 프로퍼티
static var typeProperty: Int = 100 // 타입 프로퍼티: SampleClass타입 자체에서만 사용 가능
func instanceMethod() { // 인스턴스 메서드
print("instanceMethod")
}
static func typeMethod() { // 타입 메서드
print("typeMethod")
}
class func classMethod() { // 재정의 기능 타입 메서드 - 상속과 관련
print("type method - class")
}
}
// 가변 인스턴스
var mutableReference: Sample = Sample()
mutable.mutableProperty = 200 // 가변 인스턴스 내부의 가변 프로퍼티: 수정 o
mutable.immutableProperty = 200 // 가변 인스턴스 내부의 불변 프로퍼티: 수정 x
// 불변 인스턴스
let immutableReference: Sample = Sample()
// 클래스 인스턴스는 참조 타입(구조체와 다른부분)
immutable.mutableProperty = 200 // 불변 인스턴스 내부의 가변 프로퍼티: 수정 o
immutable.immutableProperty = 200 // 불변 인스턴스 내부의 불변 프로퍼티: 수정 x
2. 구조체와 클래스의 차이점
그럼 위에서 다룬 내용 말고도 구조체와 클래스의 차이점은 어떤 것들이 있을까?
여기서는 표로 몇 가지만 쉽게 정리해보도록 하겠다.
구조체 (Struct) | 클래스 (Class) |
값 타입(Call by Value) | 참조 타입(Call by Reference) |
상속 불가능 | 상속 가능 |
레퍼런스 형태가 아니기 때문에 공유가 불가능 | 레퍼런스 형태이므로, 공유가 가능 |
스택 메모리 영역에 할당 (속도가 빠름) | 힙 메모리 영역에 할당 |
AnyObject로 타입 캐스팅이 불가능 | AnyObject로 타입 캐스팅이 가능 |
3. 값 타입(Call by Value)과 참조 타입(Call by Reference)
위에서 정리한 구조체와 클래스의 차이점 중에서 데이터 전달 방법 부분을 조금 더 자세하게 알아보자. (중요하니깐 ^__^)
데이터를 전달할 때, 값을 복사해서 전달하느냐, 해당 값의 메모리 위치를 전달하느냐에 따라서
값 타입(Call by Value)과 참조 타입(Call by Reference)이 나뉘게 된다. (값 복사-> 값 타입, 메모리 위치 전달 -> 참조 타입)
위에서는 구조체(Struct)는 값 타입, 클래스(Class)는 참조 타입이라고 구분되어 있는데, 어떻게 다른 건지 코드에서 직접 확인해보도록 하자!
// 구조체와 클래스를 하나씩 선언해두고, 그 안에 가변 프로퍼티를 하나씩 만들어두자.
struct ValueType {
var property: Int = 0
}
class ReferenceType {
var property: Int = 0
}
구조체는 값 타입이기 때문에, 데이터를 전달할 때 값을 복사하여 전달할 것이다.
즉, firstStructInstace라는 이름의 구조체를 secondStructInstace에 대입해서 또 다른 인스턴스를 하나 더 만들어준 후, secondStructInstace 구조체에 담겨있는 프로퍼티 값을 수정한 경우,
firstStructInstace와 secondStructInstace는 값만 복사됐을 뿐, 그 실체는 엄연히 다른 애이기 때문에 secondStructInstace의 프로퍼티 값을 2로 수정하더라도 firstStructInstace의 프로퍼티 값이 함께 수정되지 않게 된다.
let firstStructInstace = ValueType()
let secondStructInstace = firstStructInstace
secondStructInstace.property = 2
print(firstStructInstace.property) // 0 (값의 위치가 복사 됬을뿐, 둘은 다르기 때문에 변경된 값이 반영되지 않는다.)
print(secondStructInstace.property) // 2
반면, 클래스는 참조 타입이기 때문에 데이터를 전달할 때, 값의 메모리 위치를 전달한다.
즉, firstClassInstace와 secondClassInstace는 같은 메모리 위치를 가리키고 있는 완벽히 뿌리까지 같은 애이기 때문에, 여기서는 어느 프로퍼티 값을 수정하더라도, 두 인스턴스의 프로퍼티 값이 모두 바뀌게 되는 것이다.
let firstClassInstace = ReferenceType()
let secondClassInstace = firstClassInstace
secondClassInstace.property = 2
print(firstClassInstace.property) // 2 (두 인스턴스가 가리키는 위치가 같으므로, 변경된 값이 반영됩니다.)
print(secondClassInstace.property) // 2
그러니 만약 특정한 데이터를 묶고자, frame을 만들고자 할 때, struct와 class 중에서 어떤 것을 활용할지 고민하고 있다면,
생성한 인스턴스가 참조가 되어야 하는지, 복사가 되어야 하는지에 따라서 차이를 둘 수 있을 것이다.
만약 복사가 되어야 한다면 구조체(Struct)를, 참조를 해야 한다면 클래스(Class)를 사용할 수 있겠다.
4. 프로퍼티를 조금 더 깊게 공부해보자
프로퍼티(Property)에 대해서도 조금 더 자세하게 알아보자.
프로퍼티는 위에도 언급했다시피, 클래스, 구조체에 구현을 해줘서 타입과 연관된 값들을 표현할 때 사용하는 녀석이다. 프로퍼티는 종류가 많기 때문에, 각 프로퍼티 별로 어떤 특징이 있는지 알아둘 필요가 있다.
일단, 공통적으로 프로퍼티의 값이 변할 수 있으면 가변, 변하지 않으면 불변이라고 앞에 붙이게 된다는 것을 알고 가보자.
우선, 가장 기본적으로 인스턴스 저장 프로퍼티가 있다.
이 프로퍼티는 말 그대로 인스턴스 내부에서 값을 저장하는 데 사용할 수 있는 프로퍼티다.
struct ExampleStruct {
let value: Int
var length: Int
}
두 번째는 타입 자체에서 사용할 수 있는 타입 저장 프로퍼티가 있다.
사용은 간단하게 static이라는 키워드만 붙여주면 된다.
타입 저장 프로퍼티란 위에 인스턴스 저장 프로퍼티와 비교했을 때, 각각의 인스턴스가 아닌 타입 자체에 속하는 프로퍼티이다.
그니깐, 인스턴스의 생성 여부와 관계없이 타입 저장 프로퍼티의 값은 하나일 수밖에 없고, 그 값은 해당 클래스/구조체 타입의 모든 인스턴스가 공통으로 사용하게 되는 것이다.
다시 말해, 모든 인스턴스에서 공용으로 사용할 값을 정의할 때는 타입 저장 프로퍼티를 사용하게 되는 것이다. (전역 변수 같은 개념이다 ^__^)
struct Student {
static var typeDescription: String = "학생"
}
세 번째는 연산 프로퍼티가 있다.
연산 프로퍼티란 말 그대로 특정한 연산을 목적으로 만든 프로퍼티를 의미하고, get(접근자)과 set(생성자) 키워드로 나뉘게 된다.
접근자와 생성자를 사용하려면, 이 값을 저장할 변수가 1개 이상 반드시 있어야 하며, 그 변수는 연산을 통해 값이 수정될 수 있기 때문에 불변(let 상수)이 아니라, 가변(var 변수) 형태로 선언되어야 한다.
접근자(get, getter)는 인스턴스 내, 외부의 값의 연산을 수행해 연산된 결괏값을 return 하는 역할,
생성자(set, setter)는 은닉화된 내부의 프로퍼티 값을 간접적으로 설정하는 역할로 사용된다고 볼 수 있다.
get과 set을 함께 사용하거나, 혹은 get만 사용하는 것이 가능하다. (set혼자만 사용하는 것은 불가능!)
더 자세하게는 아래 코드를 살펴보며 이해해보자.
struct Person {
// 인스턴스 저장 프로퍼티
var name: String = ""
var koreanAge: Int = 22
// 인스턴스 연산 프로퍼티
var westernAge: Int {
get { // get을 사용해서 해당 westernAge를 호출했을 때, 연산값을 반환
return koreanAge - 1
}
set(inputValue) { // set을 설정해서 기존 저장 프로퍼티의 값에 새로운 값을 할당
koreanAge = inputValue + 1
}
}
}
var minjae: Person = Person()
print(minjae.westernAge) // 21 (get 사용)
minjae.westernAge = 25
print(minjae.koreanAge) // 26 (set 사용)
추가로, 프로퍼티를 배운 김에 프로퍼티 감시자까지 배워보려고 한다.
프로퍼티 감시자란 저장 프로퍼티에만 사용 가능한 기능으로,
해당 프로퍼티에 값을 할당하기 전/후(= 프로퍼티의 값이 변경된 직전/직후)에 원하는 동작이 있다면 추가할 수 있는 부분을 의미한다.
뷰 컨트롤러의 생명주기가 will과 did로 나누어졌던 것처럼, 여기서도 프로퍼티가 변경되기 직전은 willSet, 변경된 이후는 didSet이라는 블록으로 사용하게 된다.
struct Money {
var currencyRate: Double = 1100 {
// 저장 프로퍼티가 변경되기 직전에 호출
willSet(newRate) {
print("환율이 \(currencyRate)에서 \(newRate)으로 변경될 예정입니다.")
}
// 저장 프로퍼티가 변경되기 직후에 호출
didSet(oldRate) {
print("환율이 \(oldRate)에서 \(currencyRate)으로 변경되었습니다.")
}
}
}
var moneyInMyPocket: Money = Money()
// 변경되기 전, "환율이 1100에서 1000으로 변경될 예정입니다."가 print됨
moneyInMyPocket.currencyRate = 1000
// 변경된 이후, "환율이 1100에서 1000으로 변경되었습니다."가 print됨
5. 상속(Inheritance)
구조체는 상속이 불가능하고, 클래스는 상속이 가능하다고 했던 것 기억하나 혹쉬.....? 중요한 부분이라 까먹으면 안ㄷ..
이번에는 구조체와 클래스의 상속에 대해 자세하게 배워보도록 하겠다.
부모와 자식 간의 재산 상속이 이뤄지는 일상생활에서의 상속 의미와 마찬가지로, Swift에서는 "클래스의 프로퍼티와 메서드가 부모에서 자식으로 전달되는 것"을 상속이라 부른다.
앞으로,
프로퍼티와 메서드를 전달해주는 클래스를 → 부모 클래스,
프로퍼티와 메서드를 전달받는 클래스를 → 자식 클래스라 통칭해서 부르도록 하겠다.
swift 상속의 특징
1️⃣ 클래스/프로토콜에서만 가능, 열거체/구조체에서는 불가능
2️⃣ 단일 상속만 가능 (단 하나의 클래스만 상속이 가능, 대신 프로토콜은 여러 개 채택 가능)
3️⃣ 상속받은 클래스도 새로운 자식 클래스에게 상속이 가능
근데 가만히 생각해 보면, 왜 굳이 클래스를 별도로 만들어주지 않고 상속을 해주는걸까? 라는 의문이 든다.
이에 대한 대답은 객체지향프로그래밍(OOP)에서의 재사용(reuse)과 확장성(expansion)이라는 특징이자 장점에서 찾을 수 있다.
만약, 우리가 운동종목을 정의해 주는 여러 클래스(야구 클래스, 축구 클래스, 농구 클래스 등..)를 만들고 싶다고 가정해 보자.
이 여러 운동 클래스에 들어갈 프로퍼티와 메서드는 되게 공통적인 부분이 많을 것이다.
예를 들어, '몇 명이서 하는지, 포지션은 무엇이 있는지, 승리와 패배의 규칙 같은 내용들'은 세부적인 내용에 있어서는 차이가 존재하지만, 큰 틀에 있어서는 분명한 공통부분이다.
그래서 우리는 공통된 틀이 있는 클래스를 따로 만들어주고, 이 공통 클래스의 내용을 상속받아 하위 클래스에서 내용을 재정의하는 식으로 운동종목 클래스를 만들어주는 것이다. 이게 상속의 필요성이다. 편리하잖아.
"아니요? Copy & Paste 방식이 더 편한데요?"라고 할 사람들을 위해 추가 설명을 하자면,
"기존 코드를 변경하지 않으면서, 기능을 확장할 수 있도록 설계하라"는 개방 폐쇄 원칙(OCP)에 따라 우리는 상속을 사용하는 것이다.
예시와 같이 클래스가 2개, 3개 있을 때는 큰 문제가 생기지는 않겠지만,
만약 같은 속성을 가진 클래스가 100개, 1000개가 있다고 했을 때는 하나의 변경 사항이 있을 때, 이를 100번 1000번 일일이 다 수정해 줄 수 있을까? 뭐 수정해줄 수는 있겠지만, 되게 비효율적이겠다.
사람이기에 발생할 수 있는 실수, 그리고 유지보수의 용이함 때문에 우리는 코드를 재사용하고 확장할 수 있도록 만드는 것이다.
class Sports {
var players = 0
var description: String {
return "이 종목은 \(players)명이서 진행하는 스포츠입니다."
}
func makeCheers() {
print("💪🏻")
}
}
// Sports 부모 클래스를 상속받은 Baseball 클래스
// Baseball 클래스에는 players, description 프로퍼티와 makeCheers 메서드까지 모두 담겨있음
class Baseball: Sports {
var isBall: Bool = true
}
기본 상속의 개념은 이미 만들어둔 코드를 재사용하거나, 더 확장하는 선이었다.
그런데, 이번에는 다른 문제가 생겼다.
앞선 코드를 "재사용"하되, 내용을 바꿔버리는 "재정의" 수준에서 부모 클래스를 받아오고 싶은 거다.
예를 들어, 응원을 할 때 💪🏻를 사용했던 Sports 클래스의 내용을 축구 클래스에서는 🔈로 바꾸고 싶은 경우.
이럴 때 사용하는 키워드가 override다.
그리고 만약 재정의를 하고 싶은데, 부모 클래스의 내용을 가져오고 싶을 때 사용할 수 있는 키워드는 super다.
재정의할 메서드나 프로퍼티 앞에 키워드를 붙이면 되는 방식이고,
"재정의"하는 것이므로 만약 부모 클래스에서 override를 붙여준 프로퍼티나 메서드가 존재하지 않는 경우에는 에러가 발생하게 될 거다.
class Sports {
var players = 0
var description: String {
return "이 종목은 \(players)명이서 진행하는 스포츠입니다."
}
func makeCheers() {
print("💪🏻")
}
}
// Sports 부모 클래스를 상속받은 Football 클래스
class Football: Sports {
override func makeCheers() {
print("🔈")
}
}
클래스의 기본 내용들은 기본적으로 위와 같이 모두 상속을 통해 재정의하고, 재사용해서 확장하는 게 모두 가능했다.
하지만 이런 특징들을 막아주는 키워드도 있는데, 그것이 바로 final이다.
클래스 자체에 final을 걸어줄 수도 있고, 특정한 프로퍼티나 메서드에 한해 제한을 걸어줄 수도 있다.
⁉️ 굳이 final 키워드를 붙여주는 이유는?
: 런타임 성능이 향상되기 때문이다.
어떻게 런타임 성능이 향상되냐면, Dynamic Dispatch로 실행돼야 하는 클래스가 final을 붙여주면, Static Dispatch로 실행되기에 성능이 향상된다.
⁉️ Dynamic Dispatch와 Static Dispatch가 뭐냐고?
- Dynamic Dispatch : 런타임에 vTable(클래스 내부의 함수 중 어떤 함수를 불러올지 결정하는 (함수 포인터) 테이블.오버라이딩된 함수가 있을 수도 있고, 확장된 함수도 있을 수 있으니 이 중에서 한 함수를 찾는 것이다.)을 찾아 함수를 호출하는 방식
- Static Dispatch : 컴파일할 때, 이미 어떤 함수를 호출할지 결정되어 있는 방식 (구조체는 상속이 불가능하니 이 방식을 사용했고, 그래서 클래스보다 빠르다고 설명한 적이 있었다.)
즉 정리하자면, 함수를 호출할 때 final 키워드가 굳이 어떤 함수를 가져와야 하는지 고민하지 않도록 도와주니까 성능이 좋아진다.
그러니 앞으로 클래스를 선언할 때, 상속이 되지 않는 클래스에는 final을 무작정 붙이면 성능이 향상되는 것을 확인할 수 있겠다!
6. 인스턴스의 생성(init)과 소멸(deinit)
프로퍼티에 대한 기본값을 설정하기가 애매하다면, (각 인스턴스마다 값이 달라져야 한다면) init(생성자) 키워드를 통해 클래스의 인스턴스를 생성할 때, 프로퍼티의 값을 어떻게 처리할지 정할 수 있다.
이때, init(이니셜 라이저) 안에는 모든 프로퍼티에 대한 초기값이 정의가 되어 있어야 한다는 것을 항상 조심해야 한다!
(물론, 프로퍼티가 옵셔널로 선언되어 있으면 정의하지 않아도 된다 ^__^)
class Person {
var name: String
var age: Int
var nickName: String? // 옵셔널로 프로퍼티 선언
// init(생성자)을 활용하여, 클래스의 인스턴스를 생성할 때 프로퍼티의 값을 어떻게 처리할지 정함
init(name: String, age: Int, nickName: String) {
self.name = name
self.age = age
self.nickName = nickName
}
// 옵셔널로 되어 있을 때는 초깃값을 정하지 않아도 좋음
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let minjae: Person = Person(name: "minjae", age: 22)
let minjae: Person = Person(name: "minjae", age: 22, nickName: "미니민")
또는 아래 코드처럼, 특정 조건을 만족하지 않을 경우, 인스턴스를 생성하지 않도록 하는 방법도 있을 수 있다.
class Person2 {
var name: String
var age: Int
var nickName: String? // 옵셔널로 프로퍼티 선언
// 특정 조건을 걸어서, 일정 조건을 충족하지 않으면 인스턴스를 생성하지 않도록 함
init?(name: String, age: Int) {
if (0...120).contains(age) == false {
return nil
}
if name.characters.count == 0 {
return nil
}
self.name = name
self.age = age
}
}
let mini: Person2? = Person(name: "mini", age: 123) // nil
let mini2: Person2? = Person(name: "", age: 22) // nil
생성하는 init(생성자)가 있다면, 반대로, 해제하는 deinit(소멸자)도 존재한다.
이 부분은 인스턴스가 메모리에서 해제되는 시점에 자동으로 호출되는 부분이며, 사용자가 직접 호출할 수는 없다고 한다.
class Person2 {
var name: String
var pet: Pyppy?
var child: Person
init(name: String, child: Person) {
self.name = name
self.child = child
}
// 인스턴스가 메모리에서 해제될 때 호출
deinit {
if let petName = pet?.name {
print("\\(name)가 \\(child.name)에게 \\(petName)를 인도합니다.")
self.pet?.owner = child
}
}
}
이정도 내용을 능숙하게 숙지해서 다룰 수 있다면,
아마 iOS 개발을 할 때, 구조체와 클래스를 능수능란하게 사용할 수 있을 것이라 생각한다!
나 또한, 이미 알고 있는 내용이라고 그냥 넘기지 않고 매일매일 꾸준히 공부하는 개발자가 되겠다고 다짐하면서 오늘 글도 마무리해보겠다 ^__^
'Swift, iOS Foundation' 카테고리의 다른 글
[iOS] iOS 화면을 구성하는 파일, Nib와 Xib 개념 정리해보기 (1) | 2023.10.08 |
---|---|
[Swift] 고차함수 개념 완전 정복하기: map, filter, reduce (0) | 2022.01.24 |
[Swift] IBOutlet Collection이란 무엇일까? (0) | 2022.01.09 |
[Swift] @IBInspectable, @IBDesignable을 사용해보자 (0) | 2021.12.20 |
[Foundation] UserDefaults를 사용해서 데이터를 전달하는 방법 (0) | 2021.12.17 |