HCN DEV

Swift ControlFlow

Swift ControlFlow feature image

일반적으로 프로그래밍 언어를 익힐 때, ControlFlow 라 하면 if 구문과 같은 것들을 활용하여 프로그램이 원하는 방향으로 작동되도록 하는 것을 말합니다. 여기서는 대부분 언어에서 발견할 수 있는 if, switch, continue, break 등에 대해 알아보기 보다는 Swift에서 발견할 수 있는 특징적인 ControlFlow 에 대해 알아보도록 하겠습니다.

Value binding

Value binding은 switch문 실행시 조건문 안에서 활용할 수 있는 변수를 할당하는 것을 말합니다.

Value binding의 예제를 살펴보겠습니다.

let myCondition = 10

switch myCondition {
case let con:
    print("Condition이 \(con)입니다.")
case let con:
    print("Condition2 : \(con)")
default:
  print("Switch default")
}
// 결과 : Condition이 10입니다.

위의 예제에서 case let con:은 해당 case의 구문 안에서 con이라는 변수를 사용할 수 있도록 해줍니다. 이것이 바로 Value binding입니다. 그런데 조금 의아한 부분이 있습니다. Value binding 자체가 하나의 조건이 될 수 있나하는 것인데, 테스트해보니 Value binding은 기본적으로 true가 되네요. Value binding은 모양 자체가 if에서 쓸 수 있는 Optional binding과 형태가 유사합니다. 다만 Value binding은 Optional 값을 unwrap 해주지는 않습니다. 반대로 Optional binding은 Optional 값일 때만 사용할 수 있는 차이점이 있습니다.

let test: Int?
test = 3

switch test {
case let con:
    print("값 - \(con)")
}

if let val = test {
    print("값 - \(val)")
}
// 결과 : 값 - Optional(3)
//        값 - 3

Where

Where는 switch문 실행시 case에 추가적인 조건을 주는 기능을 합니다.

Where는 case 자체의 조건에 추가적인 조건을 부여할 수 있도록 해줍니다. 예시를 살펴보겠습니다.

let myCondition = 10
let condition2 = 3

switch myCondition {
case 10 where condition2 < 5:
    print("조건을 만족합니다.")
default:
    print("Switch default")
}
// 결과 : 조건을 만족합니다.

여기서 myCondition이 10인 case의 조건과 condition2가 5보다 작은 두 가지 조건을 모두 만족해야만 조건을 만족합니다.가 출력됩니다. 쉽게 생각하면 if의 &&로 조건을 연결하는 것을 생각하시면 됩니다.

Fallthrough

fallthroughswitch와 연관된 키워드로 switch를 사용할 때, 코드 순서상의 다음 조건문을 실행시켜 줍니다. 이 때, 다음 조건문이 없다면 컴파일 에러가 발생합니다. Swift의 switch는 case별로 break를 따로 작성해주지 않아도 해당 case의 조건을 만족한다면, 그 case의 내용만 실행하고 switch에서 빠져나오게 됩니다. 그런데 여기서 fallthroughfallthrough가 작성된 시점에서 case를 더이상 실행하지 않고, 바로 다음 조건문을 실행하고, switch를 종료합니다.

enum MyCase {
    case a, b, c
}

let myCase = MyCase.a

switch myCase {
case .a:
    print("a")
    fallthrough
    print("after a") // will never be executed
case .c:
    print("c")
case .b:
    print("b")
}

// 결과 : a c

switch myCase {
case .a, .b, .c:
    print("a b c")
    fallthrough // 'fallthrough' without a following 'case' or 'default' block
}
  1. fallthrough는 조건문 작성 순서에 영향을 받습니다. 위의 예시에서 a 다음 b가 아닌 c가 출력된 것에서 이를 확인할 수 있습니다. 조건을 a 다음 b로 쓴다면 a, b가 출력됩니다.
  2. fallthrough는 실행할 다음 조건문이 없다면 컴파일 에러가 발생합니다.

Labeled Statement

Swift에서는 if, switch, 혹은 Loop에 이름을 부여할 수 있습니다. 이렇게 부여된 이름은 여러개가 중첩된 조건문, 반복문에서 어떤 것을 break하고 continue할지를 명시할 수 있게 해줍니다. 이름은 해당 조건문, 반복문 시작에 쓰입니다. 예제를 살펴보겠습니다.

let myCondition = 10

LoopOne: for i in 1...5 {
    switchOne: switch myCondition {
    case 10 where i < 3:
        print("조건을 만족합니다.")
        break switchOne
    default:
        print("Switch default")
        break LoopOne
    }
}
// 결과 : 조건을 만족합니다.
//        조건을 만족합니다.
//        Switch default

위의 예제에서 나오는 for 루프는 LoopOne이라는 이름을 갖게 됩니다. 또한 switch 구문은 switchOne이라는 이름을 갖습니다.(이름을 갖는 것일 뿐, 객체로 활용할 수 있는 것은 아닙니다.) 그에 따라 break 이름이라는 형태를 사용할 수 있습니다. 특정 조건문, 반복문을 멈추는 것을 명시적으로 보여줄 수 있도록 해주는 것입니다.

guard

guardifswitch 같은 조건문을 만드는 데 쓰이는 것입니다. 독특한 점은 항상 else를 동반한다는 점입니다. 그 기본 형태는 아래와 같습니다.

guard 조건 else {
  // 조건을 만족하지 않으면 실행
}
// 조건 만족시 계속 진행

형태는 이렇습니다만, if처럼 아무데나 쉽게 쓸 수 있지는 않습니다. 왜냐하면 guard실행되는 위치(enclosing scope)에서 벗어나는 것 을 항상 필요로 하기 때문입니다. 그래서 일반적으로 main thread에서 쉽게 if를 쓰는 것과는 달리, guard를 사용하게 되면 main thread를 빠져나가는(프로그램이 꺼지거나 죽는) 것을 만들어주어야 하는 것을 필요로 하므로 거의 사용하지 않습니다.(혹시 사용하는 예제가 있다면 알려주시면 감사하겠습니다.)

guard는 이러한 이유 때문에 주로 함수 안에서, 함수를 return하는 조건(실행되는 위치에서 벗어나는 조건)과 함께 사용됩니다. 예제를 살펴보면 다음과 같습니다.

func hello() -> Int {
    let a = 1
    guard a == 1 else {
        print("else 안입니다.")
        return 0
    }
    return 1
}
print(hello())
// 결과 : 1

위의 예제에서 a==1이라는 조건은 만족되므로, else 안은 실행되지 않고 그대로 통과합니다. 그러므로 1이 return 됩니다.

guard with Optional binding

guard가 많이 사용되는 경우는 Optional Binding을 할 경우입니다. guard의 조건문은 if 조건문에서처럼 Optional Binding을 활용할 수 있습니다. 이 때, 값이 nil이 아니어서 변수가 할당 된다면 이는 guard가 끝난 후에도 남아 있어서 값이 unwrap된 상태로 사용할 수 있습니다. 예시를 봐보겠습니다.

func myPrint(name: String?) {
    guard let name = name else {
        print("exiting...")
        return
    }
    print("이름 - \(name)")
}
myPrint(name: "Tom")
myPrint(name: nil)

// 결과 : 이름 - Tom
//        exiting...

위의 예시에서 name은 guard를 통해 unwrap되고, guard가 끝난 후에도 남아 있습니다. 그렇기 때문에 print에서 name은 unwrap된 상태가 되고 Tom이 출력됩니다. 이 때, name의 값이 nil이라면 exiting...이 출력되면서 함수는 return됩니다.

guard in Practice

guard의 가장 큰 장점은 가독성을 높여주는 것에 있습니다. 사용자가 회원가입을 하는 경우를 생각해보겠습니다. 이 때, 올바른 정보를 받기 위해서는 사용자가 어떤 정보를 누락하고, 어떤 정보를 입력했는지를 정확히 알 필요가 있습니다. 사용자에게 이름, 이메일, 비밀번호의 정보를 받는 회원가입을 할 경우를 if let으로 작성해보면 아래와 같습니다.

func checkValidation() {
  if let name = nameLabel.text {
    if let email = emailLabel.text where email.containsString("@") {
      if let password = passwordLabel.text {
        // 모든 경우 만족
        let newUser = User()
        newUser.name = name
        newUser.email = email
        newUser.password = password
      } else {
        // 비밀번호 nil
        passwordLabel.becomeFirstResponder()
      }
    } else {
      // 이메일 nil 혹은 @ 없음
      emailLabel.becomeFirstResponder()
    }
  } else {
    // 이름 nil
    nameLabel.becomeFirstResponder()
  }  
}

유저의 정보가 올바르게 입력됐는지 확인하기 위해서는 이처럼 중첩으로 if let을 사용해야 합니다. 이는 정보가 많아지면 잘못된 부분을 쉽게 고치기 어렵습니다. 이 때 guard 를 쓰면 가독성이 매우 좋아집니다. 위의 내용을 guard를 활용해서 써보면 다음과 같습니다.

func checkValidation() {
  guard let name = nameLabel.text else {
      // 이름 nil
      nameLabel.becomeFirstResponder()
      return
  }
  guard let email = emailLabel.text where email.containsString("@") else {
      // 이메일 nil 혹은 @ 없음
      emailLabel.becomeFirstResponder()
      return
  }
  guard let password = passwordLabel.text else {
      // 비밀번호 nil
      passwordLabel.becomeFirstResponder()
      return
  }

  // 모든 경우 만족
  let newUser = User()
  newUser.name = name
  newUser.email = email
  newUser.password = password
}

guard를 사용하면, 중첩으로 조건문을 사용하는 것을 방지할 수 있고 코드의 가독성이 좋아집니다. 이외에도 json 데이터를 parsing할 경우에도 누락된 데이터를 파악하기 위한 조건문을 작성할 때 코드의 가독성이 높아지게 만들 수 있습니다.


참고자료