# 유니코드와 문자열

스위프트의 문자열은 각각 인코딩된 유니코드 문자들로 구성되어 있다. 각 문자를 유니코드 UTF-32로 저장하고, 선택에 따라 UTF-8, UTF-16으로도 인코딩 할 수 있다. (메모리 관련 효율을 위해)

let dogString = "Dog‼🐶"
print(dogString.utf8) // dogString을 utf8체계로 인코딩

// str.utf16
// str.unicodeScalars : UTF-32체계

UTF-8체계는 HTTP통신할때 일반적으로 사용하는 체계라고 한다.

유니코드와 한글

한글의 자모를 분리하여 유니코드를 표기하면 해당 자모를 합친 한 글자의 유니코드와 구성이 분명하게 다르다.

하지만 유니코드 체계 내에서는 두 글자를 동일글자로 취급하여 글자수를 셀때 한글자로 취급한다. (일상의 언어로 인식)

var hangul1 = "\u{D55C}"     // "한"

print("\"한\"의 글자수: ", hangul1.count)
print("기모링".count)


var hangul2 = "\u{1112}\u{1161}\u{11AB}"      // "ㅎ" "ㅏ" "ㄴ"
print("\"ㅎ\"+\"ㅏ\"+\"ㄴ\"의 글자수: ", hangul2.count) // 1


hangul1 == hangul2 // true!

위와 유사한 예시로 영단어에 강세가 추가되더라도 글자수에는 변함이 없다.

이러한 유니코드 특성상 스위프트 문자열에서 단순 인덱싱 접근이 불가능하다. (한 -> ㅎ + ㅏ + ㄴ)

String vs NSString

스위프트 String은 구조체이고 NSString은 클래스이다. NSString은 오브젝티브-C에서 사용하는 타입이며 둘은 거의 유사한 역할을 하기 때문에 서로 타입캐스팅하여 사용하면 된다. as를 통한 타입캐스팅 시 옵셔널로 타입캐스팅 할 필요가 없다.as?가 아닌 as로 캐스팅!

위와 같이 서로 문제없이 타입캐스팅 가능한 타입들을 Toll-Free Bridged라고 한다.

글자 길이를 셀때 NSStringlength를, Stringcount를 사용하는 등의 차이가 있다. (사실상 거의 String을 사용함)

# 문자열 입력

문자열 여러줄을 한 공간에서 입력할때 쌍따옴표 세개로 감싸면 된다.

let quotation = """
hello
my name
is
Jun
"""
print(quotation)

이스케이프 시퀀스 종류는 아래와 같다.

\0 (null문자)
\\ (백슬레시)
\t (탭)
\n (줄바꿈 - 개행문자)
\r (캐리지 리턴 - 앞줄이동)
\" (쌍따옴표)
\' (작은따옴표)
\u {유니코드값} (1~8자리의 16진수)

raw 스트링

샾(#)으로 감싼 문자열의 내부에서는 이스케이프 시퀀스 등이 모두 문자 그대로 사용된다.

var name = #"Steve"#
print(name) // Steve
// #""Steve""# -> "Steve"

let string1 = #"Line 1\nLine 2"#
print(string1) // Line 1\nLine 2

let string2 = ###"Line 1\###nLine 2"###
print(string3) // Line 1 (줄바꿈) Line 2

let string3 = ###"Line 1\n\###nLine 2"###
print(string3) // Line 1\n (줄바꿈) Line 2

let string4 = #"My name is \#(name)"#   // 스트링 인터폴레이션

# 문자열 보간법

struct Dog {
    var name: String
    var weight: Double
}

let dog = Dog(name: "초코", weight: 15.0)
print("\(dog)")
print(dog)

위 코드를 실제로 사용해보면 Dog(name: "초코", weight: 15.0)으로 출력된다.

문자열 보간법의 실제 출력 형태를 직접 구현이 가능하다.

protocol CustomStringConvertible {
    var description { get }
}

위 프로토콜을 채택하여 description 계산속성의 get을 구현해주면 문자열 보간법이 위의 형태로 출력된다.

extension Dog: CustomStringConvertible {
    var description: String {
        return "강아지의 이름은 \(name)이고, 몸무게는 \(weight)kg 입니다."
    }
}
print(dog) // 강아지의 이름은 초코이고, 몸무게는 15.0kg 입니다.

문자열 보간법의 \()는 결국 description 속성을 읽는 것이다.

# 서브스트링

스위프트에서 부분 문자열은 String 타입이 아닌 String.SubSequence 라는 특별한 타입이다.

var greeting = "Hello, world!"

let index: String.Index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning: String.SubSequence = greeting[..<index]

greeting 문자열 내의 ,문자가 첫 번째로 등장하는 인덱스 값을 firstIndex라는 메서드를 통해 찾을 수 있다. 닐 코얼레싱 문법을 통해 해당 문자를 찾지 못했다면 endIndex를 반환받고, 이를 index 변수에 저장한다.

서브스트링 문법을 통해 greeting 문자열에서 0번째 인덱스부터 index 변수에 저장된 위치까지 부분 문자열을 뽑아내는데, 이때 뽑아낸 문자열은 String.SubSequence 타입이다.

이 타입이 의미하는 바는 서브스트링 대상 문자열인 greeting 문자열이 저장된 곳의 메모리 주소를 공유하겠다는 의미이다. 서브스트링은 새로운 메모리를 사용하지 않는다.

원본 문자열이 달라지게 되는 경우 서브스트링을 위한 메모리 공간을 즉시 새로 할당하여 새롭게 저장한다. 서브스트링으로 뽑아낸 부분 문자열을 String타입으로 캐스팅을 하면 새로운 메모리 공간을 할당받는다.

# 문자열 관련 인덱스

문자열은 각 문자가 String 타입으로 이루어진 배열로 보아도 된다.

var array: [String] = someString.map { String($0) }
array.joined() // 다시 합치기

map 고차함수와 내부에 전달되는 클로저를 통해 문자열 전체를 각각 쪼개어 String 배열로 만든다. (Character타입이 아닌 String타입임에 주의하자)

자바스크립트 join메서드와 유사하게 String타입으로 이루어진 문자열들을 한 문자열로 합쳐줄 수 있다.

# 문자열 메서드 정리

  1. str.randomElement() - 문자열에서 랜덤 원소 추출
  2. str.shuffled() - 문자열을 무작위로 섞은 후 Character 배열로 리턴
  3. str.lowercased() - 문자열 전체를 소문자로 바꾼 문자열 리턴 (원본 유지)
  4. str.uppercased() - 문자열 전체를 대문자로 바꾼 문자열 리턴 (원본 유지)
  5. str.capitalized - 문자열의 첫 번째 글자를 대문자로 바꾼 문자열 리턴 (원본 유지)
  6. str.count - 문자열 길이를 반환하는 속성
  7. str.isEmpty - 빈 문자열인지 판단하는 속성 (문자열 길이가 0인지 체크)
  8. str.startIndex - 문자열 배열의 시작 인덱스, str[str.startIndex]
  9. str.index(str.startIndex, offsetBy: 정수형) - startIndex로부터 offsetBy에 전달된 정수값만큼 떨어진 곳의 문자열 인덱스를 뽑아내는 메서드
  10. str.index(after: str.startIndex) - after에 전달된 인덱스의 바로 다음 인덱스를 얻어낸다.
  11. str.index(before: str.beforeIndex) - before에 전달된 인덱스의 바로 이전 인덱스를 얻어낸다.
  12. str.findIndex(of: "문자") - of로 전달된 String타입 문자가 처음 등장하는 곳의 인덱스를 얻어낸다.
  13. str.indices - 문자열의 각 인덱스들에 대한 값을 갖는 객체. 반복문을 통해 순회하며 각 문자들을 뽑아낼때 사용한다.
  14. str.range(of: "부분문자열") - 문자열 str의 부분 문자열에 대해 startIndex, endIndex범위를 반환한다. 범위값을 서브스트링에 전달하면 부분문자열 출력이 된다. of에 전달된 문자열 아규먼트가 부분 문자열이 아닌경우 nil을 리턴한다. 반환값 타입이 옵셔널이므로 언래핑해줘야함
  15. str.distance(from: lower, to: upper) - 전달된 인덱스의 시작부터 끝까지 거리에 대해 정수형 값으로 계산하여 리턴해준다.
  16. str.insert(newElement: Character,at: 스트링 인덱스)
    • str.insert(contentsOf: String, at: 스트링 인덱스)
  17. str.replaceSubrange(문자열 range, with: 문자열) - str.range(of: )로 뽑아낸 문자열을 대체한다.
    • 옵셔널이므로 if let 바인딩
    • 원본을 변경한다.
  18. str.replacingOccurrence(of: 문자열, with: 대체 문자열, options: [문자열 탐색 옵션 배열], range: 문자열 탐색 Range)
    • of에 전달된 문자열이 발견된 경우 with에 전달된 문자열로 대체
    • 원본을 변경하지 않는다.
  19. str.append("추가할 문자열") - 문자열을 추가한다
  20. str.remove(at: 문자열 인덱스) - 문자 하나 삭제, 원본 변환
  21. str.removeSubrange(인덱스 range) - 부분 문자열 삭제
  22. str.removeAll() - 문자열 전체 삭제, 빈 문자열로 대체됨
  23. str.removeAll(keepingCapacity: Boolean) - 문자열 전체를 삭제하지만 메모리 공간은 유지
  24. str.caseInsensitiveCompare(비교할 문자열) - 대소문자 무시하여 비교
    • 리턴값은 Boolean이 아니고 열거형임. 타입명은 ComparisonResult
    • ComparisonResult.orderedSame : 문자열이 동일한 사전상의 순서일때
    • ComparisonResult.orderedAscending : 파라미터 문자열이 사전상으로 더 높을때
    • ComparisonResult.orderedDescending : 파라미터 문자열이 사전상으로 더 낮을때
  25. str.compare(문자열, options:[옵션 배열])
    • 메서드에서 options 아규먼트로 배열을 전달할 수 있는 이유는 OptionSet 프로토콜을 내부적으로 채택했기 때문이다.
    • .caseInsensitive // 대소문자 무시하고
    • .diacriticInsensitive // 발음구별기호 무시하고
    • .widthInsensitive // 글자 넓이 무시하고
    • .forcedOrdering // 강제적 오름차순/내림차순 정렬순 (대소문자 무조건 구별 의미)
    • .literal // (유니코드 자체로) 글자그대로
    • .numeric // 숫자 전체를 인식해서 비교
    • .anchored // (앞부분부터) 고정시키고 (접두어)
    • .backwards // 문자 뒷자리부터
    • .regularExpression // 정규식 검증 ⭐️
  26. str.contains("문자열") - 문자열이 str에 포함되어 있는지
  27. str.hasPrefix("문자열") - 접두어에 문자열이 포함되어 있는지
  28. str.hasSuffix("문자열") - 접미어에 문자열이 포함되어 있는지
  29. str.prefix(Number), str.suffix(Number) - 첫글자로부터 ~글자, 마지막글자로부터 ~글자까지 얻어내기
  30. str.commonPrefix(with: "문자열"), str.commonSuffix(with: "문자열") - with 문자열과 공통된 접두어, 접미어 반환
  31. str.dropFirst(Number), str.dropLast(Number) - 처음부터 ~글자, 마지막부터 ~글자를 지운 나머지 문자열 반환
  32. str.trimmingCharacters(in: ["문자열"]) - in 아규먼트 배열에 전달된 문자열을 찾아 앞 뒤에서 지운다.
    • trimmingCharacters의 in 아규먼트에는 애플이 만들어 놓은 CharacterSet 열거형도 사용 가능하다. (자주 사용하는 것들 - .whiteSpace 등)
  33. str.components(separatedBy: "문자열") - 전달된 문자열을 구분자로 하여 문자열을 자른 후 배열로 만든다.
    • separatedByCharacterSet타입을 전달해도 된다.
    • 배열도 전달 가능 (trimmingCharacters와 동일하게 ["!", "?"])
  34. str.split(separator: "문자열") - 전달된 문자열을 구분자로 하여 구분한다. components메서드와 유사하지만 메모리를 덜 쓴다. (서브스트링 타입으로 반환하기 때문)
    • split메서드 separator는 클로저 형태로도 구현 가능하다. 문자열 각 문자를 순회하며 해당 문자를 분기, 맞는 조건일 경우 구분자로 사용하는 방식
// 정규식 사용 예시
// rawString 형식으로 사용한다. 이스케이프 문자를 자주 사용하기 때문
// range메서드의 of 아규먼트와 함께 주로 사용한다.
var telephoneNumRegex = #"[0-9]{3}-[0-9]{4}-[0-9]{4}"#

if let _ = number.range(of: telephoneNumRegex, options: [.regularExpression]) {
    print("유효한 전화번호로 판단")
}

문자열 인덱싱

정수형 데이터를 저장할때는 일정한 간격으로 메모리 공간을 할당받기 때문에 정수형 인덱싱에 문제가 없지만, 문자열 데이터의 경우 각 문자가 갖는 메모리값이 다르기 때문에 정수형 인덱싱이 불가능하다.

문자열 자체적으로 갖는 인덱싱 속성들을 통해 접근해야 한다.

각종 인덱스 메서드들은 인덱스를 얻어내는 것이므로, 반환받은 후 서브스트링에 전달하면 된다.

부분 문자열 뽑아내기

let lower = greeting.index(greeting.startIndex, offsetBy: 2)
let upper = greeting.index(greeting.startIndex, offsetBy: 5)
greeting[lower...upper] // 기억하기
// split 클로저 사용 예시
str.split { $0 == " " }

# Reference

  1. 인프런 - 앨런 swift 문법 마스터 스쿨 (opens new window)