콘텐츠로 건너뛰기
Home » Swift의 조건절 패턴 매칭 총정리

Swift의 조건절 패턴 매칭 총정리

C 언어의 if 문은 조건이 되는 표현식을 평가하여 0이면 거짓, 0이 아니면 참으로 판단한다. 많은 언어들이 C언어와 같은 방식으로 작동하는 분기문을 지원하고, 따라서 많은 프로그래머들이 여기에 익숙하기 때문에, Swift의 복잡한(?) if 문법이 당혹스러울 수 있다. 이번 글에서는 Swift의 복잡한 조건절을 어떻게 사용하는지 살펴보도록 하겠다.

Swift는 조건값이 아닌 조건에 대한 패턴매칭을 기반으로 작동한다. 즉 if 다음에는 “조건 리스트”가 오게 된다. 그리고 이 조건 리스트는 다음 중 하나의 패턴이 된다.

  • 조건 리스트 : 콤마(‘,’)로 연결되는 1개 이상의 단일 조건의 목록. 각각의 조건은 이하의 패턴 중 하나이며, 콤마는 각 조건에 대한 AND 연산으로 간주된다.
  • 표현식 패턴 : true / false 중 하나로 평가되는 표현식
  • 이니셜라이저 패턴 : 이니셜라이저 패턴은 ‘=’를 사용하는 패턴으로 case 조건 패턴과 호환성 체크 패턴이 여기 해당한다.
  • case 조건 패턴 : 두 개의 패턴을 등호(‘=’)로 연결하여 서로 비교하거나 매칭한다.
    • 식별자 패턴 : 두 개의 변수나 상수를 비교한다.
    • 값 바인딩 패턴 : 등호의 우변을 평가하여 좌변에 바인딩한다. 바인딩에 성공하면 true
    • 튜플 패턴: 등호의 우변에 튜플이 오고, 좌변의 패턴이 우변의 튜플에 매치하면 true
    • enum-case 패턴 : 좌변에 특정한 enum 의 case 이고, 좌변이 그와 같으면 true
    • 옵셔널 패턴 : 우변을 평가한 값이 nil 이 아닐 때, 좌변에 바인딩하고 성공하고 true
    • 타입 캐스팅 : is 나 as 를 사용하여 타입을 매칭하거나 캐스팅하고 성공하면 true
    • case 표현식 패턴 : 우변의 값이 좌변의 패턴에 매칭할 때 true
  • 옵셔널 바인딩 : case 조건 패턴 중 옵셔널 패턴에 대한 문법적 장식

옵셔널 바인딩

우선 가장 많이 등장하는 패턴 중 하나인 옵셔널 바인딩 먼저 살펴보자. 흔히 if let 문이라고 표현하는 것이다. 이 패턴은 = 오른쪽의 표현식을 평가하고 그 결과가 nil이 아니면 옵셔널을 벗겨서(unwrapping) 좌변의 변수 이름에 바인딩한다. 바인딩된 이름은 if 구문 내에서만 유효하다.

let someOptional: Int? = 42
if let x = someOptional {
//     ^ x:Int = 42
  ...
}

이 패턴에서 우변의 표현식이 nil로 평가되면 조건 패턴은 false로 판정한다. 사실 이 패턴은 후술할 “case-옵셔널 패턴”의 축약 형태라고 할 수 있다.

if case let x? = someOptional {
//      Optional은  nil |  x? 중 하나의 형식이다.
//      someOptional == nil 이면 x? 의 패턴에 해당하지 않으므로 바인딩할 수 없다.
//      nil이 아니면 x에 바인딩한다. 
//      패턴에서 x? 라고 썼고, 블럭에서는 x만 사용하므로 x는 언래핑된 값이다.
}

식별자 패턴(identifier-pattern)

식별자 패터는 case a = b 와 같은 식으로 두 개의 식별자를 등호로 연결하는 것이다. 이는 바인딩과는 달리 좌변의 값이 우변의 값에 매칭이 가능한가를 묻는다. 여기서 매칭은 주로 특정 객체를 평가한 값이 서로 같은지를 보는 것이다. 이것이 == 연산자와 다른 것이 무엇이냐고 물을 수도 있는데, 사실 이 패턴은 switch 구문에서 의미가 있으며, if 구문에서는 == 연산자와 같은 것으로 봐도 무방하다.

let a = 42
var b = 0
b = b + .....

if case a = b {
    print("a is equal to b.")
}

값-바인딩 패턴(value-binding-pattern)

case let a = b 의 모양을 값 바인딩 패턴이라고 한다. 이 패턴은 b를 평가한 값을 a에 바인딩하는데, 바인딩에 성공하면 그 값에 상관없이 true 이다. 언뜻 이 패턴 역시 if 문에서는 크게 유용하지 않을 수 있는데, 주로 switch 구문에서 요긴하게 사용될 수 있다. if 문에서도 상수로 정의된 값을 변수에 바인딩하여 if 블럭 내에서 변경하고자 할 때 사용한다.

let value = ..... 
if let case x = value, x % 2 == 0 {
    print("value is even number.")
}

튜플 패턴(tuple-pattern)

이니셜라이저 패턴 중에서 가장 복잡할 수 있는 패턴으로, 튜플인 값의 특정 성분들이 특정한 값일 때, 매칭하는 조건을 표현한다. 특히 바인딩 패턴과 결합할 수 있는데, 튜플 전체 혹은 특정한 요소만 변수에 바인딩하여, 추가적인 조건을 판단하거나 if 블럭 내에서 사용할 수 있다.

let a = (4, 0)

// 아래 패턴은 튜플 a의 .1 요소가 0인지를 판단한다. 
if case (_, 0) = a {
  print("a is on the X axis.")
}

// .0 요소가 짝수이고, .1 요소는 0일 때
if case (let x, 0) = a, x % 2 == 0 {
  ..
}

// 튜플의 두 요소가 같은 값일 때
if case let (x, y) = b, x == y {
  ..
}

enum-case 패턴

검사하고자 하는 값이 특정 enum 타입의 case 중 하나인 경우에 이를 확인한다. 특히 내부에 다른 값을 가질 수 있는 enum case 타입에 대해서는 바인딩 패턴과 결합하여 복잡해질 수 있는 if 절을 간단하게 만들 수 있다.

enum Either {
    case left(Int, Int)
    case right(Double, Double)
}

var a = Either.Left(13, 169)
if case .left = a {
  print("a is .left case")
}


// 바인딩 패턴과 결합하기
// 한 요소는 13으로 고정되고, 두번째 요소를 y에 바인딩
if case .left(13, let y) { ... }

// 두 요소값을 바인딩하고 추가 판단하기
if case .left(let x, let y) = a, x * x == y {  ...  }

// 튜플 패턴처럼 아래와 같이 사용하는 것도 가능하다.
if case let .left(x, y) = a { ... }

옵셔널 패턴(optional-pattern)

옵셔널 패턴은 식별자 패턴에서 좌변이 옵셔널 표기로 된 것을 말한다. 어떤 옵셔널 타입의 식별자를 언래핑했을 때 다른 값과 매칭할 수 있는지를 보고, 매칭할 수 있다면 true 가 된다. 즉 어떤 값이 nil 이 아니면서 다른 값과 매칭할 수 있는지를 한 번에 검사하게 된다.

let a: Int? = 1
let b: Int = 1
if case b? = a { 
    print("a is not nil and its value is equal to 'b'.")
}

이 것이 값 바인딩 패턴과 결합할 때, 이를 옵셔널 바인딩이라 하고 특별히 case 와 좌변의 ? 를 생략할 수 있게 된다.

let a: Int? = ....

if case let b? = a { 
    print("a is not nil and its unwrapped value is \(b).")
}

// if case let X? = a 를 줄여서 if let x = a 로 쓴다.
if let b = a { print("a is not nil and its unwrapped value is \(b).") }

다른 패턴들의 결합이 가능한 것처럼, 이 패턴 역시 튜플패턴이나 enum 패턴, 바인딩 패턴과 결합할 수 있다. 아래의 if 구문을 이러한 패턴 매칭이 없다면 몇 단계의 if 블럭을 만들어야 할까?

let a:Either? = .left(13, 169)
if case let .left(x, y)? = a, x * x == y { ... }
//          a != nil 이고, .left 케이스 일 때, 
//          내부의 두 값을 x, y 에 바인딩하고 true



if case let line1? = readline(),
   case let line2? = readline(),
   case let (x, y) = (Int(line1), Int(line2)),
   x == y
{
    print("you entered two same integers.")
}


let x:(Int?, Int?)? = (0, 4)
if case (0?, let y?)? = x {
    print("x is on the Y axis and it's y position is \(y).")
}

타입 캐스팅 패턴(type-casting-pattern)

어떤 변수가 A 라는 타입의 인스턴스인지를 확인하는 패턴으로 is 연산자를 사용한다. is 연산자는 등호를 대체할 수 있기 때문에, 등호를 사용하지는 않았지만 “initializer-pattern”의 한 갈래로 구분하고 있다. 실제로 타입 캐스팅 패턴으로 주로 사용되는 패턴은 as 연산자를 사용하는 캐스팅 패턴이다. 이 방법은 주로 바인딩 패턴과 결합하여 사용하는데, x라는 값이 A라는 타입일 때 (혹은 A타입으로 캐스팅 가능할 때) 캐스팅하고, 캐스팅한 결과를 바인딩하는 식이다.

  • if case let y = x as A : x를 A 타입에 캐스팅하고 캐스팅에 성공하면 y에 바인딩한다.
  • if case let y = x as? A : x를 A타입으로 캐스팅한다. 성공하면 A?타입으로 옵셔널로 감싸져서 y에 바인딩된다. 만약 캐스팅이 안된다면 y에는 nil이 바인딩된다.
  • if case let y? = x as? A : x를 A타입으로 캐스팅할 수 있다면 y에 바인딩하고 그렇지 않다면 false로 평가된다. (응? 첫번째랑 같은 거 아님?)

as? 는 캐스팅을 시도한 후 무조건 옵셔널로 감싸려 한다. 만약 as? Int? 와 같이 옵셔널 타입에 대해서 캐스팅을 시도한다면 그 결과는 이중 옵셔널이 된다.

if case let y = x as A {
//  x를 타입 A로 캐스팅하고 y에 바인딩한다.
}

if case let y = x as? Int {
//  x를 타입 A로 캐스팅한다.
//  캐스팅에 실패하면 y는 nil 이므로 y의 타입은 Int? 이다. 

if case let y? = x as? Int {
//  캐스팅에 실패하면 바인딩되지 않는다. 
//  첫번째 예시랑 똑같다. 


if case let y = x as? Int? {
   print(y)
// Optional(Optional(3))
}

if case let y?? = x as? Int? {
  print(y)
// 3
}

case 표현식 패턴(case-expression-pattern)

Swift의 공식 문서에서는 발견하기 어렵지만, ~= 이라는 기본 연산자가 존재하고, 몇몇 기본 타입들에 대해서도 정의가 되어 있다. case A = B 는 사실 A ~= B 의 코드로 치환된다고 보면 된다. 이 연산의 대표적인 케이스가 Range 이다.

let a = 7
if case (1..<10) = a {
    print("a is in the range.")
}

특정한 타입들에 대해서 이 연산자의 구현을 제작하면, 패턴 매칭을 사용하여 간단하게 case a = b 의 체크를 사용할 수 있다 예를 들어 다음과 같이 연산자를 정의하면 if case 구문에서 정수와 문자열을 비교하는 것을 수행할 수 있다.

func ~= (pattern: String, value: Int) -> Bool 
{
    return pattern == "\(value)"
}

let a = "10"
let b = 10
if case a = b {
//          ^ 정수값이 문자열로 표현된 값에 매칭하는지 볼 수 있다.
    ...
}

패턴 매칭으로 특정한 동작을 수행할 수 있다면 정규식 패턴 문자열과 문자열을 매치하여 검사하는 기능도 간단하게 추가할 수 있다. 아래는 어떤 문자열이 핸드폰 전화번호인지 여부를 확인하는 것이다.

func ~= (pattern: String, value: String) -> Bool {
    guard let regex = try? NSRegularExpression(pattern: pattern, options:[])
        else { return false }
    return regex.numberOfMatches(in:value, options:[],
        range:NSMakeRange(0, value.characters.count)) > 0
}
let s = "011-123-3456"
if case "(?:010[-\\.\\s]?\\d{4}[-\\.\\s]?\\d{4})|(?:011[-\\.\\s]?\\d{3,4}[-\\.\\s]?\\d{4})" = s {
    print("it is phone number.")
}

      정리

      지금까지 case 조건패턴을 구성하는 기본 패턴들의 사용방법을 살펴보았다. 이러한 조건절 패턴매칭의 문법은 if 구문 뿐만 아니라 switch, while, guard 등 조건을 비교하고 패턴 매칭을 수행하는 코드에서 공통적으로 사용되며, 간단한 각각의 패턴이 적용하기에 따라서는 아주 복잡한 패턴으로도 발전할 수 있는데, 이는 활용하기에 따라서는 복잡한 조건을 처리하면서 if 구문의 단계를 줄여 코드를 간결하고 명확하게 만드는데 도움이 될 수 있다.