@escaping과 @autoclosure

1 답변 글타래를 보이고 있습니다
  • 글쓴이
    • HyunJi
      참가자
      • 글작성 : 9
      • 답글작성 : 10

      @escaping

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

      func callback(fn: () -> Void) {
      fn()
      }
      
      callback {
      print("Closure가 실행되었습니다.")
      }
      

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

      func callback(fn: () -> Void) {
      let f = fn // 클로저를 상수 f에 대입
      f() // 대입된 클로저를 실행
      }
      

      만약 함수를 위와 같이 바꾼다면 오류가 출력된다.
      전달된 클로저를 변수에 대입한 후 실행하는 것이 안된다는 것이다.

      오류의 내용은 Non-escaping 파라미터인 ‘fn’은 오직 직접 호출하는 것만 가능하다는 의미인데, 어떤 뜻일까?
      스위프트에서 함수의 인자값으로 전달되는 클로저는 기본적으로 탈출 불가(non-escaping)의 성격을 가진다.
      이는 해당 클로저를 함수내에서 직접 실행을 위해서만 사용해야 하는 것을 의미하고, 이 때문에 함수 내부여도 변수나 상수에 대입할 수 없다.
      변수나 상수에 대입하는 것을 허용한다면 내부 함수를 통한 캡처 기능을 이용해 클로저가 함수 바깥으로 탈출할 수 있기 때문이다.

      또한, 인자값으로 전달된 클로저는 중첩된 내부 함수에서 사용할 수도 없다.
      내부 함수에서 사용할 수 있도록 허용할 경우, 이 역시 콘텍스트의 캡처를 통해 탈출될 수도 있기 때문이다.

      하지만 클로저를 변수나 상수에 대입하거나 중첩 함수 내부에서 사용해야할 경우도 있다.
      이때 사용되는 것이 @escaping 속성이다.
      이 속성을 클로저에 붙여준다면 해당 클로저는 탈출이 가능한 인자값으로 설정된다.
      (앞의 탈출불가 제약 조건들이 모두 사라진다.)

      func callback(fn: @escaping () -> Void) {
      let f = fn // 클로저를 상수 f에 대입
      f() // 대입된 클로저를 실행
      }
      

      @escaping 속성을 추가하면 오류 발생 없이 실행이 가능하다.
      (@escaping 속성은 인자 값에 설정되는 값이므로, 함수 타입 앞에 넣어주어야 한다.)

      그렇다면 인자값으로 전달되는 클로저의 기본 속성이 탈출 불가로 설정된 이유는 어떤 것일까?

      가장 큰 이점은 컴파일러가 코드를 최적화하는 과정에서의 성능 향상이다.
      또한, 탈출불가 클로저 내에서는 self 키워드를 사용할 수 있다.

      (이 클로저는 해당 함수가 끝나서 리턴되기 전에 호출될 것이 명확하기 때문이다.)

      따라서 클로저 내에서 self에 대한 약한 참조를 사용할 필요가 없다.

      @autoclosure

      @autoclosure 속성은 인자 값으로 전달된 일반 구문이나 함수 등을 클로저로 래핑 하는 역할을 한다.

      이 속성이 붙어 있을 경우, 일반 구문을 인자값으로 넣어도 컴파일러가 알아서 클로저로 만들어서 사용한다.

      이 속성을 적용하면 인자값을 직접 클로저 형식으로 넣어줄 필요가 없기 때문에 {} 형태가 아니라 () 형태로 사용할 수 있다는 장점이 있다.

      func condition(stmt: () -> Bool) {
      if stmt() == true {
      print("결과가 참입니다.")
      } else {
      print("결과가 거짓입니다.")
      }
      }
      
      // 실행 방법 1 : 일반 구문
      condition(stmt: {
      4 > 2
      })
      
      // 실행 방법 2 : 클로저 구문
      condition { () -> Bool in
      return (4 > 2)
      }
      
      condition {
      return (4 > 2)
      }
      
      condition {
      4 > 2
      }
      

      위에서 작성된 실행 방법 1, 2에서 실제 전달하고 싶은 것은 ‘4 > 2’ 구문이다.

      하지만 일반 실행 구문이나 트레일링 클로저 어느 것을 적용해도, 해당 구문을 {} 형태로 감싸 클로저 형태로 만든 후 인자 값으로 전달해야 한다.

      func condition(stmt: @autoclosure () -> Bool) {
      if stmt() == true {
      print("결과가 참입니다.")
      } else {
      print("결과가 거짓입니다.")
      }
      }
      
      // 실행 방법
      condition(stmt: (4 > 2))
      

      하지만 @autoclosure 속성을 적용한다면 위와 같이 간단하게 구문 실행이 가능하다.

      (@autoclosure를 사용한다면 더이상 일반 클로저를 인자 값으로 사용할 수 없기 때문에 실행 방법 1, 2와 같은 방법으로 구문 실행이 불가능하다.)

      또한, @autoclosure 속성을 적용하면 지연된 실행이 가능하다.

      var arrs = [String]()
      
      func addVars(fn: @autoclosure () -> Void) {
      arrs = Array(repeating: "", count: 3)
      fn()
      }
      
      // arrs.insert("KR", at: 1) 오류
      addVars(fn: arrs.insert("KR", at: 1))
      

      arrs.insert(“KR”, at: 1)은 arrs 배열의 두번째 인덱스에 “KR”을 입력하는 것인데 아직 배열의 인덱스가 그만큼 확장되어있지 않기 때문에 오류가 발생한다.

      하지만 addVar(fn:)를 사용해 위의 구문을 적용한다면 오류가 발생하지 않는데, 이것이 지연된 실행이다.

      @autoclosure 속성이 부여된 인자값은 컴파일러에 의해 클로저, 즉 함수로 감싸 지기 때문에 위와 같이 작성해도 addVars(fn:) 함수 실행 전까지는 실행되지 않으며, 해당 구문이 실행될 때에는 이미 배열의 인덱스가 확장된 후이므로 오류도 발생하지 않는다.

      즉, @autoclosure 속성이 인자값에 부여된다면 해당 인자 값은 컴파일러에 의해 클로저로 자동 래핑 된다.

      이 때문에 함수를 실행할 때에는 {} 형식의 클로저가 아닌 () 형식의 일반값을 인자 값으로 사용해야 하며, 인자 값은 코드에 작성된 시점이 아니라 해당 클로저가 실행되는 시점에 맞추어 실행된다.

    • 야곰
      키 마스터
      • 글작성 : 37
      • 답글작성 : 580

      멋진 글이네요!

      질문이 있습니다!
      @autoclosure에 전달하는 코드가 여러줄은 불가능한가요?

      • HyunJi
        참가자
        • 글작성 : 9
        • 답글작성 : 10

        더 공부를 해보았는데요!

        @autoclosure는 전달인자를 갖지 않고 호출되었을 때 자신이 감싸고 있는 코드의 결과값을 반환합니다.
        이 @autoclosure를 쓰는 이유가 문법적 편의를 위해서라고 생각되는데 전달 코드가 여러줄인 것은 가능하지만, 지양하는 방식 아닐까 생각해봤습니다!

        혹시 명확한 답변이 있으시다면 기다리고 있겠습니다! ☺️

        • 야곰
          키 마스터
          • 글작성 : 37
          • 답글작성 : 580

          음…아마 여러줄은 불가한 것으로 알고 있어요.
          오토 클로저는 클로저로 써야 하는데 한줄인 경우가 많다면 그냥 매개변수로 전달할 수 있도록 자동으로 클로저로 변환해 주는 것인데요,
          경우에 따라서 읽거나 이해하기 어려우므로 그냥 클로저를 쓰는 것이 좋을 것 같다는 제 개인적인 의견입니다.
          물론 오토클로저가 있다는 것은 잘 알고 있어야겠죠 🙂

          condition(stmt: @autoclosure () -> Bool) 메서드에서 Bool 타입의 값을 매개변수로 받아오도 되는데 굳이 오토클로저를 사용한 이유는 ‘지연실행’을 해야하기 때문입니다. 이 메서드의 예저로는 지연실행이 가지는 효과에 대해서 명확히 설명하지 못하지만 아래 예제는 조금 설명해볼 수 있겠네요.

          결론은 ‘오토클로저는 클로저를 써야할 곳(대체적으로 지연실행이 필요한 곳)에 한 줄 클로저 대신 간결한 한 줄 코드를 작성할 수 있게 해주지만, 경우에 따라 남용하면 헷갈리거나 읽기 어려운 코드가 될 수 있으므로 꼭 필요한 경우가 아니라면 지양하는 것이 좋겠다.’ 겠네요.

          물론 필요한 경우도 있겠죠? Swift 라이브러리에서는 assert 함수 등에서 사용하고 있습니다. assert에선 왜 오토클로저가 필요할까요? 한 번 생각해보세요 🙂
          이 글에도 좋은 내용이 있네요.

1 답변 글타래를 보이고 있습니다
  • 답변은 로그인 후 가능합니다.

logo landscape small

사업자번호 : 743-81-02195
통신판매업 신고번호 : 제 2022-충북청주-1278 호
고객센터 : 카카오톡채널 @yagom