스위프트(Swift) 시작하기

🗓 ⏰ 소요시간 1:34

About Swift

Swift is a new programming language for iOS, OS X, watchOS, and tvOS apps that builds on the best of C and Objective-C, without the constraints of C compatibility

Apple Inc.The Swift Programming Language

iOS 개발을 시작하면서 스위프트 (Swift) 공부를 시작했습니다. 스위프트는 애플이 새롭게 소개한 iOS, OS X 개발을 위한 언어입니다. 새로운 언어로의 전환은 굉장히 큰 일이죠. 구글이 안드로이드를 만드는 언어로 자바와 완전히 호환되는 새로운 언어를 공개한다고 상상해보니 이게 얼마나 중요한 일인지 더 와닿는 것 같습니다. 처음 시작하는 저에게는 Objective-C 나 스위프트나 새롭기는 매한가지지만, 이왕 배우려면 스위프트를 배우는 게 낫겠죠?

스위프트를 간단히 공부해보면서 느낀 점은 두 가지입니다. 재밌다, 그리고 생소하다. 세미콜론도 없고 간단한 syntax 는 불필요한 작업 없이 코드를 빠르게 작성하게 해주었습니다. 스위프트 (Swift; 재빠른, 신속한)의 이름만 봐도 알 수 있죠. 하지만 생략할 수 있는 부분이 많다보니, 같은 코드라도 여러 가지로 표현이 가능했습니다. 생산성이 높아지겠지만 읽기가 어려웠습니다. 물론 자바도 개발자마다 코딩 스타일이 달라서 프로젝트마다 코딩 스타일 가이드가 있긴 합니다만 이 정도로 다르진 않았습니다. 게다가 너무 줄여놓으면 다른 사람이 만든 코드를 읽기가 어려울 것 같았습니다. 유지보수를 위해 소스의 가독성이 높은 코드가 인정을 받았다면 이제는 생산성이 더 중요한 것인가 싶기도 합니다.

애플이 만든 Swift 공식 가이드를 iBooks 에서 다운받을 수 있었습니다. 혹은 애플 개발자 홈페이지 에서 확인가능합니다. 그래서 공부할 자료가 따로 필요 없었죠. 한 문장 한 문장 번역을 할 순 없을 것 같고 코드만 가져오겠습니다. 가이드 내에 있는 모든 코드를 작성해보고 공부한 내용을 정리하는 식으로 진행하려고 합니다. 좋은 책은 예제에 모든 내용을 함축하고 있다고 생각하거든요.

Swift Tour

첫 장은 Swift Tour 입니다. 자세한 설명보다는 전체적으로 스위프트를 훑어보는 장이군요. 먼저 ‘Hello, world!’ 를 출력해봅시다. 세미콜론을 생략할 수 있습니다. 세미콜론 안쓰는게 처음에는 낯설었는데 금방 익숙해지네요.

1
print("Hello, World!")

Simple Values

변수는 값을 할당한 후 변경이 가능하고, 상수는 값을 한번만 할당 가능합니다. 변수와 상수 모두 선언 시 반드시 값을 할당해야 합니다.

1
2
3
4
5
6
// 변수
var myVariable = 42
myVariable = 50

// 상수
let myConstant = 42

타입 선언입니다. 타입을 지정하지 않으면 초기값으로 타입을 유추합니다. 타입이 맞지 않으면 에러가 납니다. 타입은 한번 정해지면 변경할 수 없습니다.

1
2
3
4
let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70
let explicitFloat: Float = 4 // 타입 에러

타입의 암묵적 변환은 할 수 없고 명시적으로만 가능합니다.

1
2
3
4
let label = "The width is"
let width = 94
let widthLabel = label + String(width) // 타입 변환
widthLabel = label + width // 타입 에러

문자열 안에 값을 표현하려면 백슬래시() 안에 표현할 값을 넣습니다.

1
2
3
4
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

배열 (Array)은 인덱스 (index)를 가지고 순차적으로 값을 저장하는 자료구조입니다.

1
2
var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"

Dictionary 는 key 와 value 쌍으로 자료를 저장하는 자료구조입니다.

1
2
3
4
5
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic"
]
occupations["Jayne"] = "Public Relations"

빈 배열과 Dictionary 를 만들려면 타입을 지정해서 초기화 함수를 사용합니다.

1
2
3
4
5
6
let emptyArray = [String]()
let emptyDictionary = [String: Float]()

// 타입을 지정하기 전에는 다음과 같이 빈 값을 할당
shoppingList = []
occupations = [:]

Control Flow

조건문과 반복문을 알아봅시다.

  • 조건문
    • if
    • switch
  • 반복문
    • for-in
    • for
    • while
    • repeat-while
1
2
3
4
5
6
7
8
9
10
let individualScroes = [75, 42, 103, 87, 12]
var teamScore = 0
for score in individualScroes {
if score > 50 {
teamScore += 3
} else {
teamScore += 1
}
}
print(teamScore)

if

if 조건부에 오는 값은 반드시 Boolean 표현식이어야 합니다.

1
2
3
if teamScore { // 타입 에러
// ...
}

타입 뒤에 물음표 (?) 를 붙이면 Optional 값이 됩니다. Optinal 값은 값이 없을 수도 있음을 의미합니다. 빈 값이라던가 의미 없는 값이라던가 하는 것도 결국 값이 있는 것이고, 값이 없다는 것은 아직 값이 할당되지 않은 상태를 명시적으로 표시하는 방법입니다. nil 은 값이 없음을 표현합니다 (기존의 null 을 생각하면 됩니다).

1
2
var optionalString: String? = "Hello"
print(optionalString == nil)

if 와 let 을 같이 쓰는 경우, optionalName 이 nil 이 아니면 true, nil 이면 false 값을 갖습니다. 만약 nil 이 아니면 해당 블록 내에서 name 이라는 상수로 사용 가능합니다.

1
2
3
4
5
6
7
8
9
var optionalName: String? = "John Appleseed"
var greeting = "Hello!"

if let name = optionalName {
greeting = "Hello, \(name)"
} else {
greeting = "Hello, Unknown"
}
print(greeting)

?? 연산자는 nickName 이 nil 인 경우에 default 값을 줍니다.

1
2
3
4
let nickName: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fullName)"
print(informalGreeting)

Switch

switch 문을 살펴봅시다. 다양한 값과 비교 연산자를 사용 가능하고, break 문이 필요없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let vagetable = "red pepper"

switch vagetable {
case "celery":
print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
// 여러 케이스를 지정하는 경우
print("that would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
print("Is it a spicy \(x)?")
default:
// default 존재하지 않으면 에러가 난다
print("Everything tastes good in soup.")
}

for-in

반복문 중 하나인 for-in 문 입니다. 배열과 Dictionary 를 순회할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let interestingNumbers = [
"Prime": [2, 3, 5, 7, 11, 13],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Square": [1, 4, 9, 16, 25]
]

var largest = 0

for (kind, numbers) in interestingNumbers {
for number in numbers {
if number > largest {
largest = number
}
}
}
print(largest)

while

while 문은 조건에 만족하는 동안 작업을 반복 수행합니다.

1
2
3
4
5
var n = 2
while n < 100 {
n = n * 2
}
print(n)

reapeat 문을 사용하면 조건을 체크하기 전에 먼저 작업을 수행하기 때문에 최소한 한 번은 작업을 수행합니다. do-while 문을 생각하시면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var m = 2
repeat {
m = m * 2
} while m < 100
print(m)

// ... 는 같은 값까지 확인
// ..< 는 값 미만까지 확인
var total = 0
for i in 0 ..< 4 {
// 0, 1, 2, 3
total += i
}
print(total)

Functions and Closures

Functions

func 를 사용해서 함수를 선언합니다. arguments 는 타입을 지정해주고, -> 뒤에는 리턴 타입을 명시합니다. 함수 사용 시에는 파라미터 이름에 맞춰 넣습니다. 여기서 첫번째 파라미터는 파라미터 이름을 생략 가능합니다.

1
2
3
4
func greet(name: String, day: String) -> String {
return "Hello \(name), today is \(day)."
}
greet("Bob", day: "Tuesday")

함수는 값을 여러 개 반환할 수 있습니다. 더 정확히 얘기하면 Tuple 로 여러 값을 묶어서 한번에 반환할 수 있습니다. Tuple 은 값을 묶어주는 것으로 구조체나 클래스 같은 것보다 간편하게 값을 묶는 용도로 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func calculateStatistics(scores: [Int]) -> (min:Int, max:Int, sum:Int) {
var min = scores[0]
var max = scores[0]
var sum = 0

for score in scores {
if score > max {
max = score
} else if score < min {
min = score
}

sum += score
}

return (min, max, sum)
}

let statistics = calculateStatistics([5, 3, 100, 3, 9])

print(statistics.sum) // . 으로 접근 가능
print(statistics.2) // 0, 1, 2 순서로 sum 을 가리킴

함수는 여러개의 가변인자를 받을 수 있고, 가변 인자는 배열처럼 접근 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
func sumOf(numbers: Int...) -> Int {
var sum = 0

print(numbers.count) // 배열처럼 사용 가능

for number in numbers {
sum += number
}
return sum
}

sumOf() // 인자가 없어도 에러가 나지 않는다
sumOf(42, 597, 12)

함수 내부에 함수를 선언할 수 있습니다 (중첩함수; Nested function). 중첩함수는 바깥쪽 함수에서 변수처럼 사용이 가능합니다. 또한 자세히 보시면 중첩함수인 add() 에서 바깥쪽 함수의 변수인 y 에 접근하고 있는 걸 볼 수 있습니다. 이것이 클로저 (Closure)의 개념 중 하나입니다.

1
2
3
4
5
6
7
8
9
10
11
12
func returnFifteen() -> Int {
var y = 10

func add() {
y += 5
}

add()
return y
}

returnFifteen()

함수는 1급 타입입니다. javaScript 의 함수를 생각하시면 됩니다.

1급 타입이라는 것은,

  • 변수에 담을 수 있다.
  • 인자로 받을 수 있다.
  • 반환이 가능하다.
    을 의미합니다.

다음 코드는 함수를 리턴하고 함수를 변수에 담는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// -> ((Int) -> Int)
// Int 를 파라미터로 받고 Int 를 리턴하는 함수 타입을 리턴
func makeIncrementer() -> ((Int) -> Int) {
func addOne(number: Int) -> Int {
return 1 + number
}

// addOne 이라는 함수 자체를 리턴
return addOne
}

// 리턴되는 함수를 변수에 받아서 실행 가능
var increment = makeIncrementer()
increment(7)

다음 코드는 함수를 파라미터로 받는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {

for item in list {
if condition(item) {
return true
}
}

return false
}

func lessThanTen(number: Int) -> Bool {
return number < 10
}

var numbers = [20, 19, 7, 12]

hasAnyMatches(numbers, condition: lessThanTen)

Closure

클로저는 어떠한 함수와 그 함수의 환경 (컨텍스트)를 묶어놓은 것입니다. 그 환경은 클로저를 어떻게 만드느냐에 따라 결정됩니다. 중첨첩 함수에서도 클로저를 볼 수 있었습니다. 클로저에 대한 자세한 내용은 따로 포스트를 만들어야 할 것 같네요. 어쨌든 Swift 의 클로저는 무명함수 (Anonymous Functions) 로 선언 가능합니다. in 키워드 앞에는 클로저의 파라미터와 리턴 타입을 명시합니다. 아래 코드의 클로저는 배열의 각 숫자에 3을 곱하는 작업을 수행하게 됩니다.

1
2
3
4
5
numbers.map({
(number: Int) -> Int in // 파라미터와 리턴 타입
let result = 3 * number
return result
})

여기서 함수의 파라미터와 리턴 타입을 아는 경우 생략할 수 있습니다. 여기서는 map 의 파라미터로 들어오는 함수의 파라미터와 리턴 타입이 정해져 있는 상태입니다. 이럴 때는 생략이 가능합니다. 리턴 구문도 생략 가능합니다.

1
2
3
4
5
let mappedNumbers = numbers.map({
number in // 클롤저의 파라미터 타입과 리턴 타입을 생략
3 * number // 리턴 구문 생략
})
print(mappedNumbers)

여기서 더 줄일 수 있습니다. 매개변수를 받는 괄호 안에 중괄호 ({}) 를 이용해서 클로저를 넣었는데요, 이 때 괄호를 생략하고 중괄호 형태로 수정이 가능합니다. 게다가 매개변수의 이름까지 생략하고 번호를 이용해서 참조할 수도 있습니다. $0 은 첫 번째 파라미터를, ‘$1’ 은 두 번째 파라미터를 말합니다. 따라서 다음과 같이 표현이 가능합니다.

1
2
3
numbers.map {
3 * $0
}

너무 생소해져서 스위프트가 싫어질 것 같네요… 하지만 익숙해지면 편할 것 같습니다. 아래는 다른 예입니다.

1
2
3
4
let sortedNumbers = numbers.sort {
$0 > $1
}
print(sortedNumbers)

Object and Classes

클래스는 변수와 메소드 (함수)로 이루어져 있습니다. 다음과 같이 선언할 수 있습니다.

1
2
3
4
5
6
7
class Shape {
var numberOfSides = 0

func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}

인스턴스를 생성하고 인스턴스에 접근하는 방법입니다.

1
2
3
var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription();

클래스 내 변수는 선언할 때 초기값을 할당하거나 init 생성자를 이용해서 초기화해야 합니다. self 키워드는 해당 클래스의 변수를 의미합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NamedShape {

var numberOfSide: Int = 0
var name: String

// 초기화
init(name: String) {
self.name = name
}

func simpleDescription() -> String {
return "A shape with \(numberOfSide) sides."
}
}

상속입니다. 상속은 클래스명 뒤에 : 를 붙여서 상속할 클래스를 표시합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Square: NamedShape {
var sideLength: Double

init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSide = 4 // 부모 클래스의 변수
}

func area() -> Double {
return sideLength * sideLength
}

override func simpleDescription() -> String {
return "A square with sides of length \(sideLength)."
}
}

let test = Square(sideLength: 5.2, name: "my test square")

test.area()
test.simpleDescription()

프로퍼티는 getter 와 setter 를 가질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class EquilateralTriangle: NamedShape {
var sideLength: Double = 0.0

init(sideLength: Double, name: String) {
self.sideLength = sideLength // 하위 클래스의 속성 값 지정
super.init(name: name) // 상위 클래스 init 호출
numberOfSide = 3 // setter 없이 그냥 접근 가능함
}

var perimeter: Double {
get {
return 3.0 * sideLength
}

set {
// newValue 는 새로운 값을 의미
sideLength = newValue / 3.0
}
}

override func simpleDescription() -> String {
return "An equilateral triangle with sides of length \(sideLength)."
}
}

var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter) // 9.3

triangle.perimeter = 9.9
print(triangle.sideLength) // 3.3

willSet 과 didSet 을 이용해서 setter 를 수행하기 전, 후에 실행되는 코드를 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class TriangleAndSquare {
var square: Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}

var triangle: EquilateralTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}

init(size: Double, name: String) {
square = Square(sideLength: size, name: name)
triangle = EquilateralTriangle(sideLength: size, name: name)
}
}

var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength) // 10
print(triangleAndSquare.triangle.sideLength) // 10

triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength) // 50

클래스를 Optional 로 선언하면 nil 값이 올 수 있습니다. 사용할 때는 물음표 (?)를 붙여서 사용해야 합니다.

1
2
let optionalSqure: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSqure?.sideLength

Enumerations and Structures

Enumerations

열거형은 값을 나열해서 표현하는 것으로 값이 중요한 것이 아니라, 순서 상 값을 구분하는 용도로 만든 자료형입니다. 열거형에는 타입을 지정할 수 있고 메서드를 포함할 수 있습니다. 열거형의 값은 자동으로 할당되는데 아래 코드에서는 첫번째 값을 1로 주었기 때문에 차례로 2, 3, 4 순으로 할당됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum Rank: Int {
case Ace = 1
case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King

func simpleDescription() -> String {
switch self {
case .Ace:
return "ace"
case .Jack:
return "jack"
case .Queen:
return "queen"
case .King:
return "king"
default:
return String(self.rawValue)
}
}
}

let ace = Rank.Ace
let aceRawValue = ace.rawValue // 실제값

print(ace) // Ace
print(aceRawValue) // 1
print(Rank.Queen) // Queen
print(Rank.Queen.rawValue) // 12

init?(rawValue:) initializer 를 이용해서 rowValue 로 열거형 인스턴스를 생성할 수 있습니다.

1
2
3
if let convertedRank = Rank(rawValue: 3) {
let threeDescription = convertedRank.simpleDescription()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Suit {

case Spades, Hearts, Diamonds, Clubs

func simpleDescription() -> String {
switch self {
case .Spades:
return "spades"
case .Hearts:
return "hearts"
case .Diamonds:
return "diamonds"
case .Clubs:
return "clubs"
}
}
}

let hearts = Suit.Hearts
let heartDescription = hearts.simpleDescription()

Struct

구조체는 struct 키워드를 이용해 선언합니다. 구조체는 클래스와 거의 비슷하지만 보통 인스턴스가 참조 복사 형태로 전달된다는 점과 달리, 값 복사 형태로 전달됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Card {
var rank: Rank
var suit: Suit

func simpleDescription() -> String {
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}

let threeOfSpades = Card(rank: .Three, suit: .Spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()

print(threeOfSpades) // Card(rank: HelloSwift.Rank.Three, suit: HelloSwift.Suit.Spades)
print(threeOfSpadesDescription) // The 3 of spades

Associatd Value 는 Enumerations 의 case 에 특정한 값을 할당해서 저장한 후에 나중에 활용할 수 있는 방식입니다. 아래 코드를 보시면 ServerResponse 는 Result(String, String) 또는 Failure(String) 둘 중 하나의 값만을 가질 수 있는 상태입니다. 값을 할당 시에 저장한 두 개의 String 값은 switch 문에서 아래 코드와 같이 사용 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum ServerResponse {
case Result(String, String)
case Failure(String)
}

let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Failure("Out of cheese.")

switch success {
case let .Result(sunrise, sunset): // case .Result(let sunrise, let sunset) 과 같은 의미

// Sunrise is at 6:00 am and sunset is at 8:09 pm.
print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .Failure(message):
print("Failure... \(message)")
}

Protocols and Extensions

Protocols

프로토콜은 자바의 인터페이스와 비슷한 개념입니다. 반드시 구현해야 하는 것을 명시할 수 있습니다. 프로토콜은 일종의 타입으로써 다형성을 구현할 수 있습니다.

1
2
3
4
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}

클래스 뿐만 아니라, Enumerations 와 구조체에도 프로토콜을 적용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Class
class SimpleClass: ExampleProtocol {
var simpleDescription: String = "A very simple class."

var anotherProperty: Int = 69105

func adjust() {
simpleDescription += " Now 100% adjusted."
}
}

var a = SimpleClass()
print(a.simpleDescription) // A very simple class

a.adjust()

let aDescription = a.simpleDescription
print(aDescription) // A very simple class. Now 100% adjusted.


// Structure
struct SimpleStructure: ExampleProtocol {
var simpleDescription: String = "A simple structure"

mutating func adjust() {
simpleDescription += " (adjusted)"
}
}

var b = SimpleStructure()
print(b.simpleDescription)

b.adjust()

let bDescription = b.simpleDescription
print(bDescription)

Extensions

Extension 은 기존의 객체 타입에 코드를 추가할 수 있는 기능입니다. 라이브러리나 프레임워크의 소스에도 추가할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
extension Int: ExampleProtocol {
var simpleDescription: String {
return "The number \(self)"
}

mutating func adjust() {
self += 42
}
}

print(7.simpleDescription)

Generics

제네릭 (Generics)는 특정 타입임을 명시하는 방법입니다.

1
2
3
4
5
6
7
8
func repeatItem<Item>(item:Item, numberOfTimes: Int) -> [Item] {
var result = [Item]()
for _ in 0..<numberOfTimes {
result.append(item)
}
return result
}
repeatItem("knock", numberOfTimes:4)

스위프트의 Optional Value 를 제네릭을 이용해 표현해보면 다음과 같습니다.

1
2
3
4
5
6
7
enum OptionalValue<Wrapped> {
case None
case Some(Wrapped)
}

var possibleInteger: OptionalValue<Int> = .None
possibleInteger = .Some(100)

where 키워드를 이용해서 특정 타입에 대한 조건을 줄 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
func anyCommonElements <T:SequenceType, U:SequenceType where T.Generator.Element:Equatable, T.Generator.Element == U.Generator.Element> (lhs:T, _ rhs: U) -> Bool {
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}

anyCommonElements([1,2,3], [3])

전체적으로 어떤 모양새인지 훑어보는 장이었습니다. 만만한 언어는 아닌 것 같네요. 하지만 여기서 이해가 가질 않는 부분이 있더라도 뒤에서 자세하게 살펴볼 것이니 크게 상관 없을 것 같습니다. 다음 포스팅에서는 앞에서부터 차근차근 다뤄보겠습니다.