※ 본 포스팅은 유튜브 Swift Programming Tutorial 강의를 기반으로 제가 직접 정리한 Swift 문법에 대한 글입니다.
Swift에 대해 처음 배워나가는 과정을 블로그에 정리하였습니다. 기본적으로 다른 프로그래밍 언어를 다룬 적이 있기에 제가 아는 부분은 생략될 수 있습니다.
강의는 freeCodeCamp 채널의 영상인데, Vandad라는 분이 진행하는 7시간 분량의 full tutorial for beginners입니다. 해당 강의는 프로그래밍 언어를 Swift로 처음 접하는 사람들에게는 다소 어려울 수 있다고 생각합니다. 저는 기초적인 부분의 디테일한 중요성을 깊게 알려주는 것 같아서 강의를 택했습니다. 영어 강의이며 영어자막 또는 자동번역 기능이 제공되기에, 혹시 프로그래밍 언어를 다룬 적이 있는 분들 중 Swift에 대해 새롭게 배우고자 한다면 강추드립니다!
< Variables >
Swift 문법을 배운다면 아마 가장 처음 접하게 될 부분입니다. 저는 오늘 이 파트에 대해서 단순히 "let은 상수 var은 변수다" 라는 설명을 하는 게 아니라 강의에서 개발자들이 많이 헷갈려하는 let 과 var 의 차이점에 대해 적어보고자 합니다.
일반적으로 `let`은 불변성(immutability)을 가진 상수(Constant)를 선언할 때 사용하고, `var`은 가변성(mutability)을 가진 변수(Variable)를 선언할 때 사용합니다. 저도 공식문서에서, 그리고 한국어로 된 강의에서 딱 이렇게까지 인지하고 있었습니다.
하지만 해당 강의에서는 let 과 var의 차이점은 딱 한 가지라고 설명합니다.
-> A let constant cannot be assigned to again whereas a var variable can be assigned to again
사실 이 부분은 특정 상황에서 let constant가 internally mutate가 가능하기 때문에 유일한 차이점이 위의 문장이라고 설명합니다. 그러니까 let으로 선언한 상수는 재할당할 수 없으며, var으로 선언한 변수는 재할당이 가능하다는 뜻입니다.
맨 처음 강의를 들을때는 제가 배운 게 틀렸다는 것 같았습니다. 하지만 강의를 통해 일반적인 상황에서는 베이직한 정의가 옳지만 디테일하게 들어가면 둘을 단지 불변 vs 변함으로 구분하는 것은 정확하게 말하자면 틀린 정의입니다. 이에 대해 일반적인 let과 var의 정의에 대해 먼저 말한 후 타입에 따른 let 상수의 속성 변경 예시를 들어 설명하겠습니다.
1. 일반적 정의
let myName = "Vandad"
let yourName = "Chaeon"
// 아래는 컴파일러 오류 문장입니다.
myName = yourName // Cannot assign to value: 'myName' is a 'let' constant
var names = [
myName,
yourName
]
names.append("Bar")
names.append("Baz")
// append 외에도 완전히 새롭게 값을 배정할 수 있음.
names = ["Bla"]
우선 let 상수를 보면 myName이라는 상수가 만들어지고, 초기값이 한 번 할당되면 아래 재할당하려는 문장에서 오류가 발생합니다. myName = yourName 에서 주석처리로 적어둔 것처럼 Cannot assign to value라는 오류가 뜨게 됩니다.
반대로, var 변수로 array를 만들었습니다. 이건 변수(variable)이기 때문에 내부적으로 변경이 가능합니다.
그 예시로 names라는 variable array에 다른 elements들을 추가해도 오류가 나지 않습니다.
또한, append외에도 완전히 새롭게 값을 재할당할 수 있습니다.
처음 names는 ["Vandad", "Chaeon"]으로 할당되었습니다. 그다음 variable은 변경이 가능하기에 append를 통해 해당 array에 "Bar"과 "Baz"를 추가했습니다 ["Vandad", "Chaeon", "Bar", "Baz"] 4개의 요소가 된것이죠.
이렇게 메서드를 통해서 내부적인 변경을 가능하게도 하지만 이와 다르게 완전히 새롭게 재할당 또한 가능한 것이 var입니다. names = ["Bla"] 라는 문장을 보시면 names라는 변수에 "Bla"라는 element가 들어있는 새로운 array를 할당한 것을 볼 수 있습니다.
만약 names가 let으로 선언된 array였다면 append를 썼을 때 오류가 나며, 새롭게 재할당 하는 것도 불가능합니다. 그래서 Swift에서 let으로 상수를 선언하면 그 상수가 새로운 값으로 재할당되는 것을 막을 뿐만 아니라 그 상수 내부적으로 변화하는 것도 막을 수 있습니다.
여기서 저희는 두 가지 포인트에 대해서 알고가겠습니다.
1. Mutation of the variable internally 변수의 내부적 변동
2. Assigning a new value to the variable to mutate 변수의 재할당
이 둘의 의미를 구분하는 것이 이후 설명할 파트에서 중요합니다.
2. Value Type vs Reference Type
Swift에서는 값 타입(value type)과 참조 타입(reference type)이라는 두가지 타입 분류가 있습니다. 갑자기 왜 이걸 설명하는가 싶겠지만 이 타입에 따라서 let의 예외적인 mutable 한 상황이 일어나기에 미리 설명하고 넘어가겠습니다.
Value type의 경우 예제 코드를 보겠습니다.
let foo = "Foo"
var foo2 = foo
// 지금 foo 와 foo2모두 "Foo"라는 String을 가지고 있는 상태임
foo2 = "Foo 2" //result : "Foo 2"
foo // result : "Foo"
foo2 // result : "Foo 2"
let으로 foo 상수를 선언 (값은 String type) / var으로 foo2 변수를 선언 -> foo 와 foo2모두 "Foo"라는 String을 가지고 있는 상태입니다. 그런 다음 foo2에 새로운 값을 할당합니다. (variable이기 때문에 가능) 그러면 foo2는 "Foo 2"라는 value를 가지게 됩니다.
이때 foo2에 foo의 값을 할당했기에 foo2 변수의 값이 변하면 foo의 값도 변해야 한다고 생각하실 수 있습니다. 하지만 foo는 변경되지 않고 기존의 값을 가지고 있습니다. 이는 String이 value type이기에 그렇습니다.
foo2에 foo의 값을 할당했을 때 "복사에 의한 전달"(copy by value)로 동작하기 때문에 각 인스턴스 간의 각각 독립적인 메모리 공간을 가지고 있으며, 한 인스턴스의 값을 변경해도 다른 인스턴스에 영향을 주지 않습니다. 그렇기에 foo의 값을 복사한 완전히 새로운 인스턴스가 foo2의 값으로 생기는 것입니다.
또 다른 예제입니다.
let moreNames = [
"Foo",
"Bar"
]
var copy = moreNames
copy.append("Baz")
moreNames
copy
위의 코드를 보면 moreNames라는 array를 let으로 선언하였습니다. array는 structure이면서 value type입니다. 그렇기에 copy 변수는 moreNames array의 메모리적 위치가 연결된 게 아니고 array의 copy를 새롭게 할당한 것입니다. 그렇기에 copy.append("Baz")를 하면 moreNames는 변경되지 않고 copy array만 변경됩니다.
그렇다면 Reference Type은 어떻게 동작할까요?
Reference type은 데이터의 주소(reference)를 전달하거나 할당하는 타입입니다. 새로운 인스턴스가 복사되어서 할당되는 것이 아닌 인스턴스 기존의 메모리 주소가 전달되기 때문에 여러 변수 또는 상수가 동일한 인스턴스를 참조할 수 있습니다. 따라서 한 인스턴스의 값이 변경되면 해당 인스턴스를 참조하는 다른 변수 또는 상수에도 변경 사항이 반영됩니다.
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var person1 = Person(name: "John")
var person2 = person1
person2.name = "Jane"
print(person1.name) // 출력 결과: Jane
print(person2.name) // 출력 결과: Jane
Reference Type에는 클래스(class), 클로저(closure), 함수(function) 등이 있습니다. 그중 위의 예제 코드는 클래스 참조타입에 대한 예제입니다.
Person 클래스는 name 속성을 가지고 있습니다. person1은 "John"이라는 이름을 가진 Person 인스턴스를 참조하고 있습니다. person2는 person1을 참조하는 것과 동일한 인스턴스를 참조합니다.
person2의 name 속성을 "Jane"으로 변경하면 person2가 참조하는 객체의 내부 상태가 변경되어 person1이 참조하는 인스턴스에도 변경사항이 반영됩니다.
즉, person1 과 person2 는 동일한 객체를 가리키고 있으므로 한쪽에서 변경이 발생하면 다른 쪽에서도 변경내용이 반영됩니다. 이러한 특징을 가지는 것이 Reference type입니다.
3. let constant can mutate
let은 상수이기에 immutable variables이며 내부적으로 변경 불가능하다고 말하지만 이는 Swift에서 예외가 있습니다.
let으로 선언된 상수가 참조 타입의 인스턴스를 가리키고 있을 때, 해당 인스턴스의 내부의 값은 변경될 수 있습니다. 그러나 상수가 가리키는 객체 그 자체에는 영향을 주지 않습니다. (여전히 같은 인스턴스를 가리키고 있는 것)
let oldArray = NSMutableArray(
array: [
"Foo",
"Bar"
]
)
oldArray.add("Baz")
// constant has changed internally
var newArray = oldArray
newArray.add("Qux")
oldArray // (4 elements) ["Foo", "Bar", "Baz", "Qux"]
newArray // (4 elements) ["Foo", "Bar", "Baz", "Qux"]
위의 예제에서 let으로 선언한 oldArray는 class instance라고 볼 수 있습니다. 이는 참조타입이기에 let 또는 var 어느 것으로 선언하는가와 관계없이 class instance 내부적인 변경이 가능합니다. 위와 같이 oldArray.add("Baz")를 했을 때 오류가 나지 않는 것을 확인할 수 있습니다. 또한 oldArray를 참조하는 new Array 변수를 만드는 경우 둘 중 하나의 내부적 변동사항이 다른 하나에 반영되는 것을 볼 수 있습니다.
그렇기에 let을 쓰면 무조건적으로 절대 변하지 않을 거라고는 생각하면 안 됩니다. let을 쓰는것이 완벽하게 mutability를 없애는 것이 아니기 때문에, reference types를 다룰때는 주의해야합니다. 특히 인스턴스를 함수로 전달할때 함수는 참조타입이기 때문에 원치 않게 인스턴스가 변경될 수 있음을 주의해야합니다.
4. 결론
let constant는 재할당은 value types이든 reference types이든 불가능합니다. 즉, 참조 타입인 경우에는 let으로 선언되어도 객체의 내부속성은 변경할 수 있지만 참조를 변경하거나 새로운 객체를 할당하는 것은 허용되지 않습니다. 이러한 동작을 통해 constant의 불변성을 유지하고, 코드의 안정성과 예측 가능성을 높이는 것이 가능합니다.
그렇기에 Reference type은 let으로 선언하고도 internally change가 일어날 수 있으며, 이에 대한 Swift 컴파일러는 아무런 에러나 경고 없이 허용한다는 사실을 잊어서는 안됩니다.
해당 강의 영상은 아래에 걸어두겠습니다. 혹시 틀린 부분이나 추가하고 싶은 부분이 있으면 알려주세요.
강의 전체를 시리즈로 적을 수 있도록 노력하겠습니다.