제목: Optimizing your Swift Codebase with Attirbutes

편집자의 노트: 우리 커뮤니티 블로그 포스트 시리즈는 buddybuild 유저와 훌륭한 모바일 개발자들에의해 작성되었습니다. 이 포스트는 Jordan Morgan에의해 작성되었는데, Buffer의 iOS 개발자이다.

스위프트 + 속성들
모든 iOS 개발자들이 모두 여기에 있을거라 생각한다: 우리는 그냥 프로그래밍을 고르거나 어떤것이 이색적인 새로운 언어를 배우기 시작했다. 그리고 몇 코드를 만날 수 있다. 우리가 이해할 수 없을 수도 있지만, 이것이 동작한다고 생각한다. 따라서 그것에대해 좋은 확신을 가지고 계속하면 나아지지 않는다.

이 과정은 정확히 내가 어떻게 내 코드베이스에 스위프트 속성을 넣는것을 시작할수 있는지에 관한 이야기이다. 스위프트는 탄탄하고 다양한 속성들을 지원하는데, 깃헙 저장소를 검색해볼때 우리가 모르는 한두개를 볼 수 있을지도 모르겠다. 나의 경험과 비슷하다면 나중에 구글에 적어두면 당신만의 방법이 된다.

왜 스위프트 속성들을 이용할까?
속성은 우리 코드베이스의 코드 질을 희생시키지 않은채 효율성을 증진시키는데 도움을 준다. 그 코드는 더 읽기 쉬워지고, 컴파일하기 쉬워지고, 궁극적으로 유지보수하기 쉽고 사용하기에 더 안전하게 된다.

개인적인 프로젝트에서나 Buffer에서나 효율성은 항상 나의 iOS 개발의 중심에 있다. 이 포스트는 스위프트 개발자로서 내 효율성을 증진시킬 수 있는 몇가지 스위프트 속성들을 열거한다. 안으로 들어가보자...

기초
수많은 정의를 보기 전에, 속성이 정확하게 무엇인지 빠르게 짚고 넘어가야한다고 생각한다. 그리고 매우, 모든 스위프트의 속성은 타입이나 선언에대한 더맣은 정보를 준다. 이 정보는 어떤것이 메모리에서 어떻게 다뤄질지 컴파일러 경고로부터 모든것을 지시할 수 있다.

어떻게 생겼든, 모두 "@" 표시를 앞에 붙인다. 또한, 속성 선언은 파라미터로 감싸진것 안에 인자를 받을 수 있다.

여기 간단한 것이 있다.
@attributeName

//Or with arguments…
@attributeName(arguments)
아래 몇가지 예시를 보자.

속성들
@available(args) 속성
스위프트 속성중에 맥가이버칼은 @available()이다. 강력한만큼 유연하고, 매번 API를 관리하고 배포하는 입장이라면 반드시 필요한 것임을 발견할 수 있다. 이것으로는 API 네이밍이 바뀌었다는 것을 지시할 수 있고, 가능한 플랫폼을 지시하는 등을 할 수 있다.

블로그 포스트를 올리는 은유적인 API로부터 나온 오브젝트를 생각해보자.
class BasicPost {}
우리 API를 쓰는 사람들은 오랫동안 BasicPost 클래스를 즐겨 사용한다. 그러나 기술 블로그 포스트를 표현하는 오브젝트에서 더 원하는 몇몇 요청을 처리한다고 생각해보자. 여러분이 읽고 있는 것과 대체로 같다. 그러니 버전1.2에서 이것을 소개하였다.
class TechnicalPost {}
이제 우리 문서를 완성하기위해서,  이것이 존재한다는것을 알리기위해 @available() 이점을 취함으로서 API 사용자에게 정보를 제공하고 코드는 분별력있게된다.
@available(*, introduced: 1.2)
class TechnicalPost {}
이 특정 속성은 몇몇 이자를 받을 수 있지만, 첫번째 인자는 항상 의도하는 플랫폼을 나타낸다. 나머지 인자들은 그 순서에따라 공급될수 있게 지원한다.

와일드카드의 이점을 얻을 수도 있다. 이 경우, 와일드카드는 첫번째 인자로 들어가있는 별표이다. 이것은 사용하는 모든 플랫폼의 API에서 소통한다. 이 클래스는 버전1.2에서 처음 소개되었다(두번째 인자가 설명하는 바).

깔끔하지만, 꽤 길다. 고맙게도 더 단축된 문법으로 초점을 맞출 수 있다.
@available(iOS 10.0, macOS 10.12)
class TechnicalPost {}
더 낫다! 이 속성에대해 잘 알지 못하더라도, 이제 우리가 만들어낸 API는 이 클래스가 사용될때 명확하게 볼 수 있다.

그러나 그대로 두면 컴파일타임 에러가 나올 것이다. 왜일까?

애플은 새로운 플랫폼을 소개하는 경향이 있는데, 우리는 우리 코드에 이것을 설명해야한다. 그러기위해서 이 코드는 제공된 플랫폼과 다른 잠재적인 미래의 플랫폼에 사용가능하다는 마지막 인자로서 와일드카드를 넣는다.
@available(iOS 10.0, macOS 10.12, *)
class TechnicalPost {}
이 방법은 여러분의 코드가 다음 애플의 계획에대해 이미 준비해 놓았다는 뜻이다. For now though, 애플은 오늘날에 존재하는 각 플랫폼을 표현한 것들을 나열하여 제공하고 있따.
  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension

다음 속성으로 가보기 전에, 다른 공통성을 생각해보자. 최근의 변경에서, 우리 API가 쓰지않게 되었고, 세상에있는 iOS 개발자들은 기술 블로그 포스트를 표현하는 사랑스러운 약간의 JSON으로 제공하도록 책임을 맡게되었다.

이처럼, 원래 클래스는 더이상 필요없게 되었다. 그리고 디프리케이트시킬 시간이 된 것이다.
@available(*, deprecated: 1.3)
class BasicPost {}
한 스위프트에서 모든 플랫폼에서 BasicPost를 디프리케이스 시켜, 와일드카드 인자가 유용해질 수 있게 명확해졌다.

게다가 이것으로 계속하면서 조금씩 리팩토링 하고 싶으면, API 네이밍 변경사항을 알도록 제공해야 할것이다. 내가 애플로부터 발견한 기술의 예의는, ㅈ사용자들이 사용하기 더 쉽게 만들기 위해 사용불가능한 인자와 타입에일리어스로 짝을 지을 수 있다는 것이다.
//From an earlier API version
class BasicPost {}

//From a new API version, where we renamed it for whatever reason
class BaseTechnicalPost {}

@available(*, unavailable, renamed: “BaseTechnicalPost”)
typealias BasicPost = BaseTechnicalPost
개인적으로 이것을 좋아하는데, 효율성 관점에서나 비용적 관점에서, 가장 명확한 코드는 항상 최고의 코드이다.

이 속성은 워닝, 에러 등을 연결하여 보다 트릭적인 메시지를 제공한다(여기서 '트릭적인'은 옛 코드를 인자로 지정하는 것을 의미함).

@discardableResult 속성
성숙하고 물려받은 코드베이스에서 작업하고 있다면, 몇몇 함수가 약간 장황하게 되있는것이 놀랍지 않을 수 있다.

아마 90년대 초반부터 만들어진 기능이 지금까지 계속 추가되고 바뀐 코드를 물려받았으며 기능 가체에 결함이 없었을 것이다. (여기엔 소프트웨어가 켜질때 일어나는 124가지의 중요한 작업들이 있을 수 있다. 데이터베이스 접근, 케스 설정, 프로세스에서 몇몇 사인을 초기화하기 - 시나리오는 끝도 없다)

그 전까지, 당신이 돌아와서 필요한 리팩토링을 할 수 있게 프로덕트 매니저를 설득하고 있다.

그렇다. 그런가?! 그렇다.
let someUnusedVarBecauseIHaveToCallThisOldInsaneFunction = anOldInsaneFunction()
그러나 이상한 커플링 이유를 위해 이 함수를 실행하게 되버려서 clang은 이것들을 더 컴파일할 것이다. 이제 우리는 이것을 위해 보여주기위해 사용하지 않는 변수도 가진다. 아래로 내려가면서 이야기해보자.

필요 없을것같은 함수의 결과를 컴파일러에게 말하여 @discardableResult 속성이 도움이 될 수 있다. 컴파일타임에 경고도 없애준다.
@discardableResult func anOldInsaneFunction() -> String{
   //Bunch of business logic occurs
   return “”
}
이제 불러진 함수를 호출한 저 코드는 과거의 소프트웨어 공학적 실수처럼만 남아있을 것이다. 그러나 에러 없이 되고 있을 것이다.

명확성을 추가하기위해 간단하게 _를 할당하여 좀 더 명확한 상태로 만들 수 있다.
_ = anOldInsaneFunction()
때론 소프트웨어 개발에선 우리가 직접 제어하거나 고칠 수 없는 함수나 아키텍처가 있으며, 이 속성이 그런 상황을 조금이라도 더 낫게 해준다.

@autoclosure 속성
@autoclosure는 여러분의 코드베이스에 손쉽게 명확성을 추가할 수 있는 또다른 속성이다. 이것은 인자로 공급된 클로저를 자동으로 감싼다. 클로저가 아무 인자도 스스로 받으면 이것으로 감싸인 표현식의 실제 값을 반환할 것이다.

보기에 좀 혼란스러울 수 있는데, 한번 만나보면 쉽게 이해된다. 고수준 관점에서, 자동으로 클로저로 만들기위해 표현식을 얻는 능력에대해 이야기하고 있다. 여러분이 만약 프로젝트에 유닛테스트를 넣으려 해보았다면, 이미 이 속성들을 몇번 만나보았을지도 모르겠다.

아래처럼 클래스에 간단한 테스트를 넣고 싶다고 가정해보자.
class Programmer  {
   var pay:Int
   init(withPay pay:Int)
   {
       self.pay = pay
   }

   func applyRaise(by amount:Int)
   {
       self.pay += amount
   }
}

class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000
       let expectedSalaryPostRaise = devsPay + raiseAmount

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(expectedSalaryPostRaise, aDev.pay, "Unexpected salary after raise was applied.")
    }
}
XCAssertEqual의 앞에 두 파라미터는 둘다 클로저인데, 이 클로저는 제네릭 표현식으로 받는다. 함수의 시그니처가 약간 위협적으로 보이지만, 처음 두 파라미터는 @autoclosure의 이점을 취하였다는 것을 인지하자.
func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> T?, _ expression2: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line) where T : Equatable
@autoclosure 속성이 제공되었기 때문에, 함수를 호출하는 것이 꽤 가독성 좋고 우리에게도 쉬워졌다. 클로저를 값처럼 무언가를 간단하게 전달하거나(이전 예제에서 했던 것처럼) 약간의 로직을 더 전달할 수 있고, 각각은 가볍게 사용할 수 있다.
class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
   }
}
첫번째 인자가 제공될때, 클로저라기보단 추가적인 연산처럼 읽힌다.
XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
반대로 @autoclosure 속성이 없을때는 어떻게 생겼을지 보자.
XCTAssertEqual({
     return aDev.pay + raiseAmount
,}, {
   return 75000}
, "Unexpected salary after raise was applied.")
여러분도 볼 수 있듯, (문법대로) 완전한 클로저를 전달한다. 읽고 쓰기에 좀 많은 편이다. 하나가 마지막 인자로 후행 클로저(trailing closure)를 사용할 수 없으면, 복합적인 문제가 될 수 있다.

@autoclosure를 사용하면 그 안에 랩핑된 실제값을 반환한다. 아마 자동으로 파라미터가 클로저가 된다고까지 말할 수 있으므로... @autoclosure이다!

이 코드는 상속적으로도 지연된다. 실제 클로저가 결국 무거운 작업을 하고 있거나 의도치않은 사이트 이팩트를 낳았다면 추가적인 이점이 있다. 제공된 코드는 안에 감싸인 클로저가 되기 전까지 절때 실행되지 않는다.

더 나아가서, 여러분의 최근 iOS 작업물의 어디에서 보았을까? assert()는 어떨까?
struct Programmer
{
   var isSenior:Bool
   var appsShipped:Int
}

let aSeniorDev = Programmer(isSenior: true, appsShipped: 13)

assert(aSeniorDev.isSenior, “This dev isn’t a senior!”)
제공된 첫번째 인자는 @autoclosure를 사용한다. 그렇지 않았다면 이렇게 호출해야 했을 것이다.
assert({
     return aSeniorDev.isSenior
}, {
    return “This dev isn’t a senior!”
})
@autoclosure를 사용하면, 코드는 작성할때 좀 더 쉬워지고, 즐겁게 코드를 읽게 만드는 경험을 제공한다고도 말할 수 있겠다.

그리고 assert() 시그니처가 어떻게 생겼는지 궁금하다면, 이렇게 생겼을 것이다.
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line)
우리는 이 시그니처에서 두가지 파라미터로부터 벗어나면, 여기에 할당된 디폴트 값이 있기 때문에 예제에서 이것들을 생략한다. 여러분도 알다시피 그렇지 않을까?

여러 속성사용하기
각 속성은 그들이 가지고 있는 것으로 도와줄 수 있는 이점을 가지지만, 어떤 시나리오에서는 이것을 같이 사용하는 것도 도움이 된다.

예를들어, @escaping 속성을 보자. 이 @escaping 속성은 클로저에 들어온 것이 보냈던 함수보다 오래 살 수 있게 해준다.
//A property on a view controller
var onFakeCompletions:[()->()] = []

func fakeNetworkOp(_ completion:@escaping ()->())

{
   //Network stuff happens
   //The closure is appended to an external array outside of the function's scope. This implies it could be invoked outside of the function - i.e., it could "escape" it
   onFakeCompletions.append(completion)
}
이것을 고려하여 같은 파라미티터에 @escaping@autoclosure를 둘다 쓸 수 있다. 예제로 인사부(H.R.)를 생각해보자.  예를들어 임금 인상을위해 직급이 "Senio"인 개발자이면서 적어도 세개의 앱을 출시한 개발자가 누구인지 알려주는 인사부(H.R.)를 생각해보자. 이 개발자는 이력적인 이유로 각각의 평가를 계속 추적해야한다(As an example, let's imagine H.R. let us know that any developer who is both a "Senior" in title and has shipped at least three apps is due for a raise, but we also need to keep track of each evaluation for historical purposes).
class Programmer
{
   var previousPayRaiseEvaluations:[()->Bool] = []
   var isSenior:Bool = false
   var appsShipped:Int = 0

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true
aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true

aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1
확실히 속성들을 함께 "채이닝(chaining)"하는 것을 금지하지 않는다. 그리고 그 상황을 위해 호출할때 꽤 매끄럽게 동작한다.

마지막 생각
속성은 항상 내 코드에서 사용에대해 특별히 열정적으로 해왔던 것이다. 나는 명확하고 정확하고 간단한 방법으로 강력하게 무거운것을 들어올리는 아이디어를 좋아한다. 그리고 이것이 정말로 속성이 하는 일이다. 물론 스위프트와 Objective-C 프로젝트 어디에나 있는 @objc 속성같은 알만한 가치가 있는 것들도 있다.

이것을 고려하여, 한 인자는 속성이 nicety 보다는 necessity에 더 가깝게 만들어질 수 있다. 끝으로 개발자로서 이 모든것들은 여러분의 작업 플로우의 최적화에대한 것이다. 여러분의 코드베이스나 다른 곳에 이렇게 만들어라. 속성은 이런 최적화를 달성하기위한 하나의 방법에 불가하다.

앱이 완성되고나면, buddybuild 같은 서비스를 사용하여 여러분의 지속적 통합(CI, Countinuous Integration)과 배포 과정같은 부담되는 일을 자동화할 수 있겠다.

Buffer에서는 이미 우리 개발 작업 플로우를 최적화하기위한 방법을 항상 찾아보고있고, 이것들은 우리를 도와줄 수 있는 방법 중 우리가 찾은 것이다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio