[Swift] guard 문과 함수

승민·2024년 10월 10일
0

iOS

목록 보기
5/12
post-thumbnail

알아야 할 점

  • 본 문서는 iOS 프로그래밍 강의 내용을 중심으로 작성되었어요.
  • 이전 내용을 모르면 이해가 어려울 수 있어요.
  • 앞으로도 계속 내용을 추가할 예정이에요.

Swift의 조금 더 심화된 문법에 대해 알아볼거에요.
guard 문을 통해 옵셔널의 언래핑과 함수의 다양한 기능을 학습할 예정이에요.
마지막으로 1급 객체와 클로저의 개념에 대해 알아볼거에요.

목적

  • guard 문에 대해 알아보고 if 문과 guard 문의 차이점을 확인해볼 거에요.
  • 함수의 여러 값 반환가변 매개변수에 대해 알아볼 거에요.
  • BMI 판정하는 로직을 다양한 방식으로 정의해서 이해를 도울 예정이에요.
  • first class objectfirst class citizen, Clousuer개념에 대해 알아볼 거에요.

학습 순서

guardfunctionBMI

1. guard 문

guard 문은 Swift에서 변수를 선언할 때, 값이 없을 수도 있는 nil 자료형의 오류를 방지하기 위한 방법 중 하나에요.

옵셔널 없애는 것을 언래핑(unwrapping)이라고 해요.
먼저, 기존 옵셔널 언래핑에 대해 간단하게 알아볼게요.

1 - 1. 옵셔널 언래핑 : optional binding

옵셔널을 언래핑하는 방법 중의 하나로 옵셔널 바인딩을 통해서 옵셔널에 할당된 값을 할당할 수 있어요.

  • 옵셔널 언래핑 : optional binding
if let constantname = optionalName {
  // 옵셔널 변수가 값이 있다면 언래핑해서 일반 상수 constantname에 대입하고 if문을 실행해요
  // 값이 없다면 if문의 조건이 거짓이 되어 if문을 실행하지 않아요
}
if var variablename = optionalName {
  // 옵셔널 변수가 값이 있다면 언래핑해서 일반 변수 variblename에 대입하고 if문을 실행해요
  // 값이 없다면 if문의 조건이 거짓이 되어 if문을 실행하지 않아요
}

간단한 예제를 통해서 확인해볼게요.

  • 아래 코드는 옵셔널 변수 xx1을 선언하고 각각 값이 있는 경우없는 경우결과를 확인할 수 있어요.
var x : Int?
x = 10
if let xx = x {
  print(x,xx)
  //옵셔널 변수 x가 값(10)이 있으므로 언래핑해서 일반 상수 xx에 대입하고 if문을 실행해요
}
else {
  print("nil")
}
var x1 : Int?
if let xx = x1 {
  print(xx)
  //옵셔널 변수 x1이 값이 없어서 if문의 조건이 거짓이 되어 if문 실행하지 않고 else문의 내용을 반환해요
}
else {
  print("nil")
}


이렇게 if 문을 활용해서 옵셔널 언래핑을 할 수 있지만, Swift 에서는 guard을 지원해요. 어떤 부분이 다른지 알아볼게요.

1 - 2. guard 문 정의

guard 문의 형태부터 확인해볼게요.

  • guard 문 정의 방법
guard <불리언 표현식> else {
  // 표현식이 거짓일 경우에 실행될 부분이에요
  <코드 블록을 빠져 나갈 구문>
}
// 표현식이 참일 경우에 실행되는 코드는 이곳에 위치해요

이렇게 guard 문은 조건식거짓이면 실행하는 구문이에요.
guard 문의 특징에 대해서 알아볼게요.

  • else 절에 속한 코드는 현재의 코드 흐름을 빠져나갈 수 있는 구문을 반드시 포함해야 해요.
  • 또는, 다른 함수else 코드블록 안에서 호출할 수도 있어요.
  • guard 문은 기본적으로 특정 조건맞지 않으면 현재의 함수나 반복문에서 빠져나갈 수 있도록 하는 조기출구(early exit) 전략을 제공해요.

내용이 길고 복잡하다보니 어렵게 느껴질 수 있어요.

  • 개념을 이해하는 것이 중요하니 간단한 예제를 통해 확인해볼게요.
func multiplyByTen(value: Int?) {
    guard let number = value else { //조건식이 거짓(nil)일 때 else 블록 실행해요
        print("nil")
    return
    }
    print(number*10) //조건식이 참일 때 실행해요
    // 주의할 점이 number를 guard문 밖인 여기서도 사용 가능해요
}
multiplyByTen(value: 3) // 30
multiplyByTen(value: nil)
multiplyByTen(value: 10) // 100


예제를 보면서 다시 얘기하면 다음과 같아요.

  • guard 문은 return, break, continue, throw 등 제어문 전환 키워드를 쓸 수 있는 상황이라면 사용할 수 있어요.
  • 그래서 함수뿐 아니라 반복문 등 특정블록 내부에 있으면 사용 가능해요.
  • 물론 함수 내부에 있다면 보통 return을 써서 해당 함수를 조기에 빠져나오는 조기출구 용도로 사용해요.

실제 앱을 만들다 보면 옵셔널 바인딩 때문다중 if~else를 사용해야 하는데, guard~let을 사용하면 다중루프 없는 훨씬 가독성이 좋은 코드가 가능해서 그런 경우 많이 사용한다고 해요.

1 - 3. if 문과 guard 문의 차이점

guard 문으로 언래핑하는 방법을 알아봤어요.

  • if 문으로 언래핑이 가능하기 때문에 차이점을 확인해볼게요.
func printName(firstName:String, lastName:String?){
    // if let 방법
    if let lName = lastName {   // lastName이 nil이 아닌 경우
        print(lName,firstName)
    }
    else {
        print("성이 없네요!")
    }

    // guard let 방법
    guard let lName = lastName else { // lastName이 nil인 경우
        print("성이 없네요!")
    return // early exit
    }
    print(lName,firstName)
}
printName(firstName: "길동", lastName:"홍")
printName(firstName: "길동", lastName:nil)


가장 먼저 코드의 길이줄어든 것을 확인할 수 있어요.
또한 else 절이 나오는 순서의 차이도 확인할 수 있어요.

조금 더 차이를 체감하기 위해 무비 데이터를 가져오는 코드 일부분if 문 방식과 guard 문 방식으로 각각 확인해볼게요.

  • if 문 방식
func getData() {
    if let url = URL(string: movieURL) {
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: url) { (data, response, error) in
            if error != nil {
                print(error!)
                return
            }
            if let JSONData = data {
                print(JSONData, response!)
                let dataString = String(data: JSONData, encoding: .utf8)
                print(dataString!)
                let decoder = JSONDecoder()
                do {
                    let decodedData = try decoder.decode(MovieData.self, from: JSONData)
                    print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm)
                    print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audienceCnt)
                    self.movieData = decodedData
                    DispatchQueue.main.async {
                        self.table.reloadData()
                    }
                } catch {
                    print(error)
                }
            }
        }
        task.resume()
    }
}
  • guard 문 방식
func getData() {
    guard let url = URL(string: movieURL) else { return }
    let session = URLSession(configuration: .default)
    let task = session.dataTask(with: url) { [self] (data, response, error) in
        if error != nil {
            print(error!)
            return
        }
        guard let JSONData = data else { return }
        let dataString = String(data: JSONData, encoding: .utf8)
        print(dataString!)
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(MovieData.self, from: JSONData)
            print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm)
            print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audienceCnt)
            self.movieData = decodedData
            DispatchQueue.main.async {
                self.table.reloadData()
            }
        } catch {
            print(error)
        }
    }
    task.resume()
}

if 문과 guard 문의 차이는 주로 코드의 흐름가독성에 있기 때문에 두 코드를 비교해볼게요.

  • if
if let url = URL(string: movieURL) {
    // url이 nil이 아닐 때 실행되는 코드
} else {
    // url이 nil일 때 실행되는 코드 (옵션: 처리할 내용이 필요)
}

코드의 흐름: if문을 사용할 경우, 조건이 만족되지 않으면 해당 블록의 코드가 실행되지 않고 아래 코드 블록(else)이 실행돼요.
가독성: 각 조건을 처리하기 위해 추가적인 블록을 작성할 수 있기 때문에 코드가 복잡해질 수 있어요. 코드가 분산되기 때문에 가독성이 떨어질 수 있어요.

  • guard
guard let url = URL(string: movieURL) else { return }
// url이 nil이 아닐 경우 이 아래 코드가 실행됨

코드의 흐름: guard 문은 조건이 만족하지 않으면 즉시 해당 함수에서 빠져나가기 때문에 흐름이 끊기지 않고 코드를 확인할 수 있어요.
가독성: guard 문을 사용하면 오류 처리 및 조건 검사를 한 곳에서 간단히 할 수 있어요. 이는 코드의 주요 흐름이 더 명확해지고 코드가 더 간결하게 유지될 수 있어요.


따라서 특수한 상황이 아닌 경우 Swift에서 if 문 보다는 guard 문을 사용하는 것이 코드 분석에 더 도움이 될 수 있어요.

2. function 심화

이전 내용은 함수를 정의하는 방법에 대해서만 알아봤어요.
이번에는 Swift에서만 나타낼 수 있는 함수의 정의 방법에 대해 알아볼게요.

2 - 1. 디폴트 매개변수

함수를 정의할 때, 인자값(argument)이 없는 경우가 있을 수 있어요.
이 경우 디폴트 매개변수(default argument)를 사용해서 초기값을 지정해줄 수 있어요.

  • 기존의 함수 선언 방법에서 간단한 수정만 하면 되기 때문에 예제를 통해서 확인해볼게요.
func sayHello(count: Int, name: String = "길동") -> String {
  return ("\(name), 너의 번호는 \(count)")
}

var message = sayHello(count:10, name: "소프트")
print(message)

message = sayHello(count:100)
print(message)


이전의 함수 정의 방법에서 자료형 뒤= <Default Value>를 추가하면 돼요.
이렇게 디폴트 매개변수를 지정해주면, 호출 시에 해당 매개변수의 값을 지정하지 않아도 정상적으로 코드가 실행되는 것을 확인할 수 있어요.

2 - 2. 함수의 여러 값 반환

이전에는 입력값이 여러 개, 반환값은 하나 또는 없는 경우의 함수만 다뤘어요.
이제 여러 값을 반환하는 방법에 대해서 알아볼게요.

여러 값을 반환하는 방법은 반환값튜플로 감싸서 반환할 수 있어요.
이해를 돕기 위해 단위 변환 함수의 예제를 통해서 확인해볼게요.

  • 여러 값 반환과 각 값의 접근 방법 예제
func converter(_ length: Float) -> (yards: Float, centimeters: Float, meters: Float) {
    let yards = length * 0.0277778
    let centimeters = length * 2.54
    let meters = length * 0.0254
    return (yards, centimeters, meters)
}

var lengthTuple = converter(10)
print(lengthTuple)
print(lengthTuple.yards)
print(lengthTuple.centimeters)
print(lengthTuple.meters)


함수가 튜플로 반환한 경우 TupleName.<method name>를 통해 값에 접근할 수 있어요.

다른 예제를 통해서 확인해볼게요.

  • 2개의 정수를 입력받아 가감승제를 반환하는 함수
import Foundation

func sss(x : Int, y : Int) -> (sum : Int, sub : Int, mul : Int, div : Double) {
    let sum = x+y
    let sub = x-y
    let mul = x*y
    let div = Double(x)/Double(y) //같은 자료형만 연산 가능
    return (sum, sub, mul, div)
}
var result = sss(x:10,y:3)
print(result.sum)
print(result.sub)
print(result.mul)
print(String(format: "%.3f", result.div))

print(type(of: sss))


  • div의 경우 String(format: "%.3f", result.div)를 사용해서 소수점 3자리에서 반올림된 값을 출력할 수 있어요.

2 - 3. 가변 매개변수

위 내용처럼 여러 값을 매개변수로 받을 수 있지만, 개수를 지정해줘야 하는 단점이 존재해요.
그러나 Swift에서는 가변 매개변수를 통해서 개수 제한을 두지 않고 함수를 정의할 수 있어요.

매개변수 자료형 부분 뒤에 세 개의 점(...)을 사용해서 가변 매개변수설정해줄 수 있어요.
예제를 통해서 확인해볼게요.

  • 가변 매개변수 예제
func displayStrings(strings: String...) {
    for string in strings {
        print(string)
    }
}
displayStrings(strings: "일", "이", "삼", "사")
displayStrings(strings: "one", "two")


다른 예제를 통해서 확인해볼게요.

  • 임의의 개수의 정수 값의 합 출력 함수
func add(numbers: Int...) {
    var sum: Int = 0
    for num in numbers {
        sum += num
    }
    print(sum)
}
add(numbers: 1, 2, 3) // 6
add(numbers: 2, 2, 2, 2, 2) // 10
add(numbers: 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) // 10
add(numbers: 1, 1, 1, 1) // 4


2 - 4. call by address

Swift는 기본적으로 call by value으로 값을 불러오는 방식이에요.

그러나 프로그래밍을 하다보면 주소값을 불러와야 할 때가 있어요.
그럴 때 Swift에서 call by address구현하는 방법에 대해 알아볼게요.

함수가 값을 반환한 후에도 매개변수에 일어난 변화를 유지하려면, 함수의 선언부에서 매개변수입출력 매개변수(inout parameter)로 선언해야 해요.

  • call by address 구현 예제
var myValue = 10
func doubleValue (value: inout Int) -> Int {
    value += value
    return(value)
}
print(myValue)
print(doubleValue(value : &myValue))
print(myValue)


기존에는 함수 선언부에서 변수명 : 자료형 방식이었지만, call by address를 구현하기 위해서는 변수명 : inout 자료형으로 선언 할 경우 구현할 수 있어요.

3. BMI 계산 결과 판정

이제 이전까지의 내용을 통대로 BMI계산하는 프로그램을 작성해볼게요.

3 - 1. if ~ else 방식

if~else로 구현한 판정 로직이에요.
이전에 사용한 String(format: "%.1f", <변수>)를 통해서 소수점 한 자리 까지만 출력해요.

import Foundation

let weight = 60.0
let height = 170.0
let bmi = weight / (height*height*0.0001) // kg/m*m
let shortenedBmi = String(format:"%.1f", bmi)
var body = ""

if bmi >= 40 {
    body = "3단계 비만"
} else if bmi >= 30 && bmi < 40 {
    body = "2단계 비만"
} else if bmi >= 25 && bmi < 30 {
    body = "1단계 비만"
} else if bmi >= 18.5 && bmi < 25 {
    body = "정상"
} else {
    body = "저체중"
}

print("BMI:\(bmi), 판정:\(body)")


3 - 2. 함수 정의 방식

import Foundation

func calcBMI(weight :Double,height :Double) -> String{
    let bmi=weight / (height*height*0.0001) // kg/m*m
    let shortenedBmi = String(format:"%.1f", bmi)
    var body = ""
    if bmi >= 40 {
        body = "3단계비만"
    } else if bmi >= 30 && bmi < 40 {
        body = "2단계비만"
    } else if bmi >= 25 && bmi < 30 {
        body = "1단계비만"
    } else if bmi >= 18.5 && bmi < 25 {
        body = "정상"
    } else {
        body = "저체중"
    }
    return "BMI:\(shortenedBmi), 판정:\(body)"
}

print( calcBMI( weight:62.5, height:172.3 ) )


3 - 3. switch ~ case 방식

import Foundation
func calcBMI (weight : Double, height : Double) {
 let bmi = weight / (height*height*0.0001) // kg/m*m
 let shortenedBmi = String(format: "%.1f", bmi)
 switch bmi {
    case 0.0..<18.5:
    print("BMI:\(shortenedBmi),판정:저체중")
    case 18.5..<25.0 :
    print("BMI:\(shortenedBmi),판정:정상")
    case 25.0..<30.0 :
    print("BMI:\(shortenedBmi),판정:1단계 비만")
    case 30.0..<40.0 :
    print("BMI:\(shortenedBmi),판정:2단계 비만")
    default :
    print("BMI:\(shortenedBmi),판정:3단계 비만")
 }
}
calcBMI(weight:62.5, height: 172.3)


3 - 4. 함수 + switch ~ case 방식

import Foundation

func calcBMI(weight : Double, height : Double) -> String {
    let bmi = weight / (height*height*0.0001) // kg/m*m
    let shortenedBmi = String(format: "%.1f", bmi)
    var body = ""
    switch bmi {
    case 0.0..<18.5:
    body = "저체중"
    case 18.5..<25.0:
    body = "정상"
    case 25.0..<30.0:
    body = "1단계 비만"
    case 30.0..<40.0:
    body = "2단계 비만"
    default:
    body = "3단계 비만"
    }
    return "BMI:\(shortenedBmi), 판정:\(body)"
}

print( calcBMI( weight:60.0, height: 170.0 ) )


4. 1급 객체와 1급 시민

1급 객체(first class object) 또는 1급 시민(first class citizen)란 다음과 같은 조건을 만족하는 언어를 의미해요.

  • 1) 변수에 저장할 수 있어요.
  • 2) 매개변수로 전달할 수 있어요.
  • 3) 리턴값으로 사용할 수 있어요.

조건을 충족하는 것을 1급 객체 또는 1급 시민이라 해요.

따라서 Swift의 함수는 1급 객체에요.
1급 객체는 프로그래밍에서 유연하게 사용할 수 있고, 다양한 방식으로 활용될 수 있다는 의미예요.
그래서 1급 객체는 함수형 프로그래밍이나 고차 함수와 같은 개념에서 매우 중요한 역할을 하게 돼요.

각각의 특성을 나눠서 설명할게요.

4 - 1. 조건1 함수를 변수에 저장 가능

  • Swift는 함수를 데이터 타입처럼 처리할 수 있어요.
  • 다음과 같이 함수를 상수 또는 변수에 할당하는 것이 가능해요.
func inchesToFeet (inches: Float) -> Float {
    return inches * 0.0833333
}
let toFeet = inchesToFeet //함수를 자료형처럼사용
  • 함수를 호출하려면 원래의 함수 이름 대신상수 이름을 이용하여 호출 가능해요.
print(inchesToFeet(inches:10))
print(toFeet(10))  //주의 : argument label인 (inches:) 사용X

4 - 2. 조건2 함수를 매개변수로 사용

func inchesToFeet (inches: Float) -> Float {
    return inches * 0.0833333
}
let toFeet = inchesToFeet
  • 위 함수는 Float형 매개변수, Float형 결과를 반환하기 때문에 함수의 데이터 타입(자료 형)이 (Float) -> Float 이에요.
  • Int와 Double형을 매개변수로 받아서 String을 반환하는 함수의 데이터 타입은 (Int, Double) -> String 이에요.
  • 매개변수로 함수를 받으려면, 함수를 받게 될 함수는 함수의 데이터 타입을 선언해요.
func outputConversion(converterFunc: (Float) -> Float, value: Float) { //함수를 매개변수로 사용
    let result = converterFunc(value) //toFeet(10)
    print("Result = \(result)")
}
outputConversion(converterFunc:toFeet, value: 10) // 피트로 변환하는 inchesToFeet함수 호출

4 - 3. 조건3 함수를 리턴값으로 사용

func inchesToFeet (inches: Float) -> Float {
    return inches * 0.0833333
}
func inchesToYards (inches: Float) -> Float {
    return inches * 0.0277778
}
let toFeet = inchesToFeet
let toYards = inchesToYards
  • 단위를 변환하고 콘솔에 결과를 출력하는 다른 함수
func outputConversion(converterFunc: (Float) -> Float, value: Float) { //함수를 매개변수로 사용
   let result = converterFunc(value)
   print("Result = \(result)")
}
  • outputConversion 함수를 호출할 때 선언된 데이터 타입과 일치하는 함수를 전달해요.
  • 매개변수로 적절한 변환 함수를 전달하면 인치를 피트 또는 야드로 변환하기위하여 동일한 함수가 호출될 수 있어요.
outputConversion(converterFunc:toYards, value: 10) // 야드로 변환하는 inchesToYards함수 호출
outputConversion(converterFunc:toFeet, value: 10) // 피트로 변환하는 inchesToFeet함수 호출
  • 반환 타입으로 함수의 타입을 선언하면 함수도 반환될 수 있어요.
  • 다음 함수는 Boolean 매개변수의 값에 따라 toFeet 함수 또는 toYards 함수를 반환하는 함수에요.
func decideFunction (feet: Bool) -> (Float) -> Float { //매개변수형 리턴형이 함수형
    if feet {
        return toFeet
    } else {
        return toYards
    }
}

4 - 4. 조건을 만족하는 1급 객체

위에서 언급된 1급 객체의 모든 조건을 코드로 직접 실행해서 확인해볼 수 있어요.
아래 코드는 위 내용을 충족하는 내용의 코드에요.

  • 1급 객체 예제
func up(num: Int) -> Int {
    return num + 1
}
func down(num: Int) -> Int {
    return num - 1
}
let toUp = up
print(up(num:10))
print(toUp(10))
let toDown = down

func upDown(Fun: (Int) -> Int, value: Int) {
    let result = Fun(value)
    print("결과 = \(result)")
}
upDown(Fun:toUp, value: 10) //toUp(10)
upDown(Fun:toDown, value: 10) //toDown(10)

func decideFun(x: Bool) -> (Int) -> Int { //매개변수형 리턴형이 함수형
    if x {
        return toUp
    } else {
        return toDown
    }
}
let r = decideFun(x:true) // let r = toUp
print(type(of:r)) //(Int) -> Int
print(r(10)) // toUp(10)


5. 클로저

클로저(Closure)란 특정 작업(함수)과 그 작업이 일어난 곳(환경 또는 상태)을 모두 기억하고 있는 도구를 뜻해요.

각 언어별로 불리는 이름이 달라요. 아래는 주요 언어에서 불리는 이름이에요.

  • C, C++, Objective-Cblock
  • JavaLambda function
  • C#Delegates

5 - 1. 클로저 표현식

클로저 표현식은 독립적인 코드 블록이에요.

func add(x: Int, y: Int) -> Int {
    return(x+y)
}
print(add(x:10, y:20))

let add1 = { (x: Int, y: Int) -> Int in
    return(x+y)
}
print(add1(10, 20))

print(type(of:add1))


클로저는 코드에서 볼 수 있듯이 함수를 정의하지 않고 바로 변수에 저장해서 사용할 수 있어요.
이전의 func <이름>(변수:자료형) ...의 형태가 아닌 변수를 통해 선언하고 호출하는 방식이에요.

클로저 표현식은 매개변수를 받거나, 값을 반환하도록 만들 수도 있어요.

{ (<매개변수 이름>: <매개변수 타입>, ... ) -> <반환 타입> in
    // 클로저 표현식 코드
}

예제를 통해서 확인해볼게요.

  • 두 개의 정수 매개변수를 받아서 정수 결과 값을 반환
let multiply = { (val1: Int, val2: Int) -> Int in // 매개변수리턴형
    return val1 * val2
} // 여기서 multiply의 자료형은 (Int, Int) -> Int
let result = multiply(10, 20) // 상수를 함수처럼 호출,200
print(result)

6. 정리

이렇게 Swift의 더 많은 내용을 알아봤어요.
학습해야 하는 내용이 많아지는 만큼 잘 구분하는 능력을 기르는 것이 중요하겠죠?
각각의 특징을 잘 이해하고 구분할 수 있도록 복습하는 것이 중요해요.

출처 : Smile Han - iOS 프로그래밍 기초

0개의 댓글