[Swift] Swift API Design Guidelines - 김종찬
이 글의 번역본입니다. 제가 개발하면서 중간중간 참고하려고 적은 글이라 문서 전체가 번역되어있지는 않습니다. 예제는 공식문서의 것도 있고, 개인적으로 틈틈히 찾아서 추가도 하고 있습니다.
Fundamentals(기본 개념)
- 사용 시점에서의 명료성은 가장 중요한 목표입니다.
- 명료성은 간결성보다 더 중요합니다.
- 모든 선언문에 문서화 주석을 작성하세요.(개인별로 의견이 다를 수 있을 것 같네요.)
**만약 당신이 당신의 API의 기능을 간단한 용어로 설명하지 못한다면, 당신은 잘못된 API 설계했을 수 있습니다. **
Naming(네이밍)
Promote Clear Usage(분명한 사용을 촉진하세요)
1. 필요한 단어들을 모두 포함해주세요.
⭕️ Good
extension List { public mutating func remove(at position: Index) -> Element } employees.remove(at: x) // at이 있어서 employees라는 List의 x번째를 제거하라는 것이 명확함.
❌ Bad
employees.remove(x) // 명확하지 않다. x를 제거하라는 건지.. x번째를 제거하라는 건지
2. 불필요한 단어를 생략하세요.
⭕️ Good
allViews.remove(cancelButton)
❌ Bad
//allViews.removeElement(cancelButton) // bad, Element가 굳이 필요없다.
3. 타입대신 역할에 따라 변수, 파라미터, 연관타입을 네이밍하세요.
⭕️ Good
var greeting = "Hello" //good, 인사라는 역할이 잘 드러난다.
❌ Bad
var string = "Hello" //bad, string이 무엇을 뜻하는지 알 수 없다.
⭕️ Good
class ProductionLine { // good // ProductionLine라는 클래스에서 떨어진 제품을 다시 채워주는 restock이라는 메소드를 구현하려고 한다. // 단순히 WidgetFactory 이라는 타입명을 파라미터로 사용해면 얘가 무슨 역할을 하는지 잘 알 수 없다. // 그래서 supplier라는 파라미터를 네이밍해서 "아 얘가 위젯을 공급해주는 공급자구나"라고 명시적으로 표현한다. func restock(from supplier: WidgetFactory) {} }
❌ Bad
func restock(from widgetFactory: WidgetFactory) {} // bad, 의미 불분명
⭕️ Good
protocol ViewController { associatedtype ContentView: View //good, 역할이 분명하게 드러나는 것이 좋다. }
❌ Bad
protocol ViewController { associatedtype ViewType: View //bad, 연관값의 역할이 모호하다. }
✅ Exception
// good // 만약 associated type이 해당 protocol 제약에 강하게 결합되어 있어서 protocol 이름 자체가 역할(role)을 표현하다면, 충돌을 피하기 위해서 protocol 이름의 마지막에 Protocol 을 붙여줄 수 있습니다. protocol Sequence { associatedtype Iterator : IteratorProtocol } protocol IteratorProtocol { ... }
4. 파라미터의 역할을 드러내기 위해 타입의 정보를 보충하세요.
⭕️ Good
final class MyNoti { private var observers: [String: Any] = [:] //good func add(_ observer: Any, forKeyPath keyPath: String) { observers[keyPath] = observer } } // good, // forKeyPath라는 아규먼트 레이블을 붙이는 것이 왜 더 좋은거냐면, observers가 [String: Any] 타입이라 그렇다. // String이 key값이기 때문에, for라고만 표현해놓으면 뭘 넣으라는 건지 도통 읽기만 해서는 알기 어렵다. // 특히 파라미터 타입이 NSObject, Any, AnyObject, 또는 기본 타입(Int 또는 String 같은 타입) 이라면, 타입 정보와 사용하는 시점의 문맥이 의도를 충분히 드러내지 못할 수 있다. noti.add("?", forKeyPath: "아 키패스를 넣으라는 거구나")
❌ Bad
final class MyNoti { private var observers: [String: Any] = [:] //good func add(_ observer: Any, for keyPath: String) { observers[keyPath] = observer } } noti.add("?", for: "for? 뭘 넣으라는거지") // bad, for만으로는 뭘 입력하라는 것인지 알 수가 없다. key값이 String 타입이기 때문에.
Strive for Fluent Usage(능숙한 사용을 위해 노력하세요)
1. 메소드 및 함수이름을 영어 문법에 맞는 형태로 사용하는 것을 선호하세요. 메소드 및 함수를 사용했을 때, 영어 문장처럼 사용되면 좋은 네이밍입니다.
⭕️ Good
x.insert(y, at: z) “x, insert y at z” x.subViews(havingColor: y) “x's subviews having color y” x.capitalizingNouns() “x, capitalizing nouns”
❌ Bad
x.insert(y, position: z) //영어 문장으로 잘 풀어지지 않는다. x.subViews(color: y) //상동 x.nounCapitalize() //상동
✅ Exception
// Exception, 첫번째 또는 두번째 argument 이후에 주요 argument가 아닌 경우에는 유창함이 좀 떨어지는 것이 허용됩니다. // 예를들면, print의 separator, terminator처럼 옵션 같은 느낌의 default value가 있는 것들. AudioUnit.instantiate( with: description, options: [.inProcess], completionHandler: stopProgressBar)
2. factory method의 시작은 “make”로 시작합니다.
//팀마다 create로 하는 경우도 있지만, Swift는 make를 추천합니다.
[1,2,3].makeIterator()
3. initalizer 및 factory method 호출에 대한 첫 번째 Argument는 영어 구절을 구성하지 마세요.
⭕️ Good
//1번에서 영어 문법에 맞는 형태로 네이밍을 하라고 했지만, 이니셜라이져와 팩토리 메소드 호출에 관해서는 예외입니다. //Color는 rgb를 이용해서 컬러값을 만드는 생성자로 아래의 havingRGBValuesRed처럼 구성하면 오히려 이해하기가 어렵다. //또 팩토리 메소드인 makeWidget도 마찬가지이다. gears, spindles이 오히려 더 직관적이라 읽고 이해하기가 쉽다. let foreground = Color(red: 32, green: 64, blue: 128) let newPart = factory.makeWidget(gears: 42, spindles: 14) let ref = Link(target: destination)
❌ Bad
let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128) let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14) let ref = Link(to: destination)
4. side-effect를 고려해서 function과 method의 네이밍을 하세요. side-effect가 없는 것은 명사로, 있는 것은 동사로 읽혀야 합니다. mutating / nonmutating method 의 네이밍을 일관성있게 작성해야 합니다. operation이 동사로 설명되는 경우 mutation에는 동사의 명령형을, nonmutating에는 “ed”, “ing”를 접미사로 붙여서 사용합니다.
x 내부의 값을 변경시킨다 ->
mutating
x 내부의 값을 변경시키지 않고 새로운 결과값을 반환한다 ->nonmutating
Mutating | NonMutating |
---|---|
x.sort() | z = x.sorted() |
x.append(y) | z = x.appending(y) |
⭕️ Good
var unsortedArray: [Int] = [5, 1, 3, 4] // 원본 unsortedArray 값이 바뀌지 않고 새로운 값을 반환한다. -> side-effect가 없다. unsortedArray.sorted() unsortedArray // 원본 unsortedArray 값이 변경된다. -> side-effect가 있다. unsortedArray.sort() unsortedArray // 즉, 접미사 "ed", "ing" 만으로 mutating/nonmutating 을 의미하는지 추측할 수 있습니다. // 접미사가 "ed"인데 mutating 메소드를 구현하면 안되겠죠? API의 일관성을 유지해주어야 합니다. // e.g 3.distance(to: 3) // return Int, 리턴 값이 있으므로 nonmutating, 명사형 단어 distance unsortedArray.append(3) // return Void, 리턴 값이 없으므로 mutating, 동사형 단어 append unsortedArray.appending(100) // 지원되지 않는 함수지만 구현한다면 명사형으로 리턴값이 있게 구현해야 할 것. //print 함수 또한 출력하면 콘솔에 내용이 출력되므로 side-effect가 있다고 판단합니다. //그래서 동사형 단어 print가 사용되었습니다. print("hello")
5. nunmutating인 Bool타입 메소드&속성은 receiver에 대한 주장으로 읽혀야 합니다.
⭕️ Good
// 일단 얘네들은 x, line1을 변경하지 않는 nonmutating의 bool 타입 속성, 메소드이다. // 그렇다면, 얘네들은 receiver(여기서는 x와 line1)에 대한 주장과 같은 느낌으로 읽히면 된다. x.isEmpty //x는 비어있는가? line1.intersects(line2) //line1은 line2에 교차하는가?
6. 능력을 설명하는 프로토콜은 -able, -ible, -ing 를 사용한 접미사로 네이밍 되어야 합니다.
⭕️ Good
Hashable 해시로 생성할 수 있는 유형을 정의하는 프로토콜 Equatable "=="과 같은 연산자를 쓸 수 있게하는 프로토콜 CaseIterable enum을 배열처럼 순회할 수 있게 하는 프로토콜. RawRepresentable struct에서도 rawValue 사용가능 ProgressReporting // 없는거 DBAccessable // 없는거 ...
7. 나머지 types, properties, variables, constants는 명사로 읽혀야 합니다. 1~6 번에 포함되지 않는 애들은 명사로 읽혀야 합니다.
Use Terminology Well(전문용어를 잘 사용하세요)
Term of Art : 명사, 특정 분야 또는 직업 내에서 정확하고 전문화된 의미를 갖는 단어 또는 구.
만약 더 일반적인 단어인 “피부skin”가 의미를 더 잘 전달한다면. “표피epidermis”를 사용하지 마세요.
전문용어를 사용한다면 명확히 확립된 의미를 사용하세요.
- 일반적인 단어보다 전문 용어를 사용하는 유일한 이유는 모호하거나 불분명한 것을 정확하게 표현하기 때문입니다.
- 전문가를 놀라게 하지 마세요 : 우리가 전문 용어에 새로운 의미를 부여하면 그 용어에 익숙한 전문가들은 놀랄 수 있습니다.
- 초보자를 혼란스럽게 하지 마세요 : 그 전문 용어를 배우려는 사람은 아마 웹서치를 할 것입니다. 그 전문 용어를 웹서치했을 때 전통적인 의미와 사용된 전문 용어의 의미가 다르다면 초보자는 혼란스러울 수 있습니다.
약어(줄임말, abbreviations)을 피하세요. 정형화 되어 있지 않은 약어를 이해하려면, 다시 풀어서 해석해야하기 때문에 전문 용어(terms of art, 아는 사람만 아는 것)라고 볼 수 있다.
사용하는 약어에 의도된 의미는 웹 사이트에서 쉽게 찾을 수 있습니다.
- 관례를 받아들이세요. 초보자를 위해 기존 문화와 다른 언어를 사용하면서까지 배려하지 마세요.
연속된 데이터 구조를 표현할 때, 비록 초심자가 List 를 쉽게 이해한다고 해도 List보다 Array로 용어를 사용하는게 낫습니다. Array는 현대 컴퓨팅의 기초기 때문에 모든 프로그래머들이 알고 있거나 곧 알게될 것입니다. 대부분의 프로그래머가 친숙한 용어를 사용하세요. 그러면 그들이 웹 검색이나 질문을 할 때 답변을 찾을 수 있을 것 입니다.
수학같은 특정 프로그래밍 도메인에서,
sin(x)
같이 광범위하게 사용되는 용어는 그대로 사용하는 것이verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
같은 네이밍보다 낫습니다. 이 경우 약어를 피하는 것 보다 관례를 따르는 것에 더 가중치가 있다는 것에 주목하세요. 비록 온전한 단어는 sine이지만sin(x)
는 프로그래머들에게 수십년간, 수학자들에게는 수세기 동안 보편적으로 사용되어 왔습니다.
Conventions(관례)
General Conventions(일반적인 관례)
1. 연산 프로퍼티의 복잡도가 O(1)이 아니라면 복잡도를 주석으로 남겨주세요.
⭕️ Good
class Company { // 보통 어떤 프로퍼티에 접근할 때, 큰 계산이 있을거라고 생각하지 않습니다. // 따라서 복잡도가 O(1)이 아니라면 아래와 같이 주석을 남겨주세요. // // Time Complexity: O(n^2) var numberOfEmployees: Int { var result: Int = 10 (0...result).forEach { (0...result).forEach { (0...result).forEach { result += 1 } } } return result } }
**2. global function 대신에 method와 property를 사용하세요. **
⭕️ Good
// global function은 특별한 경우에만 사용됩니다. min(1, 3) //1. 명확한 self가 없는 경우 max(4, 5) print("hello") //2. 함수가 Generic으로 제약 조건이 걸려있지 않는 경우 sin(3.0) //3. 함수 구문이 해당 도메인의 표기법인 경우
3. UpperCamelCase, lowerCamelCase를 따르세요.
type, protocol의 이름은 UpperCamelCase, 나머지는 lowerCamelCase를 따릅니다.
✅ Exception
// 미국 영어에서 보통 all upper case로 사용되는 Acronyms and initialisms(단어의 첫글자들로 말을 형성하는 것)은 // 대소문자 컨벤션에 따라 통일성있게 사용되어야 합니다. // UTF8, URL, HTTP 와 같이 all upper case로 사용되는 단어들이 있습니다. // 다만, 제일 첫번째 단어로 나오면 소문자로시작. 그러나 두번째 단어로 나오면 all upper case로 사용한다. var utf8Bytes: [UTF8] = [] var wewbsiteURL: URL? var urlString: String?
4. 기본 뜻이 같거나 구별되는 서로 구별되는 도메인에서 작동하는 method는 base name을 동일하게 사용할 수 있습니다.
⭕️ Good
// good // 입력되는 파라미터는 다르지만 포함되어있다는 그런 결과를 도출하는 것은 똑같기 때문에 이런 경우에는 이름을 똑같이 해도 된다. extension Shape { /// Returns `true` iff `other` is within the area of `self`. func contains(_ other: Point) -> Bool { true } /// Returns `true` iff `other` is entirely within the area of `self`. func contains(_ other: Shape) -> Bool { true } /// Returns `true` iff `other` is within the area of `self`. func contains(_ other: LineSegment) -> Bool { true } }
⭕️ Good
// good // 이것도 마찬가지다. 위에녀석과 구별되는 도메인이기때문에 같은 이름을 써도 된다. extension Collection where Element : Equatable { /// Returns `true` iff `self` contains an element equal to /// `sought`. func contains(_ sought: Element) -> Bool { return true } }
❌ Bad
// bad // 엄연히 동작이 다른데 이름이 같아서 안된다. // 위에는 데이터베이스의 검색 인덱스 다시 작성 // 아래는 주어진 테이블에서 n 번째 row 반환 struct Database {} extension Database { /// Rebuilds the database's search index func index() { } /// Returns the `n`th row in the given table. func index(_ n: Int, inTable: String) -> Int { 0 } }
❌ Bad
// bad // 메소드 이름은 value로 같은데 리턴타입이 다르다. 좋지 않다. // 타입캐스팅을 해서 사용해야 되니 불편하기도 하고 타입추론을 해줘도 되지만, 너무 모호하다. struct Box { private let rawValue: Int init(_ rawValue: Int) { self.rawValue = rawValue } func value() -> Int? { rawValue } func value() -> String? { "\(rawValue)" } } let myBox = Box(100) myBox.value() as Int? myBox.value() as String? let intBoxValue: Int? = myBox.value()
Parameters(매개변수)
1. 주석을 읽기 쉽게 만들어주는 매개변수 이름을 선택하세요.
⭕️ Good
// good // 파라미터(함수내에서 사용되는 애)는 사용할 땐 보이지않지만(사용할 때 보이는건 아규먼트) 펑션이나 메소드를 설명해주는 역할을 한다. // 잘 보면, 주석의 파라미터와 실제 파라미터가 같다. // 또, 문법적으로 읽기도 쉽다. Array, predicate, self 처럼. // 근데 Bad 예제를 보면 "includedInResult" 라고 읽기 문법적으로 어렵게 되어있기도 하고, // 또 심지어 r과 같이 되어있는 경우도 있다. 이건 좋지않다. // /// Return an `Array` containing the elements of `self` /// that satisfy `predicate`. func filter(_ predicate: (Int) -> Bool) -> [Int] { [] } /// Replace the given `subRange` of elements with `newElements`. mutating func replaceRange(_ subRange: NSRange, with newElements: [Int]) { }
❌ Bad
/// Return an `Array` containing the elements of `self` /// that satisfy `includedInResult`. func filter(_ includedInResult: (Int) -> Bool) -> [Int] { [] } /// Replace the range of elements indicated by `r` with /// the contents of `with`. mutating func replaceRange(_ r: NSRange, with: [Int]) { }
2. 일반적인 사용을 단순화 할 때, defaulted parameter를 사용하세요.
⭕️ Good
// good // swift는 defaulted parameter 기능을 지원하기 때문에, 상대적으로 잘 사용되지 않는 매개변수에 // 기본값이 있다면 간결하게 사용할 수 있다. lastName.compare(royalFamilyName)
❌ Bad
// bad, 길다. lastName.compare(royalFamilyName, options: [], range: nil, locale: nil)
3. defaulted parameters는 매개변수 리스트의 끝부분에 두는 것이 좋습니다.
⭕️ Good
// good // 필수파라미터 other가 앞에, 나머지 defaulted parameters들은 뒤에 있는 경우 // 필요에 따라서 defaulted parameters들에 값을 주더라도 호출이 일괄된 패턴을 보인다. extension String { /// ...description... public func compare( _ other: String, options: CompareOptions = [], range: Range? = nil, locale: Locale? = nil ) -> Ordering }
❌ Bad
// bad // 호출이 일관되지 않아 상대적으로 읽고 이해하기 힘들다. extension String { /// ...description 1... public func compare(_ other: String) -> Ordering /// ...description 2... public func compare(_ other: String, options: CompareOptions) -> Ordering /// ...description 3... public func compare( _ other: String, options: CompareOptions, range: Range) -> Ordering /// ...description 4... public func compare( _ other: String, options: StringCompareOptions, range: Range, locale: Locale) -> Ordering }
Argument Labels(인자 레이블)
1. Argument Label을 사용했음에도 유용하게 구분되지 않는다면, 모든 Argument Labels을 생략하세요.
⭕️ Good
min(number1, number2) zip(sequence1, sequence2)
2. 값을 유지하면서 타입 변환을 해주는 initializer에서, 첫 번째 Argument Label을 생략하세요.
- 첫번째 argument는 항상 source of convesion(변환의 근원)이어야 합니다.
⭕️ Good
// 이게 무슨말이냐면, extension String { // Convert `x` into its textual representation in the given radix init(_ x: BigInt, radix: Int = 10) //← Note the initial underscore } // 이런거다. Int -> String 할건데 그거의 첫번쨰 파라미터는 항시 값이 바뀌는 그 근원인 Int여야된다. 그말이다. text += String(veryLargeNumber) // veryLargeNumber -> Int
- 값의 범위가 좁혀지는 타입 변환의 경우, label을 붙여서 설명하는 것을 추천합니다.
⭕️ Good
extension UInt32 { init(_ value: Int16) // 이건 범위가 넓어지는 거니까 아규먼트 생략가능 init(truncating source: UInt64) // 근데 이건 좁아지는거라 안돼. } // good UInt32(Int16()) // 생략가능 UInt32(truncating: UInt64()) // 생략불가
3. 첫 번째 Argument label은 일반적으로 전치사구로 시작합니다.
⭕️ Good
x.removeBoxes(havingLength: 12)
✅ Exception
// 아래 예시를 보면 x,y는 동일한 추상화 레벨에 있다. a.move(toX: b, y: c) // 마찬가지로 red, green, blue도 동일한 추상화 레벨에 있다. a.fade(fromRed: b, green: c, blue: d) // // 그렇다면, 예외적으로 이런 경우에는 함수이름에 전치사(to, from)를 포함시켜버리고 // 아규먼트 레이블을 동일한 형식으로 맞춰줍니다. 추상화 레벨을 동일하게 일치시켜 추상화를 명확히 해주는 겁니다. a.moveTo(x: b, y: c) a.fadeFrom(red: b, green: c, blue: d)
4. 만약 첫번째 Argument가 문법적 구절을 만든다면 Argument label은 제거하고, 함수 이름에 base name을 추가합니다.
⭕️ Good
// 말이 좀 꼬여있는데 직역해보면 그런 뜻이다. // 문법적으로 말이 된다면? 아규먼트를 제거하고 메소드 이름에 붙이고, 그렇지 않으면 아규먼트를 붙여라는 말이다. // good // x에 add를 할건데 subview를 할거야. 그럼 말이 된다. 그럼 그냥 메소드 이름으로 붙여버린다. x.addSubview(y)
⭕️ Good
// good // view를 dismiss할거야. 더 이상 뭘 넣기가 어렵다. 이런 것은 Argument label을 그대로 둔다. view.dismiss(animated: false) // 마찬가지로 words를 split할거야. 문법적으로 뭘 더 어떻게 해볼 수 없다. 그대로 둔다. let text = words.split(maxSplits: 12) let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
❌ Bad
// bad // 뷰를 dismiss하는데 animate를 false한다는 건지, 아니면 bool값을 dismiss한다는건지, // 아니면 Dismiss를 하지 말라는 (false니까) 건지 알 수가 없다. // 이런 경우엔 모호하므로 명확하게 animated라는 Argument label를 넣어주어야 한다. view.dismiss(false) words.split(12) // 12를 스플릿하라는건지, 맥스스플릿이 12라는 건지, 12번째 위치로 스플릿하라는건지 뭔지 모른다. 이럴땐 아규먼트 레이블을 넣어야한다.
❌ Bad
// bad // 마찬가지다. // 12를 스플릿하라는건지, 맥스스플릿이 12라는 건지, 12번째 위치로 스플릿하라는건지 뭔지 모른다. // 이럴땐 아규먼트 레이블을 넣어야한다. words.split(12)
5. 그 외의 모든 경우에, Argument Label을 붙여야 합니다.
default value를 가진 argument는 생략될 수 있으며, 이 경우 문법 구문의 일부를 형성하지 않으므로 항상 레이블이 있어야 합니다.
Special Instructions(특별 지침)
1. tuple members와 closure parameters에 label을 지정하세요.
⭕️ Good
// 이게 튜플에서 좋은건 뭐냐면, 일단 이해하기쉽다. // 또 주석에서 `reallocated` 과 같은식으로 설명하기도 좋다. // 또 expressive access를 제공하는데 이건 이 뜻이다. struct Storage { func doSomething() -> (reallocated: Bool, capacityChanged: Bool) { return (true, false) } } let storage = Storage() let result = storage.doSomething() // 이렇게 쓸 때. 파라미터를 안적었으면, 0,1로 써야된다. result.0 result.1 // 근데 적어서 이게 된다. result.reallocated result.capacityChanged // 근데 아쉽게도. 클로저는 지원을 안한다. 무슨말이냐면, // 아래 클로저에 byteCount 라는 파라미터가 있는데 func ensureUniqueStorage(minimumCapacity requestedCapacity: Int, allocate: (_ byteCount: Int) -> Int) -> (reallocated: Bool, capacityChanged: Bool) { return (true, true) } // 여기서 사용할땐 그게 자동으로 나오지 않는다는 말이다. // 파라미터로 클로저에서 allocate: (_ byteCount: Int) -> Int 와 같이 byteCount라는 것을 // 네이밍 해주고 있는데, 밑에 allocate: { num in num + 1 } 부분에 byteCount가 제공되지 않는다. // 그래서 임의로 num 이라는 변수를 적어준거다. 튜플은 되지만 클로저는 지원하지 않는다. let bb = ensureUniqueStorage(minimumCapacity: 1, allocate: { num in num + 1 }) bb.capacityChanged bb.reallocated
2. overload set에서의 모호함을 피하기 위해, 제약 없는 다형성에 각별히 주의하세요.
⭕️ Good
struct Array { public mutating func append(_ newElement: Element) public mutating func append(contentsOf newElements: S) where S.Generator.Element == Element } // 위와 같은 overload set이 있다 // 문제는 Any, AnyObject, and unconstrained generic parameters 과 같은 타입에서 발생한다. var values: [Any] = [1, "a"] values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]? // 위와 같은 상황에서 만약 Argument Label "contentsOf"가 생략되었다면, 모호하다. // [Any]이기 때문에 1도 [1, "a"]도 다 똑같이 받아들이기 때문이다. // 따라서 이런 상황을 피하기 위해 명시적으로 이런 경우라면 꼭 주의해야 한다.