Naming: Swift API Design Guideline
Naming : 노수진님 블로그
뒤에 Array, Dictionary 같은 타입을 직접 명시하기보다는 -s 접미사를 붙이는 것이 깔끔하다
단점이라면 -s라는 음절 자체가 작다보니 여러 개라는 느낌을 바로 파악하기 어려운 경우가 있을 수 있습니다 (특히 'data'같은 불규칙 복수형)
이 경우 선언부에 타입을 명시해주어 보완한다
let randomNumbers: [Int] = generateRandomNumbers()
타입캐스팅 혹은 어떤 인스턴스를 소스로 다른 타입의 인스턴스를 생성하는 등 대상은 같으나 타입이 변경되는 경우 일반적으로 변수명에 타입을 포함한 차이를 주면 명확성을 향상시킬 수 있을 것입니다
하지만, 아래 예제와 같이 unwrapping만 하는 경우 타입이 변경되기는 하지만 해당 인스턴스의 역할은 유의미하게 바뀌지 않는 경우가 많습니다
이런 경우 오히려 변수명에 차이를 주고자 unwrapped 등의 표현을 넣는 것이 큰 장점을 갖지 않고 오히려 코드의 가독성만 낮추는 단점이 훨씬 부각되므로 동일한 이름을 작성합니다
let foo: Int?
guard let foo = foo else {
return
}
기준1. 프로퍼티/메서드 종류별로 묶어줍니다
(ex. Life cycle메서드끼리, private메서드끼리 등)
기준2. 같은 종류 내에서는 open된 메서드를 우선합니다
(open된 메서드가 호출 시점이 빠르므로 위쪽에 배치하는 것이 읽기 편하다)
기준3. Life cycle 메서드의 경우, 호출 순으로 정렬
기준4. 뷰컨처럼 타입의 정의부가 커지는 경우 extension과 주석을 활용하여 분리
다른 방법으로,
하나의 flow를 따라가는데 있어 여기저기 이동하지 않아도 되도록 서로 연관있는 메서드끼리 묶어주는 방법이 있습니다. 이 방법도 일반적으로 선호되나 하나의 메서드가 여기저기서 재사용될 경우 배치가 모호해지는 단점이 있어 채택하지 않았습니다
//MARK: - Properties
class ViewController: UIViewController {
let apple: String
let banana: String
@IBOutlet weak var appleButton: UIButton!
@IBOutlet weak var bananaButton: UIButton!
}
//MARK: - Life Cycle Methods
extension ViewController {
override func viewDidLoad() {
....
}
override func viewDidAppear() {
....
}
}
//MARK: - Private Methods
extension ViewController {
private func changeLabel() {
....
}
private func changeButton() {
....
}
}
//MARK: - API Methods
extension ViewController {
func setApple() {
...
}
func setBanana() {
...
}
}
스페이스바로 하면 여러 번 입력해야 하므로..
if 문의 경우 항상 줄바꿈을 적용하고, guard 문은 99칸 이상인 경우에만 줄바꿈을 적용합니다
if let foo = foo else {
return
}
guard let foo = foo else { return }
guard let foo = itIsAmazinglyAbsolutelylonglongFunction(onlyFor: foo) else {
throw AmazinglyAbsolutelyLongLongErrorType.LongLongError
}
99칸 이상이 되면 줄바꿈 스타일 적용. 기존 Apple 스타일은 함수 이름이 길면 가독성이 떨어지는 문제가 있어 아래 스타일 채택
func longlongFunction1(
firstArgument: String,
secondArgument: String,
thirdArgument: String
) -> Int {
//한줄공백
....
....
....
}
또한, return line만으로도 99칸이 넘는 경우 아래와 같이 처리합니다
//return line만으로도 99칸이 넘는 경우
func longlongFunction2(firstArgument: String,
secondArgument: String,
thirdArgument: String
) -> NetworkingAPI.ErrorType {
//한줄공백
....
....
....
}
기준1. AppDelegate/SceneDelegate는 최상위 폴더에 그대로 둡니다
기준2. 외부 모듈을 extension하는 파일은 해당 모듈이름으로 된 폴더로 묶습니다
(ex. Int는 Swift / NumberFormatter는 Foundation)
기준3. custom type은 Model/ViewController/Controller/Error로 분류합니다
기준4. 정말 어디 넣을지 모를 파일이 있다면 Meta 폴더로 정리
(ex. Asset 이름 리스트를 정리해놓은 열거형)
타입+Extension 형태로 지정합니다
Int+Extension.swift
String+Extension.swift
self는 파라미터명과 겹치거나 클로저에서 캡처되는 등 반드시 써야하는 경우와 구분하기 위해 self를 사용하지 않아도 되는 경우에는 명시하지 않습니다
특히 메모리 해제를 컨트롤하기 위해 클로저 캡처리스트에 [weak self]
를 명시해줘야 할 곳을 찾을 때, 이렇게 꼭 필요한 부분에만 self를 명시하는 것이 도움이 됩니다
Optional type을 반환하는 새로운 subscript를 구현합니다
다른 방법으로, Collection의 count
프로퍼티와 index를 비교하는 방법이 있습니다
이 방법은 subscript를 새로 구현하는 것보다 코드량이 상대적으로 적다는 장점이 있습니다
하지만, 이 방법은 count와의 비교구문을 항상 기억해야 하므로 실수를 유발하기 쉽습니다
반면, subscript를 새로 구현하면 따로 기억할 필요없이 optional binding이 필요함을 컴파일러가 알려주기에 실수 예방에 유리합니다
// subscript 구현
extension Array where Element == Int {
subscript(safe index: Int) -> Int {
return indices.contains(index) ? self[index] : nil
}
}
// 사용 예시
guard let element = arr[safe: 5] else {
return
}
(Foundation을 제외하고) 최소한의 모듈만 import하도록 합니다
해당 파일에 어떤 타입들이 사용되는지 import 모듈로 대략적인 range를 파악할 수 있게 하기 위함입니다
프레임워크 내 특정 한 모듈만 사용하는 경우, 그 모듈만 import합니다
(ex. import UIKit.NSDataAsset
)
다른 인스턴스와 소통하는 API 메서드를 제외한 모든 프로퍼티/메서드는 private을 부여하도록 노력합니다
해당 block에 대한 정확한 condition 파악이 어려운 문제로 else는 사용하지 않는 것이 좋다.
//수정 전
func goodbyeElse() throws {
if name == "yohan" {
age = 5
} else {
throw HumanError.invalidOwner
}
}
//1차 수정
func goodbyeElse() {
if name == "yohan" {
age = 5
}
throw HumanError.invalidOwner
}
throw 구문의 condition은 좀 더 명확해졌지만 Error Handling이 이렇게 밖에 나와있으면 누군가의 실수로 바로 Error로 직행하게 된다. Defensive Programming 관점에서 이를 고쳐보자
//2차 수정
func goodbyeElse() {
guard name == "yohan" else {
throw HumanError.invalidOwner
}
age = 5
}
Swift의 guard문은 이럴 때 쓰라고 있는거구나
어떤 타입이 외부 타입의 정보를 지나치게 많이 알게 되면 Open-Close Principle을 위반하여 수정이 필요할 가능성이 높아진다.
예로, 뷰컨 내 특정 버튼의 text style을 변경하는 경우를 살펴보자. 이런 코드는 없겠지만 극단적인 예시를 들어보았다
//수정 전
func changeText(viewController: UIViewController) {
viewController.button.titleLabel?.text = "yohanBlessYou"
}
위 코드의 경우, 불필요하게 뷰컨의 정보를 많이 알고 있다. 함수의 기능이 Label의 text를 바꾸는 것뿐임에도 만약 뷰컨이 변경될 경우, 매우 높은 확률로 위 코드도 변경되어야 한다.
외부 타입의 정보를 최소화하여 이를 어느정도 해결할 수 있다
//수정 후
func changeText(label: UILabel) {
label.text = "yohanBlessYou"
}
이를 디미터의 원칙
혹은 최소 지식의 원칙
이라고도 한다
함수의 메인 기능, 메인 알고리즘을 보여주는 곳은 길더라도 본문에 남기고 그 외의 부분은 최대한 그룹핑하여 세분화한다