본캠프 3주차 금요일 TIL

호씨·2024년 11월 8일
0

1. 옵셔널(Optional)

  • Swift는 기본적으로 nil(값없음)을 허용하지 않지만 개발을 하다 보면
    값이 없는 경우가 생기게 된다.
  • 예를들어 앞에서 만든 Person struct에서 프로퍼티로 소유하고 있는 자동차를 넣고 싶은데 모든 사람이 자동차를 갖고있지는 않다.
    이럴때 사용할 수 있는 것이 Optional 이다.

Optional을 사용하면 값이 없는 상황을 개발할 수 있다.

struct Person {
    var name: String
    var age: Int
    var car: String? // ?를 붙여서 옵셔널 타입이라고 명시하여 값이 없을수도 있다고 알려줄수 있다
        
    func introduce() {
        print("안녕하세요. 제 이름은 \(name)이고, 나이는 \(age)살 입니다.")
    }
}

Swift는 기본적으로 nil 을 허용하지 않지만, Optional를 사용하면 값이 없을 수 있는 상태를 안전하게 처리할 수 있다.

  • nil 키워드는 값이 없음을 의미한다.
  • 값이 없을 수 있는 상태옵셔널이라고 하며, 옵셔널 타입은 nil 값을 저장할 수 있다.
  • 기존 타입 뒤에 ? 키워드를 사용하여 옵셔널 타입으로 선언할 수 있다.
    • 기본 타입 (Int, String, Float … )
    • Collection Type (Array, Dictionary, Set … )
    • Custom Type( struct, class, enum)
    • 익명함수인 클로저
  • 값을 할당할 때는 기존의 타입과 동일하게 사용하면 된다.
  • 옵셔널 타입의 값에 접근하면 Optional로 감싸진 값이 나온다.
    Optional로 래핑된 값 이라고 부른다.

옵셔널 타입 선언 및 사용 방법

  1. 옵셔널 타입으로 선언하는 방법
// 기본 타입 옵셔널로 선언하기
var intValue: Int? 
var stringValue: String?

// Collection Type 옵셔널로 선언하기
var array: [String]? 
var dictionary: [String: String]?

// struct, class, enum 을 타입처럼 사용하므로 Optional로 선언할 수 있다.
struct Person {
	let name: String
}

var optionalPerson: Person?

// 클로저 옵셔널로 선언하기
var closure: (() -> Void)?

옵셔널 타입을 사용하는 방법 (할당과 접근)

// 값을 할당할 때는 기존의 타입과 동일하게 사용하면 된다.
var intValue: Int? = 1

var stringValue: String?
stringValue = "안녕하세요"

var nilValue: String? = nil

struct Person {
	let name: String
}

var optionalPerson: Person? = Person(name: "Brody")

var closure: (() -> Void)? = {
	print("Fire")
}

// 값에 접근하면 Optional로 래핑된 값으로 나온다.
print(intValue) // Optional(1) : 옵셔널로 래핑된 값 1이 출력된다.
print(stringValue) // Optional("안녕하세요") : 옵셔널로 래핑된 값 "안녕하세요"가 출력된다.
print(nilValue) // nil : 값이 없음을 의미하는 nil이 출력된다.

Optional 값 기본 타입과 연산이 불가능하다.

  • 옵셔널 타입기본타입은 다른 타입이기 때문에 연산이 불가능하다.
  • 비교 연산자 사용 가능하다.
var intValue: Int? = 5
intValue += 5 // Error 발생
			  // Value of optional type 'Int?' must be unwrapped to a value of type 'Int'
			  // Int? 타입은 Int 타입과 다르기 때문에 컴파일 오류 발생!
  • 옵셔널과 기본 타입의 차이로 인해 연산이 불가능하다.
  • 옵셔널 값에서 값만 추출하는 언래핑 작업을 통해 기본 타입으로 변환하여 사용해야 한다.

2. 옵셔널 체이닝(Optional Chaining)

옵셔널 체이닝은 옵셔널 타입을 포함하는 복잡한 데이터 구조에서 옵셔널 값이 nil 인지를 간결하게 체크하고 접근할 수 있는 방법이다.

  • 여러 중첩된 프로퍼티나 메소드 호출을 한줄로 처리할 수 있으며, 중간에 nil 이 있는 경우 자동으로 nil 을 반환한다.
  • 옵셔널체이닝은 옵셔널 값에 대해 접근할 때마다 안전하게 처리할 수 있다.
  • 옵셔널 값에 접근할 때 프로퍼티나 메소드 이름 뒤에 ? 를 붙여서 체이닝할 수 있다.
struct Person {
    var name: String = "Default Name"
    var animal: Animal? // 사람 struct은 반려동물이 있을 수도 있고 없을수도 있어요!
}

struct Animal {
    let name: String
    var age: Int? // 언제 태어난지 모른다면 나이를 정할 수 없어서 Optional 타입으로 설정
}

let person: Person? = Person(name: "Ryu", animal: nil)
print(person?.animal?.name) // person은 옵셔널 값이여서 ?를 붙였으며, nil이 아다. 다음으로 넘어간다.
                            // animal?을 확인해보니 nil이여서 nil을 반환한다.
                            // 출력 값 : nil                      

let person2 = Person(name: "Ryu", animal: Animal(name: "Dog", age: 5))
print(person2.animal?.name)  // person2는 옵셔널이 아니기 때문에 ?를 붙이지 않는다.
                             // animal?을 확인하니 값이 있다. 다음으로 넘어갑니다.
                             // name은 값이 있어서 Animal의 "Dog"를 옵셔널 래핑하여 반환한다
                             // 출력 값 : Optional("Dog")
let stringValue: String? = "안녕하세요"

print(stringValue?.count) // Optional(5)
						  // 옵셔널 체이닝으로 연결되어 있기 때문에 Optional로 래핑된 값이 출력된다. 
						  // 옵셔널 값이기 때문에 언래핑하여 사용하면 된다고한다.

3. 옵셔널 언래핑(Optional Unwrapping)

옵셔널 타입은 값이 없을수도 있는 경우를 안전하게 처리하기 위해 사용되지만,
값에 접근하면 Optional(값) 으로 래핑되어 있어서 바로 사용하지 못하는 불편함이 있다.
따라서 이 옵셔널로 래핑된 값에서 옵셔널을 제거하고 값으로 변환하는 과정을
옵셔널 언래핑 이라고 부르며, 몇가지 방법으로 이를 수행할 수 있다.

  • 옵셔널 바인딩
  • 기본값 제공
  • 강제 언래핑
  • 옵셔널 묵시적 언래핑

옵셔널 바인딩 (Optional Binding)

  • 조건문 if , guard 구문을 사용하여 안전하게 옵셔널을 언래핑하는 방법이다.
  • if let
    • if let 을 사용하여 옵셔널 바인딩을 할 수 있다.
    • 값이 있다면 if let 코드블록이 실행되고, 이 블록 안에서 언래핑 된 값을 사용할 수 있다.
    • 값이 없을 때 작업을 하고 싶다면 else 코드블록을 작성하면 된다.
// 값이 있을 때 if let 옵셔널 바인딩 코드
var intValue: Int? = 10

if let intValue = intValue { 
    print(intValue) // 출력 값 : 10
                    // Optional 언래핑되어 실제 값이 출력된다.
                    // 옵셔널 언래핑된값은 해당 블록에서만 사용 가능하다.
}

print(intValue) // Optional(10) 
// 값이 없을 때 if let 옵셔널 바인딩을 사용하여 else에서 작업 진행

var optionalValue: String? // 아무런 값을 주지않아서 nil인 상태
if let optionalValue = optionalValue {
    print(optionalValue) // optionalValue가 nil이 아니면 해당 코드블록이 실행되어 값이 출력된다.
} else {
    print("optionalValue 값은 nil 입니다.") // 출력 값 : optionalValue 값은 nil이다.
}
  • guard let
    • guard let 구문은 옵셔널 바인딩 결과가 nil일 경우,
      해당 코드 블록을 빠져나가게 하여 이후 코드가 실행되지 않도록 한다.
    • 클로저나 메소드 내부에서 주로 사용된다.
    • 코드의 가독성을 높이고, 중첩된 조건문을 피할 수 있다.
// 값이 없는 경우
func guardLetFunction() {
    var optionalValue: String? // 아무런 값을 주지않아서 nil인 상태
    guard let optionalValue = optionalValue else {
        print("guard 실행") // optionalValue가 nil이면 해당 코드블록이 실행된다.
        return // 함수를 종료하여 아래의 코드로 내려가지 못하게 막는다.
    }
    
    print(optionalValue) // 위의 guard let에서 함수가 종료되어 실행되지 못함
}

guardLetFunction()

/* 출력 값
	"guard 실행"
*/
func guardLetFunction() {
    var optionalValue: String? = "Hello"
    guard let optionalValue = optionalValue else {
        print("guard 실행") 
        return // 함수를 종료하여 아래의 코드로 내려가지 못하게 막는다.
    }
    
    print(optionalValue) // Optional이 언래핑된 값 "Hello"가 출력된다.
}

guardLetFunction() 

/* 출력 값
	"Hello"
*/

기본값 제공

  • Nil-결합 연산자(Nil-Coalescing Operator) 방법을 사용하는 방법이다.
  • 옵셔널 값 뒤에 ?? 를 붙인 후 기본값을 제공할 수 있다.
  • 옵셔널 값이 nil이면 ?? 뒤에있는 기본 값을 사용한다.
let name: String? = nil
print(name ?? "Default Name") // 값이 nil이여서 ?? 뒤에 있는 "Default Name"이 출력된다.

let greeting = "안녕하세요 \(name ?? "A")님!"
print(greeting) // "안녕하세요 A님!" 이 호출된다.
                // name이 nil이여서 기본 값 "A"를 사용한다.

옵셔널 강제 언래핑(force unwrapping)

  • 옵셔널 값에 ! 를 붙여서 사용한다.
  • nil이 아니라면 언래핑이 되지만 nil이면 런타임오류를 발생시킨다!
  • nil 이 아님을 확신할 때 사용한다.
    • 지양하는것이 좋다.
var name: String? = "Ryu"
print(name!)  // "Ryu" 출력

name = nil
print(name!) // 런타임 오류 발생

옵셔널 묵시적 언래핑(Implicitly Unwrapped Optional)

  • 옵셔널 타입을 선언할 때 ?가 아닌 !를 사용하면 묵시적 언래핑 되는 옵셔널로 정의할 수 있다.
  • 값을 사용할 때 자동으로 언래핑되어, 별도의 언래핑 작업 없이 직접 사용할 수 있다.
  • 값이 nil 일 때 접근하면 런타임 오류가 발생한다.
  • 값이 있다고 확신이 들 때 사용해야 한다.
  • 옵셔널 바인딩이 가능하다.
var name: String! = "Ryu" // 타입을 String!으로 설정하여 묵시적 옵셔널 언래핑을 사용

print(name) // Optional("Ryu") : 값 자체는 옵셔널
print(name.count) // 5 : 값에서 가져온 값은 옵셔널값이 아닌 일반 값 출력

if let name = name {
    print(name) // "Ryu" 출력
}

4. 중첩된 타입(Nested Type)

중첩된 타입은 하나의 타입 안에 다른 타입을 정의하는 것을 의미한다.

  • 구조적으로 복잡한 클래스나 구조체 등을 더 조직적으로 관리할 수 있다.
  • class, struct, enum 등에서 사용할 수 있다.
  • 중첩된 타입을 사용하면 코드의 가독성을 높이고, 타입 간의 연관성을 명확히 할 수 있다.
  • 타입의 블록 안에서 다른 타입을 정의하고 사용하는 방식으로 구현한다.
struct Car {
    struct Company { // Car 안에 중첩된 Company 구조체
        var name: String
        var phoneNumber: String
        
        func contact() {
            print("\(name) 회사의 A/S 센터 번호는 \(phoneNumber)입니다.")
        }
    }
    
    enum Model {
        case sedan
        case hatchback
        case suv
    }
    
    var model: Model
    var company: Company
    var name: String
    var color: String
}


let myCar = Car(model: .sedan, 
                company: Car.Company(name: "스파르타!", phoneNumber: "000-000-000"),
//                company: .init(name: "스파르타!", phoneNumber: "000-000-000"), // .init을 해서 만들어도 된다.
                name: "붕붕이",
                color: "Black")


myCar.company.contact() // myCar의 company 프로퍼티의 contact 함수를 호출합니다.
												// 결과값 : 스파르타! 회사의 A/S 센터 번호는 000-000-000다.
print(myCar.model) // myCal의 model 프로퍼티를 출력한다.

5. 접근제어자

  • class, struct을 사용하다가 보면 외부에서 특정 데이터에 접근을 제한하고 싶을 때가 있다.
  • 예를들어, Person struct에서 내가 가진돈을 표현하는 havingMoney 프로퍼티를 추가하면 된다.
struct Person {
    var name: String
    var age: Int
    var havingMoney: Int
    
    func introduce() {
        print("안녕하세요. 제 이름은 \(name)이고, 나이는 \(age)살 입니다.")
    }
}

var me = Person(name: "Ryu", age: 28, havingMoney: 10000)
print(me.havingMoney) // Person 외부에서 havingMoney 에 접근이 가능하다.
  • 내가 가진 돈은 나만 알고 싶은데 다른 사람에게 알리고 싶지 않은경우가 있다.
    이럴 때 접근제어자를 사용하면 외부에서 접근을 못하도록 막을 수 있다.
  • 접근제어자는 외부에서 코드에 대한 접근을 제한하는 기능을 제공한다.
    접근을 제어함으로써, 불필요한 정보의 노출을 막을 수 있다.
  • class, enum, struct 등에서 사용 가능하며, 프로퍼티, 메소드 에 적용할 수 있다.

Swift에서 제공하는 접근제어자 종류

open

  • 모든 외부 모듈에서 접근할 수 있는 접근제어자
  • 가장 개방적인 접근 수준
  • 유일하게 class 에서만 사용 가능
    • 상속이 가능하고 재정의 가능하기 때문에!

public

  • 모든 외부 모듈에서 접근할 수 있는 접근제어자
  • 가장 개방적인 접근 수준

internal

  • 기본값 접근제어자로 설정하지 않았다면 internal
  • 동일한 모듈에서 접근 가능

fileprivate

  • 동일한 파일 내부에서만 접근할 수 있는 접근 제어자

private

  • 가장 제한적인 접근 제어자
  • 해당 요소를 선언한 스코프(예를 들어 class, struct, enum의 코드블록)에서만 사용 가능
  • struct 에서 private 프로퍼티가 있다면 멤버와이즈 init을 사용할 수 없어서 직접 init을 작성해야 한다.

모듈이란?

  • 모듈은 코드 배포의 단일 단위.
  • Swift에서는 import를 사용하여 다른 모듈에서 가져올 수 있다.
struct Person {
    var name: String
    public var age: Int
    private var havingMoney: Int
    
    
    init(name: String, age: Int, havingMoney: Int) {
        self.name = name
        self.age = age
        self.havingMoney = havingMoney
    }
    
    private func printMoney() {
        print("나는 \(havingMoney) 원 있다!")
    }

    func test() {
        printMoney()
    }
}


let person = Person(name: "Ryu", age: 20, havingMoney: 3000)

✅ 
person.test() // test 함수는 internal(default) 이여서 호출이 가능하다.
			  // test 함수 안에서는 private printMoney 함수에 접근이 가능하다.
			  // 출력 값 : 나는 3000 원 있다.
person.age // public 이기 때문에 접근 가능하다.

// ❌ 빌드 오류 발생
person.havingMoney // person의 havingMoney는 private 프로퍼티 이기 때문에 접근할 수 없다.
				   // 'havingMoney' is inaccessible due to 'private' protection level

person.printMoney() // person의 printMoney함수는 private 메소드여서 접근할 수 없다.
					// 'printMoney' is inaccessible due to 'private' protection level
  • 힌트에서도 private 프로퍼티, 메소드는 제공하질 않는다.
// 프로퍼티와 메소드뿐만 아니라 struct, class, enum도 동일하게 사용 가능
public struct Person2 {
}

fileprivate class Animal {
}

private enum Season {
}

6. 프로토콜(Protocol)

class, struct, enum 에서 공통으로 구현해야 하는 메소드와 프로퍼티의 청사진을 정의하는 기능.

  • 프로토콜 자체는 기능을 구현하지 않으며, 오직 설계만 제공한다.
  • class, struct, enum 에서 프로토콜을 채택할 수 있으며, 프로토콜에서 정의한 프로퍼티와 메소드를 모두 구현해야 한다.
    • 프로토콜을 채택하는 방법은 타입의 이름 뒤에 : 콜론을 넣은 후 프로토콜 이름을 작성하면 된다.
    • 프로토콜은 여러 개를 채택 할 수 있으며, 프로토콜 이름을 , 로 구분한다.
  • 프로토콜에서 정의된 프로퍼티는 항상 var 로 선언되어야 한다.
  • 프로토콜에서 정의하는 프로퍼티는 읽기 전용 { get } 또는 읽기-쓰기 가능 { get set } 으로 설정할 수 있다.
    • { get } 으로만 설정해도 프로퍼티의 값을 변경할 수 있지만, 명시적으로 작성하면 코드의 의도를 쉽게 파악할 수 있다.
  • 프로토콜에서 정의하는 메소드는 이름,파라미터, 리턴타입만 선언하며, 구현부 { } 는 작성하지 않는다.
  • Swift에서 프로토콜은 다른 언어에서 말하는 인터페이스 개념과 유사하다.

프로토콜 정의 방법

  1. 기본 정의 방법
protocol 프로토콜이름 {
	// 프로퍼티 정의
	// 메소드 정의
}

protocol FullyNamed {
	var fullName: String { get } 
	
	func sayMyFullName() -> String // 구현부는 작성하지 않는다.
}
  1. 프로토콜 채택하여 구현하는 방법
// 1개의 프로토콜 채택

protocol FullyNamed {
	var fullName: String { get } 
	
	func sayMyFullName() -> String // 구현부는 작성하지 않는다.
}

class Person: FullyNamed { // FullyName 프로토콜을 채택합니다.
    var fullName: String  // FullyName 프로토콜에 있는 fullName 프로퍼티를 구현해야 한다.
    
    func sayMyFullName() -> String { // 프로토콜에 있는 메소드를 구현해야 한다.
        return fullName
    }
    
    init(fullName: String) {
        self.fullName = fullName
    }
}

var person = Person(fullName: "Ryu")

print(person.fullName) // "Ryu" 출력
print(person.sayMyFullName()) // "Ryu" 출력
  1. 여러개의 프로토콜 채택 하는 방법
// 여러개의 프로토콜 채택

protocol FullyNamed {
    var fullName: String { get }
    
    func sayMyFullName() -> String
}

protocol ShortNamed {
    var shortName: String { get }
}

class Person: FullyNamed, ShortNamed { // 프로토콜 여러개를 채택하는 클래스다.
    var fullName: String
    
    func sayMyFullName() -> String {
        return fullName
    }
    
    var shortName: String {
        return "ShortName"
    }
    
    init(fullName: String) {
        self.fullName = fullName
    }
}


var person = Person(fullName: "Ryu")

print(person.fullName) // "Ryu" 출력
print(person.sayMyFullName()) // "Ryu" 출력
print(person.shortName) // "ShortName" 출력

클래스 전용 프로토콜 만들기

  • class 전용 프로토콜은 struct, enum에서 사용될 수 없다.
  • 프로토콜 정의 시, AnyObject를 채택하면 클래스 전용 프로토콜로 만들 수 있다.
protocol OnlyClassProtocol: AnyObject {

}

7. 확장(extension)

기존의 class, struct, enum, protocol 타입에 새로운 기능을 추가할 수 있는 키워드다.
타입의 원본 코드를 수정하지 않고도 수평적인 기능을 확장 할 수 있어 코드의 유지 보수와 가독성이 향상된다.

  • extension 키워드를 사용하여 기존 타입을 확장할 수 있다.
  • 하나 이상의 프로토콜을 extension으로 추가해 적용할 수 있다.
    이를 통해 기존 타입을 수정하지 않고 프로토콜 요구사항을 구현할 수 있어 코드 유지보수가 편리해진다.
  • 하나의 타입에 extension 여러 번 가능하다.
  • 확장할 수 있는 것들은 아래와 같다.
    • 연산 프로퍼티
      • 확장된 곳에서 저장 프로퍼티는 사용할 수 없다.
    • 메소드
    • 새로운 초기화 init
    • 중첩된 타입(Nested Type)

사용 방법

  1. 기본 사용 방법
// 기본 사용 방법

struct Person {
	 let name: String
}

// extension 키워드 작성 후 확장시키고 싶은 타입 이름을 명시한다.
extension Person {

}

// 특정 타입의 프로토콜을 확장시키고 싶을 때
extension Person: Equatable {

}
  1. 확장할 수 있는 것들
// 확장할 수 있는 것들

struct Person {
    let lastName: String
    let firstName: String
    let age: Int
}

protocol FullyNamed {
    var fullName: String { get }
    
    func sayMyFullName() -> String
}

// extension에서 연산 프로퍼티를 구현할 수 있다.
extension Person {
    var nameAge: String {
        return "\(firstName)(\(age)세)"
    }
}

// extension에서 메소드를 구현할 수 있다.
extension Person {
    func sayHello() {
        print("\(firstName)님 안녕하세요?")
    }
}

// extension에서 protocol을 채택하여 구현할 수 있다.
extension Person: FullyNamed {
    var fullName: String {
        return "\(lastName)\(firstName)"
    }
    
    func sayMyFullName() -> String {
        return "제 이름은 \(fullName)입니다."
    }
}


let person = Person(lastName: "홍", firstName: "길동", age: 20)

print(person.nameAge) // extension에서 구현한 연산프로퍼티를 사용할 수 있다.
person.sayHello()     // extension에서 구현한 메소드를 호출할 수 있다.
print(person.fullName) // extension에서 구현한 프로토콜을 사용할 수 있다.
print(person.sayMyFullName()) // extension에서 구현한 프로토콜을 사용할 수 있다.

/* 출력 값
길동(20세)
길동님 안녕하세요?
홍길동
제 이름은 홍길동입니다.
*/
profile
이것저것 많이 해보고싶은 사람

0개의 댓글