1. 첫 번째 학습 내용: Step1 PR Review

테스트 코드에서의 강제 언패링

이건 개인적인 취향인데, 저는 테스트 코드에서 캐싱을 위한 프로퍼티는 옵셔널 바인딩을 하지 않고 강제 언래핑을 해요. 굳이 필요하지 않은 코드 같거든요. 이미 setUpWithError메소드에서 매번 프로퍼티에 값을 할당해주고 있는 상황에서 sut_marketItemList가 nil이라면 테스트 자체가 성립이 안되니까요.

실제 프로덕트에서 옵셔널 바인딩을 하는 경우라면 테스트를 해야겠지만, 지금은 프로퍼티로 캐싱을 하는데, 초기화를 할 수 없으니 옵셔널 프로퍼티를 선언한 것이라고 생각했거든요.

setUpWithError가 이니셜라이즈 역할 하는데
각각의 프로퍼티가 어떤 값을 가질지 값을 넣어줌
매번 값을 할당해주는 상황에서
nil이라면 테스트 자체가 성립이 안됨
= nil일 가능성이 없으니까
옵셔널 바인딩 안해줘도 됨

decoder.keyDecodingStrategy

private enum CodingKeys: String, CodingKey {
        case id, title, price, currency, stock, descriptions, thumbnails, images
        case discountedPrice = "discounted_price"
        case registrationDate = "registration_date"
    }

이건 그냥 참고만 하셔도 되는데, 지금 CodingKeys를 써서 snake case를 camel case로 바꾸고 있네요. 이런 경우는 JsonDecoder의 keyDecodingStrategy 값을 convertFromSnakeCase로 줘도 됩니다.
https://developer.apple.com/documentation/foundation/jsondecoder/2949119-keydecodingstrategy?changes=latest_minor

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

참고 자료
Swift ) Key decoding strategy by ZeddiOS

2. 두 번째 학습 내용: 이스케이핑 클로저 (Escaping Closures) ⭐️⭐️⭐️

클로저를 함수의 파라미터로 넣을 수 있는데, 함수 밖(함수가 끝나고)에서 실행되는 클로저 예를 들어, completionHandler로 사용(= 비동기로 실행)되는 클로저는 파라미터 타입 앞에 @escaping이라는 키워드를 명시해야 합니다.

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

위 함수에서 인자로 전달된 completionHandlersomeFunctionWithEscapingClosure 함수가 끝나고 나중에 처리 됩니다. 만약 함수가 끝나고 실행되는 클로저에 @escaping 키워드를 붙이지 않으면 컴파일시 오류가 발생합니다.

@escaping 를 사용하는 클로저에서는 self를 명시적으로 언급해야 합니다.

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()    // 함수 안에서 끝나는 클로저
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // 명시적으로 self를 적어줘야 합니다.
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100

클로저 (Closures) - The Swift Language Guide (한국어)

@escaping과 @autoescape

클로저를 함수나 메소드의 인자값으로 사용할 때에는 용도에 따라 @escaping과 @autoescape 속성을 부여할 수 있습니다.

@escaping

@escaping 속성은 인자값으로 전달된 클로저를 저장해 두었다가, 나중에 다른 곳에서도 실행할 수 있도록 허용해주는 속성입니다.

func callback(fn: () -> Void) {
    fn()
}

callback {
    print("Closure가 실행되었습니다.")
}

정의된 함수 callback(fn:)은 매개변수를 통해 전달된 클로저를 함수 내부에서 실행하는 역할

func callback(fn: () -> Void) {
    let f = fn
    f()
}

에러가 안나는데 ???
꼼꼼한 재은씨 책에서는 Non-escaping parameter 'fn' may only be called라고 에러가 난다는데...🤔

스위프트에서 함수의 인자값으로 전달된 클로저는 기본적으로 탈출불가(non-escape)의 성격을 가짐

  • 해당 클로저를 1. 함수 내에서 2. 직접 실행을 위해서만 사용해야 하는 것을 의미
    ⇒ 함수 내부라 할지라도 변수나 상수에 대입할 수 없음
    Why? 변수나 상수에 대입하는 것을 허용한다면 내부 함수를 통한 캡처(Capture) 기능을 이용하여 클로저가 함수 바깥으로 탈출할 수 있기 때문
    (여기서 말하는 탈출이란, 함수 내부 범위를 벗어나서 실행되는 것을 의미함)
    ⇒ 인자값으로 전달된 클로저는 중첩된 내부 함수에서 사용할 수도 없음
    Why? 내부 함수에서 사용할 수 있도록 허용할 경우, 이 역시 컨텍스트(Context)의 캡처를 통해 탈출될 수 있기 때문

모르겠음....
이해가 안됨 🤔

func callback(fn: () -> Void) {
    func innerCallback() {
        fn()
    }
}

이것도 오류가 나야 한다는데 안남

  • 코드를 작성하다 보면 클로저를 변수나 상수에 대입하거나 중첩 함수 내부에서 사용해야 할 경우도 있는데, 이때 사용되는 것이 @escaping 속성
  • 이 속성을 클로저에 붙여주면, 해당 클로저는 탈출이 가능한 인자값으로 설정됨
  • 이 속성은 인자값에 설정되는 값이므로, 함수 타입 앞에 넣어줘야 함
func callback(fn: @escaping () -> Void) {
    let f = fn // 클로저를 상수 f에 대입
    f() // 대입된 클로저를 실행
}

callback {
    print("Closure가 실행되었습니다.")
}

이제 입력된 클로저는 변수나 상수에 정상적으로 할당될 뿐만 아니라, 중첩된 내부 함수에 사용할 수 있으며, 함수 바깥으로 전달할 수도 있음. 말 그대로 탈출 가능한 클로저가 된 것.

출처

꼼꼼한 재은씨의 Swift 문법편

  • 실행안됨(위) → 실행되는 예시(아래)
import Foundation

class Myclass {
    var a = 10
    
    func callFunc() {
        withEscaping { self.a = 100 }
        withoutEscaping { a = 200 }
    }

    var completionHandlers: [() -> Void] = []

    func withEscaping(completion: @escaping () -> Void) {
        completionHandler.append(completion)
    }

    func withoutEscaping(completion: () -> Void) {
        completion()
    }
}

let mc = MyClass()

mc.callFunc()
print(mc.a)     // 200

mc.completionHandlers.first?()
print(mc.a)    // 100
import Cocoa

class Myclass {
    var a = 10
    var completionHandlers: [() -> Void] = []
    
    func callFunc() {
        withEscaping { self.a = 100 }
        withoutEscaping { a = 200 }
    }

    func withEscaping(completion: @escaping () -> Void) {
        completionHandlers.append(completion)
    }

    func withoutEscaping(completion: () -> Void) {
        completion()
    }
}

let mc = Myclass()

mc.callFunc()
print(mc.a)     // 200

mc.completionHandlers.first?()
print(mc.a)    // 100

James의 closure 특강 👏

이미지를 배열에다가 넣어줘야지
셀 하나하나에 넣어줄 수 있음

파싱한 작업을 메소드 안에서 실행한 다음에
결과물을 외부의 배열에 넣어줘야 함
UIImage타입의 배열에

그냥 closure는 할 수가 없음
메소드 안에서만 존재해야 하니까
(escaping은 갖고 나갈 수 있음)

import Cocoa

var completionHandlers: [() -> Void] = []
// () -> Void 아무것도 반환하지 않는 클로저

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()    // 함수 안에서 끝나는 클로저
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }
        someFunctionWithEscapingClosure { self.x = 100 } // 명시적으로 self를 적어줘야 합니다. (값 캡쳐랑도 연관있음)
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.last?()
// ?는 옵셔널이라서. 값이 있을수도 있고 없고라서 붙임
// ()배열의 마지막 요소를 뜻하는거
// @inlinable public var last: Element? { get } 연산 프로퍼티 찾아보기

print(instance.x)
// Prints "100"

여기서의 핵심은 completionHandlers가 클로저로 이루어진 빈 배열이라는 것!! ⭐️

클로저는 모다? 코드 블럭이다!
그래서 completionHandlers를 프린트문으로 찍어보면 (Function)이라는 이름 없는 코드 블록으로 채워진 배열이 찍히는 걸 알 수 있음 !! ⭐️

참고 자료
Swift Escaping Closure 이해하기
[SWIFT] Escaping Closure(탈출 클로저)

  • 우아한 형제들 iOS Networking 예시코드

    class JokesAPIProvider {
    
        let session: URLSession
        init(session: URLSession = .shared) {
            self.session = session
        }
    
        func fetchRandomJoke(completion: @escaping (Result<Joke, Error>) -> Void) {
            let request = URLRequest(url: JokesAPI.randomJokes.url)
    
            let task: URLSessionDataTask = session
                .dataTask(with: request) { data, urlResponse, error in
                    guard let response = urlResponse as? HTTPURLResponse,
                          (200...399).contains(response.statusCode) else {
                        completion(.failure(error ?? APIError.unknownError))
                        return
                    }
    
                    if let data = data,
                        let jokeResponse = try? JSONDecoder().decode(JokeReponse.self, from: data) {
                        completion(.success(jokeResponse.value))
                        return
                    }
                    completion(.failure(APIError.unknownError))
            }
    
            task.resume()
        }
    }

네트워크 요청 작업이 있고
비동기적으로 이를 처리하고 이 처리가 끝난 후 동작하는 것을 Completion Handler에 명령하는 것.

Q. result가 뭘 가르치는지?
여기서 Result는 completionHandler의 인자값으로 클로저가 들어간 것...?? 🤔

 if let data = data,
		let jokeResponse = try? JSONDecoder().decode(JokeReponse.self, from: data) {
    completion(.success(jokeResponse.value))
    return
}

json 데이타를 성공적으로 decode해오면
completion을 실행하라는 의미?

3. 세 번째 학습 내용: atomic/non atomic

data는 atomic하다
(atomic = 더 이상 쪼갤 수 없음)
⇒ 모든 데이터를 갖고 있어야 한다

Class

NSData

Writing Data Atomically

NSData provides methods for atomically saving their contents to a file, which guarantee that the data is either saved in its entirety, or it fails completely. An atomic write first writes the data to a temporary file and then, only if this write succeeds, moves the temporary file to its final location.

데이터를 작성해야 read할 수 있는데
write할 때 데이터는 임시파일에 해당 내용 작성함
이 작업이 끝까지 성공해야지만
해당 임시 파일을 finalDestination = 저장 경로로 이동해서
사용할 수 있음

jsonParsing을 하다가
만약에 실패를 하면
파싱된 데이터를 못 쓰는 것

근데 json을 파싱하다가
파싱하면서 바로바로 그거를 앱 UI에 적용하려고하면
안됨
why? 데이터를 다 받지를 않았으니까

  1. 데이터를 순서대로 받아야함
  2. (1을 성공 해야지만) 데이터를 딴데로 보내줌

Thanks to 제임스 👍

참고 자료
atomic/non atomic

profile
iOS Developer

0개의 댓글