코드를 이해하고 작성하는 데에, 더 도움을 주고 명확하게 소통하기 위한 MSG의 Swift Style Guide입니다. 코드 리뷰때에서 아래 내용을 바탕으로 신명나게 지적해주세요! 구성원들의 의사결정에 따라 수시로 변경될 수 있습니다.
스타일 가이드를 준수함으로써
- 보다 쉽게 읽고 익숙하지 않은 코드를 이해할 수 있도록 합니다.
- 코드를 보다 쉽게 유지 관리
- 간단한 프로그래머 오류 감소
- 코드 작성 시 인지 부하 감소
간결함이 주 목표가 아닙니다. 코드는 다른 좋은 코드 품질(가독성, 단순성, 명확성 등)이 동일하게 유지되거나 개선될 경우에만 보다 간결하게 만들어야 합니다.
- 해당 스타일 가이드에 없는 내용은 https://swift.org/documentation/api-design-guidelines/ 를 따릅니다.
- 모든 규칙을 구체화하기 위해 노력합니다.
- 예외사항은 거의 두지 않아야 하고, 있더라도 정당성이 높아야합니다.
- MSG Swift Style Guide
- 목표
- Guide
- 목차
- Code Layout
- Basic
- 한 줄은 120자를 넘지 않아야한다.
- 들여쓰기 및 indent는 4개의 space를 사용한다.
- 줄 끝에는 공백을 제거한다.
- MARK 구문은 위와 공백을 추가한다.
- Scope의 끝에는 빈 줄을 제거한다.
- Scope의 시작에는 빈 줄을 제거한다.
- Scope와 Scope 사이에는 공백을 추가한다.
- 들여쓰기 방식은 K&R 스타일을 따른다.
- 공백줄은 1줄까지만 허용한다.
- 공백은 1칸까지만 허용한다.
- 선언부 앞의 주석은 Doc주석으로 작성한다.
- guard의 else는 가독성을 해치지 않거나 120줄을 넘지 않으면 같은 줄에 작성한다.
- namespace는 오직 enum으로만 만든다.
- extension에서 접근제어자를 설정할때는 선언이 아닌 extension에 설정한다.
- FileHeader는 제거한다.
- let, var를 바인딩할 때는 hoist 형태로 바인딩한다.
- 파일의 끝에는 빈 공백줄을 추가한다.
- 파라미터에 Protocol을 받을때는 필요하지 않다면 Generic 대신 some을 사용한다.
- 고차함수를 사용할 때 KeyPath로 접근할 수 있다면 KeyPath를 사용한다.
- 중괄호 주위에 공백을 준다.
- 식별자 바로 뒤에 콜론( : )을 배치하고 그 뒤에 공백을 넣는다.
- 가독성을 위해 함수의 리턴 화살표 양쪽에 공백을 둔다.
- TODO와 FIXME는 Warning을 발생시켜야 한다.
- Functions
- Operators
- Basic
- Naming
- Format
- 타입(struct, class, enum)과 protocol 이름은 PascalCase를 사용하고, 그 외에는 lowerCamelCase 를 사용한다.
- Bool타입은 is, has 와같이 Bool타입임을 분명히하는 접두사를 붙여서 사용합니다.
- 약어로 시작하는 경우 소문자로 표기하고, 그 외의 경우에는 항상 대문자로 표기한다.
- 이름이 모호할 경우 이름 타입에 대한 힌트를 포함한다.
- Action 함수의 네이밍은 '동사 + 목적어'의 형태를 사용한다.
- Delegate method를 만들 때 이름 없는 첫 번째 매개 변수가 delegate source가 되어야 한다.
- 함수 이름 앞에는 되도록 get과 같은 광범위한 의미를 지닌 단어의 사용을 지양한다.
- 뷰(UIView 및 하위 타입)타입 변수의 이름을 지을 때 타입 이름을 뒤에 붙여준다.
- 테스트 이름을 한글로 작성하는 것을 허용한다.
- Format
- Style
- Patterns
- Basic
- 강제 언랩핑 옵션을 사용하지 않고 가능하면 init 타임에 속성을 초기화하는 것을 선호한다.
init()
에서 의미 있는 작업이나 time-intensive 작업을 수행하는 것을 피한다.- scope 시작 부분에 guard 사용을 선호한다.
if
문 보다는guard
문을 사용하여 중첩을 최소화한다.- 여러 종료지점에서 정리 코드가 필요한 경우 defer block을 사용하는 것을 고려한다.
- 접근 제어는 가능한 한 엄격한 수준이어야 한다.
- 가능한 한 전역 함수를 정의하지 않는다.
- public 또는 internal 상수 및 함수를 네임스페이스로 묶고 싶을 때는 case 없는 enum을 사용한다.
- 네임스페이스 없이 전역 상수와 함수를 만들지 않는다. 명료함을 위해서라면 네임스페이스를 제한없이 중첩한다.
- 가능한 한 immutable 값을 선호한다.
- Type method는 기본적으로 static 으로 정의한다.
- 클래스는 기본적으로 final로 정의한다.
- Objects
- Types
- Basic
- File
- Programming Recommendations
예시
- GOOD 👏
// MARK: - Layout
func setLayout() {}
// MARK: Reusable
extension AStore {
...
}
- BAD 👎
// MARK: - Layout
func setLayout() {}
// MARK: Reusable
extension AStore {
...
}
예시
- GOOD 👏
override func viewDidLoad() {
super.viewDidLoad()
}
- BAD 👎
override func viewDidLoad() {
super.viewDidLoad()
}
예시
- GOOD 👏
extension ViewController {
func asdf() {}
}
- BAD 👎
extension ViewController {
func asdf() {}
}
들여쓰기 방식은 K&R 스타일을 따른다.
예시
- GOOD 👏
func asdf() {
let asdf = ""
print(asdf)
}
- BAD 👎
func asdf() {
let asdf = ""
print(asdf)
}
예시
- GOOD 👏
switch case let .foo(a, b):
...
- BAD 👎
switch case .foo(let a, let b):
...
예시
- GOOD 👏
func foo<T: AProtocol>(bar: T) {}
- BAD 👎
func foo(bar: some AProtocol) {}
예시
- GOOD 👏
let foo = bar.map(\.name)
- BAD 👎
let foo = bar.map { $0.name }
예시
- GOOD 👏
foo.filter { true }.map { $0 }
- BAD 👎
foo.filter{true}.map{$0}
예시
- GOOD 👏
let foo: String
struct Foo: Bar {}
- BAD 👎
let foo : String
let bar :String
struct Foo : Bar {}
예시
- GOOD 👏
func foo() -> String {}
- BAD 👎
func foo()->String {}
예시
- GOOD 👏
func foo() {
#warning("TODO: 추가 로직")
}
#warning("FIXME: 파라미터 이름 변경 예정")
func bar(param: String) {}
- BAD 👎
func foo() {
// TODO: 추가 로직
}
// FIXME: 파라미터 이름 변경 예정
func bar(param: String) {}
예시
- GOOD 👏
func reticulateSplines(
spline: [Double],
adjustmentFactor: Double,
translateConstant: Int,
comment: String
) -> Bool {
// reticulate code goes here
}
let actionSheet = UIActionSheet(
title: "정말 계정을 삭제하실 건가요?",
delegate: self,
cancelButtonTitle: "취소",
destructiveButtonTitle: "삭제해주세요"
)
- BAD 👎
func reticulateSplines(spline: [Double], adjustmentFactor: Double, translateConstant: Int, comment: String) -> Bool {
// reticulate code goes here
}
let actionSheet = UIActionSheet(title: "정말 계정을 삭제하실 건가요?", delegate: self, cancelButtonTitle: "취소", destructiveButtonTitle: "삭제해주세요")
예시
- GOOD 👏
UIView.animate(
withDuration: 0.25,
animations: {
// doSomething()
},
completion: { finished in
// doSomething()
}
)
- BAD 👎
UIView.animate(
withDuration: 0.25, animations: {
// doSomething()
},
completion: { finished in
// doSomething()
}
)
- 이 규칙은 범위 연산자에는 적용되지 않는다(예: 1...3) 및 postfix 또는 접두사 연산자 (예: guest? , -1)
- 많은 연산자가 있는 문장을 시각적으로 그룹화하기 위해 공백의 폭을 다양하게 하기 보다는 괄호를 선호한다.
예시
- GOOD 👏
let capacity = 1 + 2
let capacity = currentCapacity ?? 0
let mask = (UIAccessibilityTraitButton | UIAccessibilityTraitSelected)
let capacity = newCapacity
let latitude = region.center.latitude - (region.span.latitudeDelta / 2.0)
- BAD 👎
let capacity = 1+2
let capacity = currentCapacity ?? 0
let mask = (UIAccessibilityTraitButton|UIAccessibilityTraitSelected)
let capacity=newCapacity
let latitude = region.center.latitude - region.span.latitudeDelta/2.0
명명은 기본적으로 Swift API Design Guidelines의 가이드를 따른다. 위 문서의 내용을 여기에 정리할 수 도 있고, 새로운 가이드라인을 추가할 수도 있다.
예외: 동일한 이름의 속성이나 메서드가 더 높은 액세스 수준을 가진 경우, private 속성에 밑줄 접두사를 붙일 수 있다.
예시
- GOOD 👏
protocol SpaceThing {
// ...
}
class SpaceFleet: SpaceThing {
enum Formation {
// ...
}
class Spaceship {
// ...
}
var ships: [Spaceship] = []
static let worldName: String = "Earth"
func addShip(_ ship: Spaceship) {
// ...
}
}
let myFleet = SpaceFleet()
- 이것은 변수가 다른 타입이 아닌 Bool이라는 것을 분명히 한다.
예시
- GOOD 👏
let html: String
let userID: String
class URLConvertible {}
- BAD 👎
let HTML: String
let userId: String
class UrlConvertible {}
예시
- GOOD 👏
let titleText: String
let cancelButton: UIButton
- BAD 👎
let title: String
let cancel: UIButton
- 주어가 명확하다면 생략할 수 있다.
- 대표적인 동사로 Tap은 버튼이 눌린 행위, Gesture는 특정 제스쳐가 발생했을 때의 행위, Scroll은 스크롤이 발생했을 때의 행위이다.
- will
은 특정 행위가 일어나기 직전이고, did는 특정 행위가 일어난 직후다. - should~은 일반적으로 Bool을 리턴하는 함수에 사용한다.
예시
- GOOD 👏
func backButtonDidTap() {
// ...
}
- BAD 👎
func back() {
// ...
}
func tapBack() {
// ...
}
예시
- GOOD 👏
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool
- BAD 👎
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool
- 다른 개발자가 읽었을 때 한 번에 이해할 수 있을 만한 함수의 액션을 나타내는 단어를 사용합니다.
예시
- GOOD 👏
func fetchUserInfo() {
...
}
- BAD 👎
func getUser() {
...
}
- 타입 이름을 줄이지 않는다.
예외:
UICollectionViewCell
,UITableViewCell
의 경우는 너무 길기 때문에 Cell만 활용하는 것을 허용한다.
- 한글로 작성하면 올바른 테스트인지 판단하기가 더 쉽다.
- 튜플의 필드가 3개이상이라면 struct를 사용하는 것을 권장하고 4개라면 필수적으로 struct로 변경한다.
예시
- GOOD 👏
func whatever() -> (width: Int, height: Int) {
return (width: 4, height: 4)
}
let rect = whatever()
rect.width
rect.y
- BAD 👎
func whatever() -> (Int, Int) {
return (4, 4)
}
let rect = whatever()
rect.0
rect.1
예시
- GOOD 👏
if userCount > 0 { ... }
switch someValue { ... }
let evens = userCounts.filter { number in number % 2 == 0 }
let squares = userCounts.map { $0 * $0 }
- BAD 👎
if (userCount > 0) { ... }
switch (someValue) { ... }
let evens = userCounts.filter { (number) in number % 2 == 0 }
let squares = userCounts.map() { $0 * $0 }
- 컴파일 시간에 영향을 주는 주요 원인이므로 지양한다.
예시
- GOOD 👏
let firstName = "김"
let secondName = "이름"
let wholeName = "\(firstName)\(secondName)"
- BAD 👎
let firstName = "김"
let secondName = "티드"
let wholeName = firstName+secondName
- 가독성과 일관성을 위하여 () -> Void로 정의한다.
예시
- GOOD 👏
let completion: () -> Void
- BAD 👎
let completion: () -> ()
let completion: (Void) -> (Void)
- 어떤 매개변수가 사용되고 어떤 매개변수가 사용되지 않는지 명확히 함으로써 closure를 읽을 때 필요한 인지 오버헤드가 감소한다.
예시
- GOOD 👏
someAsyncThing() { _, _, arg3 in
print(arg3)
}
- BAD 👎
someAsyncThing() { arg2, arg2, arg3 in
print(arg3)
}
- 둘 이상이라면 평범하게 사용한다.
예시
- GOOD 👏
UIView.animate(withDuration: 1.0) {
self.myView.alpha = 0
}
UIView.animate(
withDuration: 1.0,
animations: {
self.myView.alpha = 0
},
completion: { finished in
self.myView.removeFromSuperview()
}
)
- BAD 👎
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
})
UIView.animate(
withDuration: 1.0,
animations: {
self.myView.alpha = 0
}
) { f in
self.myView.removeFromSuperview()
}
예외: 단위 테스트에서는 ! 로 선언하는 것이 허용된다.
- !를 사용한 강제 언래핑은 앱 크래시의 가능성이 있기 때문이다.
예시
- GOOD 👏
class MyClass: NSObject {
init() {
someValue = 0
super.init()
}
var someValue: Int
}
- BAD 👎
class MyClass: NSObject {
init() {
super.init()
someValue = 5
}
var someValue: Int!
}
- 데이터베이스 연결 열기, 네트워크 요청, 디스크에서 대량의 데이터 읽기 등의 작업을 수행하지 않는다. 객체를 사용할 준비가 되기 전에 이러한 작업을 수행해야 하는 경우 start() 메서드과 같은 것을 만든다.
- 모든 guard 문을 상단에 모아놓는 것이 비즈니스 로직과 섞일때 code block에 대해 추론하는 것이 더 쉽다.
- 탈출 조건에서 throws나 return 문 등을 실행하거나 옵셔널 바인딩을 해야할 때 guard 문을 사용하면 중첩도를 줄여서 가독성과 유지보수성을 높일 수 있다.
- 그런 행동이 필요하지 않는 한 open보다는 public, fileprivate보다는 private 쪽을 선호한다.
- 타입 정의부안에서 메서드를 정의하는 것을 선호한다.
예시
- GOOD 👏
enum Namespace {
static let constant = ""
}
- BAD 👎
struct Namespace {
static let constant = ""
}
- 새 컬렉션에 추가하는 대신 map 과 compactMap 을 사용한다. 변경 가능한 컬렉션에서 요소를 제거하는 대신 filter를 사용한다.
- 메소드를 재정의가 필요한 경우에 class 키워드를 사용한다.
- 클래스를 재정의해야하는 경우에는 final 키워드를 생략한다.
- 디미터 법칙을 따르는 코드는 메시지 전송자가 수신자의 내부 구현에 결합되지 않는다.
- 디미터 법칙을 위반한 코드를 수정하는 일반적인 방법은
묻지 말고 시켜라
를 따르는 것이다.
- E.g. 속성은 private으로 만들어서 직접 접근하지 않고, 메서드만 사용한다.
- struct는 value sementics, class와 actor는 reference sementics를 가지고 있다.
- 상황에 따라 적절한 타입을 선택한다.
- Nested Type
- Property Wrapper 변수
- 상수
- 변수
- 연산 프로퍼티
- 생성자 및 소멸자
- override 함수
- public 함수
- private 함수
- extension
// Example
final class AViewController: UIViewController {
private enum Metric {}
@IBOutlet weak var asdf: UILabel!
private let constant = ""
private var variable = ""
private var computed: String {
""
}
init() {}
deinit() {}
override func viewDidLoad() { suepr.viewDidLoad() }
public func publicFunction() {}
private func privateFunction() {}
}
extension AViewController {}