[Swift.org와 친해지기] 기본 (The Basics)

Minji Kim·2021년 12월 10일
0
post-thumbnail

개요

드디어 본격적으로 Swift.org에서 제공하는 Language Guide를 통해서 Swift에 대해서 하나씩 살펴볼 것이다.
먼저, 이번에는 상수와 변수, 주석, 세미콜론, 타입 유추, 숫자 리터널, 숫자 타입 변환, 부울, 튜플, 옵셔널, 에러 처리 등 Swift 언어에 있어 기본적인 내용을 살펴볼 것이다.


상수와 변수

상수와 변수 선언

상수는 let 키워드로 선언하고 변수는 var 키워드로 선언한다.
상수의 값은 최초 지정 후 변경할 수 없지만, 변수는 다른 값으로 변경할 수 있다.
그리고 상수와 변수는 사용하기 전에 반드시 선언되어야 한다.

let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0

이 예제에서 최대 로그인 시도 횟수는 절대 변경되지 않아야 하므로 상수로 선언하였고, 현재 로그인 시도 횟수는 로그인 실패 시 값을 증가시켜야 하므로 변수로 선언하였다.

✏️ NOTE
저장한 값이 변경되지 않는다면 상수로 선언해야 한다.
변수는 오직 값을 저장하고 변경이 필요할 때 선언한다.

타입 명시

상수 또는 변수를 선언할 때 저장할 수 있는 값의 종류를 명확하게 하기 위해서 타입을 명시할 수 있다.
상수 또는 변수 이름 뒤에 콜론(:)과 공백 한 칸 뒤에 사용할 타입 이름을 적는다.

var welcomeMessage: String

또한, 같은 타입인 여러 개의 변수를 한꺼번에 타입 명시할 수 있다.

var red, green, blue: Double

✏️ NOTE
실제로 타입 명시가 필요한 경우는 드물다.
상수 또는 변수를 선언할 때 초기값을 지정하면 사용될 타입을 유추할 수 있다.

상수와 변수의 이름

상수와 변수 이름은 유니코드 문자를 포함한 대부분 문자를 사용할 수 있다.

let n = 3.14159
let 你好 = "你好世界"
let 🐶🐮 = "dogcow"

단, 공백, 수학적 기호, 화살표, 개인전용 유니코드, 스칼라 값 또는 선과 박스를 그리는 문자는 사용할 수 없다.
그리고 숫자로 시작하는 이름은 선언할 수 없다.

특정 타입으로 상수 또는 변수를 선언하면 동일한 이름으로 다시 선언하거나, 다른 타입의 값을 저장할 수 없다.
상수를 변수로 바꾸거나 변수를 상수로 바꿀 수도 없다.

✏️ NOTE
Swift 키워드와 동일한 이름의 상수 또는 변수를 선언하려면 이름을 백틱 (`)으로 묶어야 한다.
웬만하면 키워드를 이름으로 사용하지 않는 것이 좋다.

상수와 변수 출력

print(_:separator:terminator:) 함수로 상수 또는 변수의 현재 값을 출력할 수 있다.

print(friendlyWelcome)
// Prints "Bonjour!"

Swift는 긴 문자열에 상수 또는 변수의 이름을 포함하여 Swift가 상수 또는 변수의 현재 값으로 대체하기 위해 문자열 삽입을 사용한다.
이름을 소괄호로 감싸고 여는 소괄호 전에 백슬래시를 추가해야 한다.

print("The current value of friendlyWelcome is \(friendlyWelcome)")
// Prints "The current value of friendlyWelcome is Bonjour!"

주석

코드에서 설명 또는 기록을 위해 실행되지 않는 문자를 추가할 땐 주석을 사용한다.
주석은 코드가 컴파일 될 때 Swift 컴파일러에 의해 무시된다.
Swift에서 주석은 C에서 주석을 다는 방법과 유사하다. 한줄 주석은 두개의 슬래시 (//)로 시작한다.

// This is a comment.

여러줄 주석은 다음과 같이 작성한다.

/* This is also a comment
but is written over multiple lines. */

Swift 여러줄 주석은 C와 다르게 중첩시킬 수 있다.

/* This is the start of the first multiline comment.
 /* This is the second, nested multiline comment. */
This is the end of the first multiline comment. */

중첩된 여러줄 주석을 사용하면 코드에 이미 여러줄 주석이 포함되어 있어도 큰 코드 블럭을 빠르고 쉽게 주석 처리할 수 있다.


세미콜론

Swift는 코드의 각 구문 후에 세미콜론 (;)은 필수조건이 아니다.
그러나 여러 구문을 한줄로 작성할 경우 세미콜론은 필수로 작성되어야 한다.

let cat = "🐱"; print(cat)
// Prints "🐱"

정수

Swift는 8, 16, 32, 64 비트 형태의 부호가 있는 정수와 부호가 없는 정수를 지원한다.
Swift의 모든 타입과 마찬가지로 정수 타입은 대문자로 시작한다.

정수 범위

각 정수 타입의 min과 max 프로퍼티를 통해 정수 타입의 최소값과 최대값을 가져올 수 있다.

let minValue = UInt8.min  // minValue is equal to 0, and is of type UInt8
let maxValue = UInt8.max  // maxValue is equal to 255, and is of type UInt8

Int

Int 정수 타입을 제공한다.
특정 크기의 정수로 작업해야 하는 경우가 아니라면 Int를 사용하는 것이 코드 일관성과 상호 운용성을 지원하기 때문에 좋다.

  • 32-bit 플랫폼에서 Int 는 Int32 와 같은 크기를 가짐
  • 64-bit 플랫폼에서 Int 는 Int64 와 같은 크기를 가짐

UInt

UInt 정수 타입을 제공한다.

  • 32-bit 플랫폼에서 Int 는 UInt32 와 같은 크기를 가짐
  • 64-bit 플랫폼에서 Int 는 UInt64 와 같은 크기를 가짐

✏️ NOTE
UInt는 부호 없는 정수 타입이 필요한 경우에만 사용해라.
저장될 값이 음수가 아니어도 Int를 더 선호한다.
정수값에 Int를 일관되게 사용하면 코드 상호 운용성을 지원하고 다른 숫자 형식 간에 변환할 필요가 없다.


부동 소수점 숫자

부동 소수점 숫자는 3.14159, 0.1, -273.15와 같은 분수 성분을 가진 숫자이다.
부동 소수점은 정수 타입보다 더 넓은 범위의 표현이 가능하다.

  • Double은 64-bit 부동 소수점 숫자를 표기
  • Float는 32-bit 부동 소수점 숫자를 표기

✏️ NOTE
Double은 최소 15자리의 소수점 정확도를 가지고 있는것에 비해 Float는 더 적은 6자리의 정확도를 가진다.
사용할 적절한 부동 소수점 타입은 코드에서 작업해야하는 값의 특성과 범위에 따라 다릅니다. 두 타입 중에는 Double 이 선호됩니다.


타입 세이프티와 타입 유추

Swift는 타입-세이프 언어이다. 타입 세이프 언어를 사용하면 코드가 사용할 수 있는 값의 타입을 명확히 알 수 있다.

타입 검사는 오류를 피하는데 도움이 된다. 그러나 모든 상수와 변수에 타입을 지정해야할 필요는 없다.
특정 타입을 지정하지 않으면 Swift는 타입 유추를 사용한다. 타입 유추를 통해 컴파일러는 타입을 자동으로 유추할 수 있다.

타입 유추는 상수 또는 변수에 초기값을 선언할 때 아주 유용하다.
선언하는 시점에 상수 또는 변수에 리터럴 값 또는 리터럴을 지정하여 수행한다. (리터럴 값은 아래 예에서 42 또는 3.14159 와 같은 값이다.)

let meaningOfLife = 42
// meaningOfLife is inferred to be of type Int

let pi = 3.14159
// pi is inferred to be of type Double

Swift는 부동 소수점 숫자의 타입을 추론할 때 항상 Double 타입으로 추론한다.

let anotherPi = 3 + 0.14159
// anotherPi is also inferred to be of type Double

숫자 리터럴

숫자 리터럴은 아래와 같이 쓸 수 있다.

  • 접두사 없는 10진수
  • 0b 접두사로 2진수
  • 0o 접두사로 8진수
  • 0x 접두사로 16진수
let decimalInteger = 17
let binaryInteger = 0b10001       // 17 in binary notation
let octalInteger = 0o21           // 17 in octal notation
let hexadecimalInteger = 0x11     // 17 in hexadecimal notation

부동 소수점 리터럴은 10진수(접두사 없음) 또는 16진수(접두사 0x) 일 수 있다.
소수점 양쪽에 항상 숫자가 있어야한다.
10진수는 e로 표시되는 지수를 가질 수 있고, 16진수는 p로 표시되는 지수를 가질 수도 있다.

아래의 예에서 모든 부동 소수점 리터럴은 10진수 12.1875 를 가진다.

let decimalDouble = 12.1875
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0

숫자 리터럴은 읽기 쉽게 포맷을 추가로 만들수도 있다.
정수와 부동 소수점 모두 0과 _을 추가로 포함하여 가독성을 높일 수도 있다.

let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1

숫자 타입 변환

정수 변환

Int8 타입은 -128 ~ 127 사이의 숫자를 저장할 수 있고, UInt8 타입은 0 ~ 255 사이의 숫자를 저장할 수 있다.
이렇게 크기가 지정된 정수 타입의 상수 또는 변수에 맞지 않는 숫자를 할당하면 컴파일 시 오류가 발생한다.

let cannotBeNegative: UInt8 = -1
// UInt8 cannot store negative numbers, and so this will report an error
let tooBig: Int8 = Int8.max + 1
// Int8 cannot store a number larger than its maximum value,
// and so this will also report an error

특정 숫자 타입을 다른 숫자 타입으로 변환하려면 아래 예제에서 UInt16(one)처럼 기존 값에 원하는 타입의 새 숫자를 초기화하면 된다.

let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one)

정수와 부동 소수점 변환

여기서 상수 three는 Double 타입의 새로운 값으로 사용되어 덧셈의 양쪽이 동일한 타입이다. 이 변환이 없으면 덧셈이 허용되지 않는다.

let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine
// pi equals 3.14159, and is inferred to be of type Double

부동 소수점을 정수로 변환하는 것 또한 명시적으로 변환해야 합니다.
부동 소수점 값은 새로운 정수 값으로 초기화할 때 소수점 아래를 버린다.

let integerPi = Int(pi)
// integerPi equals 3, and is inferred to be of type Int

✏️ NOTE
숫자 상수와 변수를 결합하는 규칙은 숫자 리터럴 규칙과 다르다.
리터럴 값 3은 숫자 리터럴에 명시적인 타입이 없으므로 리터럴 값 0.14159 에 직접 추가할 수 있다.

,
let pi = 3 + pointOneFourOneFiveNine 는 되고
let pi = three + pointOneFourOneFiveNine 는 안된다.

타입 별칭

타입 별칭은 이미 존재하는 타입을 다른 이름으로 정의하는 것이다.
typelias 키워드를 사용하여 정의할 수 있다.

typealias AudioSaple = UInt16

타입 별칭을 정의하면 원래 이름을 사용할 수 있는 모든 위치에서 별칭을 사용할 수 있게 된다.

var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound is now 0

부울

Swift는 Bool 타입이 있는데, 이는 오직 참(true)과 거짓(false) 값만 가진다.

let orangesAreOrange = true
let turnipsAreDelicious = false

Bool 값은 if 구문과 같은 조건문으로 동작할 때 유용하다.

if turnipsAreDelicious {
    print("Mmm, tasty turnips!")
} else {
    print("Eww, turnips are horrible.")
}
// Prints "Eww, turnips are horrible."

Swift는 Bool이 아닌 값이 Bool로 대체되는 것을 방지한다.
아래 예제는 컴파일 시 에러를 발생한다.

let i = 1
if i {
    // this example will not compile, and will report an error
}

그러나 아래와 같은 경우 i == 1 비교 결과가 Bool 타입이므로 정상적으로 동작한다.

let i = 1
if i == 1 {
    // this example will compile successfully
}

튜플

튜플은 여러 값을 단일 복합 값으로 그룹화한다. 튜플 안에 값은 어떠한 타입도 가능하고 서로 같은 타입일 필요도 없다.

아래 예제에서 (404, "Not Found") 튜플은 Int 와 String 을 함께 그룹화하였다. "튜플의 타입은 (Int, String)" 이라고 설명할 수 있다.

let http404Error = (404, "Not Found")
// http404Error is of type (Int, String), and equals (404, "Not Found")

튜플의 내용을 별도의 상수 또는 변수로 분해할 수도 있다.

let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
// Prints "The status code is 404"
print("The status message is \(statusMessage)")
// Prints "The status message is Not Found"

또한, 튜플의 값 중 일부만 필요한 경우 튜플을 분해할 때 밑줄 (_)로 튜플의 일부를 무시할 수 있다.

let (justTheStatusCode, _) = http404Error
print("The status code is \(justTheStatusCode)")
// Prints "The status code is 404"

또는 0에서 시작하는 인덱스를 사용하여 튜플의 개별 요소 값에 접근할 수 있다.

print("The status code is \(http404Error.0)")
// Prints "The status code is 404"
print("The status message is \(http404Error.1)")
// Prints "The status message is Not Found"

튜플을 정의할 때 튜플의 요소에 이름을 정하고, 요소 이름으로 접근할 수 있다.

let http200Status = (statusCode: 200, description: "OK")

print("The status code is \(http200Status.statusCode)")
// Prints "The status code is 200"
print("The status message is \(http200Status.description)")
// Prints "The status message is OK"

✏️ NOTE
튜플은 복잡한 데이터 구조가 아닌 관련된 값의 간단한 그룹을 생성하는데 유용하다. 데이터 구조가 복잡한 경우 클래스 또는 구조체를 사용하는 것이 좋다.


옵셔널

값이 없는 경우에 옵셔널을 사용한다. 옵셔널은 2가지의 경우가 있는데, 값이 있어서 옵셔널을 풀고 값에 접근하는 경우와 값이 없는 경우로 나뉜다.

✏️ NOTE
옵셔널의 개념은 C 또는 Objective-C에는 존재하지 않는 개념이다.
Objective-C에서 가장 비슷한 것은 객체를 반환하는 메서드에서 nil 을 반환하는 것인데, 이는 구조체, 기본 C 타입, 또는 열거형 값에서는 동작하지 않는다.
이러한 경우 Objective-C 메서드는 일반적으로 값이 없음을 나타내는 NSNotFound 와 같은 특수한 값을 반환한다.
Swift의 옵셔널은 특수한 상수 없이 어떠한 타입 에 대해서 값이 없음을 나타낼 수 있다.

아래 예제에서 String을 Int로 초기화하는데 실패할 수 있으므로 Int가 아닌 옵셔널 Int를 반환한다.
여기서 옵셔널 Int는 Int가 아닌 Int?로 작성한다.

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber is inferred to be of type "Int?", or "optional Int"

nil

옵셔널 변수에 특수한 값 nil 로 지정하여 값이 없는 상태를 나타낸다.

var serverResponseCode: Int? = 404
// serverResponseCode contains an actual Int value of 404
serverResponseCode = nil
// serverResponseCode now contains no value

✏️ NOTE
옵셔널이 아닌 상수와 변수에는 nil 을 사용할 수 없다.
코드에서 상수 또는 변수가 값이 없는 상태에서 동작이 필요하다면 항상 해당 타입의 옵셔널 값으로 선언해야 한다.

기본값이 없이 옵셔널 변수를 정의하면 이 변수는 자동적으로 nil 로 설정된다.

var surveyAnswer: String?
// surveyAnswer is automatically set to nil

✏️ NOTE
Swift의 nil 은 Objective-C의 nil 과 다르다.
Objective-C에서의 nil 은 존재하지 않는 객체에 대한 포인터다.
Swift에서의 nil 은 특정 타입의 값이 없음을 나타내며 포인터가 아니다.
객체 타입뿐만 아니라 모든 타입의 옵셔널을 nil로 설정할 수 있습니다.

if 구문과 강제로 풀기

if 구문은 옵셔널과 nil 을 비교하여 옵셔널에 값이 포함되어 있는지 확인할 수 있다. == 또는 !=로 비교할 수 있다.

if convertedNumber != nil {
    print("convertedNumber contains some integer value.")
}
// Prints "convertedNumber contains some integer value."

옵셔널에 값이 포함되어 있다고 확신하면 옵셔널 이름 끝에 느낌표 (!)를 추가하여 값에 접근할 수 있다. 이를 unwrapping 이라고 한다.

if convertedNumber != nil {
    print("convertedNumber has an integer value of \(convertedNumber!).")
}
// Prints "convertedNumber has an integer value of 123."

✏️ NOTE
값이 없는 옵셔널에 ! 를 사용하면 런타임 에러가 발생한다.
항상 ! 를 사용하여 값을 강제로 풀기 전에 옵셔널에 nil 이 아닌 값이 있다는 것을 확실시해야 한다.

옵셔널 바인딩

옵셔널 바인딩이란, 옵셔널이 값을 포함하고 있는지 확인하고 값이 있다면 해당 값을 임시 상수 또는 변수로 사용할 수 있게 해주는 것이다.

if 구문에서 옵셔널 바인딩은 아래와 같이 사용할 수 있다.
unwrapping 말고도 옵셔널 바인딩을 사용하여 옵셔널을 풀 수 있다.

if let actualNumber = Int(possibleNumber) {
    print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
    print("The string \"\(possibleNumber)\" could not be converted to an integer")
}
// Prints "The string "123" has an integer value of 123"

필요한 경우 if 구문에 쉼표로 구분하여 여러개를 포함할 수 있다.

if let firstNumber = Int("4"), <let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
    print("\(firstNumber) < \(secondNumber) < 100")
}
// Prints "4 < 42 < 100"

if let firstNumber = Int("4") {
    if let secondNumber = Int("42") {
        if firstNumber < secondNumber && secondNumber < 100 {
            print("\(firstNumber) < \(secondNumber) < 100")
        }
    }
}
// Prints "4 < 42 < 100"

✏️ NOTE
if 구문에서 옵셔널 바인딩으로 생성된 상수와 변수는 오직 if 구문의 본문 안에서만 사용할 수 있다.
반면 guard 구문으로 생성된 상수와 변수는 guard 구문 다음 코드 라인부터 사용할 수 있다.

암시적으로 언래핑된 옵셔널

때로 옵셔널이 값이 항상 있다고 가정할 수 있으므로 접근할 때마다 옵셔널 값을 확인하고 언래핑할 필요가 없다.

이럴 때 옵셔널은 타입 뒤에 물음표(String?)를 작성하는 대신에 느낌표(String!)로 암시적으로 언래핑된 옵셔널로 정의한다.

암시적으로 언래핑된 옵셔널은 내부적으로 옵셔널이지만 접근할 때마다 값을 풀 필요 없어 옵셔널이 아닌 것처럼 사용할 수 있다.
다음 예제는 옵셔널 문자열과 암시적으로 언래핑된 옵셔널 문자열의 동작 차이를 보여준다.

let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // requires an exclamation point

let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // no need for an exclamation point

암시적으로 언래핑된 옵셔널이 nil 이고 래핑된 값에 접근하려고 하면 런타임 에러가 발생한다. 이 결과는 값이 없는 옵셔널 뒤에 느낌표를 배치한 것과 같다.
암시적으로 언래핑된 옵셔널은 일반 옵셔널과 같은 방법으로 nil 체크를 할 수 있다.

if assumedString != nil {
    print(assumedString!)
}
// Prints "An implicitly unwrapped optional string."

옵셔널 바인딩과 함께 암시적으로 언래핑된 옵셔널은 단일 구문으로 해당 값을 확인하고 언래핑할 수 있다.

if let definiteString = assumedString {
    print(definiteString)
}
// Prints "An implicitly unwrapped optional string."

✏️ NOTE
나중에 변수가 nil 이 될 가능성이 있다면 암시적으로 언래핑된 옵셔널을 사용하지 말아야 한다. 변수의 수명에 따라 nil 값을 확인해야 한다면 항상 기본 옵셔널을 사용하자.


에러 처리

에러 처리를 사용하면 에러 원인을 판별하고 필요한 경우 에러를 프로그램의 다른 부분으로 전파할 수 있다.

throws 키워드를 사용해 에러를 발생시킬 수 있다.

func canThrowAnError() throws {
    // this function may or may not throw an error
}

do 구문은 에러를 하나 이상의 catch 절로 전파할 수 있는 새로운 범위를 만든다.
그리고 try 키워드를 사용하여 에러를 발생할 수 있는 표현식 앞에 붙인다.
Swift는 catch 절에 의해 에러가 처리될 때까지 현재 범위에서 에러를 자동으로 전파한다.

do {
    try canThrowAnError()
    // no error was thrown
} catch {
    // an error was thrown
}

다음은 에러 처리를 사용하여 다양한 에러 조건에 응답하는 예시이다.
이 예에서 makeASandwich() 함수는 에러를 발생할 수 있으므로 try 표현식으로 래핑되었다.
에러가 발생하지 않으면 eatASandwich() 함수가 호출되고, SandwichError.outOfCleanDishes 에러가 발생하면 washDishes() 함수가 호출된다.

func makeASandwich() throws {
    // ...
}

do {
    try makeASandwich()
    eatASandwich()
} catch SandwichError.outOfCleanDishes {
    washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
    buyGroceries(ingredients)
}

주장과 전제조건

주장과 전제조건은 런타임시 발생하는 조건으로, 추가 코드를 실행하기 전에 사용하여 필수조건이 충족되는지 확인할 수 있다.
true라면 코드는 그대로 진행되고, false라면 코드 실행이 종료되고 앱도 종료된다.

주장을 통한 디버깅

assert(::file:line:) 함수로 주장을 작성할 수 있다. 이 함수에 true 또는 false 로 판단될 표현식과 조건이 false 일 경우 출력될 메세지를 전달한다.

아래 예에서 age >= 0 이 true 일 경우 assert문이 실행된다. age 가 음수이면 age >= 0 은 false 가 되고 애플리케이션은 종료된다.

let age = -3
assert(age >= 0, "A person's age can't be less than zero.")
// This assertion fails because -3 is not >= 0.

메세지를 생략할 수도 있다.

assert(age >= 0)

코드가 이미 조건이 체크되었다면 주장이 실패되었는지를 알 수 있는 assertionFailure(_:file:line:) 함수를 사용한다.

if age > 10 {
    print("You can ride the roller-coaster or the ferris wheel.")
} else if age >= 0 {
    print("You can ride the ferris wheel.")
} else {
    assertionFailure("A person's age can't be less than zero.")
}

강제 전제조건

precondition(::file:line:) 함수로 전제조건을 작성할 수 있다. 이 함수에 true 또는 false 로 판단될 표현식과 조건이 false 일 경우 출력될 메세지를 전달한다.

// In the implementation of a subscript...
precondition(index > 0, "Index must be greater than zero.")

preconditionFailure(_:file:line:) 함수를 호출하여 실패가 발생했음을 알릴 수 있다.

✏️ NOTE
컴파일러는 전제조건은 항상 참이라고 가정하고 코드에 알맞게 최적화 하지만 fatalError(_:file:line:) 함수는 이와 무관하게 실행을 중지시킨다.
fatalError("Unimplemented") 와 같이 작성할 수 있는데, 주장 또는 전제조건과 다르게 치명적인 에러는 절대 최적화 되지 않기 때문에 이 구현을 만나면 항상 중지된다.


마무리

Swift 언어의 가장 Basic 한 내용을 살펴보았다.
필자가 Swift.org의 Document를 읽어보고 이해한 내용으로 정리해 보았다.
다음에는 기본 연산자에 대한 내용을 살펴보자.

profile
iOS Developer

0개의 댓글