[Swift] 배열, 옵셔널 체이닝, 제네릭 활용하기

승민·2025년 4월 9일

Swift

목록 보기
5/10
post-thumbnail
  • 스위프트 프로그래밍에서 가장 많이 사용되는 기능 중 하나는 데이터를 효과적으로 다루는 것이에요.
  • ArrayCollection Type, 그리고 Optional ChainingGeneric을 통해 데이터를 처리하는 방법을 예제와 함께 알아볼게요.

옵셔널 체이닝 (Optional Chaining)

Optional Chaining은 객체의 속성이나 메서드에 접근할 때, 해당 속성이나 메서드가 존재하지 않더라도 오류를 발생시키지 않고 undefined 또는 null을 반환하는 문법이에요.
먼저 Optional Chaining 을 지원하는 언어부터 알아볼게요.

옵셔널 체이닝을 지원하는 언어는 JavaScript, TypeScript, Swift, Kotlin, C# 가 있어요.

각 언어에 대해서 예제와 함께 사용하는 방법을 정리하면 다음과 같아요.

옵셔널 체이닝을 지원하는 언어

  • JavaScript
    • JavaScript 에서는 ?. 연산자로 옵셔널 체이닝을 구현할 수 있어요.
const user = {
  name: "Alice",
  address: {
    city: "Seoul"
  }
};

// 옵셔널 체이닝 사용
console.log(user?.address?.city); // "Seoul"
console.log(user?.contact?.phone); // undefined (오류 없이 반환)
  • TypeScript
    • TypeScriptJavaScript의 상위 집합으로, ?.를 동일하게 지원해요.
interface User {
  name: string;
  address?: {
    city: string;
  };
}

const user: User = { name: "Bob" };
console.log(user?.address?.city); // undefined
  • Swift
    • Swift에서는 ?. 연산자를 사용해 옵셔널 체이닝을 지원해요.
struct User {
    var address: Address?
}

struct Address {
    var city: String
}

let user = User(address: nil)
print(user.address?.city) // nil
  • Kotlin
    • Kotlin은 안전 호출 연산자(?.)로 옵셔널 체이닝을 지원해요.
data class Address(val city: String)
data class User(val address: Address?)

val user = User(null)
println(user?.address?.city) // null
  • C#
    • C#에서는 ?. (Null-Conditional Operator)를 사용해요.
class Address {
    public string City { get; set; }
}

class User {
    public Address Address { get; set; }
}

var user = new User();
Console.WriteLine(user?.Address?.City); // null

옵셔널 체이닝이란?

  • 옵셔널 언래핑을 하고 text에 접근해야 함
  • 성공하면 옵셔널 값, 실패하면 nil
  • 만약 cell.textLabel!.text로 작성하면 옵셔널이 아닌 일반형으로 반환하여 편하지만 textLabelnil일 경우 크래시가 남

옵셔널 체이닝 사용 이유

  • 옵셔널 타입으로 정의된 값이 프로퍼티나 메서드를 가지고 있을 때, 다중 if를 쓰지 않고 간결하게 코드를 작성하기 위해
  • 옵셔널 타입의 데이터는 연산이 불가능
    • 연산을 하기 위해서는 옵셔널을 해제해야 하는데, 많은 양의 옵셔널 타입의 데이터의 경우 다시 한번 옵셔널 타입으로 변경을 하면서 해제를 시켜줌

오류 처리 (Error Handling)

오류 처리가 생긴 이유

  • Swift에서는 optional을 사용하여 값의 유무를 전달함으로써 작업의 성공/실패 유무를 판단할 수 있지만 작업이 실패할 때 코드가 적절히 응답할 수 있도록 함으로써 오류의 원인을 이해하는 데 도움을 줄 수 있다.
  • 디스크상의 파일을 읽어서 처리하는 작업에서 발생할 수 있는 오류
    • '존재하지 않는 파일', '읽기 권한 없음', '호환되는 형식이 아님' 등 다양
    • 오류의 원인에 따라 다양한 대응이 필요한 경우, 오류의 정보를 정확히 전달함으로써 오류를 복구하는데 도움을 줄 수 있음
  • Swift 2.0 이후부터는 error handling을 도입

오류 제어

  • do-catch 구문
    • 가장 일반적이고 널리 사용되는 방법으로, 에러를 명시적으로 처리할 수 있음
  • 옵셔널 값으로 에러 처리 (try?)
    • 에러를 간단히 무시하거나 실패 여부만 확인하고 싶을 때 사용
  • 에러 전파 (Error Propagation)
    • 함수에 throws 키워드를 사용하여 발생한 에러를 호출한 코드로 전파
  • 호출한 코드에서 직접 에러를 처리하거나 다시 전파할 수 있음
    • 네트워크 호출이나 복잡한 로직에서 사용
  • 단정 (try!)
    • 에러가 절대 발생하지 않을 것이라고 가정할 때 사용
    • 에러가 발생하면 프로그램이 크래시

try

  • try?
    • 에러를 간단히 무시하거나 실패 여부만 확인하고 싶을 때 사용
  • try!
    • 에러가 절대 발생하지 않을 것이라고 가정할 때 사용
    • 만약 에러가 발생하면 프로그램이 크래시

throwing function

  • 실행 중에 문제가 발생할 가능성이 있을 때, 에러를 던질 수 있는 함수
  • 매개변수 괄호 다음에 throws라는 키워드가 있는 함수는 그냥 사용할 수 없고 error handing을 해야 함
  • func can() throws
    • 리턴값이 없는 throwing function
  • func canThrowErrors() throws -> String
    • error handing을 해야하는 함수
  • func cannotThrowErrors() -> String
    • error handing할 수 없는 함수

오류 발생 가능 함수의 호출 방식(do~try~catch)

  • 이렇게 그냥 호출할 수는 없음
    • AVAudioPlayer(contentsOf: audioFile)
  • do~try~catcherror handing해야 함
    • 하지 않으면 Call can throw, but it is not marked with 'try' and the error is not handled 오류가 발생

do-catch을 이용한 error handling

do {
	try <오류 발생 코드>
    <오류가 발생하지 않으면 실행할 코드>
} catch <오류패턴1> {
    <처리 코드>
} catch <오류패턴2> where <조건> {
    <처리 코드>
} catch {
    <처리 코드>
}

제네릭 (Generic)

제네릭의 정의

제네릭은 특정 타입에 의존하지 않고, 재사용 가능한 코드를 작성할 수 있도록 설계된 문법입니다. 함수, 구조체, 클래스, 열거형 등에서 사용되며, 타입을 플레이스홀더(예: T)로 추상화하여 다양한 타입에 동일한 로직을 적용할 수 있습니다.

주요 특징

  • 타입 안정성: 컴파일 시점에서 타입 오류를 감지해 런타임 오류를 방지합니다.
  • 코드 재사용: 하나의 구현으로 여러 타입에 대해 동작하도록 만듭니다.
  • 추상화: 구체적인 타입 대신 임의의 타입을 나타내는 파라미터를 사용해 일반화된 코드를 작성합니다.

동작 방식

  • 타입 파라미터(예: <T>)를 정의해 임의의 타입을 나타냅니다.
  • 코드 실행 시 타입 파라미터는 실제 타입으로 대체됩니다.
  • 제네릭은 주로 꺾쇠 괄호(<>)를 통해 타입 파라미터를 명시합니다.

제네릭 사용

기본적인 정의 방법을 활용하여 실제로 어떻게 적용되는지 확인해볼게요.

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

// Int 사용
var x = 5
var y = 10
swapValues(&x, &y)
print(x, y)  // 10, 5

// String 사용
var str1 = "Hello"
var str2 = "World"
swapValues(&str1, &str2)
print(str1, str2)  // "World", "Hello"
  • 타입 파라미터는 <T>와 같이 정의되며, T는 임의의 이름(관례적으로 T, U 등 사용)
  • 타입 파라미터는 실제 타입으로 대체됨

4가지 괄호의 의미

코드를 작성하다보면 많은 내용을 간략화하거나 특정 영역으로 묶기 위해 괄호를 많이 사용해요.
이제는 그 괄호의 종류가 점점 늘어나다 보니 헷갈릴 수 있어요.
아래 이미지를 통해 좀 더 직관적으로 각 괄호의 역할과 사용법을 정리했어요.

Collection Type

프로그래밍 언어에서 Collection Type은 여러 개의 값을 하나로 묶어서 다룰 수 있는 자료형을 의미해요.
하나의 변수로 여러 데이터를 저장하고, 쉽게 반복하거나 검색, 수정할 수 있게 도와주는 역할을 해요.

특성타입순서 있음중복 허용키-값 구조
Array배열예 (인덱스 순서 유지)예 (값 중복 가능)아니오 (인덱스 기반)
Dictionary딕셔너리아니오 (Swift 4.2부터 삽입 순서 유지 가능)예 (값 중복 가능, 키는 고유)예 (키로 값 접근)
Set집합아니오아니오 (고유성 보장)아니오 (값만 저장)

Array

  • 기본 선언
let arr = [1, 2, 3]
print(arr)  // [1, 2, 3]
  • 순서 있음: 예 (인덱스 순서 유지)
let arr = [1, 2, 3]
print(arr[1])  // 2
  • 중복 허용: 예 (값 중복 가능)
let arr = [1, 1, 2]
print(arr)  // [1, 1, 2]
  • 키-값 구조: 아니오 (인덱스 기반)
let arr = [1, 2]
print(arr[0])  // 1

Dictionary

  • 기본 선언
let dict = ["a": 1]
print(dict)  // ["a": 1]
  • 순서 있음: 아니오 (Swift 4.2부터 삽입 순서 유지 가능)
let dict = ["a": 1, "b": 2]
print(dict)  // ["b": 2, "a": 1] (Swift 4.1 이하에서는 순서 무작위 가능)
  • 중복 허용: 예 (값 중복 가능, 키는 고유)
let dict = ["a": 1, "b": 1]
print(dict)  // ["a": 1, "b": 1]
  • 키-값 구조: 예 (키로 값 접근)
let dict = ["a": 1]
print(dict["a"])  // Optional(1)

Set

  • 기본 선언
let set = Set([1, 2])
print(set)  // [2, 1] (순서 무관)
  • 순서 있음: 아니오
let set = Set([1, 2])
print(set)  // [2, 1] (순서 보장되지 않음)
  • 중복 허용: 아니오 (고유성 보장)
let set = Set([1, 1, 2])
print(set)  // [1, 2]
  • 키-값 구조: 아니오 (값만 저장)
let set = Set([1, 2])
print(set.contains(1))  // true

Array

Swift의 배열과 제네릭

var x : [Int] = [] //빈 배열
var y = [Int]()
var z : Array<Int> = []

var a : [Int] = [1,2,3,4]
var b : Array<Int> = [1,2,3,4]
var c : Array<Double> = [1.2,2.3,3.5,4.1]

빈 배열 주의사항

  • 빈 배열을 let으로 만들 수 있지만 초기값 설정 이후 변경 불가하여 배열의 의미가 없어요.
// 가변형(mutable)
var animal = ["dog", "cat","cow"]

// 불변형 (immutable)
// 초기화 후 변경 불가
let animal1 = ["dog", "cat","cow"]
  • 배열의 항목을 접근하는 경우 미리 선언된 배열에 대해서만 접근이 가능해요.
var number : [Int] = []
//number[0]=1 //crash, 방을 만든 후 사용하라!
number.append(1)
print(number)
number[0]=10
print(number)

배열 초기화

배열을 초기화 하는 방법 중에 Array(repeating:count:)를 이용해서 초기화하는 방법이 있어요.
일반적인 배열 선언 방법과 달리 클래스를 선언하는 방식으로 배열을 초기화할 수 있어요.

  • Array(repeating:count:)
    • 특정값(repeating)으로 원하는 개수(count)만큼 초기화해요.
var x = [0,0,0,0,0]
print(x)
var x1 = Array(repeating: 0, count: 5)
print(x1)
var x2 = [Int](repeating: 1, count: 3)
print(x2)
var x3 = [String](repeating: "A", count: 4)
print(x3)

배열 항목 접근

  • for-in 문법 사용
    • 배열의 요소에 대해 in 키워드로 하나씩 가져와서 처리할 수 있어요.
let colors = ["red", "green", "blue"]
print(colors)
for color in colors {
  print(color)
}
  • countisEmpty
    • 배열의 countisEmpty를 프로퍼티를 통해 갯수와 빈 배열의 여부를 반환할 수 있어요.
let num = [1, 2, 3, 4]
var x = [Int]()
print(num.isEmpty) // 배열이 비어있나? false
print(x.isEmpty)
if num.isEmpty {
  print("비어 있습니다")
}
else {
  print(num.count) // 배열 항목의 개수
}
  • firstlast
    • 배열을 담고 있는 객체에 firstlast 프로퍼티를 호출하여 맨 앞과 맨 뒤 요소를 반환할 수 있어요.
let num = [1, 2, 3, 4]
let num1 = [Int]()
print(num.first, num.last)//Optional(1) Optional(4)
print(num1.first, num1.last)//nil nil
if let f = num.first, let l = num.last {
  print(f,l) //1 4
}
  • 첨자(subscript)로 항목 접근
var num = [1, 2, 3, 4]
print(num[0], num[3]) // 출력: 1 4
print(num.first!) // 배열의 첫 번째 요소를 출력: 1 (first는 !로 강제 언래핑)
for i in 0...num.count-1 { // 0부터 배열 길이-1(0...3)까지 반복
    print(num[i]) // 출력: 1, 2, 3, 4
}
print(num[1...2]) // 배열의 1부터 2까지 부분을 출력: [2, 3]
num[0...2] = [10, 20, 30] // 배열의 0부터 2까지를 [10, 20, 30]으로 교체
print(num) // 출력: [10, 20, 30, 4]

배열 추가 및 제거

  • append(_:)
    • 배열의 맨 끝에 단일 요소를 추가합니다.
var numbers = [1, 2, 3]
numbers.append(4)
print(numbers) // 출력: [1, 2, 3, 4]
  • append(contentsOf:)
    • 배열의 맨 끝에 다른 배열이나 컬렉션의 요소들을 추가합니다.
var numbers = [1, 2, 3]
numbers.append(contentsOf: [4, 5, 6])
print(numbers) // 출력: [1, 2, 3, 4, 5, 6]
  • insert(_:at:)
    • 지정한 인덱스 위치에 요소를 삽입합니다. 기존 요소들은 뒤로 밀려납니다.
var numbers = [1, 2, 3]
numbers.insert(0, at: 0)
print(numbers) // 출력: [0, 1, 2, 3]
numbers.insert(10, at: 2)
print(numbers) // 출력: [0, 1, 10, 2, 3]
  • remove(at:)
    • 지정한 인덱스의 요소를 제거하고, 제거된 요소를 반환합니다.
var numbers = [1, 2, 3, 4]
let removed = numbers.remove(at: 1)
print(removed) // 출력: 2
print(numbers) // 출력: [1, 3, 4]
  • removeFirst()removeLast()
    • 배열의 첫 번째 또는 마지막 요소를 제거하고 반환합니다.
var numbers = [1, 2, 3, 4]
let first = numbers.removeFirst()
print(first) // 출력: 1
print(numbers) // 출력: [2, 3, 4]

let last = numbers.removeLast()
print(last) // 출력: 4
print(numbers) // 출력: [2, 3]
  • removeAll()
    • 배열의 모든 요소를 제거합니다.
var numbers = [1, 2, 3, 4]
numbers.removeAll()
print(numbers) // 출력: []
  • 주의사항
    • 빈 배열에서 removeFirst()removeLast()를 호출하면 런타임 에러가 발생하니, 배열이 비어 있는지 확인 후 사용하세요.
var emptyArray = [Int]()
if !emptyArray.isEmpty {
    emptyArray.removeFirst() // 빈 배열이므로 실행되지 않음
}
print(emptyArray) // 출력: []

배열의 요소

배열의 요소는 배열에 저장된 개별 값들을 의미하며, Swift에서는 배열이 제네릭 타입이므로 모든 요소가 동일한 타입이어야 합니다. 요소에 접근하거나 수정하는 방법은 위에서 다룬 첨자(subscript)나 프로퍼티(first, last)를 활용할 수 있습니다.

  • 요소 수정
    • 첨자를 사용해 특정 위치의 요소를 직접 변경할 수 있습니다.
var fruits = ["apple", "banana", "orange"]
fruits[1] = "grape"
print(fruits) // 출력: ["apple", "grape", "orange"]
  • 요소 범위 수정
    • 범위를 지정해 여러 요소를 한꺼번에 변경할 수 있습니다.
var numbers = [1, 2, 3, 4, 5]
numbers[1...3] = [20, 30]
print(numbers) // 출력: [1, 20, 30, 5]
  • 배열 요소의 개수와 순회
    • count로 요소 개수를 확인하고, for-in으로 요소를 순회할 수 있습니다.
let animals = ["dog", "cat", "bird"]
print(animals.count) // 출력: 3
for animal in animals {
    print(animal) // 출력: dog, cat, bird
}
  • 배열 요소의 동적 특성
    • 배열의 크기는 고정되지 않고, 요소를 추가하거나 제거하면서 동적으로 변합니다.
var dynamicArray = [1, 2]
print(dynamicArray.count) // 출력: 2
dynamicArray.append(3)
print(dynamicArray.count) // 출력: 3
dynamicArray.remove(at: 0)
print(dynamicArray.count) // 출력: 2
print(dynamicArray) // 출력: [2, 3]

접근 제어

Swift에서 접근 제어는 코드의 접근 가능 범위를 제어하는 메커니즘으로, 모듈과 소스 파일 간의 접근 권한을 설정합니다. 이를 통해 코드의 캡슐화를 강화하고, 의도치 않은 접근을 방지할 수 있습니다.
Swiftopen, public, package, internal, fileprivate, private 총 6가지 접근 수준을 제공하며, 접근 권한은 넓은 순서부터 좁은 순서로 정렬됩니다.

open 접근, public 접근

openpublic 접근 수준은 모듈 외부에서도 접근이 가능하도록 설정합니다. 주로 앱, 프레임워크, UI킷, 혹은 라이브러리에서 사용됩니다.

  • open: 상속과 오버라이드가 허용됩니다. 외부 모듈에서 해당 요소를 사용할 수 있으며, 서브클래싱도 가능합니다.
  • public: 외부 모듈에서 접근은 가능하지만, 상속과 오버라이드는 허용되지 않습니다.
import Alamofire // 외부 모듈 임포트

open class SomeOpenClass {
    open var someOpenVariable = 0
    public var somePublicVariable = 0
}

public class SomePublicClass {
    public var somePublicVariable = 0
    // open var someOpenVariable = 0 // public 클래스 내에서는 open 사용 불가
}

// 사용 예시
let openInstance = SomeOpenClass()
print(openInstance.someOpenVariable) // 출력: 0
print(openInstance.somePublicVariable) // 출력: 0

let publicInstance = SomePublicClass()
print(publicInstance.somePublicVariable) // 출력: 0

package 접근

package 접근 수준은 Swift 6.0부터 도입된 접근 제어로, 패키지 내부의 모든 소스 파일에서 접근 가능하지만, 패키지 외부의 소스 파일에서는 접근할 수 없습니다. 이는 모듈 내부에서만 공유해야 하는 코드를 정의할 때 유용합니다.

package class SomePackageClass {
    package var somePackageVariable = 0
}

// 같은 패키지 내에서 접근 가능
let packageInstance = SomePackageClass()
print(packageInstance.somePackageVariable) // 출력: 0
// 패키지 외부에서는 접근 불가

internal 접근

internal 접근 수준은 기본 접근 수준으로, 해당 모듈 내부의 모든 소스 파일에서 접근 가능하지만, 외부 모듈에서는 접근이 차단됩니다. 앱이나 프레임워크에서 내부 구조를 정의할 때 주로 사용됩니다.

internal class SomeInternalClass {
    internal let someInternalConstant = 0
    internal func someInternalFunction() {
        print("Internal function called")
    }
}

// 같은 모듈 내에서 접근 가능
let internalInstance = SomeInternalClass()
print(internalInstance.someInternalConstant) // 출력: 0
internalInstance.someInternalFunction() // 출력: Internal function called
// 모듈 외부에서는 접근 불가

fileprivate 접근

fileprivate 접근 수준은 해당 소스 파일 내에서만 접근이 가능합니다. 같은 파일 내에서는 사용 가능하지만, 다른 파일에서는 접근할 수 없습니다. 이는 특정 파일 내에서만 공유해야 하는 코드를 보호할 때 유용합니다.

fileprivate class SomeFilePrivateClass {
    fileprivate func someFilePrivateFunction() {
        print("Fileprivate function called")
    }
}

let filePrivateInstance = SomeFilePrivateClass()
filePrivateInstance.someFilePrivateFunction() // 출력: Fileprivate function called
// 다른 파일에서는 접근 불가

private 접근

private 접근 수준은 가장 제한적인 접근 수준으로, 클래스나 구조체 내부에서만 접근이 가능합니다. 외부에서 접근하거나 상속된 클래스에서도 접근할 수 없습니다. 확장(extension)에서도 접근이 불가능합니다.

class SomeClass {
    private var somePrivateVariable = 0
    private func somePrivateFunction() {
        print("Private function called")
    }
    
    func accessPrivate() {
        print(somePrivateVariable) // 내부에서 접근 가능
        somePrivateFunction() // 내부에서 호출 가능
    }
}

let instance = SomeClass()
instance.accessPrivate() // 출력: 0 \n Private function called
// print(instance.somePrivateVariable) // 오류: private 변수는 외부에서 접근 불가

정리

  • Swift에서 많이 사용되는 기능들을 알아봤어요.
  • ArrayCollection Type, 그리고 Optional ChainingGeneric의 내용을 실습 코드와 함께 확인해볼 수 있었어요.

출처 : Smile Han - iOS 프로그래밍 기초

0개의 댓글