Swift API Design Guidelines


この文章の訳本です.これは私が開発の過程で中間参考のために書いた文章なので、ドキュメント全体を翻訳していません.例には、正式なドキュメントもあれば、個人が検索して追加しているものもあります.

ベース(基本概念)

  • を使用する場合の明瞭性が最も重要な目標である.
  • の明瞭性は簡潔性よりも重要である.
  • すべての宣言にドキュメントコメントを記入してください.△人によって意見が違うかもしれません.
  • 簡単な用語でAPIの機能を説明できない場合は、APIを設計し間違えた可能性があります.

    名前を付ける


    リモートClearの使用(明確な使用を促進)


    必要な単語をすべて含めてください.
    ⭕️ 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)
    工場メソッドは「make」で始まります.
    //팀마다 create로 하는 경우도 있지만, Swift는 make를 추천합니다. 
    [1,2,3].makeIterator()
    3.initializerメソッドとfactoryメソッドで呼び出された最初の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)
    副作用を考慮して、関数とメソッドに名前を付けてください.
    side-effectがないのは名詞で、あるのは動詞であるべきだ.
    mutating/non-muttingメソッドの名前を一貫して作成する必要があります.
    operationが動詞と解釈される場合、変異時には動詞のコマンド型を使用し、非変異時には接尾辞として「ed」と「ing」を使用します.
    x内部の値の変更->mutatingx内部値を変更せずに新しい結果値を返す->nonmutatingMutatingNonMutatingx.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.numutatingのBoolタイプメソッド&属性としてreceiver宣言を読み込む必要があります.
    ⭕️ Good
    // 일단 얘네들은 x, line1을 변경하지 않는 nonmutating의 bool 타입 속성, 메소드이다. 
    // 그렇다면, 얘네들은 receiver(여기서는 x와 line1)에 대한 주장과 같은 느낌으로 읽히면 된다. 
    x.isEmpty //x는 비어있는가?
    line1.intersects(line2) //line1은 line2에 교차하는가? 
    6.コンピテンシーを記述するプロトコルは、-able、-bible、および-ingを使用する接尾辞と命名されるべきである.
    ⭕️ Good
    Hashable 해시로 생성할 수 있는 유형을 정의하는 프로토콜 
    Equatable "=="과 같은 연산자를 쓸 수 있게하는 프로토콜
    CaseIterable enum을 배열처럼 순회할 수 있게 하는 프로토콜.
    RawRepresentable struct에서도 rawValue 사용가능
    ProgressReporting // 없는거 
    DBAccessable // 없는거
    ...
    7.残りのタイプ、属性、変数、定数は名詞に読み込む必要があります.
    1日から6日まで含まれていない子供は名詞と読むべきだ.

    ターミナルWellの使用(専門用語)


    Term of Art:名詞、特定の分野または職業において正確な専門化の意味を持つ単語またはフレーズ.
    もっと一般的な単語「皮膚皮膚」がもっと意味を伝えられるとしたら.「表皮」は使わないでください.
    専門用語を使用する場合は、明確な意味を使用してください.
    専門用語が常用語と比較して
  • を使用する唯一の理由は、曖昧または不明確な内容を正確に表現できることである.
  • の専門家を驚かせないでください.専門用語に新しい意味を与えれば、この用語に詳しい専門家は驚くかもしれません.
  • 初心者を困惑させないでください.この専門用語を勉強したい人はインターネットを利用するかもしれません.この専門用語をネットで検索するときに、伝統的な意味と使用する専門用語の意味が異なる場合は、初心者が困惑する可能性があります.
  • 略語は避けてください.定型化されていない略語を理解するには,再解釈が必要であるため,専門用語(用語of art,知っている人しか知らない)と考えられる.
    略語を使う意味は、Webサイトで簡単に見つけることができます.
    -慣例を受け入れてください.初心者のために既存の文化とは異なる言語を使わないでください.
    連続したデータ構造を表す場合,初心者であればリストは容易に理解できるが,リストよりもArrayの方がよい.Arrayは現代の計算の基礎なので、すべてのプログラマーが知っているか、すぐに知っています.ほとんどのプログラマーはよく知っている用語を使用しています.これにより、ネット検索や質問で答えを見つけることができます.
    いくつかのプログラミングドメイン(数学など)では、sin(x)のように広く使用されている用語は、verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)の使用よりもそのままのままであることが好ましい.この場合、略語を避けるよりも慣例に従うことが重要であることに注意してください.完全な単語はsineであるにもかかわらず、sin(x)はプログラマーや数学者の中で何十年も一般的に使われている.

    慣例


    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、プロトコル名は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.同じまたは異なるデフォルトの意味を持つドメインで動作する方法は、同じbasenameを使用できます.
    ⭕️ 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()

    パラメータ


    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.一般的な使用を簡略化する場合は、デフォルトのパラメータを使用します.
    ⭕️ Good
    // good
    // swift는 defaulted parameter 기능을 지원하기 때문에, 상대적으로 잘 사용되지 않는 매개변수에 
    // 기본값이 있다면 간결하게 사용할 수 있다.
    lastName.compare(royalFamilyName)
    ❌ Bad
    // bad, 길다.
    lastName.compare(royalFamilyName, options: [], range: nil, locale: nil)
    3.パラメータリストの末尾にデフォルトのパラメータを配置することを推奨します.
    ⭕️ Good
    // good 
    // 필수파라미터 other가 앞에, 나머지 defaulted parameters들은 뒤에 있는 경우 
    // 필요에 따라서 defaulted parameters들에 값을 주더라도 호출이 일괄된 패턴을 보인다. 
    public func compare(_ other: String, options: CompareOptions = [], range: Range? = nil, locale: Locale? = nil) { }
    compare("A")
    compare("B", locale: nil)
    compare("B", options: [], locale: nil)
    compare("B", options: [], range: nil, locale: nil)
    ❌ Bad
    // bad
    // 호출이 일관되지 않아 상대적으로 읽고 이해하기 힘들다. 
    public func compare(options: CompareOptions = [], range: Range? = nil, locale: Locale? = nil,_ other: String, ) { }
    compare("A")
    compare(locale: nil, "B")
    compare(options: [], locale: nil, "B")
    compare(options: [], range: nil, locale: nil, "B")

    Argument Labels(パラメータラベル)


    1.Argument Labelを使用しているが区別されていない場合は、すべてのArgument Labelを省略します.
    ⭕️ Good
    min(number1, number2)
    zip(sequence1, sequence2)
    2.値を保持しながらタイプ変換を行うイニシエータで、最初のArgumentラベルを省略します.
  • 最初のパラメータは、常に変換のソースである必要があります.
  • ⭕️ 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
  • の値範囲を縮小するタイプ変換については、ラベルを付けて説明することをお勧めします.
  • ⭕️ 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を削除し、関数名にbasenameを追加します.
    ⭕️ 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ラベルを貼る必要があります.
    構文構文の一部を構成しないため、default valueを持つパラメータは省略できます.常にラベルを付けなければなりません.

    特別な説明


    1.tuple membersおよびclosureパラメータの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.過負荷設定でのぼやけを避けるためには、コンストレイントのない多形性に特に注意してください.
    ⭕️ 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"]도 다 똑같이 받아들이기 때문이다.
    // 따라서 이런 상황을 피하기 위해 명시적으로 이런 경우라면 꼭 주의해야 한다.