Swift 기본 문법

2022. 3. 10. 22:49Soft_Ware/ios

오늘은 IOS 개발을 위한 언어인 Swift에 대해서 간략히 다뤄본다.

IOS를 개발하기 위해서 과거에는 객체지향 언어인 Objective-C라는 언어를 활용했지만, 최근..이라고 하기도 뭐하고 여하튼 요즘은 Swift언어를 활용한다. 다른 언어로 프로그래밍을 진행해본적이 있으면 아마 매우 쉽게 느껴질 것이다.

스파르타 코딩의 앱 개발 기초반을 수강하며 쓰는 글이며, 아주 기초적인 내용을 다뤄보도록 한다. 

  • 변수

가장 흔히 확인할 수 있는 변수이다. Swift에서는 var라는 키워드를 통해 변수를 선언하고 빌드 시 Xcode에서 알아서 자료형을 지정해주는 방식일 것이다.

var name = "Swift"

위와 같은 방식으로 선언한다. 프로그래밍을 하다 보면 개발자들끼리의 Convention이 존재한다.

Convention중에서도 Naming Convention에 대해서 확인해 볼건데, 이거는 네이밍 하는 관습? 이라고 생각하면 편하다.

크게 Snake case와 Camel case로 나뉜다.

var snake_case = "키워드를 밑줄 문자로 구분"
var camelCase = "키워드를 대문자로 구분"

위에 변수로 표현을 해봤는데, 

Snake case는 변수나 함수를 네이밍할 때 첫 글자는 소문자이고 키워드를 _ 문자로 구분한다. 모든 문자가 소문자로 이루어져 있다.

ex) snake_case_variable

Camel case는 변수나 함수를 네이밍할 때 첫 글자는 소문자이고 구분할 키워드의 첫 문자를 대문자로 쓴다.

ex)camelCaseVariable

이렇게 두가지의 Convention이 있고 Swift에서는 변수, 함수를 네이밍 할 때 Camel case를 활용한다. 아마 파일 네이밍 할 때는 Snake case를 활용할거다.

또한, 자료형을 명시하면서 변수를 선언하고 싶다면

var integerValue : Int = 1
var strValue : String = "StringValue"

와 같은 방식을 활용하면 된다. 그리고 Swift는 C나 Java와는 달리 구분자로 세미콜론(;)을 사용하지 않는다.

이렇게 하나의 값만 담는 변수와는 다르게 여러가지의 값을 담는 변수도 존재한다.

dictionary

딕셔너리는 해석한 그대로 사전과 같은 역할을 한다. 키/값 쌍으로 존재하며 특정 키에는 특정 값이 들어가 있다. 영한사전을 찾아보면 영어로 된 apple이 한국어로 된 사과라고 번역되어 있는 것을 알 수 있는데, 여기서 apple은 키(key) 라고 생각하면 되고 사과는 값(value)이라고 생각하면 된다. 코드로 살펴보자.

var dictionary : [String : String] = [:]

dictionary = [
    "apple" : "사과",
    "Orange" : "오렌지",
    "WaterMelon" : "수박",
]

print(dictionary["WaterMelon"])

dictionary를 선언만 하고 싶을 때는 위와 같이 키/값의 자료형을 명시해주어야 한다. 그렇게 하지 않으면 iteral 뭐 어쩌구 에러가 뜨기 때문에 자료형을 명시해주자. 선언과 동시에 초기화도 가능하다. 선언 및 초기화는 대괄호 안에서 이루어지며 특정 키에 대한 값을 입력할 때는 콜론(:)을 사용해준다. 아래와 같이 print함수에 dictionary를 활용하고 싶다면 변수에 해당 키 값을 대괄호 안에 명시해주면 된다.

Array

변수의 마지막인 배열이다. 배열은 흔히 같은 자료형의 변수를 여러개 선언하고 싶을 때 사용하곤 한다. 

배열의 선언에 관련된 부분이다.

//빈 배열 선언
var emptyArray : [String] = []
var emptyArray2 = [String]()

//선언과 동시에 초기화 작업
var emptyArray : [String] = ["apple", "banana", "otherFruit"]
var emptyArray3 : [Int] = [15,20,25,30]

흔히 두가지로 값이 없는 배열만을 선언하는 경우와 값이 존재하는, 즉 초기화와 함께 선언이 이루어지는 경우로 나누어서 예시를 들어보았다. 배열은 인덱스라고 하는 순서에 따라서 값이 저장되는데 배열의 참조는 아래와 같이 인덱스를 활용하면 된다.

 var exampleArray : [String] = ["First", "Second", "Third"]
 print(exampleArray[0]) //First
 print(exampleArray[1]) //Second
 print(exampleArray[2]) //Third

  

  • 출력문

출력문은 다른 언어와 같이 print 함수를 사용한다.

var message = "WelCome"
print(message)

변수와 함께 print함수에 직접 무언가를 출력하고 싶은 경우도 있을텐데, 이 경우에는 

var name = "Harry Potter"

print("Name is \(name)")

위와 같은 방법을 활용하면 된다.

-> "쓰고 싶은말   \(변수 이름)  쓰고 싶은 말"

  • 조건문

흔하지만 중요한 조건문이다. swift도 다른 언어와 거의 비슷한 형식으로 조건문이 사용된다.

크게 두가지로 if-else문과 switch-case문이다.

if-else문

if-else문은 키워드 자체로 유추가 가능한데, 만약에 ~라면 ~ 실행, 그리고 아니라면 ~실행 으로 쓰여질 수 있다.

코드로 살펴보자.

//if-else문

if 조건 {
	명령문1
} else {
	명령문2
}

//또는

if 조건1 {
	명령문1
} else if 조건2 {
	명령문2
} else {
	명령문3
}

다른 언어와 마찬가지로 조건에 해당하거나 해당하지 않을 때 명시한 명령문을 실행시킨다. 다른 언어와 특히 다른점은 조건문을 괄호 안에 명시하는데 swift는 괄호가 없다. 대체적으로 괄호가 많이 생략되어 있다.

예시 코드로 확인해보자.

var age : Int = 20

if age > 20 {
	print("성인입니다.")
} else {
	print("미성년자입니다.")
}

여기까지가 간략한 if else문이다.

Switch-Case문 

또 다른 조건문으로는 switch-case문이 있다.

if else문은 조건문안에 명사와 동사가 같이 존재한다면, switch-case문은 switch에 명사를 대입하고 case에 동사를 대입하는 느낌이다. 코드로 살펴보자.

switch 변수{
case 조건1:
	명령문1
case 조건2:
	명령문2
case 조건3:
	명령문3
default:
	명령문4
}

앞서 언급한 명사가 변수이고 동사가 조건에 해당한다고 생각하면 된다.

특이한 점은 default가 없으면 안되고, break문이 없다는 점이다. 실제 쓰이는 것을 보면 다음과 같다.

var exampleValue : Int = 5

switch exampleValue {
case 1:
	print("value is 1")
case 2:
	print("value is 2")
case 5:
	print("value is 5")
default:
	print("value is another")
}

default에 대한 부분을 명시해주지 않으면 Switch must be exhaustive라는 에러가 발생한다.

그리고 경험상 var로 선언한 자료형이 명시되지 않은 변수의 경우 추후에 찾기 힘든 에러를 발생시킬 우려가 있다.

이럴 경우를 위해서 항상 자료형을 명시하는 습관을 기르는것이 좋은 것 같다.  

  • 반복문

반복문도 역시 빼 놓을수 없는 기본 문법이다. 반복문 없이 프로그래밍하는데 있어서 반복되는 코드를 그대로 쓸 경우 가독성 뿐만 아니라 컴파일러나 인터프리터 입장에서도 프로세싱에 트래픽을 발생시킬 수 있다. 물론 요즘은 다 똑똑해져서 반복문 처럼 알아서 돌릴 가능성도 있긴 하다. 그래도 코드를 작성하는 개발자 입장에서 반복문은 필수적이다.

간단하게 사용법을 알아보자.

//1부터 10까지 출력하는 코드

var i = 1

//type1 ...
for i in 1 ... 10{
	print(i)
}

//type2 ..<
for i in 1 ..< 11{
	print(i)
}

for문은 위와 같이 두가지 방식으로 표현이 가능하다. 표현이 워낙 직관적이라서 이정도만 해도 이해가 가능할 것이라고 생각한다.

  • 함수

함수도 역시 프로그래밍을 하는데 있어서 필수적인 개념이다. 수학에서 사용하는 함수의 개념과 같다. input을 줬을 때 output을 주는 logic. 함수는 한개의 task만 처리하는 것이 좋다고 한다. 그래야 이름에 맞는 함수의 기능을 다 할수 있고 가독성 또한 증진되기 때문일 거다. 코드를 통해 알아보자 함수는 네가지 종류의 타입이 있다.

//인자, 리턴값 모두 없음
func type1Func(){
	print("call type1 Function")
}

//인자 있고 리턴값 없음
func type2Func(name : String){
	print("my name is \(name)")
}

//인자 없고 리턴값 있음
func type3Func() -> String{
	return "Return String Value"
}

//인자, 리턴값 모두 있음
func type4Func(age : Int) -> String{
	return "my age is \(age)"
}

함수는 이렇게 네가지 종류의 타입이 있고, 특이한 점은 리턴 자료형을 명시해줄때 type3, 4와 같이 ->로 표현을 해준다는 점이다. 그리고 호출을 할 때 인자가 있으면, 괄호 안에 인자 명을 명시해주어야 한다는 점이 특징이다.

print(type4Func(age:25))
  • 옵셔널

옵셔널은 정말 생소한 키워드이다. 흔히들 값이 비어있다라고 표현할 때 Null이라는 값을 많이 사용하는데 이와 관련된 용어이다. 참고로, 많은 언어에서는 Null이라고 명시하지만 Swift에서는 Nil이라고 한다.

본격적으로 옵셔널에 대해서 살펴보면, 옵셔널이라고 함은 Nil일수도 있는 값이다. 예를 들어 다음 코드를 살펴보자.

var dic : [String : Int] = [:]
dic = ["ID Number" : 123]
print(dic["ID Number"])

위 코드의 출력은 어떻게 될까?

답은 Optional(123) 이다. 여기의 Optional이 설명하려는 옵셔널이다.

옵셔널은 딕셔너리와 같이 값이 있을수도 있지만 없을수도 있는, 즉 Nil일수도 있는 값을 칭한다.

필자는 플러터 프레임워크를 통해 앱 개발을 해본적이 있는데 앱 개발 중에서 UI작업을 할 때에 보통 이런 옵셔널 변수가 자주 등장한다.

간단하게 옵셔널은 Nil일수도 있는 변수라고 생각하면 된다.

 

옵셔널의 특징 두가지만 간단하게 살펴보자.

첫째, 옵셔널 체인 : 어디선가 nil값이면  그 뒤의 값들은 모두 nil

둘째, 강제 옵셔널 해제 : 개발자가 개발을 하다가 이 값은 분명 nil이 아니라고 생각이 된다면 (!) 키워드를 통해 옵셔널을 해제시켜 줄 수 있다. 다음과 같은 예제를 보자.

var value: Int = 3 
var valueToBeSet: Int! = 4
var valueCanBeNil: Int? = 5

value = nil // 에러!
valueToBeSet = nil // 가능
valueCanBeNil = nil // 가능

value = valueToBeSet // 가능
value = valueCanBeNil // 불가능
value = valueCanBeNil! // 가능

위 코드에서 세개의 변수를 확인해 볼 수 있는데, 하나는 nil이 될 수 없는 일반 변수이고, 두번째 변수는 옵셔널을 강제 해제시킨 변수, 마지막은 옵셔널 변수이다. 이렇게 세가지 변수에 nil을 대입한다고 했을 때, 일반 변수만 에러가 발생한다. 애초에 자료형이 Int로 되어 있는 변수로 nil 값이 들어오면 에러를 발생시킨다. 하지만 옵셔널은 당연히 nil 이 될수도 있는 값이니까 가능한 것이고, valueToBeSet변수는 옵셔널 변수와 일반 변수 사이에 존재한다고 생각하면 된다.

사실 옵셔널을 강제 해제인 (!)키워드를 사용한 변수에 nil값을 넣으면 에러가 날 것이라고 생각하였는데, 그렇지 않았다.

이 부분에 대해서는 조금 더 공부해보고 나서 정리해야겠다. 

  • 클래스

클래스라는 것은 무엇일까?

흔히 클래스는 붕어빵 틀에 많이 비유하곤 한다. 각각을 만드는 기계같은 느낌이랄까..

클래스는 어떤 특성을 가지는 '' 이라고 생각하면 된다. 특성을 가지는 여러 것 중 하나 혹은 여럿을 의미하는 게 아니라

해당 특성을 가지는 대명사를 의미한다고 생각하면 될 것 같다.

그렇다면 대명사 중에서도 개개인 고유의 특성을 가지는 명사들은 무엇일까? 객체지향 프로그래밍 언어에서는 이러한 명사들을 보통 인스턴스, 객체라고 표현을 한다.

앞서 프로그래밍에서의 클래스를 토대로 예시를 살펴보자.

class Student{
    //Initializer
    init(age : Int, name : String){
        self.age = age
        self.name = name
    }
    
    //Member Function
    func introduce(){
        print("\(name)'s age is \(age)")
    }
    
    //Member Variable
    var age : Int
    var name : String
}

//make Instance & call function
var student1 = Student(age: 23, name: "Kate")
student1.introduce()

위 코드는 학생이라는 클래스를 선언해주고 생성자를 활용하여 특정 학생의 이름과 나이를 출력하는 코드이다.

여기서 초점을 둘 세가지 개념이 있다.

  • 생성자

생성자라는 것은 위에서 보이는 Initializer 주석에 보이는 코드이다.

클래스는 멤버 변수, 멤버 함수들이 존재하는데 이것들을 초기화 시켜줄 때 사용하곤 한다.

인자가 있는 생성자도 가능하고 생략 또한 가능하다.(생략할 경우 디폴트로 빈 생성자가 생성됨.)

  • 멤버 변수

멤버 변수는 클래스의 특성을 나타내주는 부분이다.

위에서는 학생이라는 클래스를 예시로 들어 설명하는데 학생의 특성을 나타내는 사람의 이름과 나이가 멤버 변수로 작용한다.

사실 사람의 이름과 나이는 굳이 학생이 아니라 사람이여도 가질 수 있는 특성이라서 사람 클래스에 멤버 변수로 두고, 학생은 학년이나 성적 등 클래스 고유의 특성을 멤버 변수로 선언하는 것이 올바르다. (+ 상속의 개념을 알고 있다면 조금 유연한 코드가 작성이 가능함. 나중에 상속 설명)

  • 멤버 함수

멤버 함수는 멤버 변수를 토대로, 혹은 특정 인자를 받아 클래스가 무언가를 작업하는 것을 뜻한다.

클래스의 특성을 살펴봤을 때 사실 인자를 받는 것보다는 멤버 변수를 토대로 무언가를 하는게 클래스의 의미론을 따져봤을 때 더 올바르기는 하다. 위에서는 학생이 자기소개하는 함수를 멤버 함수로 선언하였다.

  • 구조체

구조체는 클래스와 비슷한 특성을 가지는 개념이다. 한번 코드를 통해 살펴보자.

struct Student{
    //Function
    func introduce(){
        print("\(name)'s age is \(age)")
    }
    
    //Variable
    var age : Int
    var name : String
}

//make Instance & call function
var student1 = Student(age: 23, name: "Kate")
student1.introduce()

구조체는 클래스와 달리, 멤버 함수 멤버 변수 이런식으로 용어를 정해서 부르지는 않는다. 다만 선언이나 호출하는 방식은 클래스와 거의 유사하다.

위의 코드도 클래스와 마찬가지로 Student라는 구조체를 선언한 후 학생의 특성에 걸맞는 자기소개에 대한 행위를 함수를 통해 구현하고 있다.

구조체라는 것을 필자는 단순히 여러 자료형을 담고 있는 자료 구조라고 생각을 하고 있다.

 

마지막으로 유사하다고 느껴지는 두 개념인 클래스와 구조체의 차이에 대해서 조금 다뤄보도록 하자.

크게 세가지가 있다.

첫째, 생성자를 가지고 있는가

둘째, A 객체를 생성하고 B 변수를 만들어 A를 대입해주면 어떤 현상이 발생하는가

셋째, 둘째의 과정을 거쳐 만들어진 두 객체에 대해서 특정 연산을 시행했을 때 어떤 결과를 낳는가

 

우선, 첫째는 위에서 보면 알겠지만 구조체는 생성자가 없다.

이거는 구조체, 클래스를 만든 개발자가 만든 룰이다.

둘째와 셋째는 연관이 있으므로 코드를 통해 살펴보자.

struct Student{
    //Function
    func introduce(){
        print("\(name)'s age is \(age)")
    }
    
    //Variable
    var age : Int
    var name : String
}

//make Instance & call function
var student1 = Student(age: 23, name: "Kate")
//Copy Instance
var student2 = student1
//change name variable
student2.name = "Dan"
print("\(student2.name) is friend of \(student1.name)")

구조체 - 복제 <-> 클래스 - 별칭

마지막 라인에서 3줄 위로 올라와 보면 student2라는 변수에 student1객체를 대입해준 것을 확인할 수 있는데, 이 때 똑같은 학생 한명을 만들었다고 이해할 수 있다. 하지만 클래스를 통해 위와 같은 대입 연산을 진행해주면 학생을 부르는 명칭, 즉 별명 같은 것이 추가되었다고 이해할 수 있다.

세번째는 이렇게 복제된 객체에 대고 변수를 바꾸면 어떻게 작용할까에 대한 문제이다. 구조체의 경우 student1과 student2가 엄연히 다른 객체이므로 서로의 이름이 다르게 출력되는 것을 알 수 있다. 클래스는 이와는 다르게 복제된 객체에 멤버 변수를 바꾸게 되면 복제해준 객체 또한 멤버 변수가 바뀌게 된다. 찡찡이라는 별명을 가진 철수가 학년이 올라간다고 해서 철수만 학년이 올라가고 찡찡이는 안올라가지 않는 것과 같은 방식이다.

가장 마지막 라인의 결과를 살펴보면

struct의 경우, 

Dan is friend of Kate 라고 출력될 것이고,

class의 경우,

Dan is friend of Dan 이라고 출력될 것이다.

 

이상, Swift 언어의 기초 문법 글을 마무리한다.