💁🏻♂️ iOS 개발자를 위한 면접 준비 - [Swift + RxSwift] 편
🙏🏻 민령이와 함께 정리한 글
💁🏻♂️ 0-1 : 변수와 상수에 대해서 설명해 주세요
💁🏻♂️💁🏻♂️ 참조하는 주소값을 바꿀 수 없다는게 정확히 어떤 의미인가요?
class Some {
var num : Int
init(num: Int) {
self.num = num
}
}
let a = Some(num: 10)
let b = Some(num: 20)
var c = Some(num: 30)
다음과 같은 인스턴스가 있을 때 상수는 값을 변경할 수 없기 때문에 아래와 같은 행위를 할 수 없습니다
a = b
a = c
b = a
b = c
class는 참조타입이기 때문에 각 변수와 상수에는 주소값(==포인터)이 들어있습니다.
즉, 상수로 선언했기 때문에 참조 주소에 대한 포인터를 바꿀 수 없다는 의미로 해석됩니다
아래와 같은 행위는 참조 주소값이 바뀌지 않기 때문에 가능합니다
a.num = b.num
b.num = c.num
그리고 아래와 같은 행위는 c가 참조하고 있는 a가 참조하는 주소값으로 변경합니다
c = a
💁🏻♂️ 0-2 : 타입추론에 대해서 설명해 주세요
💁🏻♂️ 1-1 : Class 와 Struct의 차이를 설명해주세요
값타입과 참조타입의 가장 큰 차이는 '무엇이 전달되느냐' 입니다.
(💡 전달 : 함수의 파라미터나 새로운 변수에 할당하는 것)
값타입을 전달할 때에는 메모리에 전달인자를 위한 인스턴스가 새로 생성됩니다. 즉, 전달되는 값이 복사 됩니다.
참조타입을 전달할 때에는 인스턴스의 참조(주소)가 전달됩니다.
💁🏻♂️💁🏻♂️ 언제 class를 사용하고, 언제 struct를 사용하나요?
Apple 가이드라인에서 다음 조건 중 하나 이상에 해당한다면 구조체를 사용하는 것을 권장합니다
즉, 상속 관계를 필요로 하거나 객체의 값이 계속 가공되는경우에는 class가 더 유리합니다
💁🏻♂️ 1-2 : Swift의 복사 방법에 대해서 설명해 주세요
💁🏻♂️💁🏻♂️ 오버헤드가 발생하는데 COW를 왜 사용하는걸까요?
swift의 COW는 시간과 공간 중에 공간의 효율을 선택했다고 볼 수 있습니다.
실제로 값의 복사만 일어나고 수정이 일어나지 않는 경우도 있기 때문에 불필요한 복사를 줄여 메모리를 절약할 수 있습니다.
📌 리소스 공유
COW의 리소스 공유와 class의 참조는 조금 다른 경향을 가집니다
class 인스턴스를 참조할 때에는 서로 다른 메모리에 '같은 인스턴스의 주소값'을 참조하고 있습니다
COW의 리소스 공유는 동일한 메모리를 가르키고 있습니다
예를들어 b가 a를 복사한 경우에는 수정이 일어나기 전까지는 b는 메모리를 할당받지 않고 a와 같은 메모리를 사용합니다
다시 말하면,
class의 a와 b는 서로 다른 메모리에 할당 되지만 그 메모리에는 같은 '주소값'이 담겨 있습니다
collection type의 a와 b는 서로 같은 메모리를 사용하고 있고 그 메모리에 있는 값은 '데이터'입니다
그리고 데이터가 변경되면 b는 메모리를 할당 받고 같은 데이터를 복사한 후에 데이터가 변경됩니다
(참고 : https://nsios.tistory.com/56)
📌 collection type
Array, Set, Dictionary
💁🏻♂️ 1-3 : class의 성능을 향상 시킬 수 있는 방법을 설명해주세요
📌 Dynamic Dispatch
- class는 기본적으로 상속이 가능하기 때문에 상위 클래스와 하위 클래스 중 어디를 참조해야할지를 런타임 시점에 결정합니다
- 런타임 과정에서 vTable에서 함수를 찾아 메모리 주소를 '읽고' 그 주소로 '점프' 해야 하기 때문에 성능상 손해가 발생합니다
📌 vTable
- 정의 : 디스패치를 지원하기 위해 프로그래밍 언어에서 사용하는 메커니즘
- 클래스 마다 가지고 있는 가상테이블을 말하며 메서드들의 포인터 값을 가지고 있습니다
- 하위 클래스는 상위 클래스의 vTable 복사본을 가지고, 오버라이딩하는 경우에는 오버라이딩 한 메서드를 가리키는 함수 포인터로 저장됩니다
💁🏻♂️💁🏻♂️ Dispatch가 뭔데요?
Dispatch는 어떤 메서드를 호출할 것인지를 결정하여, 그것을 실행하는 메커니즘입니다. Static, Dynamic 두 가지 방식이 있습니다
Static Dispatch는 호출할 함수를 컴파일 타임에 결정합니다. Dynamic Dispatch는 호출할 함수를 런타임에 결정합니다
기본적으로 값타입은 Static Dispatch를 사용하며, 참조타입에서 어떤 Dispatch를 선택하는지는 오버라이딩 가능성에 따라 결정됩니다
그래서 final 키워드를 붙이면 Static Dispatch를 사용하게 되는것 입니다
🤔 WMO안하면 왜 자동으로 final을 못붙여주는걸까?
- 기본적으로 swift는 파일을 하나씩 컴파일 하기 때문에 서로 다른 파일에서 클래스를 상속했는지 컴파일 타임에 알 수 없어서 final을 붙일 수 없습니다
💁🏻♂️ 1-4 : swift의 타입캐스팅에 대해서 설명해주세요
💁🏻♂️💁🏻♂️ 캐스팅하면 타입이 바뀌지 않나요?
💁🏻♂️ 2-1 : 접근 제어자의 종류엔 어떤게 있는지 설명해주세요
💁🏻♂️ 2-2 : open과 public 키워드의 차이를 설명해보세요
💁🏻♂️ 2-3 : fileprivate과 private 키워드의 차이를 설명해보세요
fileprivate은 그 요소가 구현된 소스파일 내부에서 사용할 수 있지만, private은 기능을 정의하고 구현한 범위 내에서만 사용할 수 있습니다.
💁🏻♂️💁🏻♂️ class나 struct를 private으로 만들면 어떻게 될까요?
아래의 코드는 정상 동작합니다.
private class A {
func someFunction(){
print("This is function")
}
}
private let a = A()
a.someFunction()
추측하건데 구현체 자체에 private을 붙이는 경우에는 구현체의 상위인 '파일'을 구현체로 인식하는 것 같습니다.
그래서 동일한 파일 내에서는 A 클래스를 사용할 수 있습니다.
즉, fileprivate 처럼 동작합니다.
그 근거로 상위 접근 수준인 fileprivate 클래스내에 private 클래스의 인스턴스를 만들 수 있습니다.
fileprivate let a = A()
a.someFunction()
💁🏻♂️ 3-1 : Convenience init에 대해 설명해주세요
💁🏻♂️💁🏻♂️ 정말 편의이니셜라이저는 꼭!!! 지정 이니셜라이저를 호출해야할까요?
편의 이니셜라이저는 여러개 존재할 수 있습니다. 편의 init이 다른 편의 init을 호출할 수도 있습니다
하지만 최종적으로 호출되는 편의 이니셜라이저는 꼭! 지정 이니셜라이저를 호출해야 합니다
그렇기 때문에 결론적으로 편의 이니셜라이저를 호출하면 마지막에 지정 이니셜라이저를 호출하게 됩니다
💁🏻♂️💁🏻♂️ 편의 이니셜라이저 꼭 써야 하나요?
아니요! 편의 이니셜라이저는 옵션입니다
차이점이 있다면 지정 이니셜라이저는 self.init을 호출할 수 없다는 것입니다
어떤 클래스에 다음과 같은 저장 프로퍼티와 지정 이니셜라이저가 있을때
var a : Int
var b : Int
var c : Int
init(a: Int, b: Int, c: Int) {
self.a = a
self.b = b
self.c = c
}
아래의 두 이니셜라이저는 똑같이 동작합니다. 그렇기 때문에 아래의 두 이니셜라이저는 동시에 선언할 수 없습니다
init(a: Int) {
self.a = a
self.b = 0
self.c = 0
}
convenience init (a: Int){
self.init(a: a, b: 0, c: 0)
}
💁🏻♂️ 3-2 : required init에 대해서 설명해 주세요
⭐️ convenience init, required init은 **class 이니셜라이저** 입니다!
💁🏻♂️ 3-3 : class의 초기화 진행 과정에 대해서 설명해 주세요
swift의 클래스 초기화는 2단계를 거칩니다
1단계 : 클래스에 정의한 저장 프로퍼티에 초깃값이 할당됩니다
2단계 : 모든 저장 프로퍼티의 초기 상태가 결정되면 사용자 정의할 기회를 가집니다
초기화 단계는 프로퍼티를 초기화하기전에 프로퍼티 값에 접근하는 것을 막아 초기화를 안전하게 할 수 있도록 합니다
컴파일러는 2단계 초기화를 오류 없이 처리하기 위해 네 가지 safety-checks를 합니다
class Perents {
var home : String
var firstName : String
init(home: String, firstName: String) {
self.home = home
self.firstName = firstName
}
}
class Child : Perents {
var lastName : String
var age : Int
❌
init(lastName: String, age: Int) {
super.init(home: "한강뷰", firstName: "김")
self.lastName = lastName
self.age = age
}
⭕️
init(lastName: String, age: Int) {
self.lastName = lastName
self.age = age
super.init(home: "한강뷰", firstName: "김")
}
}
init(lastName: String, age: Int) {
self.lastName = lastName
self.age = age
self.home = "강남" ❌
super.init(home: "한강뷰", firstName: "김")
self.home = "강남" ⭕️
}
convenience init(age: Int){
self.age = 20 ❌
self.home = "한강뷰" ❌
self.init(lastName: "홍길동", age: age)
self.age = 20 ⭕️
self.home = "한강뷰" ⭕️
}
초기화 1단계를 마치기 전까지 이니셜라이저는 메서드를 호출하거나 프로퍼티 값을 읽을 수 없습니다.
init(lastName: String, age: Int) {
sayHello() ❌ //프로퍼티에 값 할당 전이므로 1단계 전임
self.lastName = lastName
self.age = age
super.init(home: "한강뷰", firstName: "김")
sayHello() ⭕️ //나와 상위에 있는 모든 프로퍼티에 값을 할당 했으므로 1단계 마침
}
func sayHello(){
print("hello")
}
💁🏻♂️ 3-4 : deinit은 언제 사용할까요?
- 클래스 인스턴스가 메모리에서 해제될 때 클래스 인스턴스와 관련하여 정리하는 작업이 필요한 경우에 사용합니다
- deinit 키워드를 사용하여 구현하면 메모리에서 해제되는 시점에 자동으로 호출됩니다
- 인스턴스가 해제될 때 가지고 있던 데이터를 어딘가에 보내주거나 저장해야 하는 경우, 인스턴스의 메모리 해제를 확인하고 싶은 경우에 사용할 수 있습니다
💁🏻♂️ 4-1 : Extension에 대해 설명하시오.
- extension은 클래스, 구조체, 열거형, 프로토콜 타입에 새로운 기능을 추가할 수 있습니다
- 기존에 있는 프로퍼티와 메서드를 재정의 할 수 없습니다
- 프로퍼티는 저장 프로퍼티를 제외한 연산 프로퍼티만 정의할 수 있습니다. 메서드는 자유롭게 정의할 수 있습니다
- deinit은 정의할 수 없고 init은 class의 경우에는 편의 이니셜라이저만 추가할 수 있습니다. 값타입은 경우에는 따라 지정 이니셜라이저를 추가할 수 있습니다.
📌 값타입에서 extension을 통해 지정 이니셜라이저를 추가할 수 있는 경우
- 모든 저장 프로퍼티에 기본값이 있어야 합니다
- 타입내에 기본 이니셜라이저와 멤버와이즈 이니셜라이저 외에 사용자 정의 이니셜라이저가 없어야 합니다
💁🏻♂️ 5-1 : Optional에 대해서 설명해주세요
옵셔널은 값이 있을 수도, 없을 수도 있음을 나타내는 스위프트의 특징 중 하나로 안전성을 강조합니다
📌 옵셔널을 사용하는 방법
- 데이터 타입 뒤에 **물음표**를 붙여서 사용할 수 있습니다 (var optinal : String?)
- **Optional**<Wrapped>을 통해 사용할 수 있습니다 (var optional : Optional<String>)
💁🏻♂️💁🏻♂️ 옵셔널을 사용하는 이유가 뭐죠?
💁🏻♂️💁🏻♂️ 옵셔널은 어떻게 구현되어 있나요?
@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
/// 중략
}
💁🏻♂️💁🏻♂️ 옵셔널을 일반 데이터 타입으로 사용하려면 어떻게 해야하나요?
📌 옵셔널 바인딩 값의 사용
- if let은 if문 내부에서만 옵셔널 바인딩한 값을 사용할 수 있습니다
- guard let은 옵셔널 바인딩이 성공 하면 그 이후의 코드에서 변수를 자유롭게 사용할 수 있습니다
📌 ExpressibleByNilLiteral
- 이 프로토콜은 nil로 해당 타입을 초기화 할 수 있는 자격요건을 명시합니다
💁🏻♂️ 5-2 : enum에 대해서 설명해주세요
💁🏻♂️💁🏻♂️ enum의 원시값에 대해서 자세히 설명해 주세요
enum Num : Int{
case zero
case one = 10
case two
case three
}
print(Num.two.rawValue) //11
🤔 원시값은 언제 자동 생성될까?
- 정수값이 들어 있는 경우 위에 선언된 값에서 +1 증가된 값을 생성합니다
- String은 원시값을 지정하지 않은 경우 case이름과 동일한 rawValue를 생성합니다
(detail : https://babbab2.tistory.com/116)
💁🏻♂️💁🏻♂️ 열거형의 연관값에 대해서 자세히 설명해 주세요
enum Apple {
case iPhone(model: String)
case iPad
case mackBook
}
var apple = Apple.iPhone(model: "14pro")
switch apple {
case .iPhone(model: "14pro"):
print("최신형 iPhone")
case .iPhone(model: let model):
print(model)
case .iPad:
print("iPad")
case .mackBook:
print("mackBook")
}
// 최신형 iPhone
💁🏻♂️ 5-3 : Swift의 컬렉션 타입에 대해서 설명해 주세요
📌 Swift의 Array는 버퍼입니다.
필요에 따라 자동으로 버퍼의 크기를 조절해주므로 요소의 삽입 및 삭제가 자유롭습니다.
📌 Hashable 프로토콜
스위프트의 기본 데이터 타입은 모두 Hashable 프로토콜을 채택합니다.
💁🏻♂️💁🏻♂️ Array보다 Set을 사용하는게 더 좋을 때는 언제일까요?
💁🏻♂️ 5-4 : Any와 AnyObject에 대해서 설명해주세요
💁🏻♂️ 6-1 : Closure에 대해 설명해 주세요
💁🏻♂️ 6-2 : Closure 의 값 캡처에 대해 설명해 주세요
var string = "hello"
let closure = { [string] in
string = "hi" // ❌error
print(string)
}
💁🏻♂️💁🏻♂️ 참조하면 메모리 이슈가 있을거 같은데요?
클로저는 참조타입의 값을 캡쳐할 때 strong으로 캡쳐하기 때문에 참조하는 인스턴스의 RC값이 증가합니다.
클래스안에서 클로저를 사용하고, 그 클로저가 클래스의 값을 self로 참조하는 경우에는 해당 인스턴스를 nil처리 하더라도 클로저에 의해 메모리에서 해제되지 않는 강한 순환 참조 문제가 발생할 수 있습니다.
클로저의 강한 순환 참조는 capture list를 사용하여 self를 weak으로 캡쳐하여 RC값을 증가시키지 않도록 할 수 있습니다.
📌 강한 순환 참조
인스턴스가 서로를 강한 참조하는 경우를 강한 순환 참조라고 합니다.
참조 타입 인스턴스 A와 B가 있고, 각각의 인스턴스가 프로퍼티로 B와 A를 가지고 있는 경우를 예로 들겠습니다.
이 경우에는 A,B에 모두 nil을 선언 하더라도 메모리에서 해제되지 않습니다. 서로의 인스턴스를 참조하여 RC값이 남아있기 때문입니다.
하지만 더이상 A,B를 접근할 수 있는 방법이 없어 사용할 수 없습니다. A,B모두 메모리에 존재하지만 사용할 수 없는 상태가 되어 메모리 누수가 발생합니다.
이러한 문제를 강한 순환 참조 문제라고 합니다.
💁🏻♂️💁🏻♂️ self를 weak 말고 unowned로 캡쳐하면 안되나요?
만약 클래스 내에서 클로저가 unowned로 self를 캡쳐하는 경우에는 시점 차이로 인해 해당 인스턴스가 nil이 할당 된 후에도 클로저의 작업이 실행되어야 하는 상황이 생길 수 있습니다.
unowned는 존재하지 않는 메모리 주소를 계속해서 참조하기 때문에 런타임 에러가 발생할 수 있습니다. 그래서 메모리에서 해제된 경우 nil을 반환하는 weak 사용을 권장합니다.
💁🏻♂️💁🏻♂️ Name Closure도 값을 캡쳐하나요?
Name Closure인 경우에는 어디에 위치 하냐에 따라 다릅니다.
전역 함수는 어떠한 값도 캡쳐하지 않습니다.
중첩 함수는 자신을 포함하고 있는 함수의 값을 reference capture 합니다.
💁🏻♂️ 6-3 : Trailing Closure에 대해서 설명해 주세요
💁🏻♂️ 6-4 : Escpaing Closure에 대해 설명해 주세요
💁🏻♂️ 6-5 : Function과 Closure의 차이점을 말해보세요
💁🏻♂️ 7-1 : ARC에 대해서 설명해 주세요
💁🏻♂️💁🏻♂️ 언제 RC값이 증가하고 언제 감소하나요?
💁🏻♂️ 7-2 : 참조는 어떤게 있나요?
💡 strong은 강한참조, weak은 약한참조, unowned는 미소유참조 라고 합니다.
💁🏻♂️💁🏻♂️ weak과 unowned은 어떤 차이가 있나요?
💁🏻♂️💁🏻♂️ 위험한거 같은데 unowned을 사용하는 이유가 뭘까요?
💁🏻♂️💁🏻♂️ strong, weak, unowned reference는 각각 언제 사용할까요?
💁🏻♂️ 7-3 : class가 메모리에서 해제되는 과정을 설명해보세요
💁🏻♂️ 7-3 : COW에 대해서 설명해 주세요
💁🏻♂️💁🏻♂️ COW를 지원하지 않는 value type에서 COW를 사용하고 싶으면 어떻게 구현해야 할까요?
💁🏻♂️ 8-1 : 프로토콜이란 무엇인지 설명해 주세요
📌 다형성
하나의 객체가 다양한 타입을 가질 수 있는 것을 의미합니다.
📌 채택의 제한
프로토콜의 상속 리스트에 class 키워드를 추가하면, class만 채택가능한 프로토콜이 됩니다.
📌 프로퍼티
- 프로퍼티는 항상 var 키워드를 사용해야 합니다.
- 해당 프로토콜을 채택한 후에는 var, let 중 어떤것으로 선언해도 무관합니다.
- 프로퍼티를 { get set }으로 요구한 경우에는 채택한 후에도 let을 사용해서는 안됩니다. set을 할 수 없기 때문입니다.
- 하지만 { get }으로 요구했더라도, 채택한 후에 필요하다면 set을 적용 해도 무관합니다.
💁🏻♂️💁🏻♂️ 프로토콜은 기능 구현을 하지 못하나요?
💡
- 프로토콜은 extension을 통해서만 구현체 작성이 가능합니다.
- 프로토콜간에 상속이 가능합니다.
*💁🏻♂️💁🏻♂️ 프로토콜에서 정의된 기능은 무조건 구현해야 할까요?
@objc optional
키워드를 요구사항 앞에 붙여줘야 합니다.@objc protocol SomeProtocol {
func essential() //프로토콜 채택시 반드시 구현해야함!!
@objc optional func option() //선택적으로 구현해도 됨
}
💁🏻♂️ 8-2 : associatedType이 무엇인지 설명해주세요
associatedType은 프로토콜 내에서 타입을 지정하지 않고, 프로토콜을 채택하여 구현할 때 타입을 지정할 수 있도록 도와줍니다. 프로토콜에서 일종의 제네릭 역할을 합니다.
💁🏻♂️ 8-3 : Codable에 대하여 설명해 주세요
📌 Encodable과 Decodable?
- 사용자 정의 데이터 타입을 다른 형식으로 인코딩(with Encodable)하거나 디코딩(with Decodable)할 수 있는 프로토콜입니다.
- Foundation 프레임워크에 있는 여러 클래스와 호환하면 다양한 기능을 할 수 있습니다.
*💁🏻♂️💁🏻♂️ Codable과 NSCoding의 차이는?
💁🏻♂️ 8-4 : Equatable 프로토콜에 대해서 설명해 주세요
//직접 메서드를 구현해줘야 하는 경우
static func == (lhs: Class, rhs: Class) -> Bool {
return lhs.first == rhs.first && lhs.second == rhs.second
}
💁🏻♂️ 8-5 : Hashable 프로토콜에 대해서 설명해 주세요
*💁🏻♂️💁🏻♂️ Equatable을 왜 채택해야 하나요?
해시값은 고유값이어야 합니다. 그래서 고유값을 식별해 줄 수 있는 == 함수가 필요합니다.
그래서 Equatable을 채택하여 ==함수를 구현해야 합니다.
💁🏻♂️ 9-1 : mutating 키워드에 대해 설명해 주세요
💁🏻♂️ 9-2 : lazy 에 대해 설명해 주세요
💁🏻♂️💁🏻♂️ lazy는 어떤 문제가 있을 수 있나요?
다중 스레드 환경에서 lazy 키워드로 생성한 변수를 여러 스레드가 동시에 접근하는 경우 여러번 초기화되는 문제가 생길 수 있습니다.
💁🏻♂️ 9-3 : typealias 가 무엇인지 말해주세요
typealias Name = String
let name : Name = "김제니"
💁🏻♂️ 9-4 : required 키워드에 대해서 설명해 주세요
💁🏻♂️ 10-1 : Swift의 map, filter, reduce에 대하여 설명해 주세요
💁🏻♂️💁🏻♂️ map의 종류인 flatMap과 compactMap에 대해서 설명해 주세요
💡 swift에서 같은 일차원 옵셔널 배열을 사용할 때, compactMap을 권장합니다.
'flatMap' is deprecated: Please use compactMap(_:) for the case where closure returns an optional value
💁🏻♂️ 10-2 : Protocol Oriented Programming과 Object Oriented Programming의 차이점을 설명하시오.
💁🏻♂️ 11-1 : function과 method의 차이를 말해보세요.
💁🏻♂️ 11-2 : instance 메서드와 class 메서드의 차이점을 설명해주세요.
💁🏻♂️ 11-3 : class 메서드와 static 메서드의 차이점을 설명해주세요.
💁🏻♂️ 11-4 : 함수에서 외부에 있는 값타입 데이터를 바꾸고 싶으면 어떻게 해야 될까요?
&
를 붙여 메모리 주소를 전달한다고 표시 해주어야 합니다.func functionName(_ value : inout String) {
value = "change"
}
var someValue: String = "hello"
functionName(&someValue)
print(someValue) // "change"
💁🏻♂️ 11-5 : defer란 무엇인지 설명해주세요.
💁🏻♂️💁🏻♂️ defer가 여러개일 경우 호출되는 순서는 어떻게 되고, defer가 호출되지 않는 경우는 없을까요?
func say(){
defer {
print("나는 마지막이다!")
}
defer {
print("나는 세번째")
}
print("내가 제일 처음 호출되겠지")
defer {
print("나는 두번째로 호출되겠지")
}
}
say() // print에 적힌 순서대로 출력
💁🏻♂️💁🏻♂️ defer를 언제 사용할까요?
💁🏻♂️ 12-1 : 저장 프로퍼티와 연산 프로퍼티의 차이점을 설명해 주세요.
💁🏻♂️💁🏻♂️ 메서드를 사용하면 되는데 굳이 연산 프로퍼티를 쓰는 이유가 뭘까요?
💁🏻♂️ 12-2 : 지연 저장 프로퍼티에 대해서 설명해 주세요.
💁🏻♂️ 12-3 : property warrper에 대해서 설명해 주세요.
🤔 보일러 플레이트 코드
특정 작업을 수행하기 위해 반복적으로 작성되는 코드를 의미합니다.
//property wrapper 임을 알려주는 어노테이션 입니다.
@propertyWrapper
struct Wrapper {
var value : String
//꼭!! 작성해야 하는 연산 프로퍼티 입니다. 중복되는 코드를 작성합니다.
var wrappedValue: String {
get { "Value : \(value)"}
set { value = newValue }
}
}
struct NotFound {
private var _alert : String = "404 not found"
**var alert : String {
get{ "Alert!! \(_alert)" }
set{ _alert = newValue }
}**
init(_ value : String){
self._alert = value
}
}
struct Forbidden {
private var _alert : String = "403 forbidden"
**var alert : String {
get{ "Alert!! \(_alert)" }
set{ _alert = newValue }
}**
init(_ value : String){
self._alert = value
}
}
@propertyWrapper
struct Alert {
private var _alert : String
var wrappedValue : String {
get{ "Alert!! \(_alert)" }
set{ _alert = newValue }
}
init(wrappedValue alert: String) {
self._alert = alert
}
}
struct NotFound {
@Alert var alert : String = "404 not found"
}
struct Forbidden {
@Alert var alert : String = "403 forbidden"
}
@propertyWrapper
struct UserDefault<T> {
let key : String
let value : T
var wrappedValue : T {
get { UserDefaults.standard.object(forKey: key) as? T ?? self.value }
set { UserDefaults.standard.set(newValue, forKey: key)}
}
}
class UserManager {
@UserDefault(key: "userName", value: nil) //기본값 설정
static var userName : String?
@UserDefault(key: "autoLogin", value: false) //기본값 설정
static var autoLogin : Bool
}
💁🏻♂️💁🏻♂️ 제네릭의 타입 제약
class MyArray {
var array = [Int]()
subscript(index: Int) -> Int {
get {
return array[index]
}
set(newValue) {
array[index] = newValue
}
}
}
var myArray = MyArray()
myArray.array = [1, 2, 3, 4, 5]
let value = myArray[2] // 3
💁🏻♂️💁🏻♂️ String도 서브스크립트로 접근할 수 있나요?
네, String은 Character 컬렉션 타입 입니다. 따라서 특정 문자에 접근하기 위해 인덱스를 사용할 수 있습니다.
class MyClass {
var myString = "Hello, world!"
subscript(index: Int) -> Character {
get {
return myString[myString.index(myString.startIndex, offsetBy: index)]
}
set {
let startIndex = myString.index(myString.startIndex, offsetBy: index)
myString.replaceSubrange(startIndex...startIndex, with: String(newValue))
}
}
}
var myClass = MyClass()
print(myClass[1]) // "e"
myClass[0] = "J"
print(myClass.myString) // "Jello, world!"
💁🏻♂️ 15-1 : Result타입에 대해 설명해 주세요.
성공 혹은 실패에 대한 정보를 담는 타입입니다. 제네릭 열거형으로 구현되어 있습니다.
💁🏻♂️💁🏻♂️ 옵셔널과 차이점?
옵셔널은 값이 없을 때 무조건 nil을 가지고 있지만, Result 타입은 실패했을 때 오류에 대한 정보를 담은 연관 값을 가지고 있습니다.
💁🏻♂️ 15-2 : 스위프트에서 추상 클래스를 만들려면 어떻게 해야할까요?
💁🏻♂️ 15-3 : Self와 self의 차이가 뭘까요?
💁🏻♂️ 15-4 : @objc는 언제 사용하나요?
💁🏻♂️ 15-5 : autoclosure attribute에 대해서 설명해보세요.
함수 인자에 autoclosure 속성을 붙이면, 전달되는 인자 코드를 감싸서 자동으로 클로저로 만들어줍니다.
다시 말해 일반 표현의 코드를 클로저 표현의 코드로 만들어주는 역할을 합니다. 이때 클로저에는 인자가 없고 리턴값만 존재해야 합니다.
클로저 호출에 필요한 중괄호를 없앰으로써 코드의 간결성을 높일 수 있습니다.
Swift에서 @autoclosure를 사용하는 대표적인 예로 assert()함수가 있습니다.
// @autoclosure 를 사용하지 않은 경우
func normalPrint(_ closure: () -> Void) {
closure()
}
// 이 함수를 호출할 때 클로저 부분은 다음과 같이 대괄호 {...}로 묶어서 인자를 넣어야 합니다.
normalPrint({ print("I'm Normal Expression") })
// @autoclosure 를 사용한 경우
func autoClosurePrint(_ closure: @autoclosure () -> Void) {
closure()
}
// @autoclosure를 이용하면 함수에 클로저를 인자로 사용할때 일반 표현을 사용할 수 있습니다.
autoClosurePrint(print("I'm AutoClosure Expression"))
💁🏻♂️ 15-6 : Never 반환 타입에 대해 설명해보세요.
func crashAndBurn() -> Never {
fatalError("Something very, very bad happened")
}
💁🏻♂️ 15-7 : 모나드에 대해서 설명해주세요.
모나드는 값이 선택적으로 존재하는 함수 객체(functor)입니다.
함수 객체(functor) = 값을 담고 있는 컨테이너 타입입니다. 고차 함수 map 을 적용시킬 수 있다는 특징이 있습니다. Swift 에서는 Array, Dictionary, Optional 등이 컨테이너 타입입니다.
Swift 에서 대표적으로 모나드는 Optional 이 있습니다.
Optional 은 값이 선택적으로 존재하고, map 을 적용시킬 수도 있기 때문입니다.
모나드는 함수 객체 안에 포함되는 개념이지만, 함수 객체와의 차이점이 있습니다. 바로 모나드에는 flatMap (옵셔널 해제 기능 추가) 를 사용할 수 있고, 모나드가 아닌 함수 객체에는 flatMap 을 사용할 수 없다는 것입니다.
💁🏻♂️ 1.1 Reactive Programming이 무엇인지 설명해주세요.
Reactive Programming 은 데이터의 흐름에 반응하며 로직을 구성하는 프로그래밍 패러다임입니다.
데이터의 상태변화나, 이벤트에 따른 로직을 작성합니다.
이 때, 데이터의 흐름을 비동기적으로 처리할 수 있기 때문에 비동기 프로그래밍에 좋습니다.
함수형 프로그래밍을 결합해 가독성 좋고 유지보수 좋은 코드를 짜는 데 유용합니다.
💁🏻♂️ 1.2 : RxSwift 를 왜 썼고 장단점이 뭐가 있나요?
함수형 프로그래밍을 지원합니다. map 이나 filter 등의 Operator 를 통해 데이터를 가공하기 좋습니다.
데이터 흐름을 다룰 때 유용한 클래스들이 많습니다. Observable, Subject 등의 클래스를 사용해서 데이터 흐름을 효과적으로 다룰 수 있습니다.
💁🏻♂️ 1.3 : RxSwift 와 Combine 차이를 설명해주세요.
RxSwift 는 애플이 만든 라이브러리가 아닙니다. Rx는 Swift 뿐 아니라 Java, Python 에서도 사용되는 라이브러리입니다. 반면에 Combine 은 애플이 만든 공식 라이브러리입니다.
이로 인해 파생되는 특징은 다음과 같습니다.
RxSwift 를 사용했을 시, Rx 를 사용하는 다른 개발자들과 소통할 수 있습니다. 로직을 통일 시킬 수 있습니다.
Combine 을 사용했을 시, 좀 더 안정적인 효과를 기대할 수 있습니다. 어느날 갑자기 Rx 라이브러리에 문제가 생긴다면 그건 애플의 책임이 아니기 때문입니다.
💁🏻♂️ 1.4 : RxCocoa 를 쓸 때 장점? 어떤 경험을 했나요
// 푸이의 유저 검색 버튼을 눌렀을 때 이벤트 처리
searchUserButton.rx.tap
// throttle 처리
.throttle(.seconds(3), latest: false, scheduler: MainScheduler.instance)
.bind(onNext: { [weak self] in
let searchUserViewController = SearchUserViewController()
self?.navigationController?.pushViewController(searchUserViewController, animated: true)
// 푸이의 피드 무한스크롤
feedTableView.rx.willDisplayCell
.subscribe(on: MainScheduler.instance)
.bind { [weak self] cell, index in
guard let self = self else { return }
// ...
// 테이블 뷰의 마지막 cell 이 다가왔을 때 새로운 피드 패치
if index.row == currentListLen - 1 {
self.viewModel.fetchFeedList()
}
}
💁🏻♂️ 2.0 : RxSwift 의 Observable 이란 무엇인지 설명해주세요.
RxSwift 의 Observable 은 데이터 스트림이며, 이벤트를 방출하는 클래스입니다. 옵저버 패턴의 Publisher 와 같다고 생각할 수 있습니다.
Observable 을 구독하게되면, 이벤트가 발생할때마다 그에 걸맞는 행동을 할 수 있게 됩니다.
next : 일반적인 데이터 or 이벤트 방출
error : 에러 방출, 스트림 종료
complete : 성공적인 이벤트 방출 + 스트림 종료
Observable 은 Disposable 타입을 반환합니다.
let observable = Observable<Int>.create { observer in
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onCompleted()
return Disposables.create()
}
observable.subscribe(onNext: { value in
print(value)
})
💁🏻♂️ 2.1 : RxSwift에서 Hot Observable과 Cold Observable의 차이를 설명하시오.
Hot Observable 은 이벤트가 구독여부와 상관없이 발생하고 있다가, 구독을 한 시점부터 이벤트를 받아볼 수 있는 Observable 입니다.
Cold Observable 은 구독을 시작했을 때 비로소 이벤트를 방출하는 Observable 입니다. RxSwift의 기본 Observable 은 Cold Observable 입니다.
Hot Observable 은 TV 지상파 방송, Cold Observable 은 넷플릭스에 비유할 수 있습니다.
💁🏻♂️ 2.2 : Single, Completable, Maybe의 차이점에 대해 설명하고, 언제 적용하면 좋을지 설명하시오.
먼저 Single, Completable, Maybe 는 모두 Observable 입니다.
Single 은 Success / Error 둘 중 하나만을 오직 한번만 방출합니다. 정확히 한 가지 요소를 한 번만 방출할 때 사용합니다. 예를 들어, 네트워크 요청을 보내고 요청에 대한 응답 한번 받아야할 경우 Single 을 사용할 수 있습니다.
func fetchData() -> Single<Data> {
return Single.create { single in
// ... 네트워크 요청 처리 ...
if let data = responseData {
single(.success(data))
} else {
single(.error(NetworkError.failed))
}
return Disposables.create()
}
}
func sendData() -> Completable {
return Completable.create { completable in
// ... 데이터 전송 처리 ...
if success {
completable(.completed)
} else {
completable(.error(NetworkError.failed))
}
return Disposables.create()
}
}
func readData() -> Maybe<Data> {
return Maybe.create { maybe in
// ... 데이터 읽기 처리 ...
if let data = readData {
maybe(.success(data))
} else {
maybe(.completed)
}
return Disposables.create()
}
}
💁🏻♂️ 3.1 : Disposable 의 개념을 설명해주세요
RxSwift 의 Disposable 은 Observable 의 구독을 해제하는 데 사용되는 개념입니다.
Observable 은 subscribe() 메서드를 통해서 구독을 시작하고, 이 subscribe 메서드는 Disposable 객체를 반환합니다.
Disposable.dispose() 를 하면 구독이 해제됩니다.
let disposable = observable.subscribe(onNext: { value in
print("Received value: \(value)")
})
disposable.dispose() // 구독 취소
💁🏻♂️ 3.2 : DisposeBag 에 대해서 설명해주세요. 언제 메모리를 해제하나요?
DisposeBag 을 사용하면, 구독을 해제할 대상들을 담아 한 번에 dispose 처리할 수 있습니다.
DisposeBag 이 Dispose() 메서드를 호출하거나, DisposeBag 이 메모리에서 해제될 때, DisposeBag 에 포함된 Disposable 객체들이 함께 해제됩니다.
일반적으로 ViewController 나 ViewModel 에서 DisposeBag 을 사용하게 되는데, 해당 객체가 메모리에서 해제될 때 DisposeBag 도 함께 메모리에서 해제되므로 메모리 누수를 방지합니다.
💁🏻♂️ 4.1 : Observable 과 Subject 의 차이를 설명해주세요
Observable 은 Cold Observable 이고, Subject 는 Hot Observable 입니다.
Subject 는 Observer 의 역할과 Observable 의 역할을 모두 할 수 있습니다.
즉, Observable 이기 때문에 이벤트를 발행할 수 있고,
Observer 이기 때문에 이벤트를 수신 받고, 그 이벤트에 맞는 처리를 할 수 있습니다.
다시말하면, Observer 는 어떤 데이터를 보낼 지 미리 정해진 형태의 스트림이지만, Subject 는 Subject 외부에서 새로 데이터를 넣어줄 수도 있고, 구독도 할 수 있는 유연성을 가지고 있습니다.
// 예시 코드 1 : Observable 과 Subject 의 동작 차이
// Observable
let observable = Observable<Int>.create { observer in
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onCompleted()
return Disposables.create()
}
observable.subscribe(onNext: { value in
print(value)
})
// Subject
let subject = PublishSubject<Int>()
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
subject.subscribe(onNext: { value in
print(value)
})
// 예시 코드 2 : 왜 Subject 가 Observer 와 Observable 의 역할을 모두 한다고 하는가.
import RxSwift
// Observable 로서 "initial value" 방출
let subject = BehaviorSubject(value: "initial value")
// 새로운 값을 수동으로 삽입 -> Observer 로서 "new value" 라는 값을 수신했음
subject.onNext("new value")
subject.subscribe(onNext: { value in
print("Received value: \(value)")
})
// 새로운 값을 수동으로 삽입
subject.onNext("another value")
/* 출력 결과
Received value: new value
Received value: another value
*/
💁🏻♂️ 4.2 : Subject의 종류와 차이점에 대해 설명하시오.
PublishSubject 와 BehaviorSubject 에 대해 설명해보겠습니다.
가장 큰 차이는 초기값이 있느냐 없느냐 인데요, PublishSubject 의 경우, 새로운 구독자가 생겼을 때 , 최근에 발생했던 이벤트의 값을 전달하지 않습니다. 반면에 BehaviorSubject 는 새로운 구독자가 생겼을 때, 최근에 발생했던 이벤트의 값을 전달합니다.
https://velog.io/@heyksw/RxSwift-BehaviorRelay-BehaviorSubject
💁🏻♂️ 4.3 : Subject와 Driver의 차이를 설명하시오.
Subject 와 Driver 는 모두 RxCocoa 의 Observable 이지만, 몇가지 차이점이 있습니다.
Subject 는 Observer 와 Observable 의 역할을 모두 수행할 수 있는 클래스입니다.
Observable 처럼 이벤트를 방출할 수 있지만, 언제든지 수동으로 이벤트를 새로 수신 받아 생성할 수 있기 때문에 유연합니다.
Driver 는 UI 이벤트를 처리하기 위해 설계된 Observable 입니다. UI 이벤트는 메인 스레드에서 작동해야 하므로, Driver 는 자동으로 MainScheduler 에서 작동됩니다.
또한 Driver 는 error 이벤트를 방출하지 않으며, 구독이 해제되기 전까지 항상 메모리에 존재합니다.
💁🏻♂️ 4.4 : Subject 와 Relay 의 차이를 설명해주세요.
공통점으로는 둘 다 RxCocoa 에 포함되었고, Observer 의 역할과 Observable 의 역할을 수행할 수 있다는 것입니다.
즉, 이벤트의 발행도 할 수 있고, 이를 구독해 이벤트에 걸맞는 행동 처리를 할 수 있습니다.
차이점 첫번째로는, Subject 는 RxSwift 의 클래스이고, Relay 는 RxCocoa 의 클래스라는 것입니다.
두번째로는, Subject 는 completed 나 error 를 통해서 스트림을 종료시키지만, Relay 는 Dispose 가 되기 전까지 계속 동작합니다.
세번째로는, Subject 의 데이터 스트림 값 출력은 구독을 통해 가능하고, Relay 의 데이터 스트림 값 출력은 .value 를 통해서 가능합니다.
https://velog.io/@heyksw/RxSwift-BehaviorRelay-BehaviorSubject
💁🏻♂️ 4.5 : Driver 와 Relay 의 차이를 설명해주세요.
Driver 는 MainScheduler 에서 행동하는 것을 보장하고, Relay 는 그렇지 않습니다.
따라서 UI 이벤트를 처리할때 Driver 를 사용하는 것이 좀 더 적절하고, Relay 는 비 UI 이벤트에서도 간단하게 처리할 때 사용하기 좋습니다.
https://github.com/JeaSungLEE/iOSInterviewquestions
https://caution-dev.github.io/swift/2019/03/16/iOS-Q&A.html
https://github.com/jeonyeohun/Getting-Ready-For-Interview/blob/main/iOS-Swift/swift.md