[Error] Escaping closure captures 'inout' parameter 'list'

오류난 코드는 다음과 같다.

    func callRequest(page: Int = 1, list: inout [TrendsMedia]) {
        let url = "https://api.themoviedb.org/3/trending/all/day?language=en-US&page=\(page)"
        AF.request(url, method: .get, headers: headers).validate().responseJSON { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                for element in json["results"].arrayValue {
                    let id = element["id"].intValue
                    let title = element["title"].stringValue
                    let mediaType = element["media_type"].stringValue
                    let genre = element["genre_ids"][0].intValue
                    let date = element["release_date"].stringValue
                    let overview = element["overview"].stringValue
                    let backdropImage = element["backdrop_path"].stringValue
                    let posterImage = element["poster_path"].stringValue
                    let newMedia = TrendsMedia(id: id, title: title, mediaType: mediaType, genre: genre, date: date, overview: overview, backdropImage: backdropImage, posterImage: posterImage)
                    list.append(newMedia)
                }
            case .failure(let error):
                print(error)
            }
        }
    }

 

 

 

이 오류는 메세지에서도 친절하게 알려 주고 있듯이 inout으로 가져오는 list의 값이 내부의 클로저에서 캡쳐되고 있기 때문에 일어난다.

 

 

오류를 해결하기 위해서는 클로저의 캡쳐 현상에 대해 알아야 하는데, 클로저의 캡쳐 현상이란 간단하다.

 

중첩 함수로 이루어져 있고, 내부 함수 외부에서 계속 사용해야 하는 값이 있는 경우 이후 실행될 때도 문제 없이 실행되기 위해 그 값을 저장하게 되는데, 이 현상을 캡쳐라고 한다. 클로저는 클래스와 같이 참조 타입이기 때문에 함수 / 클로저를 변수에 저장하는 시점에서 해당 값을 캡쳐하게 되는 것이다. ^_^

 

이렇게만 들으면 참 괜찮은 현상 같다. 이 값이 없어도 돌아간다는 뜻이 아닌가?

앞선 말을 들었을 때 어? 근데 이래도 되나?! 싶은 게 정상일 것이다. 왜냠... 나도 치고서 어? 그래도 되나??? 했으니까 ㅋ.ㅋ 

당근 우리가 의도하지 않은 현상이므로 괜찮은 현상은 아니다. 캡쳐 현상이 일어난다면 우리가 의도한대로 결과가 나오지 않을 수 있기 때문이다. (대부분 그렇다.)

 

 

먼저 예시를 보자!

 

 

 

캡쳐 현상이 일어나지 않는, 일반적인 함수의 예시는 다음과 같다.

// 함수 내에서 함수를 실행하고, 값을 리턴하는 일반적인 함수

func calculate(number: Int) -> Int {
    
    var sum = 0
    
    func square(num: Int) -> Int {
        sum += (num * num)
        return sum
    }
    
    let result = square(num: number)
    
    return result
}


calculate(number: 10) // 100
calculate(number: 20) // 400
calculate(number: 30) // 900

 

 

 

하지만, 중첩 함수의 경우에는...

// 중첩 함수로 이루어져 있고, 내부 함수 외부에서 계속 사용해야 하는 값이 있기 때문에 캡쳐 현상이 발생함
// 클로저는 클래스와 같이 참조 타입이기 때문에 함수 / 클로저를 변수에 저장하는 시점에서 해당 값을 캡쳐하게 됨

func calculateFunc() -> ((Int) -> Int) {
    
    var sum = 0
    
    func square(num: Int) -> Int {
        sum += (num * num)
        return sum
    }
    
    return square
}




// 변수에 저장하는 경우(Heap 메모리에 유지)
var squareFunc = calculateFunc()


squareFunc(10) // 100
squareFunc(20) // 400 X => 500
squareFunc(30) // 900 X => 1400



// 변수에 저장하지 않는 경우(Heap메모리에 유지X)

calculateFunc()(10) // 100
calculateFunc()(20) // 400
calculateFunc()(30) // 900

변수를 저장하는 경우 클로저 내부에서 캡쳐 현상이 일어나 heap 메모리에 유지되기 때문에 제대로 된 값이 나오지 않는다.

 

 

 

길고길엇다. 따라서~~~~!!! 이 오류에서는 inout parameter인 list가 closure에서 직접 저장되기 때문에 그런 것이다.

 

아 이래서 여기서 @escaping keyword를 이용한 함수를 사용하는구나~~~~~!!! 싶다

@escaping keyword를 정확히 언제 사용하는지 다양한 어트리뷰트 키워드를 언제 왜 사용하는지에 대해서는 조금 더 공부를 해야겠지만 ㄱ-

아무튼 이런 상황에서는 다른 클로저를 이용해서 함수를 호출할 때 이 함수의 값을 이용해서 어떤 식으로 이용할 건지를? 호출한 곳에서 설정해 주면 좋을 것 같다.

 

 

 

아무튼! 처음의 오류 났던 코드는...

 

 

class TestViewController: UIViewController {

    var list: [TrendsMedia] = []
    override func viewDidLoad() {
        super.viewDidLoad()

        TMDBManager.shared.callRequest { element in
            let id = element["id"].intValue
            let title = element["title"].stringValue
            let mediaType = element["media_type"].stringValue
            let genre = element["genre_ids"][0].intValue
            let date = element["release_date"].stringValue
            let overview = element["overview"].stringValue
            let backdropImage = element["backdrop_path"].stringValue
            let posterImage = element["poster_path"].stringValue
            let newMedia = TrendsMedia(id: id, title: title, mediaType: mediaType, genre: genre, date: date, overview: overview, backdropImage: backdropImage, posterImage: posterImage)
            self.list.append(newMedia)
            print("================")
            print("totalList: \(self.list)")
        }
        
        ... ...
    }
}
    func callRequest(page: Int = 1, completionHandler: @escaping (JSON) -> ()) {
        let url = "https://api.themoviedb.org/3/trending/all/day?language=en-US&page=\(page)"
        AF.request(url, method: .get, headers: headers).validate().responseJSON { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                for element in json["results"].arrayValue {
                    completionHandler(element)
                }
            case .failure(let error):
                print(error)
            }
        }
    }

요런 식으루 나누어 주었다.

근데 지금 고민하고 있는 게 ㅠ_ㅠ 일단 데이터를 따로 모듈에 모아놓고 뷰는 말 그대로 뷰의 기능만 하게 하고 싶어서 callRequest() 친구를 따로 싱글톤 패턴으로 구현해서 나누었던 건데

이러면 그런 의미가 없지 않나?!

 

근데 또 생각해 보면 이거 말고는 어떻게 구현해야 할지 생각이 안 난다......

동료분들께도 함 여쭤보구 멘토님께두 조언을 구해봐야겠음.