기초: 타입, 상수·변수, 옵셔널

기초: 타입, 상수·변수, 옵셔널

Swift를 처음 시작하면 가장 먼저 만나는 게 '값을 어떻게 담을까'일 거예요. Swift는 정수(Int), 실수(Double), 참/거짓(Bool), 텍스트(String) 같은 기본 타입과, Array·Set·Dictionary 컬렉션 타입을 기본으로 제공해요. 그리고 값을 이름으로 묶어 두는 상수와 변수, 값이 없을 수도 있음을 타입에 담아내는 옵셔널까지 이 기초 페이지에서 한 번에 설명해요.

출처: 공식문서

본문

상수와 변수

값을 나중에 바꿀 일이 없다면 let으로 상수를, 바꿔야 한다면 var로 변수를 선언해요. 로그인 시도 횟수를 추적하는 코드로 보면 이렇게 돼요.

let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0

최대 허용 횟수는 절대 안 바뀌는 값이니 상수로, 현재 시도 횟수는 실패할 때마다 늘어나야 하니 변수로 두는 거죠. 이런 코드는 그대로 읽을 수 있어요. "상수 maximumNumberOfLoginAttempts를 새로 선언하고 값 10을 주고, 변수 currentLoginAttempt를 선언해 초깃값 0을 준다."

선언할 때 값을 바로 정하지 않고, 프로그램 어딘가에서 나중에 처음 읽기 전까지 확실히 값을 갖도록 채울 수도 있어요. 아래처럼 분기로 초기화해 주면 어떤 경로로 들어가든 상수가 값을 갖게 되죠.

var environment = "development"
let maximumNumberOfLoginAttempts: Int
// maximumNumberOfLoginAttempts has no value yet.


if environment == "development" {
    maximumNumberOfLoginAttempts = 100
} else {
    maximumNumberOfLoginAttempts = 10
}
// Now maximumNumberOfLoginAttempts has a value, and can be read.

여러 상수나 변수를 한 줄에 쉼표로 나란히 선언하는 것도 가능해요.

var x = 0.0, y = 0.0, z = 0.0

타입 명시(Type Annotation)

콜론(:)을 붙이면 상수나 변수가 담을 값의 종류를 분명히 밝힐 수 있어요. 이름 다음에 콜론, 공백, 그리고 타입 이름 순서예요.

var welcomeMessage: String

이 선언은 "변수 welcomeMessageString 타입이야"로 읽히고, 다시 말해 어떤 String 값이든 오류 없이 담을 수 있어요.

welcomeMessage = "Hello"

같은 타입의 변수 여러 개를 한 줄에 선언하면서, 마지막 변수 뒤에 타입을 한 번만 적어도 돼요.

var red, green, blue: Double

이름 짓기

Swift의 상수·변수 이름은 거의 모든 문자를 쓸 수 있어요. 유니코드 문자도 이름이 될 수 있어요.

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

이름에 공백 문자, 수학 기호, 화살표, 개인용 유니코드 스칼라 값, 선 그리기 문자는 못 들어가요. 숫자로 시작할 수도 없지만, 첫 글자가 아니면 숫자를 써도 돼요.

한번 선언한 상수·변수는 같은 이름으로 다시 선언하거나 다른 타입 값을 담도록 바꿀 수 없어요. 변수의 값은 바꿀 수 있지만요. 아래 예를 보면 friendlyWelcome"Hello!"에서 "Bonjour!"로 바뀌는 걸 확인할 수 있어요.

var friendlyWelcome = "Hello!"
friendlyWelcome = "Bonjour!"
// friendlyWelcome is now "Bonjour!"

상수는 설정된 후 값이 바뀔 수 없고, 바꾸려고 시도하면 컴파일할 때 오류로 잡혀요.

let languageName = "Swift"
languageName = "Swift++"
// This is a compile-time error: languageName cannot be changed.

출력하기

print(_:separator:terminator:) 함수로 값을 출력할 수 있어요. separatorterminator는 기본값이 있어 생략할 수 있고, 줄 바꿈 없이 출력하고 싶다면 terminator에 빈 문자열을 넘기면 돼요.

print(friendlyWelcome)
// Prints "Bonjour!"

문자열 안에 상수·변수 값을 끼워 넣을 때는 문자열 보간(string interpolation)을 써요. 이름을 괄호로 감싸고 그 앞에 백슬래시를 붙여요.

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

주석은 실행되지 않는 텍스트예요. 한 줄 주석은 //로, 여러 줄 주석은 /* ... */로 써요.

// 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. */

이 덕분에 이미 여러 줄 주석이 들어 있는 큰 코드 블록도 쉽게 통째로 주석 처리할 수 있죠.

정수 타입

정수는 42, -23처럼 분수 부분이 없는 온전한 수예요. 부호 있음(양·음·0)과 부호 없음(양·0)으로 나뉘고, 크기는 비트 수로 정해져요. 타입 이름에 크기와 부호가 다 들어 있어요. 예컨대 8비트 부호 없는 정수는 UInt8, 32비트 부호 있는 정수는 Int32예요. 대부분은 정확한 크기를 고를 필요가 없으니 Int를 쓰면 돼요.

정수는 손으로 하는 계산처럼 반올림 없이 결과가 나와요. 그래서 텍스트 파일에서 가장 긴 줄 찾기나 게임 점수 배율 적용처럼 정확한 양을 나타내는 계산에 잘 맞아요.

각 정수 타입의 최솟값과 최댓값은 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는 현재 플랫폼의 네이티브 단어 크기와 같아요. 32비트 플랫폼에선 Int32, 64비트 플랫폼에선 Int64 크기예요. 특정 크기의 정수가 필요하지 않다면 항상 Int를 쓰는 게 일관성과 상호운용성에 좋아요.

부호 없는 정수 타입 UInt도 있어요. 이것도 플랫폼 네이티브 단어 크기와 같아요.

부동소수점 타입

부동소수점 수는 3.14159, 0.1, -273.15처럼 분수 부분을 갖는 수예요. 특정 크기가 필요 없으면 Double을 쓰고, 아니면 Float16, Float80처럼 크기를 이름에 담은 타입을 골라요. 관례상 Float는 32비트, Double은 64비트예요. 그래픽 코드는 GPU의 가장 빠른 타입에 맞추려고 Float를 자주 쓰죠. 일부 부동소수점 타입은 특정 플랫폼에서만 지원되지만 FloatDouble은 모든 플랫폼에서 쓸 수 있어요.

옵셔널

Swift에서 값이 없을 수도 있는 상황은 옵셔널(optional) 타입으로 표현해요. 옵셔널은 값이 있으면 some(어떤 값 x), 없으면 nil이라는 두 가지 상태 중 하나예요. 값이 빠져 있는지 항상 확인하게 만드는 게 옵셔널의 핵심이고, 옵셔널이 아닌 값은 절대 빠지지 않는다고 보장돼요.

옵셔널을 언랩(unwrap)하는 방법은 몇 가지가 있어요. if let 같은 옵셔널 바인딩으로 한 문장에서 확인과 함께 값을 꺼낼 수도 있고, 상황이 확실하고 값이 반드시 있다는 걸 알고 있다면 느낌표(!)로 강제 언랩을 하기도 해요. 강제 언랩은 값이 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."

안전성과 에러 처리

Swift는 타입 안전성과 메모리 안전성을 같이 갖춰요. 타입 안전성은 코드가 다루는 값의 타입을 분명히 하게 하고, 메모리 안전성은 초기화되지 않은 메모리나 해제된 객체 같은 잘못된 메모리를 다루지 않게 보호해요. 대부분의 안전성 검사는 빌드할 때 일어나고, 일부는 실행 중에 추가로 이뤄져요.

메모리 안전성은 구체적으로 이런 요구를 지켜요. 값을 읽기 전에 설정할 것(확정 초기화), 배열·버퍼를 유효한 인덱스로만 접근할 것(경계 안전성), 값의 수명 동안만 메모리에 접근할 것(수명 안전성), 동시성 코드에서 메모리 접근이 겹치지 않을 것(스레드 안전성)이에요.

에러 처리는 프로그램이 실행 중 만나는 오류 조건에 대응하는 방식이에요. 옵셔널이 함수의 성공·실패를 값의 유무로 전달한다면, 에러 처리는 실패의 근본 원인을 파악하고 필요하다면 다른 부분으로 전파할 수 있게 해요. 함수가 에러를 던질 수 있으면 선언에 throws를 붙이고, 호출할 때는 try를 앞에 붙여요.

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

에러는 현재 스코프 밖으로 자동 전파되다가 catch 절에서 처리돼요. do 문이 새 스코프를 만들어 에러가 하나 이상의 catch 절로 전파되게 해요.

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

더 알아보기

  • 타입이 실제로 어떻게 구조화되는지는 Collection Types에서 다뤄요.
  • 함수에서 값을 여러 개 반환할 때 쓰는 튜플은 Functions에서 만나요.
  • 옵셔널을 더 깊이 다루는 Optional Chaining도 참고해요.