🔥 조건문

2345자
27분

프로그래밍을 하다 보면 특정 조건에 따라 코드를 다르게 실행해야 할 때가 있습니다. 예를 들어, 오류가 발생했을 때 추가적인 코드를 실행하거나, 값이 너무 크거나 작을 때 메시지를 표시하는 등의 상황이 있겠죠. 이럴 때 코드의 일부를 조건부로 만들어야 합니다.

Swift에서는 조건부 분기를 코드에 추가할 수 있는 두 가지 방법을 제공합니다. 바로 if문과 switch문입니다. 일반적으로 if문은 간단한 조건을 평가하고 몇 가지 가능한 결과만 있는 경우에 사용합니다. 반면에 switch문은 더 복잡한 조건과 여러 가지 가능한 순열이 있는 상황에 더 적합하며, 패턴 매칭을 통해 적절한 코드 분기를 선택하는 데 유용합니다.

If문

가장 단순한 형태의 if문은 하나의 if 조건만 가집니다. 해당 조건이 true일 때만 일련의 명령문을 실행하죠.

var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
    print("너무 추워요. 목도리를 두르는 게 어때요?")
}
// "너무 추워요. 목도리를 두르는 게 어때요?"를 출력합니다.
swift

위의 예제는 온도가 화씨 32도(물의 어는점) 이하인지 확인합니다. 그렇다면 메시지를 출력하겠죠. 그렇지 않으면 아무 메시지도 출력되지 않고, 코드 실행은 if문의 닫는 중괄호 다음으로 계속됩니다.

if문은 if 조건이 false일 때 실행할 대체 명령문 집합인 else 절을 제공할 수 있습니다. 이러한 명령문들은 else 키워드로 나타냅니다.

temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
    print("너무 추워요. 목도리를 두르는 게 어때요?")
} else {
    print("그렇게 춥지는 않아요. 티셔츠를 입으세요.")
}
// "그렇게 춥지는 않아요. 티셔츠를 입으세요."를 출력합니다.
swift

이 두 분기 중 하나는 항상 실행됩니다. 온도가 화씨 40도로 올라갔기 때문에, 목도리를 두르라고 조언하기에는 충분히 춥지 않아요. 그래서 else 분기가 대신 실행되는 거죠.

여러 개의 if문을 함께 연결하여 추가 절을 고려할 수 있습니다.

temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
    print("너무 추워요. 목도리를 두르는 게 어때요?")
} else if temperatureInFahrenheit >= 86 {
    print("정말 더워요. 선크림 바르는 거 잊지 마세요.")
} else {
    print("그렇게 춥지는 않아요. 티셔츠를 입으세요.")
}
// "정말 더워요. 선크림 바르는 거 잊지 마세요."를 출력합니다.
swift

여기서는 특별히 더운 온도에 대응하기 위해 추가적인 if문이 추가되었습니다. 마지막 else 절은 그대로 있고, 너무 덥지도, 춥지도 않은 온도에 대한 응답을 출력하죠.

하지만 마지막 else 절은 선택 사항이며, 조건 집합이 완전할 필요가 없다면 제외할 수 있습니다.

temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
    print("너무 추워요. 목도리를 두르는 게 어때요?")
} else if temperatureInFahrenheit >= 86 {
    print("정말 더워요. 선크림 바르는 거 잊지 마세요.")
}
 
swift

온도가 if 조건을 실행하기에 충분히 춥지 않거나 else if 조건을 실행하기에 충분히 덥지 않기 때문에, 아무 메시지도 출력되지 않습니다.

Swift는 값을 설정할 때 사용할 수 있는 if의 축약 표현을 제공합니다. 예를 들어, 다음 코드를 살펴보겠습니다:

let temperatureInCelsius = 25
let weatherAdvice: String
 
if temperatureInCelsius <= 0 {
    weatherAdvice = "너무 추워요. 목도리를 두르는 게 어때요?"
} else if temperatureInCelsius >= 30 {
    weatherAdvice = "정말 더워요. 선크림 바르는 거 잊지 마세요."
} else {
    weatherAdvice = "그렇게 춥지는 않아요. 티셔츠를 입으세요."
}
 
print(weatherAdvice)
// "그렇게 춥지는 않아요. 티셔츠를 입으세요."를 출력합니다.
swift

여기서 각 분기는 weatherAdvice 상수에 대한 값을 설정하고, if문 다음에 그 값이 출력됩니다.

대체 구문인 if 표현식을 사용하면, 이 코드를 더 간결하게 작성할 수 있습니다:

let weatherAdvice = if temperatureInCelsius <= 0 {
    "너무 추워요. 목도리를 두르는 게 어때요?"
} else if temperatureInCelsius >= 30 {
    "정말 더워요. 선크림 바르는 거 잊지 마세요."
} else {
    "그렇게 춥지는 않아요. 티셔츠를 입으세요."
}
 
print(weatherAdvice)
// "그렇게 춥지는 않아요. 티셔츠를 입으세요."를 출력합니다.
swift

if 표현식 버전에서, 각 분기는 하나의 값을 포함합니다. 어떤 분기의 조건이 참이라면, 그 분기의 값이 weatherAdvice에 할당되는 전체 if 표현식의 값으로 사용됩니다. 모든 if 분기는 대응하는 else if 분기나 else 분기를 가지므로, 어떤 조건이 참인지와 관계없이 if 표현식은 항상 값을 생성합니다.

구문이 if 표현식 바깥에서 시작되므로, 각 분기 내에서 weatherAdvice =를 반복할 필요가 없습니다. 대신 if 표현식의 각 분기는 weatherAdvice에 대한 세 가지 가능한 값 중 하나를 생성하고, 할당은 그 값을 사용하는 거죠.

if 표현식의 모든 분기는 같은 유형의 값을 포함해야 합니다. Swift는 각 분기의 유형을 개별적으로 확인하기 때문에 nil과 같이 하나 이상의 유형과 함께 사용할 수 있는 값은 Swift가 if 표현식의 유형을 자동으로 결정하는 것을 방해합니다. 대신 명시적으로 유형을 지정해야 합니다. 예를 들면 다음과 같습니다:

let freezeWarning: String? = if temperatureInCelsius <= 0 {
    "어는점 아래예요. 빙판길 조심하세요!"
} else {
    nil
}
swift

위의 코드에서 if 표현식의 한 분기는 문자열 값을 가지고, 다른 분기는 nil 값을 가집니다. nil 값은 옵셔널 유형의 값으로 사용될 수 있기 때문에, Type Annotations에서 설명한 대로 freezeWarning이 옵셔널 문자열임을 명시적으로 작성해야 합니다.

이 유형 정보를 제공하는 다른 방법은 freezeWarning에 대한 명시적인 유형을 제공하는 대신, nil에 명시적인 유형을 제공하는 것입니다:

let freezeWarning = if temperatureInCelsius <= 0 {
    "어는점 아래예요. 빙판길 조심하세요!"
} else {
    nil as String?
}
swift

if 표현식은 위의 예제처럼 할당의 오른쪽에 사용하는 것 외에도, 오류를 던지거나 fatalError(_:file:line:)와 같이 절대 반환하지 않는 함수를 호출하여 예기치 않은 실패에 대응할 수 있습니다. 예를 들면 다음과 같습니다:

let weatherAdvice = if temperatureInCelsius > 100 {
    throw TemperatureError.boiling
} else {
    "온도가 적당해요."
}
swift

이 예제에서 if 표현식은 예상 온도가 물의 끓는점인 섭씨 100도보다 더 뜨거운지 확인합니다. 이렇게 뜨거운 온도는 if 표현식이 텍스트 요약을 반환하는 대신 .boiling 오류를 던지게 합니다. 이 if 표현식이 오류를 던질 수 있더라도, 앞에 try를 쓰지 않습니다. 오류 처리에 대한 정보는 Error Handling을 참조하세요.

위의 예제에서 보여준 것처럼 할당의 오른쪽에서 if 표현식을 사용하는 것 외에도, 함수나 클로저가 반환하는 값으로 사용할 수 있습니다.

Switch문

switch문은 값을 고려하고 그것을 여러 가능한 일치 패턴과 비교합니다. 그런 다음 성공적으로 일치하는 첫 번째 패턴을 기반으로 적절한 코드 블록을 실행하죠. switch문은 여러 잠재적 상태에 대응하기 위한 if문의 대안을 제공합니다.

가장 단순한 형태로, switch문은 같은 유형의 하나 이상의 값과 값을 비교합니다.

switch <#some value to consider#> {
case <#value 1#>:
    <#respond to value 1#>
case <#value 2#>,
     <#value 3#>:
    <#respond to value 2 or 3#>
default:
    <#otherwise, do something else#>
}
swift

모든 switch문은 case 키워드로 시작하는 여러 개의 가능한 case로 구성됩니다. 특정 값과 비교하는 것 외에도 Swift는 각 case가 더 복잡한 일치 패턴을 지정할 수 있는 몇 가지 방법을 제공합니다. 이러한 옵션은 이 장의 뒷부분에서 설명할 거예요.

if문의 본문과 마찬가지로, 각 case는 코드 실행의 별도 분기입니다. switch문은 어떤 분기를 선택해야 할지 결정하죠. 이 절차를 고려 중인 값에 대해 switching한다고 합니다.

모든 switch문은 exhaustive해야 합니다. 즉, 고려 중인 유형의 모든 가능한 값은 switch case 중 하나와 일치해야 합니다. 모든 가능한 값에 대해 case를 제공하는 것이 적절하지 않은 경우, 명시적으로 처리되지 않은 값을 다루기 위해 기본 case를 정의할 수 있습니다. 이 기본 case는 default 키워드로 표시되며 항상 마지막에 나와야 합니다.

이 예제는 someCharacter라는 단일 소문자를 고려하기 위해 switch문을 사용합니다:

let someCharacter: Character = "z"
switch someCharacter {
case "a":
    print("라틴 알파벳의 첫 글자")
case "z":
    print("라틴 알파벳의 마지막 글자")
default:
    print("그 밖의 다른 문자")
}
// "라틴 알파벳의 마지막 글자"를 출력합니다.
swift

switch문의 첫 번째 case는 영어 알파벳의 첫 번째 글자인 a와 일치하고, 두 번째 case는 마지막 글자인 z와 일치합니다. switch는 모든 알파벳 문자뿐만 아니라 모든 가능한 문자에 대한 case를 가져야 하므로, 이 switch문은 az 이외의 모든 문자와 일치하는 default case를 사용합니다. 이는 switch문이 빠짐없이 완전하도록 보장합니다.

if문과 마찬가지로, switch문도 표현식 형식을 가집니다:

let anotherCharacter: Character = "a"
let message = switch anotherCharacter {
case "a":
    "라틴 알파벳의 첫 글자"
case "z":
    "라틴 알파벳의 마지막 글자"
default:
    "그 밖의 다른 문자"
}
 
print(message)
// "라틴 알파벳의 첫 글자"를 출력합니다.
swift

이 예제에서, switch 표현식의 각 case는 anotherCharacter와 일치할 때 사용할 message에 대한 값을 포함합니다. switch는 항상 빠짐없이 완전하므로, 항상 할당할 값이 있습니다.

if 표현식과 마찬가지로, 위의 예제에서 보여준 것처럼 할당의 오른쪽에서 switch 표현식을 사용할 수 있고, 함수나 클로저가 반환하는 값으로 사용할 수 있습니다.

암시적인 Fallthrough가 없음

C와 Objective-C의 switch문과는 대조적으로, Swift의 switch문은 기본적으로 각 case의 끝에서 다음 case로 계속 실행되지 않습니다. 대신 전체 switch문은 첫 번째 일치 switch case가 완료되는 즉시 실행을 종료합니다. 이는 명시적인 break 문 없이도 switch문을 더 안전하고 쉽게 사용할 수 있게 하며, 실수로 여러 switch case를 실행하는 것을 방지합니다.

각 case의 본문은 반드시 최소한 하나의 실행 가능한 문을 포함해야 합니다. 첫 번째 case가 비어 있기 때문에 다음 코드를 작성하는 것은 유효하지 않습니다:

let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // 유효하지 않음, case에 빈 본문이 있음
case "A":
    print("문자 A")
default:
    print("문자 A가 아님")
}
// 이는 컴파일 시간 오류를 보고할 것입니다.
swift

C의 switch문과는 달리, 이 switch문은 "a""A" 둘 다 일치시키지 않습니다. 오히려 case "a":가 실행 가능한 문을 포함하지 않는다는 컴파일 시간 오류를 보고하죠. 이 접근 방식은 한 case에서 다른 case로 실수로 fallthrough하는 것을 방지하고, 의도가 더 명확한 안전한 코드를 만듭니다.

"a""A" 둘 다 일치하는 단일 case를 가진 switch를 만들려면, 값들을 쉼표로 구분하여 복합 case로 결합하세요.

let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
    print("문자 A")
default:
    print("문자 A가 아님")
}
// "문자 A"를 출력합니다.
swift

가독성을 위해 복합 case를 여러 줄에 걸쳐 작성할 수도 있습니다. 복합 case에 대한 더 많은 정보는 Compound Cases를 참조하세요.

간격 매칭

switch case의 값은 간격에 포함되는지 확인할 수 있습니다. 이 예제는 숫자 간격을 사용하여 어떤 크기의 숫자에 대해서도 자연어 카운트를 제공합니다:

let approximateCount = 62
let countedThings = "새턴 주위를 도는 달"
let naturalCount: String
switch approximateCount {
case 0:
    naturalCount = "없음"
case 1..<5:
    naturalCount = "몇 개"
case 5..<12:
    naturalCount = "여러 개"
case 12..<100:
    naturalCount = "수십 개"
case 100..<1000:
    naturalCount = "수백 개"
default:
    naturalCount = "매우 많음"
}
print("\(countedThings)\(naturalCount) 있습니다.")
// "새턴 주위를 도는 달은 수십 개 있습니다."를 출력합니다.
swift

위의 예제에서 approximateCountswitch문에서 평가됩니다. 각 case는 해당 값을 숫자 또는 간격과 비교합니다. approximateCount의 값이 12와 100 사이에 있기 때문에 naturalCount"수십 개"라는 값이 할당되고, 실행은 switch문 밖으로 옮겨집니다.

튜플

같은 switch문에서 여러 값을 테스트하기 위해 튜플을 사용할 수 있습니다. 튜플의 각 요소는 다른 값이나 값의 간격에 대해 테스트될 수 있습니다. 또는 밑줄 문자(_)로 알려진 와일드카드 패턴을 사용하여 가능한 모든 값과 일치시킬 수도 있죠.

아래 예제는 (Int, Int) 유형의 간단한 튜플로 표현되는 (x, y) 점을 가져와서 예제 다음에 나오는 그래프에서 분류합니다.

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    print("\(somePoint)은 원점에 있습니다.")
case (_, 0):
    print("\(somePoint)은 x축 위에 있습니다.")
case (0, _):
    print("\(somePoint)은 y축 위에 있습니다.")
case (-2...2, -2...2):
    print("\(somePoint)은 상자 안에 있습니다.")
default:
    print("\(somePoint)은 상자 밖에 있습니다.")
}
// "(1, 1)은 상자 안에 있습니다."를 출력합니다.
swift

lecture image

switch문은 점이 원점(0, 0)에 있는지, 빨간색 x축 위에 있는지, 초록색 y축 위에 있는지, 원점을 중심으로 한 파란색 4x4 상자 안에 있는지, 상자 밖에 있는지 결정합니다.

C와는 달리, Swift에서는 여러 switch case가 동일한 값이나 값들을 고려할 수 있습니다. 사실, 점 (0, 0)은 이 예제의 네 가지 case 모두와 일치할 수 있습니다. 하지만 여러 일치가 가능한 경우, 항상 첫 번째로 일치하는 case가 사용됩니다. 점 (0, 0)은 먼저 case (0, 0)과 일치할 것이므로, 다른 모든 일치 case는 무시됩니다.

값 바인딩

switch case는 case의 본문에서 사용하기 위해 일치하는 값이나 값들을 임시 상수나 변수로 이름을 지을 수 있습니다. 이를 값 바인딩이라고 하는데, 값들이 case의 본문 내에서 임시 상수나 변수에 바인딩되기 때문입니다.

아래 예제는 (Int, Int) 유형의 튜플로 표현되는 (x, y) 점을 가져와서 그 뒤에 나오는 그래프에서 분류합니다:

let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
    print("x축 위의 x값은 \(x)입니다.")
case (0, let y):
    print("y축 위의 y값은 \(y)입니다.")
case let (x, y):
    print("다른 곳의 (\(x), \(y))에 있습니다.")
}
// "x축 위의 x값은 2입니다."를 출력합니다.
swift

lecture image

switch문은 점이 빨간색 x축 위에 있는지, 초록색 y축 위에 있는지, 아니면 다른 곳(어느 축 위에도 없음)에 있는지 결정합니다.

switch case는 anotherPoint에서 하나 또는 두 개의 튜플 값을 임시로 가져오는 자리 표시자 상수 xy를 선언합니다. 첫 번째 case인 case (let x, 0)y 값이 0이고 점의 x 값을 임시 상수 x에 할당하는 모든 점과 일치합니다. 마찬가지로, 두 번째 case인 case (0, let y)x 값이 0이고 점의 y 값을 임시 상수 y에 할당하는 모든 점과 일치합니다.

임시 상수가 선언된 후에는 case의 코드 블록 내에서 사용할 수 있습니다. 여기서는 점을 분류하기 위해 사용되죠.

switch문에는 default case가 없습니다. 마지막 case인 case let (x, y)는 모든 가능한 나머지 값과 일치할 수 있는 두 개의 자리 표시자 상수를 선언합니다. anotherPoint는 항상 두 값의 튜플이기 때문에, 이 case는 다른 모든 가능한 값과 일치하므로 default case가 switch문을 완전하게 만들기 위해 필요하지 않습니다.

Where

switch case는 추가 조건을 확인하기 위해 where 절을 사용할 수 있습니다.

아래 예제는 다음 그래프에서 (x, y) 점을 분류합니다:

let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
    print("(\(x), \(y))는 x == y 선 위에 있습니다.")
case let (x, y) where x == -y:
    print("(\(x), \(y))는 x == -y 선 위에 있습니다.")
case let (x, y):
    print("(\(x), \(y))는 그냥 어떤 임의의 점입니다.")
}
// "(1, -1)는 x == -y 선 위에 있습니다."를 출력합니다.
 
swift

lecture image

switch문은 점이 x == y인 초록색 대각선 위에 있는지, x == -y인 보라색 대각선 위에 있는지, 아니면 둘 다 아닌지 결정합니다.

switch case는 임시로 yetAnotherPoint의 두 튜플 값을 가져오는 자리 표시자 상수 xy를 선언합니다. 이 상수들은 동적 필터를 만들기 위해 where 절의 일부로 사용됩니다. switch case는 해당 값에 대해 where 절의 조건이 true로 평가되는 경우에만 point의 현재 값과 일치합니다.

이전 예제와 같이 마지막 case는 가능한 모든 나머지 값과 일치하므로 default case가 switch문을 완전하게 만들기 위해 필요하지 않습니다.

복합 Cases

동일한 본문을 공유하는 여러 switch case는 case 다음에 쉼표로 구분된 여러 패턴을 작성하여 결합할 수 있습니다. 패턴 중 어떤 것이라도 일치하면 해당 case가 일치하는 것으로 간주됩니다. 목록이 길면 패턴을 여러 줄에 걸쳐 작성할 수 있습니다. 예를 들면 다음과 같습니다:

let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
    print("\(someCharacter)는 모음입니다.")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
     "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
    print("\(someCharacter)는 자음입니다.")
default:
    print("\(someCharacter)는 모음도 자음도 아닙니다.")
}
// "e는 모음입니다."를 출력합니다.
swift

switch문의 첫 번째 case는 영어에서의 다섯 개의 소문자 모음과 모두 일치합니다. 마찬가지로 두 번째 case는 영어의 모든 소문자 자음과 일치하죠. 마지막으로 default case는 다른 모든 문자와 일치합니다.

복합 case에는 값 바인딩도 포함될 수 있습니다. 복합 case의 모든 패턴은 동일한 값 바인딩 집합을 포함해야 하며, 각 바인딩은 복합 case의 모든 패턴에서 동일한 유형의 값을 얻어야 합니다. 이렇게 하면 복합 case의 어떤 부분이 일치했는지에 관계없이 case 본문의 코드가 항상 바인딩에 대한 값에 접근할 수 있고 그 값이 항상 동일한 유형을 갖게 됩니다.

let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
    print("원점에서 \(distance) 떨어진 축 위에 있습니다.")
default:
    print("축 위에 있지 않습니다.")
}
// "원점에서 9 떨어진 축 위에 있습니다."를 출력합니다.
swift

위의 case에는 두 개의 패턴이 있습니다: (let distance, 0)은 x축 위의 점과 일치하고 (0, let distance)는 y축 위의 점과 일치합니다. 두 패턴 모두 distance에 대한 바인딩을 포함하며 distance는 두 패턴 모두에서 정수입니다. 이는 case의 본문 코드가 항상 distance에 대한 값에 접근할 수 있음을 의미합니다.