애플사이다의 iOS 개발 일지

[Swift Language Guide 정독 시리즈] 1. The Basics 본문

Swift/영문 공식문서 뜯어보기-Swift

[Swift Language Guide 정독 시리즈] 1. The Basics

Applecider 2021. 9. 17. 05:40

안녕하세요. 애플 사이다입니다.

[Swift Language Guide 정독 시리즈]의 첫 번째 챕터 The Basics에 대해 정리해보겠습니다.

*Swift Language Guide를 읽어야 하는 이유는 시리즈 0. Language Guide란? 포스팅을 참고해주세요.

 


The Basics

Swift는 iOS, macOS, watchOS, and tvOS 앱 개발을 위한 프로그래밍 언어이다. 

 

Swift의 기본 데이터 타입 (Data Type)으로 1) Int 타입, 2) Float 타입과 Double 타입, 3) Bool 타입, 4) String 타입이 있다.

그리고 기본 Collection Type으로 1) Array, 2) Set, 3) Dictionary가 있다. (Collection Type은 1개의 상수/변수에 여러 가지 데이터를 저장할 수 있는 타입이다.)

참고 - Swift에서 모든 타입 (Type)의 이름은 대문자로 시작한다. (대문자 카멜케이스를 사용한다.)

 

다른 프로그래밍 언어와 마찬가지로 메모리에 어떤 값 (Value)을 저장할 때, Swift는 식별 가능한 이름 (identifying name)으로 네이밍한 변수 (Variable)를 사용한다. 또한 값을 변경할 수 없는 상수 (Constant)를 통해 보다 안전하고 의도를 명확히 담는 프로그래밍을 할 수 있다.

 

Object-C에는 없는 타입인 Tuple 타입도 있는데, 여러 값들을 묶은 형태 (grouping of values)이다. 

 

상수/변수에 값이 없는 nil 상태에 대비하여 옵셔널 타입 (Optional Type)을 사용할 수 있다. 옵셔널은 1) 값이 있거나, 2) 값이 없는 상태를 나타낸다. 옵셔널은 Object-C의 nil 포인터와 비슷하지만, Swift에서는 옵셔널을 클래스뿐만 아니라 모든 타입에 사용할 수 있다. 

 

Swift는 값의 타입을 명확히 하는 type-safe한 언어이다. 실수로 String 타입이 올 위치에 Int 타입이나 Optional String 타입의 값을 전달하지 않도록 방지한다. 

Constants and Variables (상수와 변수)

Constants and variables associate a name (such as welcomeMessage) with a value of a particular type (such as the string "Hello").

 

상수/변수는 특정 타입의 값을 메모리에 저장하기 위해 어떤 이름에 연관 짓는다.

즉, string 타입의 "Hello"라는 값을 메모리에 저장할 때, welcomeMessage라는 이름에 묶어서 저장하는 것이다.

 

상수는 한 번 설정하면 값 변경이 불가하고, 변수는 여러 번 값 변경이 가능하다.

Declaring Constants and Variables (상수와 변수 선언하기)

상수/변수는 사용하기 전에 선언해야 한다. 상수는 let 키워드로, 변수는 var 키워드로 선언한다. 

아래의 예시는 사용자의 로그인 시도 횟수를 기록하기 위해 어떻게 상수와 변수를 사용하는지 설명한다.

let maximumNumberOfLoginAttempts = 10 // maximumNumberOfLoginAttempts라는 이름의 상수를 선언하고, 10이라는 값을 할당한다.
var currentLoginAttempt = 0 // currentLoginAttempt라는 이름의 변수를 선언하고, 0이라는 초기값을 할당한다.

// 복수의 상수/변수를 선언할 때, 한 줄에 나타내려면 comma (,)로 구분한다.
var x = 0.0, y = 0.0, z = 0.0

여기서 maximumNumberOfLoginAttempts를 변수가 아닌 상수로 선언한 이유는 로그인 시도가 가능한 최대 횟수를 10이라는 고정값으로 설정했으므로 앞으로 이 값을 변경할 필요가 없기 때문이다. currentLoginAttempt는 로그인을 실패할 때마다 증가하는 값을 나타내므로 변수로 설정했다.

// 참고
let sum: Int
sum = 10 // 가능

let sumError: Int
// print(sumError) // 값 할당 전에 상수를 사용하면 컴파일 에러 발생 - initialize 전에 사용했다는 오류 메세지가 뜸 (Constant 'sum' used before being initialized)
Note: If a stored value in your code won’t change, always declare it as a constant.

 

❗️선언 이후 값을 변경하지 않는다면, 항상 변수가 아니라 상수로 선언한다. (상수는 "앞으로 값을 변경하지 않을 것"이라는 의도를 명시하므로 다른 개발자들이 코드의 의도를 쉽게 파악할 수 있고, 개발 과정에서 실수로 값을 변경하는 것을 방지할 수 있기 때문이다.)

Type Annotations (타입 지정)

상수/변수를 선언할 때, 타입을 지정할 수 있다. 해당 상수/변수에 저장될 값이 어떤 타입일지 명시하는 것이다.

var welcomeMessage: String 
// Declare a variable called welcomeMessage that’s of type String.
// welcomeMessage라는 변수를 선언한다. 이 변수는 String 타입의 값을 저장한다. (of type String)

var red, green, blue: Double // 동일한 타입의 변수들을 한 줄에 선언할 때, comma (,)로 구분하고 마지막에 타입 지정을 한다.
Note: It’s rare that you need to write type annotations in practice. If you provide an initial value for a constant or variable at the point that it’s defined, Swift can almost always infer the type to be used for that constant or variable

 

❗️상수/변수를 선언하는 시점에 초기값을 할당하면, Type Annotation을 하지 않아도 컴파일 에러가 발생하지 않는다. Swift가 Type Inference (타입 추론)를 제공하기 때문이다. 

하지만 초보자 단계에서는 Type Annotation으로 타입을 명시하는 것이 좋다. 잘못된 Type Inference로 인해 디버깅이 어려워질 수 있기 때문이다. (예를 들어 초기값을 2로 지정하면, 원래 Double 타입을 의도했더라도 Type Inference로 인해 자동으로 Int 타입으로 선언된다.)

또한 타입을 명시하면, 컴파일 속도도 개선된다. 

Naming Constants and Variables (상수와 변수 이름짓기)

상수/변수의 이름에는 유니코드 문자 (Unicode character)를 포함하여 거의 모든 문자 (chracter)를 사용할 수 있다.

단, 공백 문자 (whitespace characters, space/tab 공백 등), 수학 기호, 화살표, 개인전용 유니코드 스칼라 값 (private-use Unicode scalar values), 선이나 박스를 그리는 문자 (line- and box-drawing characters)는 사용 불가하다.

이름에 숫자를 사용할 수 있지만, 이름의 맨 앞에 숫자가 올 수 없다.

 

특정 타입의 상수/변수를 한 번 선언한 이후에는 동일한 이름으로 재선언할 수 없고, 다른 타입으로 변경할 수 없다. 또한 변수를 상수로, 상수를 변수로 변경할 수 없다.

let π = 3.14159
let 你好 = "你好世界"
let 🐶🐮 = "dogcow"
var 한글변수 = "변경 가능한 문자열"

 

Note: Swift의 키워드 (reserved Swift keyword)를 상수/변수 이름으로 사용할 경우, 키워드 양 옆에 backtick (`)을 입력한다.

단, 불가피한 상황일 때만 키워드를 이름으로 사용한다.

(backtick 입력 방법 : option + 키보드에서 1키 왼쪽의 ₩키)

Printing Constants and Variables (상수와 변수 출력)

print(_:separator:terminator:) 함수를 사용하여 상수/변수의 현재 값을 출력 가능하다.

print 함수는 전역 함수 (global function) 이고, 출력 내용은 Xcode의 console 화면에 표시된다. 

separator 및 terminator 매개변수는 기본값 (default value)을 가지므로 이 함수를 호출할 때 해당 매개변수를 생략할 수 있다.

terminator 기본값이 "\n"이므로 따로 설정하지 않아도 출력을 끝낼 때, 줄바꿈 (line break)을 한다. 

*print 함수에 대한 추가적인 설명은 print 함수 포스팅을 참고해주세요.

 

String Interpolation (문자열 보간법)은 긴 문자열 사이에 특정 값을 출력할 때 유용하다. 

상수/변수 이름을 넣고, 문자열 사이에 배치하면, 해당 상수/변수의 값이 문자열로 대체된다.

var friendlyWelcome = "Bonjour!"
print(friendlyWelcome) // Prints "Bonjour!"

// String Interpolation (문자열 보간법)
print("The current value of friendlyWelcome is \(friendlyWelcome)") // Prints "The current value of friendlyWelcome is Bonjour!"

참고 - 백슬래쉬 (backslash), 괄호 (parenthese)

*String Interpolation (문자열 보간법)의 자세한 내용은 Strings and Characters 챕터에서 다루겠습니다.

Comments (주석)

주석을 사용하면 코드에 실행 불가능한 텍스트를 작성할 수 있다. 나를 포함한 다른 개발자가 참고할 만한 내용을 기입한다. 

Swift 컴파일러는 주석을 무시한다. 

주석의 종류는 아래와 같이 1) Single-line comment, 2) Multiline comment, 3) Nested multiline comment가 있다.

// This is a comment.

/* This is a multiline comment,
which is written over multiple lines. */

/* This is a Nested multiline comment. 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. */

참고 - 슬래쉬 (forward-slash), * (asterisk)

Semicolons (세미콜론)

Swift에서는 하나의 구문 (statement)이 끝날 때마다 세미콜론 (;)을 붙이지 않아도 된다. 

단, 여러 개의 구문을 한 줄에 나타내고 싶다면 세미콜론을 사용할 수는 있다.

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

참고 - expression 및 statement의 차이

*expression : 수식, 연산 (evaluate)을 통해 값으로 환원됨

*statement : 구문, 실행 가능한 독립적인 코드 조각 (statement은 expression을 포함할 수 있음)

Integers (정수)

정수는 42, -23과 같은 숫자를 말한다. (소수점 수가 없다.)

1) 부호가 있는 정수 (Signed Integer)는 "양수, 0, 음수", 2) 부호가 없는 정수 (Unsigned Integer)는 음수를 제외한 "양수, 0"을 포함한다.

 

Int (부호가 있는 Integer) 및 UInt (부호가 없는 Unsigned Integer)는 8/16/32/64 비트의 형태가 있다.

각각 UInt8/UInt16/UInt32/UInt64 그리고 Int8/Int16/Int32/Int64로 나타낸다.

 

Integer 타입의 최소값/최대값은 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 

대부분의 경우, 위의 UInt8 처럼 정수의 크기를 특정해서 사용할 필요가 없다. 

추가적인 integer 타입인 "Int 타입"을 주로 사용하기 때문이다. Int 타입의 크기는 플랫폼의 시스템 아키텍처에 따라 달라진다.

  • 32-bit 플랫폼에서 Int의 크기는 Int32와 같다.
  • 64-bit 플랫폼에서 Int의 크기는 Int64와 같다.

참고 - 시스템 아키텍처는 32-bit 또는 64-bit가 있다. 최근 대부분의 컴퓨터 및 모바일의 아키텍처는 64-bit이다.

(아키텍처는 CPU가 1회 연산할 때 처리 가능한 bit 수를 나타낸다.)

UInt 

부호가 없는 정수 (Unsigned Integer)는 UInt 타입으로 나타낸다. 

Note: Use UInt only when you specifically need an unsigned integer type with the same size as the platform’s native word size. If this isn’t the case, Int is preferred, even when the values to be stored are known to be nonnegative.
A consistent use of Int for integer values aids code interoperability, avoids the need to convert between different number types.

 

❗️특수한 상황이 아니면, 정수 값을 나타낼 때 항상 Int 타입을 사용한다.

Swift에서는 다른 타입끼리 연산이 불가하므로 같은 정수더라도 Int 타입과 UInt 타입을 연산하려면 타입 변환 (Type Conversion)이 필요하기 때문이다.

Floating-Point Numbers (부동소수점 수)

부동소수점 수는 3.14159 / 0.1 / -273.15 과 같이 소수점 수를 포함한 숫자이고, 두 가지 타입이 있다.

  • Double represents a 64-bit floating-point number. (Double 타입은 64 비트 부동소수점 수를 나타낸다.)
  • Float represents a 32-bit floating-point number. (Float 타입은 32 비트 부동소수점 수를 나타낸다.)
Double has a precision of at least 15 decimal digits, whereas the precision of Float can be as little as 6 decimal digits. The appropriate floating-point type to use depends on the nature and range of values you need to work with in your code. In situations where either type would be appropriate, Double is preferred.

 

❗️Double 타입의 정밀도 (precision)는 십진수 15자리이다. 즉, 최소 15자리의 십진수를 표현할 수 있다. 반면 Float 타입은 6자리의 십진수를 표현할 수 있다. 사용하는 값의 범위에 따라 타입을 결정하면 되지만, 둘 다 가능한 경우 Double 타입을 사용하는 것이 좋다.

var someDouble: Double = 3.14
someDouble = 3  // 정수도 할당 가능

Type Safety and Type Inference (타입 안전성 및 타입 추론)

Swift는 값의 타입을 명확히 하는 type-safe한 언어이다. 실수로 String 타입이 올 위치에 Int 타입이나 Optional String 타입의 값을 전달하지 않도록 방지한다. 컴파일러는 컴파일 시 타입이 일치하는지 확인하는 타입 확인 (type check)을 수행하므로 개발 과정에서 발생하는 에러를 초기에 고칠 수 있다.

 

하지만, 상수/변수 선언 시 타입을 지정하지 않아도 할당된 값을 기준으로 컴파일러가 알아서 타입을 추론하는 타입 추론 (Type Inference) 기능이 있다. 특히 상수/변수를 선언하면서 초기값을 지정하는 경우 유용하다. 리터럴값 (literal value) 또는 리터럴 (literal)을 할당하는 경우가 이에 속한다. (리터럴 값은 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

let anotherPi = 3 + 0.14159
// anotherPi is also inferred to be of type Double
  • 42는 실제로 42.0일 수 있지만, 타입 추론으로 인해 Double/Float이 아닌 Int 타입이 된다.
  • 부동소수점 수를 타입 추론하면, 항상 Float이 아닌 Double이 된다.
  • Int 타입 리터럴과 Double 타입 리터럴을 포함하는 수식 (expression)이 있으면, 결과값은 Double 타입으로 추론된다.

Numeric Literals (숫자 리터럴)

10진수 17을 나타내는 2/8/16진수 리터럴은 아래와 같다. (정수 리터럴)

let decimalInteger = 17
let binaryInteger = 0b10001    // 17 in binary notation (2진수 prefix 0b)
let octalInteger = 0o21        // 17 in octal notation (8진수 prefix 0o)
let hexadecimalInteger = 0x11  // 17 in hexadecimal notation (16진수 prefix 0x)

부동소수점 수 리터럴은 10진수 또는 16진수이다. 항상 소수점의 양쪽에는 10진수 또는 16진수가 있어야 한다.

(They must always have a number (or hexadecimal number) on both sides of the decimal point.)

 

10진수는 필요 시 지수 (exponent)를 가질 수 있으며, "E" 또는 "e"로 나타낸다. (x 10exp)

  • 1.25E2 = 1.25e2 = 1.25 x 10^2 = 125.0
  • 1.25E-2 = 1.25e-2 = 1.25 x 10^(-2) = 0.0125

16진수 소수점 수는 반드시 지수를 가지며, "P" 또는 "p"로 나타낸다. (x 2exp)

  • 0xFP2 = 0xFp2 = 15 x 2^2 = 60.0 (참고 OxF = Oxf = 십진수 15)
  • 0xFP-2 = 0xFp-2 = 15 x 2^(-2) = 3.75
let decimalDouble = 12.1875  // 세 리터럴 모두 십진수 값 12.1875와 일치한다.
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0

가독성을 위해 숫자 리터럴에 0을 추가하거나 (can be padded with extra zeros), 밑줄 (underscore)을 넣을 수 있다. 값에는 영향이 없다.

let paddedDouble = 000123.456  // 123.456과 동일함
let oneMillion = 1_000_000  // 1000000과 동일함
let justOverOneMillion = 1_000_000.000_000_1

Numeric Type Conversion (숫자 타입 변환)

정수를 다룰 때는 항상 Int 타입을 사용하는 것이 호환성 측면에서 유리하다. 

다른 integer 타입은 외부로부터 특정 크기의 데이터를 전달받거나, 성능, 메모리 사용, 최적화 등에 관련된 특수한 상황일 때만 사용한다.

크기를 명시한 타입을 사용하면, 오버플로우 (overflow)를 방지하고, 암시적으로 해당 데이터의 특성을 문서화할 수 있다.

Integer Conversion (정수 변환)

Integer 타입의 상수/변수에 저장 가능한 숫자의 범위는 타입마다 다르다. 

데이터 표현 범위를 보면, Int8 (8비트 Int 타입)은 -128 ~ 127 이고, UInt8 (8비트 UInt 타입)은 0 ~ 255 이다.

❗️둘 다 8비트 크기이므로 2^8 = 256개의 숫자를 나타낼 수 있다. 부호가 없는 UInt8은 0과 양수 1~255를 나타낸다.

부호가 있는 Int8은 양수/음수를 구분하기 위해 8비트 중 1비트를 부호에 사용하므로 UInt8과 범위가 다르다.

let cannotBeNegative: UInt8 = -1
// UInt8은 음수를 저장할 수 없으므로 컴파일 에러 발생 - Negative integer '-1' overflows when stored into 'UInt8'

let tooBig: Int8 = Int8.max + 1
// Int8의 최대값보다 큰 값을 저장할 수 없으므로 컴파일 에러 발생 - Arithmetic oeration '127+1' (on type 'Int8') results in an overflow

데이터 표현 범위가 다른 타입 간에는 연산이 불가하다. 따라서 필요할 때마다 타입 변환을 해야 한다.

let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one) // 타입 변환 (UInt8 -> UInt16)

예제에서 상수 one은 UInt8에서 UInt16으로 타입을 변환했다. 이러한 타입 변환은 새로운 타입의 수를 기존의 값으로 초기화하는 것이다. 즉, 상수 one의 기존 값은 1이고, 변환하려는 타입은 UInt16이므로 UInt16 타입의 수를 1로 초기화한 것이다.
(To convert one specific number type to another, you initialize a new number of the desired type with the existing value.)

 

SomeType(ofInitialValue) 는 초기값을 전달받는 특정 타입의 이니셜라이저 (initializer)를 호출하는 기본적인 방법이다.

즉, 엄밀히 말하면 UInt8 값을 전달인자로 받는 UInt16의 이니셜라이저를 호출한 것이다.

(SomeType(ofInitialValue) is the default way to call the initializer of a Swift type and pass in an initial value. Behind the scenes, UInt16 has an initializer that accepts a UInt8 value, and so this initializer is used to make a new UInt16 from an existing UInt8.)

단, UInt16 구조체 타입에 정의된 이니셜라이저처럼 초기화가 가능한 타입만 전달될 수 있다.

(ex. init(_ source: Double) - Creates an integer from the given floating-point value, rounding toward zero.)

 

사용자 정의 타입을 포함하여 새로운 타입의 이니셜라이저를 사용하려면, Extensions 기능이 필요하다. 

*Extension은 [Swift Language Guide 정독 시리즈] Extensions 챕터에서 자세히 다루겠습니다.

Integer and Floating-Point Conversion (정수 및 부동소수점 수 변환)

정수 타입과 부동소수점 수 타입 간의 타입 변환은 명시적으로 이루어진다.

부동소수점 수에서 정수로 타입을 변환할 때도 명시해야 한다.

let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine 
// pi equals 3.14159, and is inferred to be of type Double (Int -> Double 타입 변환)

let integerPi = Int(pi) 
// integerPi equals 3, and is inferred to be of type Int (Double -> Int 타입 변환)

마지막 줄의 Int(pi)처럼 부동소수점 수를 정수로 타입 변환하면, 항상 소수점 수를 자른다 (truncated).

예를 들어 Int(4.75) = 4, Int (-3.9) = -3 이다.

Note: The rules for combining numeric constants and variables are different from the rules for numeric literals. The literal value 3 can be added directly to the literal value 0.14159, because number literals don’t have an explicit type in and of themselves. Their type is inferred only at the point that they’re evaluated by the compiler.

 

❗️숫자 리터럴 3과 숫자 리터럴 0.14159는 직접 연산이 가능하다. 숫자 리터럴 자체는 타입이 명시되지 않은 상태이기 때문이다.

숫자 리터럴의 타입이 추론되는 시점은 컴파일러가 평가 (evaluate)를 할 때이다.
(숫자 리터럴 3 자체는 Double 타입의 상수에도, Int 타입의 상수에도 할당 가능하다.)

*값 (value)은 수식 (expression)이 평가 (evaluate)되어 생성된 결과이다.

*평가 : 수식을 해석해서 값을 생성하거나 참조하는 것이다.

Type Aliases (타입 별칭)

타입 별칭이란 기존에 존재하는 타입에 별칭 (대체 이름)을 정의하는 것이다.

해당 타입을 사용하는 의도를 명시할 수 있으므로 가독성이 개선된다.

타입 별칭을 지정한 이후에는, 원래 이름을 사용하는 모든 위치에 별칭을 사용할 수 있다. (원래 이름 또한 사용할 수 있다.)

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

Booleans (불리언)

불리언 값은 참 (true) 또는 거짓 (false)을 나타내므로 logical (논리 데이터 타입)이라고도 부른다.

true 및 false는 불리언 리터럴 (Boolean literal)이다.

if문 (is statement)등의 조건문 (conditional statement)에서 유용하다.

let orangesAreOrange = true // 불리언 리터럴을 통해 자동으로 Bool 타입으로 추론됨
let turnipsAreDelicious = false

 

❗️Swift에서는 0/1이 false/true를 나타내지 않는다. 

let i = 1
if i {
    // 컴파일 에러 발생 - Type 'Int' cannot be used as a boolean; test for '!= 0' instead
}

print(true || false) // true 출력 
print(true && false) // false 출력

print(1 || 0) // 컴파일 에러 발생 - Type 'Int' cannot be used as a boolean; test for '!= 0' instead
print(1 && 0)

Tuples (튜플)

튜플 타입이란 다수의 값을 하나의 복합 값 (a single compound value)으로 묶은 것이다.

튜플 내부의 값 (요소)들은 어떤 타입이든 지정할 수 있고, 서로 동일한 타입일 필요도 없다. 또한 튜플 요소의 개수도 자유롭게 정할 수 있다.

 

튜플은 이름이 따로 정해져 있지 않은 타입이다. 따라서 1) 이름 없이 특정 타입의 값을 나열하는 것만으로 튜플을 생성할 수 있다.

원한다면 2) 튜플 요소의 이름을 지정할 수 있고, 3) 타입 별칭을 통해 튜플의 별칭을 지정할 수도 있다.

 

아래 예시에서 (404, "Not Found")는 HTTP 상태 코드를 나타내는 튜플이다. (HTTP 상태 코드는 사용자가 웹 페이지를 요청할 때마다 웹 서버가 반환하는 특수한 값이다. 상태 코드 404 Not Found는 요청한 웹 페이지가 존재하지 않을 때 반환된다.)

❗️(특수한 숫자, 사람이 읽을 수 있는 설명) 처럼 연관성이 높은 여러 개의 값을 묶어서 저장할 때 튜플이 유용하다.

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

// 튜플 요소 접근방법-1. 개별적인 상수로 튜플 분해 (decompose)
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"

// 분해 이후 접근할 필요가 없는 요소는 underscore (_) 처리
let (justTheStatusCode, _) = http404Error
print("The status code is \(justTheStatusCode)") // Prints "The status code is 404"

// 튜플 요소 접근방법-2. index 사용
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"

// 튜플 요소 접근방법-3. 튜플 생성 시 요소이름 지정
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"

// 참고 - 타입 별칭으로 튜플 이름 지정
typealias HttpStatusTuple = (statusCode: Int, description: String)
let errorStatus: HttpStatusTuple = (statusCode: 404, description: "Not Found")
let normalStatus: HttpStatusTuple = (200, "OK") // 요소이름을 생략 가능함

print("\(errorStatus.0) means \(errorStatus.1)") // Prints "404 means Not Found"
print("\(normalStatus.statusCode) means \(normalStatus.description)") // Prints "200 means OK"

또한 함수가 다수의 값을 반환할 때, 튜플이 유용하다. 함수의 반환 타입을 튜플로 지정하면 된다.

Note : Tuples are useful for simple groups of related values. They’re not suited to the creation of complex data structures. If your data structure is likely to be more complex, model it as a class or structure, rather than as a tuple.

 

❗️튜플은 연관 값의 묶음이 단순할 때 사용하는 것이 좋다. 복잡한 데이터 구조인 경우 클래스 또는 구조체를 사용해야 한다.

Optionals (옵셔널)

옵셔널은 상수/변수에 값이 존재하지 않을 가능성이 있을 때, 즉 nil 상태일 가능성이 있을 때 사용한다. 

옵셔널은 두 가지 상태를 나타낸다. 1) 값이 존재하며, 해당 값을 추출 (Unwrap)하여 접근할 수 있다. 2) 값이 존재하지 않는다.

 

Note: 옵셔널은 C 또는 Object-C에는 존재하지 않는 개념이다. Swift의 옵셔널은 사용자 정의 타입을 포함하여 모든 타입에 대해 nil 상태일 가능성을 암시한다. (가장 유사한 개념으로 Object-C에서는 객체를 반환하는 메서드의 경우, nil을 반환할 수 있는다. 단, 이때 nil은 유효한 객체가 존재하지 않음을 의미한다. 구조체, 열거형 등 다른 타입에서는 사용할 수 없다.)

 

❗️nil이란?

nil은 empty String 또는 empty Array와 다르다. 상수/변수에 empty String를 할당하면, 메모리에 빈 문자열 ""이 들어있는 상태이고, empty Array를 할당하면, 메모리에 빈 배열 []이 들어있는 상태가 된다. 반면 nil을 할당하면, 메모리에 아무것도 없는 상태가 된다.

nil인 상태의 상수/변수에 접근하면, 잘못된 메모리 접근으로 인해 런타임 오류가 발생한다. 옵셔널은 상수/변수가 nil일 수 있으므로 조심하라는 뜻을 내포한다.

 

Int 타입에는 String 값을 전달받는 이니셜라이저가 정의되어 있다. (앞서 타입 변환은 타입의 이니셜라이저를 호출하는 것이라고 했다.)

하지만 이니셜라저에 전달된 모든 String이 Int로 타입 변환되는 것은 아니다. 예를 들어 String "123"은 Int 123으로 변환되지만, String "Hello"는 Int로 변환할 숫자가 없으므로 변환이 불가하며 nil을 반환하다.

이처럼 이니셜라이저의 변환 실패가 발생할 가능성이 있을 때, "nil 가능성이 있다"고 하며, 이니셜라이저는 옵셔널 (Int?)을 반환한다.

(이니셜라이저를 통한 타입 변환 결과, 1) Int가 되거나, 2) 변환에 실패하여 nil이 될 수 있다 라는 의미이다.) 따라서 이니셜라이저를 할당하는 상수/변수도 옵셔널 타입이 된다.

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber is inferred to be of type "Int?" ("optional Int"라고도 함)

❗️ Int로 변환 가능한 "123"을 전달했는데, 왜 상수 convertedNumber은 옵셔널 타입으로 추론됐을까?

개발자 입장에서는 "123"을 할당했으므로 변환이 가능할 것임을 알 수 있지만, 컴퓨터 입장에서는 실제로 컴파일하기 전까지 알 수가 없다. 따라서 최대한 방어적인 가정을 하는 것이다. 컴퓨터 입장에서는 Int() 안에 String 값이 전달되었다는 것만 알 수 있다. 

nil

nil은 옵셔널 타입에만 할당 가능하다. (Any 타입에도 nil을 할당할 수 없다.)

옵셔널 타입의 변수에 nil을 할당하면, 아무런 값이 없는 상태 (valueless state)가 된다.

어떤 변수를 옵셔널 타입으로 선언하고, 초기값을 지정하지 않으면 어떻게 될까? 자동으로 값이 없는 상태, 즉 nil이 할당된 상태가 된다.

let nonOptional: Any = nil // 컴파일 에러 발생 - 'nil' cannot initialize specified type 'Any'
let optional: Any? = nil   // 옵셔널 타입이므로 nil 할당 가능

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

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

❗️옵셔널이 왜 필요할까? 즉, 상수/변수에 nil이 있음을 가정하는 이유는 무엇일까?

예를 들어 함수에서 전달인자의 값이 잘못된 경우 nil을 반환하여 개발자에게 알려줄 수 있다.

또한, 옵셔널은 필수적인 요소가 아니라는 뜻을 알려주기도 한다. 예를 들면 함수의 매개변수 중에서 required parameter가 아닌 경우 (필수적으로 값을 전달받지 않아도 되는 경우)에 해당 매개변수를 optional로 정의한다.

func makeAppleCider(apple: String, sugar: String, cinnamon: String?) { // 매개변수 cinnamon을 옵셔널로 정의
    var appleWithFlavor: String = apple
    
    if let cinnamon = cinnamon { // 매개변수 cinnamon이 nil이 아니면, appleWithFlavor을 수정함
        appleWithFlavor = "\(cinnamon)향이 첨가된 \(appleWithFlavor)"
    }
    
    print(appleWithFlavor, sugar, separator: " 그리고 ", terminator: "을 냄비에 넣고 끓인다\n")
}

makeAppleCider(apple: "사과", sugar: "설탕", cinnamon: "시나몬") // "시나몬향이 첨가된 사과 그리고 설탕을 냄비에 넣고 끓인다" 출력
makeAppleCider(apple: "사과", sugar: "설탕", cinnamon: nil)    // "사과 그리고 설탕을 냄비에 넣고 끓인다" 출력
Note: Swift’s nil isn’t the same as nil in Objective-C. In Objective-C, nil is a pointer to a nonexistent object. In Swift, nil isn’t a pointer—it’s the absence of a value of a certain type. Optionals of any type can be set to nil, not just object types.

 

Swift의 nil은 Object-C의 nil과 다르다. Object-C에서 nil은 존재하지 않는 객체를 가리키는 포인터이다. 반면, Swift의 nil은 포인터가 아니다. 특정 타입의 값이 존재하지 않음을 의미한다. 객체뿐만 아니라 모든 타입의 옵셔널에 대해 nil을 할당할 수 있다.

If Statements and Forced Unwrapping (if문 및 강제 추출)

옵셔널이 값을 갖고 있는지 어떻게 확인할까? if문을 사용하여 옵셔널과 nil을 비교하는 방법이 있다. 비교는 == (“equal to” 연산자) 그리고 != (“not equal to” 연산자)를 통해 수행한다. 옵셔널이 값을 갖고 있으면, "nil과 동일하지 않다"고 판단한다.

if convertedNumber != nil { // 상수 convertedNumber에는 Int 123이 할당된 상태이므로 nil과 동일하지 않음
    print("convertedNumber contains some integer value.")
}
// Prints "convertedNumber contains some integer value."

 

✅옵셔널 추출이란 "옵셔널에 들어있는 값"을 "옵셔널이 아닌 값"으로 꺼내는 것이다. 옵셔널 추출 방법에는 1) 강제 추출 (Forced Unwrapping), 2) 옵셔널 바인딩 (Optional Binding), 3) 암시적 추출 옵셔널 (Implicitly Unwrapped Optionals)이 있다.

 

옵셔널이 반드시 값을 갖고 있다고 확신하는 경우, 강제 추출 (Forced Unwrapping)을 통해 값에 접근할 수 있다. 상수/변수 이름 뒤에 ! (exclamation point)을 붙인다.

if convertedNumber != nil {
    print("convertedNumber has an integer value of \(convertedNumber!).") // 강제 추출
}
// Prints "convertedNumber has an integer value of 123."
Note: Trying to use ! to access a nonexistent optional value triggers a runtime error. Always make sure that an optional contains a non-nil value before using ! to force-unwrap its value.

 

❗️강제 추출을 했는데, 상수/변수가 nil인 경우에는 런타임 에러가 발생한다. 옵셔널에 반드시 값이 있다고 100% 확신할 수 있는 경우는 드물기 때문에 강제 추출을 사용하지 않는 것이 바람직하다.

Optional Binding (옵셔널 바인딩)

강제 추출보다는 옵셔널 값에 안전하게 접근할 수 있는 옵셔널 바인딩을 사용하는 것이 바람직하다. 

옵셔널 바인딩을 사용하면, 옵셔널에 값이 있는 경우 해당 값을 꺼내어 임시 상수/변수에 할당하여 사용할 수 있다. 

if문과 while문을 통해 옵셔널 바인딩을 사용할 수 있다.

if let actualNumber = Int(possibleNumber) { // if-let을 사용한 옵셔널 바인딩 (상수 possibleNumber == String "123")
    print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
    print("The string \"\(possibleNumber)\" couldn't be converted to an integer")
}
// Prints "The string "123" has an integer value of 123"

즉, 옵셔널 바인딩은 "Int(possibleNumber)이 nil이 아니라면, 해당 값을 꺼내서 상수 actualNumber에 할당하겠다." 라는 의미이다.

참고로 만약 if문 내부에서 actualNumber의 값을 변경하려면, if-let이 아니라 if-var을 사용하여 변수로 만들 수 있다.

 

if문의 조건부에는 여러 개의 옵셔널 바인딩 및 여러 개의 불리언 조건문 (Boolean conditions)을 넣을 수 있다. 조건1, 조건2, 조건3 의 형태로 commas (,)로 구분만 하면 된다. (조건1 && 조건2 && 조건3 의 형태와 혼동 주의)

옵셔널 바인딩 중 하나라도 nil이 있거나, 조건문을 평가한 결과가 false이면, if문의 조건부는 최종적으로 false로 간주된다.

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: Constants and variables created with optional binding in an if statement are available only within the body of the if statement. In contrast, the constants and variables created with a guard statement are available in the lines of code that follow the guard statement.

 

if문 외부에서도 상수/변수를 사용하고 싶다면, guard문 (guard-let, guard-var)을 사용하면 된다.

*guard문은 [Swift Language Guide 정독 시리즈] Control Flow 챕터의 Early Exit 섹션에서 자세히 다루겠습니다.

Implicitly Unwrapped Optionals (암시적 추출 옵셔널)

앞서 nil은 옵셔널에만 할당이 가능하다고 했다. nil을 할당할 수 있는 옵셔널이지만 프로그램 로직상 상수/변수에 항상 100% 값이 있다고 확신할 때, 값을 꺼낼 때마다 옵셔널 바인딩을 사용하기가 번거로울 수 있다. 이때, 암시적 추출 옵셔널을 사용한다. 

일반적인 옵셔널과 달리 !가 아닌 ? (question mark)를 사용한다.

 

클래스 초기화 시 매우 유용하게 사용할 수 있다.

*[Swift Language Guide 정독 시리즈] utomatic Reference Counting 챕터의 Unowned References and Implicitly Unwrapped Optional Properties 섹션에서 자세히 다루겠습니다.

 

✅암시적 추출 옵셔널도 옵셔널에 속한다. 하지만 일반적인 옵셔널과 달리 nil 상태가 아니면 일반적인 값 (non-optional value)처럼 연산이 가능하다. 따라서 "nil 할당이 가능하지만, 옵셔널이 아닌 상수/변수가 필요할 때" 사용한다고도 정리할 수 있다.

(An implicitly unwrapped optional is a normal optional behind the scenes, but can also be used like a non-optional value, without the need to unwrap the optional value each time it’s accessed.)

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 - 자동으로 강제 추출이 되므로 !가 필요 없음
// *implicitString이 옵셔널이 아닌 String 타입이므로 일반 옵셔널로 사용될 수 없는 상황임. 따라서 강제 추출됨
let forcedimplicitString: String = assumedString! // 참고 - 직접 !를 사용하여 강제 추출도 가능

let optionalString = assumedString 
// *일반 옵셔널로 사용할 수 있는 상황이므로 강제 추출되지 않음. 따라서 optionalString이 옵셔널인 String?으로 타입 추론됨

암시적 추출 옵셔널은 "필요 시 언제든 값을 강제 추출을 하도록 허락한 옵셔널"이라고도 볼 수 있다. (You can think of an implicitly unwrapped optional as giving permission for the optional to be force-unwrapped if needed.)

✅강제 추출 시 !를 사용하는데, (앞으로 여러 번 강제 추출을 할 것이 예상되므로) 암시적 추출 옵셔널을 선언하면서 미리 !를 붙여둔 것이라고 이해하면 쉽다.

 

암시적 추출 옵셔널을 사용하면, 컴파일러는 1) 먼저, 그것을 일반적인 옵셔널값으로 사용하려고 한다. 하지만 2) 옵셔널로 사용하는 것이 불가할 때, 값을 강제 추출한다.

위 예시에서 상수 assumedString의 값은 상수 implicitString에 할당되기 직전에 강제 추출된다. implicitString의 타입이 옵셔널이 아닌 String이기 때문이다. 반면, 상수 optionalString은 String으로 타입이 명시되지 않았으므로 assumedString은 강제 추출되지 않고 일반 옵셔널로 사용되었다.

 

암시적 추출 옵셔널을 사용하는 본래 의도와 달리, 암시적 추출 옵셔널이 nil인 경우 값에 접근하면 런타임 에러가 발생한다. 일반적인 옵셔널이 nil일 때 강제 추출을 하면 런타임 에러가 발생하는 것과 동일하다. 

암시적 추출 옵셔널도 옵셔널이므로 옵셔널 바인딩을 사용 가능하며, if문을 통해 nil인지 확인할 수 있다.

if assumedString != nil { // 방법-1. != 연산자로 확인
    print(assumedString!)
}
// Prints "An implicitly unwrapped optional string."

if let definiteString = assumedString { // 방법-2. 옵셔널 바인딩으로 확인
    print(definiteString)
}
// Prints "An implicitly unwrapped optional string."

Error Handling (오류 처리)

프로그램 실행 과정에서 발생하는 여러 가지 오류를 확인하고 대응하기 위해 Error Handling (오류 처리)를 사용한다.

옵셔널과 오류 처리는 비슷하면서도 다르다. 옵셔널은 값의 유무를 사용하여 함수의 성공/실패 여부를 개발자에게 알려준다. 반면 오류 처리는 실패의 원인을 밝히고, 필요 시 오류를 프로그램의 다른 부분으로 전가 (propagate)하여 처리하도록 한다. 

 

1) throws 키워드는 함수를 실행하면서 오류가 발생할 수 있다는 것을 의미한다. 만약 함수를 실행하는 과정에서 오류가 발생하면, 함수는 현재 범위 외부로 오류를 던진다. (throws an error)

2) 해당 함수를 호출한 곳 (function’s caller)이 오류를 잡아서 (catch the error) 처리한다.

3) 에러를 던질 수 있는 함수를 호출할 때, try 키워드를 함수 앞에 붙인다.

4) do-catch문에서 do문은 에러를 전가하고, catch절 (catch clause)은 전가된 오류를 받아서 처리한다. 

func makeASandwich() throws { // 이 함수는 오류를 던질수도, 던지지 않을수도 있다.
    // ...
}

do {
    try makeASandwich()
    eatASandwich() // 1) 만약 함수가 오류를 던지지 않았으면, 이 코드를 실행한다.
} catch SandwichError.outOfCleanDishes { // 2) 만약 함수가 오류를 던졌다면, 그리고 SandwichError 열거형의 outOfCleanDishes case에 해당하는 오류라면,
    washDishes()                         // 이 코드를 실행한다.
} catch SandwichError.missingIngredients(let ingredients) { // 참고 - 상수 ingredients는 missingIngredients case의 연관값임
    buyGroceries(ingredients) // buyGroceries 함수를 호출할 때 상수 ingredients을 전달인자로 전달함
}

*Error Handling은 [Swift Language Guide 정독 시리즈] Error Handling 챕터에서 자세히 다루겠습니다.

Assertions and Preconditions (단언 및 선제조건)

Assertions 및 Preconditions이란 런타임 시점에 진행하는 확인 작업 (check)이다.

코드를 모두 실행하기 전에 미리 필수 조건이 충족되었는지 확인할 때 사용한다. assertion 또는 precondition의 불리언 조건이 true로 평가되면, 코드는 계속 실행된다. 반면 false로 평가되면, 프로그램이 무효화되어 (invalid state) 코드 실행이 중지되고 앱이 종료된다.

 

코딩을 하는 시점에 가정 (assumptions)한 것을 코드에 나타내면, 부적절한 가정으로 인해 발생한 문제를 쉽게 발견할 수 있다. Assertions은 개발 과정에서 발생한 문제를, Preconditions은 production 과정에서 발생한 문제를 찾아낸다.

 

특정 에러에 대응하거나 프로그램이 무효화되지 않도록 설계하기 위한 방법은 아니다. 하지만 프로그램이 유효한 상태인 조건을 명시하고 강제한다. 이를 통해 프로그램이 무효화되는 상황이 발생하면, 즉시 앱을 종료시키므로 신속히 디버깅하고, 피해를 최소화할 수 있다.

 

assertions와 preconditions의 차이는 확인 작업을 하는 시점이다. Assertions은 debug builds 시점에서만 확인 작업이 가능하지만, Preconditions은 debug builds와 production builds 시점 모두 가능하다. production builds에서 assertion 조건은 평가되지 않는다. 따라서 assertions는 production 단계의 성능에 영향이 없으며, 개발 과정에서 원하는 만큼 assertions을 사용할 수 있다.

❗️실제 배포할 코드라면 assert 함수가 아니라 precondition 함수를 사용해야 한다.

Debugging with Assertions (Assertions을 통한 디버깅)

assert(_:_:file:line:) 함수를 호출하여 assertion을 작성한다. asset 함수는 전통적인 C 스타일의 assert를 수행한다.

첫 번째 매개변수에는 true/false로 평가될 조건문을 전달하고, 두 번째 매개변수에는 조건이 false인 경우 출력할 메시지를 전달한다. 

  • Playgrounds 및 -Onone builds (Xcode의 디버그용 빌드 기본 구성)에서 조건을 평가한다.
  • -O builds (Xcode의 배포용 빌드 기본 구성)에서는 조건을 평가하지 않으며, 프로그램에 영향을 주지 않는다.
  • -Ounchecked builds에서는 조건을 평가하지 않으며, 최적화 기능 (optimizer)은 조건을 true로 평가할 것을 가정한다.
func assert(_ condition: @autoclosure () -> Bool, 
	    _ message: @autoclosure () -> String = String(), 
            file: StaticString = #file, line: UInt = #line)
let age = -3
assert(age >= 0, "A person's age can't be less than zero.") // This assertion fails because -3 isn't >= 0.
// 조건이 false로 평가되었으므로 assertion에 실패하여 앱이 즉시 종료된다.

assert(age >= 0) // assertion message는 생략 가능하다.

// 이미 코드의 조건을 확인했으면, assertionFailure(_:file:line:) 함수를 호출하여 assertion이 실패했음을 알릴 수 있다.
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("assertionFailure - A person's age can't be less than zero.")
}
// 에러 메시지 확인 가능 - Fatal error: assertionFailure - A person's age can't be less than zero.: file test/main.swift, line 13

참고 - assertFailure 함수는 배포 버전 빌드 (release builds)에는 영향을 미치지 않지만, debug builds에서 프로그램을 중단하고 디버그 가능한 상태로 전환할 때 사용한다.

Enforcing Preconditions (선제조건 강제)

precondition은 조건이 false가 될 가능성이 있는 경우에 사용한다. 반드시 true여야 코드가 계속 실행된다. 

예를 들어 서브스크립트 (subscript)가 범위를 벗어나지 않았는지 여부를 확인하거나, 함수에 유효한 값이 전달되었는지 확인할 때 precondition을 사용한다.

*서브스크립트는 [Swift Language Guide 정독 시리즈] Subscripts 챕터에서 자세히 다루겠습니다.

 

precondition(_:_:file:line:) 함수를 호출하여 precondition을 작성한다. 

첫 번째 매개변수로 true/false로 평가될 조건문을 전달하며, 두 번째 매개변수로 조건이 false인 경우 출력할 메시지를 전달한다. 

  • playgrounds 및 -Onone builds에서 조건을 평가한다. 평가 결과 false이면, 에러 메시지를 출력한 후 프로그램을 종료하고 디버그 가능한 상태로 전환한다.
  • -O builds에서 조건 평가 결과가 false이면, 프로그램을 종료한다.
  • -Ounchecked builds에서는 조건을 평가하지 않으며, 최적화 기능 (optimizer)은 조건을 true로 평가할 것을 가정한다.
func precondition(_ condition: @autoclosure () -> Bool, 
		  _ message: @autoclosure () -> String = String(), 
            	  file: StaticString = #file, line: UInt = #line)
// In the implementation of a subscript...
precondition(index > 0, "Index must be greater than zero.")

또한, preconditionFailure 함수를 호출하여 precondition이 실패했음을 알릴 수 있다.

If you compile in unchecked mode (-Ounchecked), preconditions aren’t checked. The compiler assumes that preconditions are always true, and it optimizes your code accordingly. However, the fatalError(_:file:line:) function always halts execution, regardless of optimization settings.
You can use the fatalError(_:file:line:) function during prototyping and early development to create stubs for functionality that hasn’t been implemented yet, by writing fatalError("Unimplemented") as the stub implementation. Because fatal errors are never optimized out, unlike assertions or preconditions, you can be sure that execution always halts if it encounters a stub implementation.

 

unchecked mode (-Ounchecked)에서 컴파일하면, preconditions가 진행되지 않는다. 컴파일러는 preconditions가 항상 true라고 가정하여 코드를 최적화한다. 단, fatalError(:file:line:) 함수는 최적화 설정에 상관없이 항상 실행을 멈춘다.

fatalError 함수는 fatalError("Unimplemented") 형태로 부분적인 기능 (stubs for functionality)을 구현하는 과정에서 prototyping 및 개발 초기단계에 사용한다. Assertions/Preconditions와 달리 fatal errors는 최적화되지 않으므로 fatal error가 나타나면 항상 코드가 중지된다.

 

 

- 출처

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments