스위프트의 중간 기록(이제까지 공부한 내용들)
~ 항목순회
스위프트
애플은 iOS, macOS,warchOS, ivOS 등 자사의 제품 개발에 활용하고자 새 프로그래밍 언어인 스위프트를 만들었다. 스위프트는 안전을 우선으로 하는 프로그래밍 패턴을 지향하며, 더욱 쉽고 재미있게 프로그래밍할 수 있도록 옵셔널, 제네릭, 프로토콜, 튜플, 익스텐션 등 새로운 기능을 많이 도입했다. 현재 애플은 스위프트를 오픈소스로 공유했으므로 애플 플랫폼 외에 다양한 플랫폼에서 사용할 수 있다.
스위프트는 편리하며 고차원적인 언어이다. 먼저 ARC(자동 참조 횟수 계산)을 지원하므로 쉽게 메모리를 관리할 수 있다. 또한 기존 Objective-c 언어에 익숙하다면 더욱 쉽게 스위프트를 익힐 수 있다. Objective-c 의 동적 객체 모델과 매개변수 형식을 스위프트에 도입했기 때문이다. 더북어 스크립트 언어처럼 프로그램을 빌드하고 실행하는 등의 수고로움 없이 플레이 그라운드를 사용해 스위프트 코드의 결과를 바로 확인할 수도 있다.
기존에 다른 프로그래밍 언어를 사용하던 프로그래머라면 스위프트의 문법이 다소 생소할 수 있다. 스위프트의 거의 모든 문법에서 소괄호( () )는 사용하거나 생략해도 무관하다. 또한 세미콜론( ; )도 생략할 수 있다. 다만 중괄호( {} )는 생략할 수 없다. 그 때문에 프로그래머마다 전혀 다른 스타일로 코딩할 수 있다. 좋은 의미로 보자면 자신이 원하는 방색대로 자유롭게 코딩할 수 있다는 뜻이지만, 다른 사람의 코드를 읽기 어려울 수 있다는 단점이 있기도 하다. 즉, 스위프트는 언어의 자유도가 높아서 작성자의 스타일에 따라 다양한 방식으로 코딩할 수 있지만 한편으로 가독성이 떨어질 수 있으므로 주의가 필요하다.
스위프트 실행 환경
스위프트는 오픈소스로 전환된 이후 macOS 외의 다른 환경, 즉 리눅스 및 윈도우 등에서도 사용할 수 있다. 그렇지만 아직 스위프트의 문법을 익히고 연습하기에는 애플에서 공식 지원하는 Xcode의 플레이그라운드와 REPL이 가장 편리하다. 그 외에 웹에서 손쉽게 스위프트 문법을 익히고 실행할 수 있도록 서비스를 제공하는 곳도 있다.
[Xcode - 플레이그라운드]
Xcode는 macOS에서 동작하는 애플의 통합 개발 환경으로 맥 앱 스토에어서 무료로 내려받아 설치할 수 있다. Xcode로는 맥용 애플리케이션 제작부터 iOS, watchOS, tvOS 등의 애플리케이션 제작, 프레임워크 및 라이브러리 제작 등 다양한 프로그래밍을 할 수 있다. 이 xCode의 특별한 기능 중 하나가 플레이그라운드이다.
플레이그라운드는 별도의 프로젝트 생성 없이 스위프트 코드를 실행할 수 있는 스위프트 코딩 환경이다. 기존 문법 연습에서부터 복잡한 코드 테스트까지 다양한 기능을 이용할 수 있다. 스위프트를 맛보고 문법 연습을 하기에 더할 나위 없이 좋은 도구이다.
[REPL]
스위프트는 인터프리터 언어에서 주로 사용되는 REPL로도 코드를 실행할 수 있다. 물론 스위프트는 인터프리터 언어가 아닌 컴파일 언어지만 기존의 인터프리터 언어의 REPL과 거의 비슷하게 사용할 수 있다. REPL은 간단한 코드를 직접 입력하여 바로 결과를 볼 수 있는 환경과 편의를 제공해주는 도구이다.
스위프트의 REPL을 사용하려면 Xcode가 설치된 macOS의 터미널 애플리케이션을 실행하면 된다. 더 정확히 말하자면 Command Line Tools for Xcode가 설치되어 있어야 한다.
[웹]
스위프트 코드를 macOS가 아닌 다른 환경에서도 쉽게 테스트할 수 있도록 여러 웹 사이트에서 플레이그라운드와 유사한 환경을 제공한다. 자신이 작성한 코드를 다른 사람들과 공유할 수도 있어서 매우 유용하다. 대표적인 웹 사이트 몇 개를 소개한다.
- Online Swift Playground : 플레이그라운드와 유사한 모습으로 스위프트 코드를 작성하고 실행해볼 수 있다. 자신이 온라인에서 작성한 코드를 쉬으프트 파일 혹은 플레이그라운드 파일로 내려받을 수도 있다.
- 구름IDE : 국내 클라우드 통합 개발 환경이다. 컨테이너를 생성하여 가상 리눅스 환경에서 프로젝트를 관리할 수 있으며, 별도의 플러그인 등을 설치하지 않고도 올라인에서 직접 빌드 및 실행이 가능하다. 리눅스 터미널 환경에서 스위프트 패키지 매니저를 사용할 수 있기 때문에 외부 라이브러리를 가져와서 프로젝트로 구현할 수 있다. 회원 가입을 하면 내 프로젝트를 반영구적으로 저장하고 관리할 수 있다.
1. 스위프트 기본 명명 규칙
모든 프로그래밍 언어가 그렇듯 프위프트 언어 자체에 명시도니 명명 규칙은 없다. 명명 규칙은 프레임워크나 코딩 환경 또는 협업 그룹마다 달라질 수 있다. 물론 언어에 따라서 권장하는 명명법이나 코딩 규칙이 있기도 하다.
코딩은 습관이라 나중에 교정하기 어려우니 처음부터 제대로 된 코딩 규칙 및 명명 규칙을 익히도록 노력하는 것이 좋다. 다음은 가이드라인 중에서 꼭 알아야 할 기본 명명 규칙이다.
- 변수, 상수, 함수, 메서드, 타입 등의 이름은 유니코드에서 지원하는 어떤 문자(한글, 한자, 영문, 숫자, 이모티콘 등등)라도 사용할 수 있다. 다만 다음과 같은 예외 경우에는 사용할 수 없다.
- 스위프트에서 미리 정한 예약어 또는 키워드
- 해당 코드 범위 내에서 미리 사용되는 기존 이름과 동일한 이름
- 연산자로 사용될 수 있는 기호(+, -, *, /)
- 숫자로 시작하는 이름
- 공백이 포함된 이름
- 함수, 메서드, 인스턴스 이름은 첫 글자를 소문자로 사용하는 소문자 카멜케이스(Lower Camel Case)를 사용한다.
- 클래스, 구조체, 익스텐션, 프로토콜, 열거형 이름은 타입의 이름이기 때문에 첫 글자를 대문자로 사용하는 대문자 카멜케이스를 사용한다.
- 대소문자를 구별한다. 예를 들어 Var와 var를 다르게 인식한다.
[Tip] - 예약어와 키워드
예약어는 프로그래밍 언어에서 미리 사용하기로 약속한 단어로, 식별자로 사용할 수 없는 단어를 뜻한다. 키워드는 프로그래밍 언어 문법의 일부로, 특별한 의미가 있는 단어를 뜻한다. 스위프트의 키워드는 대부분 예약어이다. 일부 예약어의 경우에는 강세표(backquote, `)를 사용하여 이름으로 사용할 수 있다.
2. 콘솔 로그
프로그램에서 로그란 애플리케이션의 상태 또는 애플리케이션 내부 로직의 흐름을 관찰할 수 있도록 출력한 정보를 의미한다. 콘솔 로그는 디버깅 중 디버깅 콘솔에 보여줄 로그를 뜻한다. 스위프트에서는 print() 또는 dump() 함수를 사용하여 콘솔 로그를 출력할 수 있다.
2.2 print( )함수
스위프트에서 콘솔 로그를 남기는 용도로 print( ) 함수를 사용한다. print( ) 함수의 기본 원형은 public func print(items: Any…, separator: Strinmg = default, terminator: String = default)로 정의되어 있다. 기본적으로 print(”Hello”)와 같이 사용하면 디버깅 콘솔에서 “Hello”라는 로그를 확인할 수 있다. print( ) 함수는 로그를 출력한 뒤 줄바꿈을 해주기 위해 줄바꿈 문자( \n )를 자동으로 삽입해준다.
[Note - print( ) 와 dump( ) 함수]
스위프트 표준 라이브러리에는 print( ) 함수 외에도 dump( ) 라는 함수가 있다. print( ) 함수는 디버깅 콘솔에 간략한 정보를 출력해주는 반면, dump( ) 함수는 조금 더 자세한 정보를 출력해준다. print( ) 함수는 출력하려는 인스턴스의 description 프로퍼티에 해당하는 내용을 출력해주고, dump( ) 함수는 출력하려는 인스턴스의 자세한 내부 콘텐츠까지 출력해준다.
struct BasicInformation{
let name: String
var age: Int
}
var lhjInfo: BasicInformation = BasicInformation(name: "lhj", age: 24)
class Person{
var height: Float = 0.0
var weight: Float = 0.0
}
let lhj: Person = Person()
lhj.height = 174.4
lhj height = 84.5
pring(lhjInfo) //BasicInformation(name: "lhj", age: 24)
dump(lhjInfo)
/*
▿ BasicInfomation
- name: "lhj"
- age: 24
*/
pring(lhj) //Person
dump(lhj)
/*
▿ Person #0
- height: 174.4
- weight: 78.5
*/
2.2.2 문자열 보간법
문자열 보간법은 변수 또는 상수 등의 값을 문자열 내에 나타내고 싶을 때 사용한다. 문자열 내에 \ (변수나 상수)의 형태로 표기하면 이를 문자열로 치환해서 넣는다. 문자열 보간법을 이용해 프로그래머가 원하는 문자열로 치환하려면 변수나 상수 타입을 CustomStringConvertible 프로토콜을 준수하는 ddescription 프로퍼티로 구현한다.
let name: String = "lhj"
print("My name is \(name)")
위의 실행 결과로 디버깅 콘솔에서 ‘My name is lhj’이라는 결과를 볼 수 있다.
2.3 주석
주석은 프로그램 소스 코드에 정보를 남기는 목적으로 사용한다. 주로 코드를 다시 봤을 때 필요한 중요 메모나 다른 프로그래머에게 설명하기 위한 메모 등을 주석으로 남긴다.
Xcode에는 말풍선의 형태로 레퍼런스 문서의 요약된 내용으 ㄹ보여주는 퀵헬프라는 기능이 있다. 코드를 작성하는 중에 레퍼런스 문서로 이동하지 않고도 ( 퀵헬프를 지원하는 ) 데이터 타입이나 메서드 드의 간단한 정보를 확인할 수 있는 아주 유용한 기능이다.
퀵헬프를 보려면 퀵헬프를 보기 원하는 항목 ( 변수, 상수, 함수, 메서드, 타입 등등 ) 위에 마수르 커서를 위치한 다음 키보드의 옵션키를 누른 상태로 클릭하면 된다. 마크업 문법에 맞춰 메서드나 변수, 클래스 등에 주석을 작성하면 퀵헬프로 다른 프로그래머가 해당 내용을 확인할 수 있다. 형식을 맞추는 일이 번거로운 수는 있으나 문서화에 큰 도움이 된다.
2.3.1 주석 남기기
스위프트에서는 여러 종류의 주석을 달 수 있다. 주석을 Xcode의 퀵헬프 기능을 통해 볼 수 있게 하려면 마크업 문법을 사용해서 작성하면 된다. 다음은 스위프트에서 사용할 수 있는 주석 종류이다.
1. 한 줄 주석
한줄 주석은 슬래시 두 개를 사용하여 나타낸다.
// 한 줄 주석은 이렇게 표현한다.
2. 여러 줄 주석
여러 줄 주석은 슬래시와 별표를 사용하여 나타낸다.
/*
여러 줄 주석을 시작할 때는 슬래시와 별표를 사용한다.
이 줄의 앞에는 별다른 표시가 없지만 이 줄도 주석으로 인식한다.
주석을 끝내고 싶을 때는 별표를 먼저 쓰면 된다.
*/
3. 중첩 주석
스위프트에서는 여러 줄 주석 안에 여러 줄 주석 또는 한 줄 주석을 넣는 중첩 주석을 지원한다.
/* 여러 줄 주석 안쪽에
/* 추가로 여러 줄 주석을 포함할 수 있으며
// 그안에 한줄 주석을 추가하여도 무방하다.
*/
이부분도 주석처리 된다.
*/
2.4 스위프트 변수와 상수
우리는 변수나 상수를 이용해 프로그램에서 사용되는 데이터를 메모리에 임시로 저장한다. 이때 변수와 상수는 특정 데이터 타입에 해당하는 값의 이름이다. 변수는 생성 후 데이터값을 변경할 수 있지만, 상수는 한번 값을 설정하면 다음에 변경할 수 없다.
2.4.1 변수
스위프트에서 변수를 생성하려면 var 키워드를 사용해야 한다. var [변수명]: [데이터 타입] = [값]의 형태로 선얺나다. 변수를 생성할 때 데이터 타입은 생략할 수 있다.
var name: String = "lhj"
var age: Int = 24
var job = "IOS Programmer" // 타입 추론이 사용됨.
var height = 174.4 // 실수 타입에 타입 추론으로 어떤 타입이 지정되는지 확인해보기.
age = 99 // 변수는 값을 변경해줄 수 있다.
job = "Android Programmer" // 값을 변경할 때는 기존과 같은 타입의 값을 할당해주어야 한다.
2.4.2 상수
스위프트에서는 let 키워드를 사용해서 상수를 생성한다. let [상수명] : [데이터 타입] = [값]의 형태로 선언한다. 변수 생성과 마찬가지로 상수 생성 때도 데이터 타입을 생략할 수 있다.
let name: String = "lhj" // 차후 변경하지 않는 값은 상수로 선언된다.
var age: Int = 24
var job = "IOS Programmer" // 타입 추론이 사용됨.
var height = 174.4 // 실수 타입에 타입 추론으로 어떤 타입이 지정되는지 확인해보기.
age = 99 // 변수는 값을 변경해줄 수 있다.
job = "Android Programmer" // 값을 변경할 때는 기존과 같은 타입의 값을 할당해주어야 한다.
name = "hyung jun" // 상수로 선언된 값은 변경할 수 없다. 오류가 발생한다.
3. 데이터 타입의 기본
데이터 타입은 프로그램 내에서 다뤄지는 데이터 종류를 뜻한다. 차후에 문법을 더 많이 익히면 좋겠지만, 스위프트의 기본 데이터 타입은 구조체를 타입의 기반으로 삼아 스위프트의 다양한 기능(익스텐션, 제네렉 등)을 두루 사용하여 구현되어 있다. 그중 가장 주목해야 할 점은 스위프트의 기본 데이터 타입이 모두 구조체를 기반으로 구현되어 있다는 것이다.
3.1 Int와 Unit
정수 타입이다. Int는 +, - 부호를 포함한 정수를 뜻하며 이 중 - 부호를 포함하지 않는 0을 포함한 양의 정수는 Unit으로 표현한다. Int와 Unit 타입의 최대값과 최솟값은 각각 max와 min 프로퍼티로 알아볼 수 있다. Int와 Unit는 각각 8비트, 16비트, 32비트, 64비트의 형태가 있다. 즉, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64등으로 조장할 수 있는 데이터의 크기에 따라 타입이 분리되어 있다. 시스템 아키텍쳐에 따라 Int와 Unit의 타입이 달라진다.
var integer: Int = -100
let unsignedInteger: UInt = 50 // Unit 타입에는 음수값을 할당할 수 없다.
print("interger 값 : \(interger), unsignedInterger 값 : \(unsignedInteger)")
print("Int 최대값 : \(Int.max), Int 최소값 : \(Int.min)")
print("UInt 최대값 : \(UInt.max), UInt 최소값 : \(UInt.min)")
let largeInteger: Int64 = Int64.max
let smallUnsignedInteger: UInt8 = UInt8.max
print("Int64 최대값 : \(largeInteger), UInt8 최대값 : \(smallUnsignedInteger)")
let tooLarge: Int = Int.max + 1 // Int의 표현 범위를 초과하므로 오류를 낸다.
let cannotBeNegetive: UInt = -5 // UInt는 음수가 될 수 없으므로 오류를 낸다.
integer = unsignedInteger // 오류! 스위프트에서 Int와 UInt는 다른 타입이다.
integer = Int(unsigendInteger) // Int 타입의 값으로 할당해주어야 한다.
다음은 각 진수에 따라 정수를 표현하는 방법이다.
- 10진수 : 우리가 평소에 쓰던 숫자와 동일하게 사용하면 된다.
- 2진수 : 접두어 0b를 사용하여 표현한다.
- 8진수 : 접두어 0o를 사용하여 표현한다.
- 16진수 : 접두어 0x를 사용하여 표현한다.
3.2 Bool
Bool은 블리언 타입이다. 불리언 타입은 참 (true) 또는 거짓 (false)만 값으로 가진다.
var boolean: Bool = true
boolean.toggle() // true - false 반전
let iLoveYou: Bool = true
let isTimeUnlimited: Boll = false
3.3 Float와 Double
Float와 Double은 부동소숫점을 사용하는 실수며 부동소수 타입이라고 한다. 흔히 우리가 말하는 소수점 자리가 있는 수이다. 부동소수 타입은 정수 타입보다 훨씬 넓은 범위의 수를 표현할 수 있다. 스위프트에는 64비트의 부동소수 표현을 하는 Double과 32비트의 부동 소수를 표현하는 Float이 있다.
// Float이 수용할 수 있는 범위를 넘어선다.
// 자신이 감당할 수 있는 만큼만 남기므로 정확도가 떨어진다.
var floatValue: Flaot = 1234567890.1
//Double은 충분히 수용할 수 있다.
let doubleValue: Double = 1234567890.1
//Float이 수용할 수 있는 범위의 수로 변경한다.
floatValue = 123456.1
3.4 Character
Character는 말 그대로 ‘문자’를 의미한다. 단어, 문장처럼 집합이 아닌 단 하나의 문자를 의미한다. 스위프트는 유니코드9 문자를 사용하므로 영어는 물론, 유니코드에서 지원하는 모든 언어 및 특수기호 등을 사용할 수 있다. 문자를 표현하기 위해서는 값의 앞 뒤에 큰 따옴표를 사용하여 표현한다.
let alphabetA: Character = "A"
//Character 값에 유니코드 문자를 사용할 수 있다.
let commandCharacter: Character = "☆"
// 한글도 유니코드 문자에 속하므로 스위프트 코드의 변수 이름으로 사용할 수 있다.
let 한글변수이름: Character = "ㄱ"
3.5 String
String은 문자의 나열, 즉 문자열이다. String은 Character와 마찬가지로 유니코드 9를 사용할 수 있으며, 값의 앞뒤에 큰따옴표를 사용하여 표현한다.
// 상수 선언된 문자열은 변경이 불가능하다.
let name: String = "lhj"
// 이니셜라이저를 사용하여 빈 문자열을 생성할 수 있다.
// var 키워드를 사용하여 변수를 생성했으므로 문자열의 수정 및 변경이 가능하다.
var introduce: String = String()
// append() 매서드를 사용하여 문자열을 이어붙일 수 있다.
introduce.append("제 이름은")
// + 연산자를 통해서도 문자열을 이어붙일 수 있다.
introduce = introduce + " " + name + "입니다."
print(introduce)
// name에 해당하는 문자의 수를 셀 수 있다.
print("name의 글자 수 \(name.count)")
// 빈 문자열인지 확인해볼 수 있다.
print("introduce가 비어있나요? : \(introduce.isEmpty)")
// 유니코드의 스칼라값을 사용하면 값에 해당하는 표현이 출력된다 - 어떤 모양이 출력될까?
let unicodeScalarValue: String = "\u{2665}")
사실 문자열을 다루는 것은 프로그래밍에서 꽤 까다로운 부분 중 하나이다. 그러나 스위프트에서는 비교적 손쉽게 문자열을 다룰 수 있다. String 타입에는 기본적으로 많은 메서드와 프로퍼티들이 구현되어 있다. 문자열과 관련된 연산자도 많이 정의되어 있기 때문에 문자열을 조금이나마 다루기 쉽게 지원해준다.
3.5.1 특수 문자
스위프트에는 문자열 내에서 일정 기능을 하는 특수문자(제어문자라고도 한다.)가 있다. 특수문자는 모두 백슬래시에 특정한 문자를 조합하여 사용한다. 가장 많이 쓰는 특수문자는 아래와 같다.
| 특수문자 | 설명 |
|---|---|
| \n | 줄바꿈 문자 |
| \ | 문자열 내에서 백슬래시를 표현하고자 할 때 사용 |
| * | 문자열 내에서 큰따옴표를 표현하고자 할 때 사용 |
| \t | 탭 문자. 키보드의 탭키를 눌렀을 때와 같은 효과 |
| \0 | 문자열이 끝났음을 알리는 null 문자 |
3.6 Any, AnyObject와 nil
Any는 스위프트의 모든 데이터 타입을 사용할 수 있다는 뜻이다. 변수 또는 상수의 데이터 타입이 Any로 지정되어 있다면 그 변수 또는 상수에는 어떤 종류의 데이터 타입이든지 상관없이 항당할 수 있다.
AnyObject는 Any보다는 조금 한정된 의미로 클래스의 인스턴스만 할당할 수 있다. 클래스에 대한 내용은 후에 다루겠다.
var someVar: Any = "lhj" // Any로 선언된 변수에는 문자열도
someVar = 50 // 정수도
someVar = 100.1 // 실수, 또는 어떤 타입의 값이라도 할당할 수 있다.
[NOTE]
nil은 사실 특정 타입이 아니라 ‘없음’을 나타내는 스위프트의 키워드이다. 즉, 변수 또는 상수에 값이 들어있지 않고 비어있음을 나타내는 데 사용한다. 변수 또는 사우셍 값이 없는 경우, 즉 nil이면 해당 변수 또는 상수에 접근했을 때 잘못된 메모리 접근으로 런타임 오류가 발생한다. 잘못된 메모리에 접근하여 발생하는 런타임 오류중 흔히 널 포인터 익셉션이라고 불리는 오류 상황에 대해서는 이후에 다루겠다.
4.1 데이터 타입 안심
애플이 처음 스위프트를 발표할 때 강조했던 스위프트의 특징 중 안정성(safe)이 가장 뚜렷하게 나타나는 부분이다. 스위프트는 타이벵 굉장히 민감하고 엄격하다. 서로 다른 타입끼리의 데이터 교환은 꼭 타입캐스팅을 거쳐야 한다. 스위프트에서 값 타입의 데이터 교환은 엄밀히 말하면 타입캐스팅이 아닌 새로운 인스턴스를 생성하여 할당하는 것이다.
4.1.1 데이터 타입 안심이란?
스위프트는 데이터 타입을 안심하고 사용할 수 있는(Type-safe)언어이다. 타입을 안심하고 사용할 수 있다는 말은 그만큼 실수를 줄일 수 있다는 의미이다. 예를 들어 Int 타입 변수에 할당하려는 값이 Character 타입이라면 컴파일 오류가 발생한다. 이런 오류는 프로그래밍 도중에 눈치채기 어려워 컴파일러가 알려주지 않으면 나중에 오류를 찾아내기도 쉽지 않다. 그렇지만 스위프트는 컴파일 오류로 알려주므로 서로 다른 타입의 값을 할당하는 실수를 줄일 수 있다. 이렇게 스위프트가 컴파일 시 타입을 확인하는 것을 타입 확인이라고 한다. 타입 확인을 통해 여러 타입을 섞어 사용할 때 발생할 수 있는 런타임 오류를 피할 수도 있다.
4.1.2 타입 추론
스위프트에서는 변수나 상수를 선언할 때 특정 타입을 명시하지 않아도 컴파일러가 할당된 값을 기준으로 변수나 상수의 타입을 결정한다. 예를 들어 let name = “hyungjun” 라는 코드를 작성하면, 컴파일러가 컴파일하면서 name의 타입을 String으로 결정한다.
// 타입을 지정하지 않았으나 타입 추론을 통하여 name은 String 타입으로 선언된다.
var name = "Hyungjun"
// 앞서 타입 추론에 의해 name은 String 타입의 변수로 지정되었기 때문에
// 정수를 할당하려고 시도하면 오류가 발생한다.
name = 100
4.2 타입 별칭
스위프트에서 기본적으로 제공하는 데이터 타입이든, 사용자가 임의로 만든 데이터 타입이든 이미 존재하는 데이터 타입에 임의로 다른 이름 (별칭) 을 부여할 수 있다. 그런 다음 기본 타입 이름과 이후에 추가한 별칭을 모두 사용할 수 있다. 아래 코드를 보면 쉽게 이해할 수 있을 것이다.기존에 사용하던 데이터 타입의 이름과 프로그래머가 만들어준 이름 모두 사용할 수 있다.
typealias MyInt = Int
typealias YourInt = Int
typealias MyDouble = Double
let age: MyInt = 100 // MyInt는 Int의 또 다른 이름이다.
var year: YourInt = 2000 // YourInt도 Int의 또 다른 이름이다.
// MyInt, YourInt 모두 Int이기 때문에 같은 타입으로 취급한다.
year = age
let month: Int = 7 // 물론 기존의 Int도 사용가능하다.
let percentage: MyDouble = 99.9 // Int 외에 다른 자료형도 모두 별칭 사용 가능하다.
4.3 튜플
튜플(Tuple)은 타입의 이름이 따로 지정되어 있지 않은, 프로그래머 마음대로 만드는 타입이다. ‘지정된 데이터의 묶음’이라고 표현할 수 있다. C 언어를 예로 들자면 원시 구조체의 형태와 비슷하다.
스위프트의 튜플은 파이썬의 튜플과 유사하다. 튜플은 타입 이름이 따로 없으므로 일정 타입의 나열만으로 튜플 타입을 생성해줄 수 있다. 튜플에 포함될 데이터의 개수는 자유롭게 정할 수 있다. 하나가 될 수도, 두 개가 될 수도, 여러개가 될 수도 있다.
// String, Int, Double 타입을 갖는 튜플
var person: (String, Int, Double) = ("lhj", 24, 174.4)
// 인덱스를 통해서 값을 빼 올 수 있다.
print("이름 : \(person.0), 나이 : \(person.1), 신장 : \(174.4)")
person.1 = 99
person.2 = 188.4 // 인덱스를 통해 값을 할당할 수 있다.
위의 코드에서 튜플은 각 요소를 이름 대신 숫자로 표현하기 때문에 간편해 보일 수 있지만, 차후에 다른 프로그래머가 코드를 볼 때 각 요소가 어떤 의미가 있는지 유추하기가 어렵다. 이름 없이 인덱스만으로 각 요소으 ㅣ데이터가 무엇을 나타내는지 쉽게 파악하기가 어렵기 때문이다. 그래서 튜플의 요소마다 이름을 붙여줄 수 도 있다. 아래 코드에서는 튜플의 요소마다 이름을 붙인 것이다.
// String, Int, Double 타입을 갖는 튜플
var person: (name: String, age: Int, height: Double) = ("lhj", 24, 174.4)
// 요소 이름ㅇ르 통해서 값을 빼 올 수 있다.
print("이름 : \(person.name), 나이 : \(person.age), 신장 : \(person.height)")
person.age = 99 // 요소 이름을 통해 값을 할당할 수 있다.
person.2 = 188.4 // 인덱스를 통해서도 값을 할당할 수 있다.
// 기존 처럼 인덱스를 이용하여 값을 빼 올 수도 있다.
print("이름 : \(person.0), 나이 : \(person.1), 신장 : \(174.4)")
또, 튜플에는 타입 이름에 해당하는 키워드가 따로 없다 보니 사용에 불편함을 겪기도 한다. 매번 같은 모양의 튜플을 사용하고 싶은데 선언해줄 때마다 긴 튜플 타입을 모두 써줘야 하는 불편함이 생길 수 있기 때문이다. 이럴 때는 타입 별칭을 사용하여 조금 더 깔끔하고 안전하게 코드를 작성할 수 있다.
typealias PersonTuple = (name: String, age: Int, height: Double)
let lhj: PersonTuple = ("lhj", 24, 174.4)
let jhl: PersonTuple = ("jhl", 24, 188.4)
print("이름 : \(lhj.name), 나이 : \(lhj.age), 신장 : \(lhj.height)")
print("이름 : \(jhl.name), 나이 : \(jhl.age), 신장 : \(jhl.height)")
4.4 컬렉션형
스위프트는 튜플 이외에도 많은 수의 데이터를 묶어서 저장하고 관리할 수 있는 컬렉션 타입을 제공한다. 컬렉션 타입에는 배열(Array), 딕셔너리(Dictionary), 세트(Set)등이 있다.
4.4.1 배열
배열은 같은 타입의 데이터를 일렬로 나열한 후 순서대로 저장하는 형태의 컬렉션 타입이다. 각기 다른 위치에 같은 값이 들어갈 수도 있다.
배열 타입을 선언해줄 방법은 다양하다. let 키워드를 사용해 상수로 선언하면 변경할 수 없는 배열이 되고, var 키워드를 사용해 변수로 선언해주면 변경 가능한 배열이 된다. 실제로 배열을 사용할 때는 Array라는 키워드의 타입 이름의 조합으로 사용한다. 또, 대괄호로 값을 묶어 Array 타입임을 표현할 수도 있다. 빈 배열은 이니셜라이저 또는 리터럴 문법을 통해 생성해줄 수 있는데 isEmpty 프로퍼티로 비어있는 배열인지 확인해 볼 수 있다. 그리고 배열에 몇 개의 요소가 존재하는지 알고 싶으면 count 프로퍼티를 확인하면 된다.
// 대괄호를 사용하여 배열임을 표현한다.
var names: Array<String> = ["lhj", "hyungjun", "limhj", "lhyungj"]
// 위 선언고 ㅏ정확히 동일한 표현이다. [String]은 Array<String>의 축약 표현이다.
var names: [String] = ["lhj", "hyungjun", "limhj", "lhyungj"]
var emptyArray: [Any] = [Any]() // Any 데이터를 요소로 갖는 빈 배열을 생성한다.
var emptyArray: [Any] = Array<Any>() // 위 선언과 정확히 같은 동작을 하는 코드이다.
// 배열의 타입을 정확히 명시해줬다면 [ ]만으로도 빈 배열을 생성할 수 있다.
var emptyArray: [Any] = []
print(emptyArray.isEmpty) // true
print(name.count) // 4
배열은 각 요소에 인덱스를 통해 접근할 수 있다. 인덱스는 0부터 시작한다. 잘못된 인덱스로 접근하려고 하면 인셉션 오류(Exception Error)가 발생한다. 또, 맨 처음과 맨 마지막 요소는 fist와 last 프로퍼티를 통해 가져올 수 있다. firstIndex(of:) 메서드를 사용하면 해당 요소의 인덱스를 알아낼 수도 있다. 만약 중복된 요소가 있다면 제일 먼저 발견된 요소의 인덱스를 반환한다. 맨 뒤에 요소를 추가하고 싶다면 append(_:) 메서드를 사용한다.
중간에 요소를 삽입하고 싶다면 insert(:at:)** 메서드를 사용하면 된다. 요소를 삭제하고 싶다면 **remove(_:) 메서드를 사용하게 되는데, 메서드를 사용하면 해당 요소가 삭제된 후 반환된다.
print(names[2]) // hyungjun
names[2] = "limhyungjun"
print(names[2]) // limhyungjun
print(names[4]) // 인덱스의 범위를 벗어났기 때문에 오류가 발생한다.
names[4] = "hyungjun" // 인덱스의 범위를 벗어났기 때문에 오류가 발생한다.
names.append("hyungjun") // 맨 마지막 hyungjun이 추가된다.
names.append(contentsOf: ["hj", "lh"] // 맨 마지막에 hj, lh가 추가된다.
names.insert("jhl", at: 2) // 인덱스 2에 삽입된다.
// 인덱스 5의 위치에 abc, cba가 삽입된다.
names.insert(contentsOf: ["abc", "cba"], at: 5)
print(names[4]) // lhyungj
print(names.firstIndex(of: "lhj")) // 0
print(names.firstIndex(of: "baob")) // nil
print(names.first) // lhj
print(names.last) // lh
let firstItem: String = names.removeFirst()
let lastItem: String = names.removeLast()
let iundexZeroItem: String = names.remove(at: 0)
print(firstItem)
print(lastItem)
print(indexZeroItem)
print(names[1 ... 3])
위 코드에서 맨 아래 줄의 names[1 … 3] 표현은 범위 연산자를 사용하여 names 배열의 일부만 가져온 것이다. 코드처럼 읽기만 가능한 것이 아니라 names[1 … 3] = [”A”, “B”, “C”]와 같이 범위에 맞게 요소를 바꾸는 것도 가능하다.
스위프트의 배열을 비롯한 컬렉션 타입을 활용할 때 서브스크립트(Subscript) 기능을 많이 사용한다.
4.4.2 딕셔너리
딕셔너리는 요소들이 순서 없이 키와 값의 쌍으로 구성되는 컬렉션 타입이다. 딕셔너리에 저장되는 값은 항상 키와 쌍을 이루게 되는데, 딕셔너리 앞에는 키가 하나이거나 여러 개일 수 있다. 단, 하나의 딕셔너리 안의 키는 같은 이름을 중복해서 사용할 수 없다. 쉽게 말해서 아래의 코드에서 “lhj”라는 키가 두 번 쓰일 수 없다는 뜻이다. 즉, 딕셔너리에서 키는 값을 대변하는 유일한 식별자가 되는 것이다.
딕셔너리는 Dictionary라는 키워드와 키의 타입과 값의 타입 이름의 조합으로 써준다. 대관호로 키와 값의 타입 이름의 쌍을 묶어 딕셔너리 타입임을 표현한다. let 키워드를 사용하여 상수로 선언하면 변경 불가능한 딕셔너리가 되고, var 키워드를 사용하여 변수로 선언해주면 변경 가능한 딕셔너리가 된다. 빈 딕셔너리는 이니셜라이저 도는 리터럴 문법을 통해 생성할 수 있다. isEmpty 프로퍼티를 통해 비어있는 딕셔너리인지 확인할 수 있다. 그리고 count 프로퍼티로 딕셔너리의 요소 개수를 확인할 수 있다.
// typealias를 통해 조금 더 단순하게 표현해 볼 수 있다.
typealias StringIntDictionary = [String: Int]
// 키는 String, 값은 Int 타입인 빈 딕셔너리를 생성한다.
var numberForName: Dictionary<String, Int> = Dictionary<String, Int>()
// 위 선언과 같은 표현이다. [String: Int]는 Dictionary<String, Int>의 축약 표현이다.
var numberForName: [String, Int] = [String: Int]()
// 위 코드와 같은 동작을 한다.
var numberForName: StringIntDictionary = StringIntDictionary()
// 딕셔너리와 키와 값 타입을 정확히 명시해줬다면 [:]만으로도 빈 딕셔너리를 생성할 수 있다.
var numberForName: [String, Int] = [:]
// 초기값을 주어 생성해줄 수도 있다.
var numberForName: [String: Int] = ["lhj": 24, "babo": 100, "bab": 300]
print(numberForName.isEmpty) // false
print(numberForName.count) // 3
딕셔너리는 각 값에 키로 접근할 수 있다. 딕셔너리 내부에서 키는 유일해야 하며, 값은 유일하지 않다. 딕셔너리는 배열과 다르게 딕셔너리 내부에 없는 키로 접근해도 오류가 발생하지 않는다. 다만 nil을 반환할 뿐이다. 특정 키에 해당하는 값을 제거하려면 removeValue(forKey: ) 메서드를 사용한다. 키에 해당하는 값이 제거된 후 반환된다.
print(numberForName["babo"]) // 100
print(numberForName["hyungjun"]) // nil
numberForName["babo"] = 150
print(numberForName.["babo"]) // 150
numberForName["max"] = 999 // max라는 키로 999라는 값은 추가한다.
print(numberForName.["max"]) // 999
print(numberForName.removeValue(forKey: "lhj")) // 24
// 위에서 lhj 키에 해당하는 값이 이미 삭제되었으므로 nil이 반환된다.
// 키에 해당하는 값이 없으면 기본값을 돌려주도록 할 수 있다.
print(numberForName.removeValue(forKey: "lhj"))
// lhj 키에 해당하는 값이 없으면 기본으로 0이 반환된다.
print(numberForName["lhj", default: 0]) // 0
4.4.3 세트
세트는 같은 타입의 데이터를 순서 없이 하나의 묶음으로 저장하는 형태의 컬렉션 타입이다. 세트 내의 값은 모두 유일한 값, 즉 중복된 값이 존재하지 않는다. 그래서 세트는 보통 순서가 중요하지 않거나 각 요소가 유일한 값이어야 하는 경우 에 사용한다. 또, 세트의 요소로는 해시 가능한 값이 들어와야 한다.
세트는 Set 키워드와 타입 이름의 조합으로 써준다. 또, 배열과 마찬가지로 대괄호로 값들을 묶어 세트 타입임을 표현한다. 배열과 달리 표현할 수 있는 축약형(예를 들어 Array
(세트의 선언과 생성)
var names: Set<String> = Set<String>() // 빈 세트 생성
var names: Set<String> = [] // 빈 세트 생성
// Array와 마찬가지로 대괄호를 사용한다.
var names: Set<String> = ["lhj", "lhjj", "hyungjun", "lhj"]
// 그렇기 때문에 타입 추론을 사용하게 되면 컴파일러는 Set가 아닌 Array로 타입을 지정한다.
var numbers = [100, 200, 300]
print(type(of: numbers)) // Array<Int>
print(names.isEmpty) // false
print(names.count) // 3 - 중복된 값은 허용되지 않아 lhj는 하나만 남는다.
세트에 요소를 추가하고 싶다면 insert(:) 메서드를 사용한다. 요소를 삭제하고 싶다면 remove(:) 메서드를 사용하는데, 메서드를 사용하면 해당 요소가 삭제된 후 반환된다.
(세트의 사용)
print(names.count)
names.insert("lhj")
print(names.count)
print(names.remove("lhj") // lhj
print(names.remove("lim hyungjun") // nil
4.5 열거형
열거형은 연관된 항목들을 묶어서 표현할 수 있는 타입이다. 열거형은 배열이나 딕셔너리 같은 타입과 다르게 프로그래머가 정희해준 항목 값 외에는 추가/수정이 불가능하다. 그렇기 때문에 딱 정해진 값만 열거형 값에 속할 수 있다.
열거형은 다음과 같은 경우에 요긴하게 사용할 수 있다.
- 제한된 선택지를 주고 싶을 때
- 정해진 값 외에는 입력받고 싶지 않을 때
- 예상된 입력 값이 한정되어 있을 때
열겨형으로 묶을 수 있는 항목들은 주변 생활에서 많이 찾아 볼 수 있다.
- 무선통신 방식 : WiFi, 블루투스, LTE, 3G, 기타
- 학생 : 초등학생, 중학생, 고등학생, 대학생, 대학원생, 기타
- 지역 : 강원도, 경기도, 경상도, 전라도, 제주도, 충청도
우리는 앞서 열거형을 통하여 연관된 항목들의 그룹을 정의할 수 있다는 사실을 알았다. 다른 프로그래밍 언어에서 열거형을 사용해봤다면 조금 의아할 수 있지만, 스위프트의 열거형은 항목별로 값을 가질 수도, 가지지 않을 수도 있다. 예를 들어 C 언어는 열거형의 각 항목 값이 정수 타입으로 기본 지정되지만, 스위프트의 열거형은 각 항목이 그 자체로 고유의 값이 될 수 있다.
기존 C 언어 등에서 열거형은 주로 정수 타입 값의 별칭 형태로 사용이 될 뿐이었다. 그렇기 때문에 모든 열거형의 데이터 타입은 같은 타입(주로 정수 타입)으로 취급한다. 이는 열거형 각각이 고유의 타입으로 인식될 수 없다는 문제 때문에 여러 열거형을 사용할 때 프로그래머의 실수로 인한 버그가 생길 수도 있다. 그러나 스위프트의 열거형은 각 열거형이 고유의 타입으로 인정되기 때문에 실수로 버그가 일어날 가능성을 원천 봉쇄 할 수 있다.
물론 열거형 각 항목이 원시 값이라는 형태로 (정수, 실수, 문자 타입 등의) 실제 값을 가질 수도 있다. 또한 연관 값을 사용하여 다른 언어에서 공용체라고 불리는 값의 묶음도 구현할 수 있다.
4.5.1 기본 열거형
스위프트의 열거형은 enum이라는 키워드로 선언할 수 있다.
enum School{
case primary
case elementary
case middle
case high
case college
case university
case graduate
}
School 이라는 이름을 갖는 열거형에는 primary, elementary, middle, high, college, university, graduate라는 항목이 있다. 각 항목은 그 자체가 고유의 값이며, 항목이 여러가지라서 나열하기 귀찮거나 어렵다면 한 줄에 모두 표현해줄 수도 있다.
enum School{
case primary, elementary, middle, high, college, university, graduate
}
(School 열거형 변수의 생성 및 값 변경)
var highestEducationLevel: School = School.university
// 위 코드와 정확히 같은 표현이다.
var highestEducationLevel: School = .university
// 같은 타입인 School 내부의 항목으로만 highestEducationLevel의 값을 변경해줄 수 있다.
highestEducationLevel = .graduate
4.5.2 원시 값
열거형의 각 항목은 자체로도 하나의 값이지만 항목의 원시 값도 가질 수 있다. 즉, 특정 타입으로 지정된 값을 가질 수 있다는 뜻이다. 특정 타입의 값을 원시 값으로 가지고 싶다면 열거형 이름 오른쪽에 타입을 명시해주면 된다. 또, 원시 값을 사용하고 싶다면 rawValue라는 프로퍼티를 통해 가져올 수 있다.
enum School: String{
case primary = "유치원"
case elemenatary = "초등학교"
case middle = "중학교"
case high = "고등학교"
case college = "대학"
case university = "대학교"
case graduate = "대학원"
}
let highestEducationLevel: School = School.university
enum WeekDays: Character{
case mon = "월", tue = "화" ...
}
let today: WeekDays = WeekDays.fri
만약 일부 항목만 원시 값을 주고 싶다면 그렇게 해도 된다. 나머지는 스위프트가 알아서 처리해준다. 문자열 형식의 원시 값을 지정해줬다면 각 항목 이름을 그대로 원시 값으로 갖게 되고, 정수 타입이라면 첫 항목을 기준으로 0부터 1씩 늘어난 값을 갖게 된다.
(열거형의 원시 값 일부 지정 및 자동 처리)
enum School: String{
case primary = "유치원"
case elemenatary = "초등학교"
case middle = "중학교"
case high = "고등학교"
case college
case university
case graduate
}
let highestEducationLevel: School = School.university
enum Numbers: Int{
case zero
case one
case two
case ten = 10
}
//0, 1 ...
print("\(Numbers.zero.rawValue), \(Numbers.one.rawValue) ...")
4.5.3 연관 값
스위프트의 열거형 각 항목이 연관 값을 가지게 되면, 기존 프로그래밍 언어의 공용체 형태를 띌 수도 있다. 열거형 내 항목(case)이 자신과 연간된 값을 가질 수 있다. 연관 값은 각 항목 옆에 소괄호로 묶어 표현할 수 있다. 다른 항목이 연관 값을 갖는다고 모든 항목이 연관 값을 가질 필요는 없다.
enum MainDish{
case pasta(taste: String)
case pizza(dough: String, topping: String)
case chicken(withSauce: Bool)
case rice
}
var dinner: MainDish = MainDish.pasta(tase: "크림")
dinner = .pizza(dough: "치즈 크러스트", topping: "불고기")
dinner = .chicken(withSauce: true)
dinner = .rice
4.5.4 항목 순회
우리는 때때로 열거형에 포함된 모든 케이스를 알아야 할 때가 있습니다. 그럴 때 열거형의 이름 뒤에 콜론(;)을 작성하고 한 칸 띄운 뒤 CaseIterable 프로토콜을 채택해주세요. 그러면 열거형 allCases라는 이름의 타입 프로퍼티를 통해 모든 케이스의 컬렌션을 생성해준다
enum School: CaseIterable{
case primary
case elementary
case middle
case high
case college
case university
case graduate
}
let allCases: [School] = School.allCases
print(allCases)
// [School.primary, School.elementary, School.middle, School.high ... School.graduate]
원시값을 갖는 열거형이라면 원시값의 타입 다음에 쉼표(,)를 쓰고 띄어쓰기를 한 후 CaseIterable 프로토콜을 채택해 주면 된다.
enum School: String{
case primary = "유치원"
case elemenatary = "초등학교"
case middle = "중학교"
case high = "고등학교"
case college = "대학"
case university = "대학교"
case graduate = "대학원"
}
let allCaces: [School] = School.allCases
위 코드 두개처럼 단순한 열거형에는 CaseIterable 프로토콜을 채택해 주는 것만으로 allCases 프로퍼티를 사용할 수 있다. 그렇지만 조금 복잡해지는 열거형은 그렇지 않을 수도 있다.
4.5.5 순환 열거형
순환 열거형은 열거형 항목의 연관 값이 열거형 자신의 값이고자 할 때 사용한다. 순환 열거형을 명시하고 싶다면 indirect 키워드를 사용하면 된다. 특정 항목에만 한정하고 싶다면 case 키워드 앞에 indirect를 붙이면 되고, 열거형 전체에 적용하고 싶다면 enum 키워드 앞에 indirect 키워드를 붙이면 된다.
다음은 산술 연산을 위해 정의한 열거형
(특정 항목에 순환 열거형 항목 명시)
enum AritmeticExpression{
case number(int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
(열거형 전체에 순환 열거형 명시)
indirect enum ArithmeticExpression{
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
위의 열거형에는 정수를 연관 값으로 갖는 number라는 항목이 있고 덧셈을 위한 addition이라는 항목, 곱셈을 위한 multiplicaion 항목이 있다.
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let final = ArithmeticExpression.nultiplication(sum, ArithmeticExpression.number(2))
func evaluate(_ expression: ArithmeticExpression) -> Int{
switch expression{
case .number(let value):
return value
case .addition(let left, let right):
return evaluate(left) + evaluate(right)
case .multiplication(let left, let right):
return evaluate(left) * evaluate(right)
}
}
let result: Int = evaluate(final)
print("(5 + 4) * 2 = \(result)")
위 코드는 ArithmeticExpression 열거형을 사용하여 (5 + 4) x 2 연산을 구현해보는 예제다.
evaluate는 ArithmeticExpression 열거형의 계산을 도와주는 순환 함수다.